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