1use futures::StreamExt;
9use mongodb::bson::{self, Bson, Document, doc, to_bson};
10
11use rig_core::{
12 Embed,
13 embeddings::embedding::Embedding,
14 vector_store::{
15 InsertDocuments, VectorSearchIdResult, VectorSearchResult, VectorStoreError,
16 VectorStoreIndex,
17 request::{DynamicSearchFilter, Filter, FilterError, SearchFilter, VectorSearchRequest},
18 },
19 wasm_compat::WasmCompatSend,
20};
21use serde::{Deserialize, Serialize, de::DeserializeOwned};
22
23#[derive(Debug, Serialize, Deserialize)]
24#[serde(rename_all = "camelCase")]
25struct SearchIndex {
26 id: String,
27 name: String,
28 #[serde(rename = "type")]
29 index_type: String,
30 status: String,
31 queryable: bool,
32 latest_definition: LatestDefinition,
33}
34
35impl SearchIndex {
36 async fn get_search_index<C: Send + Sync>(
37 collection: mongodb::Collection<C>,
38 index_name: &str,
39 ) -> Result<SearchIndex, VectorStoreError> {
40 collection
41 .list_search_indexes()
42 .name(index_name)
43 .await
44 .map_err(VectorStoreError::datastore)?
45 .with_type::<SearchIndex>()
46 .next()
47 .await
48 .transpose()
49 .map_err(VectorStoreError::datastore)?
50 .ok_or_else(|| VectorStoreError::DatastoreError("Index not found".into()))
51 }
52}
53
54#[derive(Debug, Serialize, Deserialize)]
55struct LatestDefinition {
56 fields: Vec<Field>,
57}
58
59#[derive(Debug, Serialize, Deserialize)]
60#[serde(rename_all = "camelCase")]
61struct Field {
62 #[serde(rename = "type")]
63 field_type: String,
64 path: String,
65 num_dimensions: i32,
66 similarity: String,
67}
68
69pub struct MongoDbVectorIndex<C>
117where
118 C: Send + Sync,
119{
120 collection: mongodb::Collection<C>,
121 model: rig_core::DynModel<rig_core::operation::Embedding>,
122 index_name: String,
123 embedded_field: String,
124 search_params: SearchParams,
125}
126
127impl<C> MongoDbVectorIndex<C>
128where
129 C: Send + Sync,
130{
131 fn pipeline_search_stage(
135 &self,
136 prompt_embedding: &Embedding,
137 req: &VectorSearchRequest<MongoDbSearchFilter>,
138 ) -> bson::Document {
139 let SearchParams {
140 exact,
141 num_candidates,
142 } = &self.search_params;
143
144 let samples = req.samples() as usize;
145
146 let thresh = req
147 .threshold()
148 .map(|thresh| MongoDbSearchFilter::gte("score", thresh.into()));
149
150 let filter = match (thresh, req.filter()) {
151 (Some(thresh), Some(filt)) => thresh.and(filt.clone()).into_inner(),
152 (Some(thresh), _) => thresh.into_inner(),
153 (_, Some(filt)) => filt.clone().into_inner(),
154 _ => Default::default(),
155 };
156
157 doc! {
158 "$vectorSearch": {
159 "index": &self.index_name,
160 "path": self.embedded_field.clone(),
161 "queryVector": &prompt_embedding.vec,
162 "numCandidates": num_candidates.unwrap_or((samples * 10) as u32),
163 "limit": samples as u32,
164 "filter": filter,
165 "exact": exact.unwrap_or(false)
166 }
167 }
168 }
169
170 async fn run_search_pipeline(
174 &self,
175 req: &VectorSearchRequest<MongoDbSearchFilter>,
176 project_stage: bson::Document,
177 ) -> Result<Vec<(f64, String, serde_json::Value)>, VectorStoreError> {
178 let prompt_embedding = self.model.embed_text(req.query()).await?;
179
180 let pipeline = vec![
181 self.pipeline_search_stage(&prompt_embedding, req),
182 self.pipeline_score_stage(),
183 project_stage,
184 ];
185
186 let mut cursor = self
187 .collection
188 .aggregate(pipeline)
189 .await
190 .map_err(VectorStoreError::datastore)?
191 .with_type::<serde_json::Value>();
192
193 let mut results = Vec::new();
194 while let Some(doc) = cursor.next().await {
195 let doc = doc.map_err(VectorStoreError::datastore)?;
196 let score = doc
197 .get("score")
198 .and_then(serde_json::Value::as_f64)
199 .ok_or_else(|| {
200 VectorStoreError::DatastoreError(
201 "MongoDB vector search result missing numeric score".into(),
202 )
203 })?;
204 let id = doc
205 .get("_id")
206 .ok_or_else(|| {
207 VectorStoreError::MissingIdError(
208 "MongoDB vector search result missing _id".to_string(),
209 )
210 })?
211 .to_string();
212 results.push((score, id, doc));
213 }
214
215 tracing::info!(target: "rig",
216 "Selected documents: {}",
217 results.iter()
218 .map(|(distance, id, _)| format!("{id} ({distance})"))
219 .collect::<Vec<String>>()
220 .join(", ")
221 );
222
223 Ok(results)
224 }
225
226 fn pipeline_score_stage(&self) -> bson::Document {
228 doc! {
229 "$addFields": {
230 "score": { "$meta": "vectorSearchScore" }
231 }
232 }
233 }
234}
235
236impl<C> MongoDbVectorIndex<C>
237where
238 C: Send + Sync,
239{
240 pub async fn new(
246 collection: mongodb::Collection<C>,
247 model: impl Into<rig_core::DynModel<rig_core::operation::Embedding>>,
248 index_name: &str,
249 search_params: SearchParams,
250 ) -> Result<Self, VectorStoreError> {
251 let search_index = SearchIndex::get_search_index(collection.clone(), index_name).await?;
252
253 if !search_index.queryable {
254 return Err(VectorStoreError::DatastoreError(
255 "Index is not queryable".into(),
256 ));
257 }
258
259 let embedded_field = search_index
260 .latest_definition
261 .fields
262 .into_iter()
263 .map(|field| field.path)
264 .next()
265 .ok_or(VectorStoreError::DatastoreError(
266 "No embedded fields found".into(),
267 ))?;
268
269 Ok(Self {
270 collection,
271 model: model.into(),
272 index_name: index_name.to_string(),
273 embedded_field,
274 search_params,
275 })
276 }
277}
278
279#[derive(Default)]
282pub struct SearchParams {
283 exact: Option<bool>,
284 num_candidates: Option<u32>,
285}
286
287impl SearchParams {
288 pub fn new() -> Self {
290 Self {
291 exact: None,
292 num_candidates: None,
293 }
294 }
295
296 pub fn exact(mut self, exact: bool) -> Self {
298 self.exact = Some(exact);
299 self
300 }
301
302 pub fn num_candidates(mut self, num_candidates: u32) -> Self {
305 self.num_candidates = Some(num_candidates);
306 self
307 }
308}
309
310#[derive(Clone, Debug, Serialize, Deserialize)]
311pub struct MongoDbSearchFilter(Document);
312
313impl SearchFilter for MongoDbSearchFilter {
314 type Value = Bson;
315
316 fn eq(key: impl AsRef<str>, value: Self::Value) -> Self {
317 let key = key.as_ref().to_owned();
318 Self(doc! { key: value })
319 }
320
321 fn gt(key: impl AsRef<str>, value: Self::Value) -> Self {
322 let key = key.as_ref().to_owned();
323 Self(doc! { key: { "$gt": value } })
324 }
325
326 fn lt(key: impl AsRef<str>, value: Self::Value) -> Self {
327 let key = key.as_ref().to_owned();
328 Self(doc! { key: { "$lt": value } })
329 }
330
331 fn and(self, rhs: Self) -> Self {
332 Self(doc! { "$and": [ self.0, rhs.0 ]})
333 }
334
335 fn or(self, rhs: Self) -> Self {
336 Self(doc! { "$or": [ self.0, rhs.0 ]})
337 }
338}
339
340impl MongoDbSearchFilter {
341 fn into_inner(self) -> Document {
342 self.0
343 }
344
345 pub fn gte(key: impl Into<String>, value: <Self as SearchFilter>::Value) -> Self {
346 let key = key.into();
347 Self(doc! { key: { "$gte": value } })
348 }
349
350 pub fn lte(key: impl Into<String>, value: <Self as SearchFilter>::Value) -> Self {
351 let key = key.into();
352 Self(doc! { key: { "$lte": value } })
353 }
354
355 pub fn not(self) -> Self {
356 Self(doc! { "$nor": [self.0] })
357 }
358
359 pub fn is_type(key: impl Into<String>, typ: &'static str) -> Self {
361 let key = key.into();
362 Self(doc! { key: { "$type": typ } })
363 }
364
365 pub fn size(key: impl Into<String>, size: i32) -> Self {
366 let key = key.into();
367 Self(doc! { key: { "$size": size } })
368 }
369
370 pub fn all(key: impl Into<String>, values: Vec<Bson>) -> Self {
372 let key = key.into();
373 Self(doc! { key: { "$all": values } })
374 }
375
376 pub fn any(key: impl Into<String>, condition: Document) -> Self {
378 let key = key.into();
379 Self(doc! { key: { "$elemMatch": condition } })
380 }
381}
382
383impl DynamicSearchFilter for MongoDbSearchFilter {
384 fn from_dynamic_filter(filter: Filter<serde_json::Value>) -> Result<Self, FilterError> {
386 Ok(filter.interpret_with(|v| to_bson(&v).unwrap_or(Bson::Null)))
387 }
388}
389
390impl<C> VectorStoreIndex for MongoDbVectorIndex<C>
391where
392 C: Sync + Send,
393{
394 type Filter = MongoDbSearchFilter;
395
396 async fn top_n<T: DeserializeOwned + WasmCompatSend>(
399 &self,
400 req: VectorSearchRequest<MongoDbSearchFilter>,
401 ) -> Result<Vec<VectorSearchResult<T>>, VectorStoreError> {
402 let project_stage = doc! {
403 "$project": {
404 self.embedded_field.clone(): 0
405 }
406 };
407
408 self.run_search_pipeline(&req, project_stage)
409 .await?
410 .into_iter()
411 .map(|(score, id, doc)| {
412 let doc_t: T = serde_json::from_value(doc).map_err(VectorStoreError::JsonError)?;
413 Ok(VectorSearchResult {
414 score,
415 id,
416 document: doc_t,
417 })
418 })
419 .collect()
420 }
421
422 async fn top_n_ids(
424 &self,
425 req: VectorSearchRequest<MongoDbSearchFilter>,
426 ) -> Result<Vec<VectorSearchIdResult>, VectorStoreError> {
427 let project_stage = doc! {
428 "$project": {
429 "_id": 1,
430 "score": 1
431 },
432 };
433
434 Ok(self
435 .run_search_pipeline(&req, project_stage)
436 .await?
437 .into_iter()
438 .map(|(score, id, _)| VectorSearchIdResult { score, id })
439 .collect())
440 }
441}
442
443impl<C> InsertDocuments for MongoDbVectorIndex<C>
444where
445 C: Send + Sync,
446{
447 async fn insert_documents<Doc: Serialize + Embed + WasmCompatSend>(
448 &self,
449 documents: Vec<(Doc, Vec<Embedding>)>,
450 ) -> Result<(), VectorStoreError> {
451 let mongo_documents = rig_core::vector_store::flatten_embedded(
452 documents,
453 |json_doc, embedding| {
454 Ok(doc! {
455 "document": mongodb::bson::to_bson(json_doc).map_err(VectorStoreError::datastore)?,
456 "embedding": embedding.vec,
457 "embedded_text": embedding.document,
458 })
459 },
460 )?;
461
462 let collection = self.collection.clone_with_type::<mongodb::bson::Document>();
463
464 collection
465 .insert_many(mongo_documents)
466 .await
467 .map_err(VectorStoreError::datastore)?;
468
469 Ok(())
470 }
471}