Skip to main content

lc_vector_stores/
mongo_document_store.rs

1// lc-vector-stores/src/mongo_document_store.rs
2//! MongoDB document store implementation
3//!
4//! MongoDB is recommended as a ChunkedDocumentStore backend in production:
5//! - persistent by nature
6//! - supports sharding and replica sets
7//! - document structure matches naturally
8//! - supports indexed queries
9
10use crate::document_store::{ChunkDocument, ChunkedDocumentStoreTrait};
11use crate::{Document, VectorStoreError};
12use async_trait::async_trait;
13use lc_shared::splitter::{RecursiveCharacterSplitter, TextSplitter};
14use mongodb::{bson::doc, options::ClientOptions, Client, Collection};
15use serde::{Deserialize, Serialize};
16use std::collections::HashMap;
17
18#[derive(Debug, Clone, Serialize, Deserialize)]
19struct MongoParentDoc {
20    #[serde(rename = "_id")]
21    id: String,
22    content: String,
23    metadata: HashMap<String, serde_json::Value>,
24}
25
26#[derive(Debug, Clone, Serialize, Deserialize)]
27struct MongoChunkDoc {
28    #[serde(rename = "_id")]
29    chunk_id: String,
30    parent_id: String,
31    content: String,
32    segment: i32,
33    metadata: HashMap<String, serde_json::Value>,
34}
35
36impl From<MongoParentDoc> for Document {
37    fn from(m: MongoParentDoc) -> Self {
38        Document {
39            content: m.content,
40            metadata: m.metadata,
41            id: Some(m.id),
42        }
43    }
44}
45
46impl From<Document> for MongoParentDoc {
47    fn from(d: Document) -> Self {
48        MongoParentDoc {
49            id: d
50                .id
51                .clone()
52                .unwrap_or_else(|| uuid::Uuid::new_v4().to_string()),
53            content: d.content,
54            metadata: d.metadata,
55        }
56    }
57}
58
59impl From<MongoChunkDoc> for ChunkDocument {
60    fn from(m: MongoChunkDoc) -> Self {
61        ChunkDocument {
62            chunk_id: m.chunk_id,
63            parent_id: m.parent_id,
64            content: m.content,
65            segment: m.segment as usize,
66            metadata: m.metadata,
67        }
68    }
69}
70
71impl From<ChunkDocument> for MongoChunkDoc {
72    fn from(c: ChunkDocument) -> Self {
73        MongoChunkDoc {
74            chunk_id: c.chunk_id,
75            parent_id: c.parent_id,
76            content: c.content,
77            segment: c.segment as i32,
78            metadata: c.metadata,
79        }
80    }
81}
82
83/// MongoDB storage configuration
84#[derive(Debug, Clone)]
85pub struct MongoStoreConfig {
86    /// MongoDB connection URI
87    pub uri: String,
88    /// Database name
89    pub database: String,
90    /// Parent document collection name
91    pub parent_collection: String,
92    /// Chunk collection name
93    pub chunk_collection: String,
94}
95
96impl Default for MongoStoreConfig {
97    fn default() -> Self {
98        Self {
99            uri: "mongodb://localhost:27017".to_string(),
100            database: "langchainrust".to_string(),
101            parent_collection: "parent_docs".to_string(),
102            chunk_collection: "chunks".to_string(),
103        }
104    }
105}
106
107impl MongoStoreConfig {
108    /// Creates a config from a URI and database name, using default collection names.
109    pub fn new(uri: impl Into<String>, database: impl Into<String>) -> Self {
110        Self {
111            uri: uri.into(),
112            database: database.into(),
113            parent_collection: "parent_docs".to_string(),
114            chunk_collection: "chunks".to_string(),
115        }
116    }
117
118    /// Sets the parent and chunk collection names.
119    pub fn with_collections(mut self, parent: impl Into<String>, chunk: impl Into<String>) -> Self {
120        self.parent_collection = parent.into();
121        self.chunk_collection = chunk.into();
122        self
123    }
124}
125
126/// MongoDB ChunkedDocumentStore implementation
127pub struct MongoChunkedDocumentStore {
128    client: Client,
129    parent_collection: Collection<MongoParentDoc>,
130    chunk_collection: Collection<MongoChunkDoc>,
131}
132
133impl MongoChunkedDocumentStore {
134    /// Connects to MongoDB per the config and creates a store instance.
135    pub async fn new(config: MongoStoreConfig) -> Result<Self, VectorStoreError> {
136        let client_options = ClientOptions::parse(&config.uri)
137            .await
138            .map_err(|e| VectorStoreError::ConnectionError(e.to_string()))?;
139
140        let client = Client::with_options(client_options)
141            .map_err(|e| VectorStoreError::ConnectionError(e.to_string()))?;
142
143        let db = client.database(&config.database);
144        let parent_collection = db.collection(&config.parent_collection);
145        let chunk_collection = db.collection(&config.chunk_collection);
146
147        Ok(Self {
148            client,
149            parent_collection,
150            chunk_collection,
151        })
152    }
153
154    /// Creates a `parent_id` index on the chunk collection to speed up queries.
155    pub async fn create_indexes(&self) -> Result<(), VectorStoreError> {
156        self.chunk_collection
157            .create_index(
158                mongodb::IndexModel::builder()
159                    .keys(doc! { "parent_id": 1 })
160                    .build(),
161                None,
162            )
163            .await
164            .map_err(|e| VectorStoreError::StorageError(e.to_string()))?;
165
166        Ok(())
167    }
168
169    /// Returns a reference to the underlying MongoDB client.
170    pub fn client(&self) -> &Client {
171        &self.client
172    }
173}
174
175#[async_trait]
176impl ChunkedDocumentStoreTrait for MongoChunkedDocumentStore {
177    async fn add_parent_document(
178        &self,
179        document: Document,
180        chunk_size: usize,
181    ) -> Result<(String, Vec<String>), VectorStoreError> {
182        let parent_id = document
183            .id
184            .clone()
185            .unwrap_or_else(|| uuid::Uuid::new_v4().to_string());
186
187        let mongo_parent = MongoParentDoc {
188            id: parent_id.clone(),
189            content: document.content.clone(),
190            metadata: document.metadata.clone(),
191        };
192
193        // M-29: re-ingesting a document with an existing id used to fail with a duplicate-key
194        // error (matching in-memory / redis, ingest should be idempotent and replace). Upsert the
195        // parent and each chunk, then drop any chunks of this parent that are no longer produced
196        // by the new split, so a later/shrunk re-ingest does not leave stale retrievable content.
197        let upsert = mongodb::options::ReplaceOptions::builder()
198            .upsert(true)
199            .build();
200        self.parent_collection
201            .replace_one(doc! { "_id": parent_id.clone() }, mongo_parent, upsert.clone())
202            .await
203            .map_err(|e| VectorStoreError::StorageError(e.to_string()))?;
204
205        let splitter = RecursiveCharacterSplitter::new(chunk_size, chunk_size / 10);
206        let chunks = splitter.split_text(&document.content);
207
208        let mut chunk_ids = Vec::new();
209
210        for (segment, chunk_content) in chunks.into_iter().enumerate() {
211            let chunk_id = format!("{}_{}", parent_id, segment);
212
213            let mongo_chunk = MongoChunkDoc {
214                chunk_id: chunk_id.clone(),
215                parent_id: parent_id.clone(),
216                content: chunk_content,
217                segment: segment as i32,
218                metadata: HashMap::new(),
219            };
220
221            self.chunk_collection
222                .replace_one(doc! { "_id": chunk_id.clone() }, mongo_chunk, upsert.clone())
223                .await
224                .map_err(|e| VectorStoreError::StorageError(e.to_string()))?;
225
226            chunk_ids.push(chunk_id);
227        }
228
229        // Remove chunks belonging to this parent but not present in the fresh split.
230        self.chunk_collection
231            .delete_many(
232                doc! { "parent_id": &parent_id, "chunk_id": doc! { "$nin": &chunk_ids } },
233                None,
234            )
235            .await
236            .map_err(|e| VectorStoreError::StorageError(e.to_string()))?;
237
238        Ok((parent_id, chunk_ids))
239    }
240
241    async fn add_parent_documents(
242        &self,
243        documents: Vec<Document>,
244        chunk_size: usize,
245    ) -> Result<Vec<(String, Vec<String>)>, VectorStoreError> {
246        let mut results = Vec::new();
247        for doc in documents {
248            let result = self.add_parent_document(doc, chunk_size).await?;
249            results.push(result);
250        }
251        Ok(results)
252    }
253
254    async fn get_parent_document(
255        &self,
256        parent_id: &str,
257    ) -> Result<Option<Document>, VectorStoreError> {
258        let result = self
259            .parent_collection
260            .find_one(doc! { "_id": parent_id }, None)
261            .await
262            .map_err(|e| VectorStoreError::StorageError(e.to_string()))?;
263
264        Ok(result.map(|m| m.into()))
265    }
266
267    async fn get_chunk(&self, chunk_id: &str) -> Result<Option<ChunkDocument>, VectorStoreError> {
268        let result = self
269            .chunk_collection
270            .find_one(doc! { "_id": chunk_id }, None)
271            .await
272            .map_err(|e| VectorStoreError::StorageError(e.to_string()))?;
273
274        Ok(result.map(|m| m.into()))
275    }
276
277    async fn get_chunk_document(
278        &self,
279        chunk_id: &str,
280    ) -> Result<Option<Document>, VectorStoreError> {
281        let chunk = self.get_chunk(chunk_id).await?;
282        Ok(chunk.map(|c| c.to_document()))
283    }
284
285    async fn get_chunks_for_parent(
286        &self,
287        parent_id: &str,
288    ) -> Result<Vec<ChunkDocument>, VectorStoreError> {
289        let options = mongodb::options::FindOptions::builder()
290            .sort(doc! { "segment": 1 })
291            .build();
292
293        let mut cursor = self
294            .chunk_collection
295            .find(doc! { "parent_id": parent_id }, options)
296            .await
297            .map_err(|e| VectorStoreError::StorageError(e.to_string()))?;
298
299        let mut chunks = Vec::new();
300        while cursor
301            .advance()
302            .await
303            .map_err(|e| VectorStoreError::StorageError(e.to_string()))?
304        {
305            let doc = cursor
306                .deserialize_current()
307                .map_err(|e| VectorStoreError::StorageError(e.to_string()))?;
308            chunks.push(doc.into());
309        }
310
311        Ok(chunks)
312    }
313
314    async fn get_chunk_documents_for_parent(
315        &self,
316        parent_id: &str,
317    ) -> Result<Vec<Document>, VectorStoreError> {
318        let chunks = self.get_chunks_for_parent(parent_id).await?;
319        Ok(chunks.into_iter().map(|c| c.to_document()).collect())
320    }
321
322    async fn delete_parent_document(&self, parent_id: &str) -> Result<(), VectorStoreError> {
323        self.chunk_collection
324            .delete_many(doc! { "parent_id": parent_id }, None)
325            .await
326            .map_err(|e| VectorStoreError::StorageError(e.to_string()))?;
327
328        self.parent_collection
329            .delete_one(doc! { "_id": parent_id }, None)
330            .await
331            .map_err(|e| VectorStoreError::StorageError(e.to_string()))?;
332
333        Ok(())
334    }
335
336    async fn parent_count(&self) -> usize {
337        self.parent_collection
338            .count_documents(doc! {}, None)
339            .await
340            .unwrap_or(0) as usize
341    }
342
343    async fn chunk_count(&self) -> usize {
344        self.chunk_collection
345            .count_documents(doc! {}, None)
346            .await
347            .unwrap_or(0) as usize
348    }
349
350    async fn get_all_chunks(&self) -> Result<Vec<ChunkDocument>, VectorStoreError> {
351        let mut cursor = self
352            .chunk_collection
353            .find(doc! {}, None)
354            .await
355            .map_err(|e| VectorStoreError::StorageError(e.to_string()))?;
356
357        let mut chunks = Vec::new();
358        while cursor
359            .advance()
360            .await
361            .map_err(|e| VectorStoreError::StorageError(e.to_string()))?
362        {
363            let doc = cursor
364                .deserialize_current()
365                .map_err(|e| VectorStoreError::StorageError(e.to_string()))?;
366            chunks.push(doc.into());
367        }
368
369        Ok(chunks)
370    }
371
372    async fn clear(&self) -> Result<(), VectorStoreError> {
373        self.parent_collection
374            .delete_many(doc! {}, None)
375            .await
376            .map_err(|e| VectorStoreError::StorageError(e.to_string()))?;
377
378        self.chunk_collection
379            .delete_many(doc! {}, None)
380            .await
381            .map_err(|e| VectorStoreError::StorageError(e.to_string()))?;
382
383        Ok(())
384    }
385
386    fn add_parent_document_blocking(
387        &self,
388        document: Document,
389        chunk_size: usize,
390    ) -> Result<(String, Vec<String>), VectorStoreError> {
391        tokio::task::block_in_place(|| {
392            tokio::runtime::Handle::current()
393                .block_on(self.add_parent_document(document, chunk_size))
394        })
395    }
396
397    fn get_parent_document_blocking(
398        &self,
399        parent_id: &str,
400    ) -> Result<Option<Document>, VectorStoreError> {
401        tokio::task::block_in_place(|| {
402            tokio::runtime::Handle::current().block_on(self.get_parent_document(parent_id))
403        })
404    }
405
406    fn get_chunk_blocking(
407        &self,
408        chunk_id: &str,
409    ) -> Result<Option<ChunkDocument>, VectorStoreError> {
410        tokio::task::block_in_place(|| {
411            tokio::runtime::Handle::current().block_on(self.get_chunk(chunk_id))
412        })
413    }
414
415    fn blocking_get_chunks_for_parent(
416        &self,
417        parent_id: &str,
418    ) -> Result<Vec<ChunkDocument>, VectorStoreError> {
419        tokio::task::block_in_place(|| {
420            tokio::runtime::Handle::current().block_on(self.get_chunks_for_parent(parent_id))
421        })
422    }
423}
424
425#[cfg(test)]
426mod tests {
427    use super::*;
428
429    #[test]
430    fn test_config_creation() {
431        let config = MongoStoreConfig::new("mongodb://localhost:27017", "test_db");
432        assert_eq!(config.uri, "mongodb://localhost:27017");
433        assert_eq!(config.database, "test_db");
434    }
435
436    #[test]
437    fn test_mongo_parent_doc_conversion() {
438        let doc = Document::new("test content").with_id("test_id");
439        let mongo: MongoParentDoc = doc.clone().into();
440        assert_eq!(mongo.id, "test_id");
441        assert_eq!(mongo.content, "test content");
442
443        let back: Document = mongo.into();
444        assert_eq!(back.content, "test content");
445    }
446
447    #[test]
448    fn test_mongo_chunk_doc_conversion() {
449        let chunk = ChunkDocument::new(
450            "chunk_0".to_string(),
451            "parent_1".to_string(),
452            "content".to_string(),
453            0,
454        );
455        let mongo: MongoChunkDoc = chunk.clone().into();
456        assert_eq!(mongo.chunk_id, "chunk_0");
457        assert_eq!(mongo.parent_id, "parent_1");
458
459        let back: ChunkDocument = mongo.into();
460        assert_eq!(back.chunk_id, "chunk_0");
461    }
462}