use std::{error::Error, sync::Arc, time::Duration};
use reqwest::Client;
use super::store::Store;
use crate::embedding::Embedder;
const METRICS: &[&str] = &["COSINE", "IP", "L2"];
const CONSISTENCY_LEVELS: &[&str] = &["Strong", "Session", "Bounded", "Eventually"];
pub struct StoreBuilder {
client: Option<Client>,
url: String,
token: Option<String>,
db_name: Option<String>,
collection: Option<String>,
embedder: Option<Arc<dyn Embedder>>,
primary_field: String,
text_field: String,
vector_field: String,
metadata_field: String,
namespace_field: String,
namespace: Option<String>,
metric: String,
consistency_level: String,
max_text_length: usize,
create_collection: bool,
load_timeout: Duration,
}
impl Default for StoreBuilder {
fn default() -> Self {
Self::new()
}
}
pub(super) fn is_milvus_name(name: &str) -> bool {
let mut chars = name.chars();
chars
.next()
.is_some_and(|c| c == '_' || c.is_ascii_alphabetic())
&& chars.all(|c| c == '_' || c.is_ascii_alphanumeric())
&& name.len() <= 255
}
impl StoreBuilder {
pub fn new() -> Self {
StoreBuilder {
client: None,
url: "http://localhost:19530".to_string(),
token: None,
db_name: None,
collection: None,
embedder: None,
primary_field: "pk".to_string(),
text_field: "text".to_string(),
vector_field: "vector".to_string(),
metadata_field: "metadata".to_string(),
namespace_field: "namespace".to_string(),
namespace: None,
metric: "COSINE".to_string(),
consistency_level: "Strong".to_string(),
max_text_length: 65_535,
create_collection: true,
load_timeout: Duration::from_secs(60),
}
}
pub fn url(mut self, url: &str) -> Self {
self.url = url.trim_end_matches('/').to_string();
self
}
pub fn token(mut self, token: &str) -> Self {
self.token = Some(token.to_string());
self
}
pub fn db_name(mut self, db_name: &str) -> Self {
self.db_name = Some(db_name.to_string());
self
}
pub fn client(mut self, client: Client) -> Self {
self.client = Some(client);
self
}
pub fn collection(mut self, name: &str) -> Self {
self.collection = Some(name.to_string());
self
}
pub fn embedder<E: Embedder + 'static>(mut self, embedder: E) -> Self {
self.embedder = Some(Arc::new(embedder));
self
}
pub fn primary_field(mut self, name: &str) -> Self {
self.primary_field = name.to_string();
self
}
pub fn text_field(mut self, name: &str) -> Self {
self.text_field = name.to_string();
self
}
pub fn vector_field(mut self, name: &str) -> Self {
self.vector_field = name.to_string();
self
}
pub fn metadata_field(mut self, name: &str) -> Self {
self.metadata_field = name.to_string();
self
}
pub fn namespace_field(mut self, name: &str) -> Self {
self.namespace_field = name.to_string();
self
}
pub fn namespace(mut self, namespace: &str) -> Self {
self.namespace = Some(namespace.to_string());
self
}
pub fn metric(mut self, metric: &str) -> Self {
self.metric = metric.to_ascii_uppercase();
self
}
pub fn consistency_level(mut self, level: &str) -> Self {
self.consistency_level = level.to_string();
self
}
pub fn max_text_length(mut self, max: usize) -> Self {
self.max_text_length = max;
self
}
pub fn create_collection(mut self, create: bool) -> Self {
self.create_collection = create;
self
}
pub fn load_timeout(mut self, timeout: Duration) -> Self {
self.load_timeout = timeout;
self
}
pub fn build(self) -> Result<Store, Box<dyn Error>> {
let collection = self.collection.ok_or("'collection' is required")?;
let embedder = self.embedder.ok_or("'embedder' is required")?;
let names = [
("collection", &collection),
("primary_field", &self.primary_field),
("text_field", &self.text_field),
("vector_field", &self.vector_field),
("metadata_field", &self.metadata_field),
("namespace_field", &self.namespace_field),
];
for (what, name) in names {
if !is_milvus_name(name) {
return Err(format!("invalid Milvus {what} {name:?}").into());
}
}
let fields = &names[1..];
for (i, (what, name)) in fields.iter().enumerate() {
if let Some((other, _)) = fields[..i].iter().find(|(_, n)| n == name) {
return Err(format!("{what} and {other} are both {name:?}").into());
}
}
if !METRICS.contains(&self.metric.as_str()) {
return Err(format!(
"unsupported Milvus metric {:?}, use one of {METRICS:?}",
self.metric
)
.into());
}
if !CONSISTENCY_LEVELS.contains(&self.consistency_level.as_str()) {
return Err(format!(
"unknown Milvus consistency level {:?}, use one of {CONSISTENCY_LEVELS:?}",
self.consistency_level
)
.into());
}
if !(1..=65_535).contains(&self.max_text_length) {
return Err("max_text_length must be between 1 and 65535".into());
}
Ok(Store {
client: self.client.unwrap_or_default(),
url: self.url,
token: self.token,
db_name: self.db_name,
collection,
embedder,
primary_field: self.primary_field,
text_field: self.text_field,
vector_field: self.vector_field,
metadata_field: self.metadata_field,
namespace_field: self.namespace_field,
namespace: self.namespace,
metric: self.metric,
consistency_level: self.consistency_level,
max_text_length: self.max_text_length,
create_collection: self.create_collection,
load_timeout: self.load_timeout,
schema: Default::default(),
})
}
}