Skip to main content

rig_mongodb/
lib.rs

1//! MongoDB vector store for Rig.
2//!
3//! [`MongoDbVectorIndex`] queries an existing MongoDB Atlas Vector Search index
4//! through an aggregation pipeline, filtered by [`MongoDbSearchFilter`]. The
5//! `rig` facade re-exports this crate as `rig::mongodb` under the `mongodb`
6//! feature.
7
8use 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
69/// Vector index over a MongoDB collection.
70///
71/// Queries are embedded with the same model that populated the collection,
72/// so results are meaningless under another model.
73///
74/// # Example
75/// ```no_run
76/// use rig_mongodb::{MongoDbVectorIndex, SearchParams};
77/// use rig_core::{providers::openai::{self, OpenAI}, vector_store::{VectorStoreIndex, VectorSearchRequest}};
78///
79/// # async fn example() -> anyhow::Result<()> {
80/// #[derive(serde::Deserialize, serde::Serialize, Debug)]
81/// struct WordDefinition {
82///     #[serde(rename = "_id")]
83///     id: String,
84///     definition: String,
85///     embedding: Vec<f64>,
86/// }
87///
88/// let mongodb_client = mongodb::Client::with_uri_str("mongodb://localhost:27017").await?; // <-- replace with your mongodb uri.
89/// let openai = OpenAI::from_env()?;
90///
91/// let collection = mongodb_client.database("db").collection::<WordDefinition>(""); // <-- replace with your mongodb collection.
92///
93/// // <-- replace with your embedding model.
94/// let model = openai.embedding(openai::TEXT_EMBEDDING_ADA_002, None);
95/// let index = MongoDbVectorIndex::new(
96///     collection,
97///     model,
98///     "vector_index", // <-- replace with the name of the index in your mongodb collection.
99///     SearchParams::new(), // <-- field name in `Document` that contains the embeddings.
100/// )
101/// .await?;
102///
103/// let req = VectorSearchRequest::builder()
104///     .query("My boss says I zindle too much, what does that mean?")
105///     .samples(1)
106///     .build();
107///
108/// // Query the index
109/// let definitions = index
110///     .top_n::<WordDefinition>(req)
111///     .await?;
112/// # Ok(())
113/// # }
114/// # let _ = example();
115/// ```
116pub 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    /// Builds the `$vectorSearch` stage. Any request threshold becomes a
132    /// `score >= threshold` condition combined with the request filter, and
133    /// `numCandidates` defaults to ten times the requested sample count.
134    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    /// Embeds the query and runs the pipeline with the given `$project` stage.
171    /// Errors when a result lacks a numeric `score` or an `_id`, whose BSON
172    /// rendering (including quotes for strings) becomes the returned id.
173    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    /// Builds the stage exposing the vector search score as a `score` field.
227    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    /// Creates an index handle after confirming the named search index exists and
241    /// is queryable. The embedded field is taken from the index's first defined
242    /// field. Errors when the index is missing, not queryable, or defines no
243    /// fields. See the MongoDB [documentation](https://www.mongodb.com/docs/atlas/atlas-vector-search/vector-search-type/)
244    /// on creating vector indexes.
245    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/// Backend search tuning for the `$vectorSearch` stage. See
280/// [MongoDB Vector Search](https://www.mongodb.com/docs/atlas/atlas-vector-search/vector-search-stage/).
281#[derive(Default)]
282pub struct SearchParams {
283    exact: Option<bool>,
284    num_candidates: Option<u32>,
285}
286
287impl SearchParams {
288    /// Initializes a new `SearchParams` with default values.
289    pub fn new() -> Self {
290        Self {
291            exact: None,
292            num_candidates: None,
293        }
294    }
295
296    /// Selects exact (ENN) search instead of the default approximate (ANN) search.
297    pub fn exact(mut self, exact: bool) -> Self {
298        self.exact = Some(exact);
299        self
300    }
301
302    /// Sets how many nearest neighbors approximate search considers. MongoDB
303    /// rejects this alongside exact search.
304    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    /// Matches values at `key` whose BSON type alias is `typ`.
360    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    /// Matches arrays at `key` containing every one of `values`.
371    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    /// Matches arrays at `key` with at least one element satisfying `condition`.
377    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    /// Values that cannot be represented in BSON become `Bson::Null`.
385    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    /// Returns matches with the search score. The embedding field is
397    /// projected out, so `T` must not require it.
398    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    /// Like `top_n` but projects only the id and score.
423    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}