1mod filter;
10
11use reqwest::StatusCode;
12use rig_core::{
13 Embed, OneOrMany,
14 embeddings::{Embedding, EmbeddingModel},
15 vector_store::{
16 InsertDocuments, TopNResults, VectorStoreError, VectorStoreIndex, VectorStoreIndexDyn,
17 request::{Filter as CoreFilter, SearchFilter, VectorSearchRequest},
18 },
19 wasm_compat::WasmBoxedFuture,
20};
21use serde::{Deserialize, Serialize};
22
23use crate::filter::Filter;
24
25pub struct MilvusVectorStore<M> {
27 model: M,
29 base_url: String,
30 client: reqwest::Client,
31 database_name: String,
32 collection_name: String,
33 token: Option<String>,
34}
35
36#[derive(Debug, Serialize, Deserialize)]
37pub struct CreateRecord {
38 document: String,
39 embedded_text: String,
40 embedding: Vec<f64>,
41}
42
43#[derive(Debug, Serialize, Deserialize)]
44#[serde(rename_all = "camelCase")]
45struct InsertRequest<'a> {
46 data: Vec<CreateRecord>,
47 collection_name: &'a str,
48 db_name: &'a str,
49}
50
51#[derive(Debug, Serialize, Deserialize)]
52#[serde(rename_all = "camelCase")]
53struct SearchRequest<'a> {
54 collection_name: &'a str,
55 db_name: &'a str,
56 data: Vec<f64>,
57 #[serde(skip_serializing_if = "String::is_empty")]
58 filter: String,
59 anns_field: &'a str,
60 limit: usize,
61 output_fields: Vec<&'a str>,
62}
63
64#[derive(Debug, Serialize, Deserialize)]
65#[serde(rename_all = "camelCase")]
66struct SearchResult<T> {
67 code: i64,
68 data: Vec<SearchResultData<T>>,
69}
70
71#[derive(Debug, Serialize, Deserialize)]
72#[serde(rename_all = "camelCase")]
73struct SearchResultData<T> {
74 id: i64,
75 distance: f64,
76 document: T,
77 embedded_text: String,
78}
79
80#[derive(Debug, Serialize, Deserialize)]
81#[serde(rename_all = "camelCase")]
82struct SearchResultOnlyId {
83 code: i64,
84 data: Vec<SearchResultDataOnlyId>,
85}
86
87#[derive(Debug, Serialize, Deserialize)]
88#[serde(rename_all = "camelCase")]
89struct SearchResultDataOnlyId {
90 id: i64,
91 distance: f64,
92}
93
94impl<M> MilvusVectorStore<M>
95where
96 M: EmbeddingModel,
97{
98 pub fn new(model: M, base_url: String, database_name: String, collection_name: String) -> Self {
106 Self {
107 model,
108 base_url,
109 client: reqwest::Client::new(),
110 database_name,
111 collection_name,
112 token: None,
113 }
114 }
115
116 pub fn auth(mut self, username: String, password: String) -> Self {
118 let str = format!("{username}:{password}");
119 self.token = Some(str);
120
121 self
122 }
123
124 fn create_insert_request(&self, data: Vec<CreateRecord>) -> InsertRequest<'_> {
126 InsertRequest {
127 data,
128 collection_name: &self.collection_name,
129 db_name: &self.database_name,
130 }
131 }
132
133 fn create_search_request(
135 &self,
136 data: Vec<f64>,
137 req: &VectorSearchRequest<Filter>,
138 id_only: bool,
139 ) -> SearchRequest<'_> {
140 const OUTPUT_FIELDS: [&str; 4] = ["id", "distance", "document", "embeddedText"];
141 const OUTPUT_FIELDS_ID_ONLY: [&str; 2] = ["id", "distance"];
142
143 let output_fields = if id_only {
144 OUTPUT_FIELDS_ID_ONLY.to_vec()
145 } else {
146 OUTPUT_FIELDS.to_vec()
147 };
148
149 let threshold = req
150 .threshold()
151 .map(|thresh| Filter::gte("distance".into(), thresh.into()));
152
153 let filter = match (threshold, req.filter()) {
154 (Some(thresh), Some(filter)) => thresh.and(filter.clone()).into_inner(),
155 (Some(thresh), _) => thresh.into_inner(),
156 (_, Some(filter)) => filter.clone().into_inner(),
157 _ => String::new(),
158 };
159
160 SearchRequest {
161 collection_name: &self.collection_name,
162 db_name: &self.database_name,
163 data,
164 filter,
165 anns_field: "embedding",
166 limit: req.samples() as usize,
167 output_fields,
168 }
169 }
170}
171
172impl<Model> InsertDocuments for MilvusVectorStore<Model>
173where
174 Model: EmbeddingModel + Send + Sync,
175{
176 async fn insert_documents<Doc: Serialize + Embed + Send>(
177 &self,
178 documents: Vec<(Doc, OneOrMany<Embedding>)>,
179 ) -> Result<(), VectorStoreError> {
180 let url = format!(
181 "{base_url}/v2/vectordb/entities/insert",
182 base_url = self.base_url
183 );
184
185 let data = documents
186 .into_iter()
187 .map(|(document, embeddings)| {
188 let json_document: serde_json::Value = serde_json::to_value(&document)?;
189 let json_document_as_string = serde_json::to_string(&json_document)?;
190
191 let embeddings = embeddings
192 .into_iter()
193 .map(|embedding| {
194 let embedded_text = embedding.document;
195 let embedding: Vec<f64> = embedding.vec;
196
197 CreateRecord {
198 document: json_document_as_string.clone(),
199 embedded_text,
200 embedding,
201 }
202 })
203 .collect::<Vec<CreateRecord>>();
204 Ok(embeddings)
205 })
206 .collect::<Result<Vec<Vec<CreateRecord>>, VectorStoreError>>()?
207 .into_iter()
208 .flatten()
209 .collect::<Vec<CreateRecord>>();
210
211 let mut client = self.client.post(url);
212 if let Some(ref token) = self.token {
213 client = client.header("Authentication", format!("Bearer {token}"));
214 }
215
216 let insert_request = self.create_insert_request(data);
217
218 let body = serde_json::to_string(&insert_request)?;
219
220 let res = client.body(body).send().await?;
221
222 if res.status() != StatusCode::OK {
223 let status = res.status();
224 let text = res.text().await?;
225
226 return Err(VectorStoreError::ExternalAPIError(status, text));
227 }
228
229 Ok(())
230 }
231}
232
233impl<M> VectorStoreIndex for MilvusVectorStore<M>
234where
235 M: EmbeddingModel,
236{
237 type Filter = Filter;
238
239 async fn top_n<T: for<'a> Deserialize<'a> + Send>(
242 &self,
243 req: VectorSearchRequest<Filter>,
244 ) -> Result<Vec<(f64, String, T)>, VectorStoreError> {
245 let embedding = self.model.embed_text(req.query()).await?;
246 let url = format!(
247 "{base_url}/v2/vectordb/entities/search",
248 base_url = self.base_url
249 );
250
251 let body = self.create_search_request(embedding.vec, &req, false);
252
253 let mut client = self.client.post(url);
254 if let Some(ref token) = self.token {
255 client = client.header("Authentication", format!("Bearer {token}"));
256 }
257
258 let body = serde_json::to_string(&body)?;
259
260 let res = client.body(body).send().await?;
261
262 if res.status() != StatusCode::OK {
263 let status = res.status();
264 let text = res.text().await?;
265
266 return Err(VectorStoreError::ExternalAPIError(status, text));
267 }
268
269 let json: SearchResult<T> = res.json().await?;
270
271 let res = json
272 .data
273 .into_iter()
274 .map(|x| (x.distance, x.id.to_string(), x.document))
275 .collect();
276
277 Ok(res)
278 }
279
280 async fn top_n_ids(
283 &self,
284 req: VectorSearchRequest<Filter>,
285 ) -> Result<Vec<(f64, String)>, VectorStoreError> {
286 let embedding = self.model.embed_text(req.query()).await?;
287 let url = format!(
288 "{base_url}/v2/vectordb/entities/search",
289 base_url = self.base_url
290 );
291
292 let body = self.create_search_request(embedding.vec, &req, true);
293
294 let mut client = self.client.post(url);
295 if let Some(ref token) = self.token {
296 client = client.header("Authentication", format!("Bearer {token}"));
297 }
298
299 let body = serde_json::to_string(&body)?;
300
301 let res = client.body(body).send().await?;
302
303 if res.status() != StatusCode::OK {
304 let status = res.status();
305 let text = res.text().await?;
306
307 return Err(VectorStoreError::ExternalAPIError(status, text));
308 }
309
310 let json: SearchResultOnlyId = res.json().await?;
311
312 let res = json
313 .data
314 .into_iter()
315 .map(|x| (x.distance, x.id.to_string()))
316 .collect();
317
318 Ok(res)
319 }
320}
321
322impl<M> VectorStoreIndexDyn for MilvusVectorStore<M>
323where
324 M: EmbeddingModel + Sync + Send,
325{
326 fn top_n<'a>(
327 &'a self,
328 req: VectorSearchRequest<CoreFilter<serde_json::Value>>,
329 ) -> WasmBoxedFuture<'a, TopNResults> {
330 Box::pin(async move {
331 let req = req.try_map_filter(Filter::try_from)?;
332 let results = <Self as VectorStoreIndex>::top_n::<serde_json::Value>(self, req).await?;
333
334 Ok(results)
335 })
336 }
337
338 fn top_n_ids<'a>(
340 &'a self,
341 req: VectorSearchRequest<CoreFilter<serde_json::Value>>,
342 ) -> WasmBoxedFuture<'a, Result<Vec<(f64, String)>, VectorStoreError>> {
343 Box::pin(async move {
344 let req = req.try_map_filter(Filter::try_from)?;
345 let results = <Self as VectorStoreIndex>::top_n_ids(self, req).await?;
346
347 Ok(results)
348 })
349 }
350}