Skip to main content

lc_vector_stores/
mongo_document_store.rs

1// lc-vector-stores/src/mongo_document_store.rs
2//! MongoDB 文档存储实现
3//!
4//! 生产环境推荐使用 MongoDB 作为 ChunkedDocumentStore 后端:
5//! - 支持持久化
6//! - 支持分片和复制集
7//! - 文档结构天然匹配
8//! - 支持索引查询
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, String>,
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, String>,
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 存储配置
84#[derive(Debug, Clone)]
85pub struct MongoStoreConfig {
86    pub uri: String,
87    pub database: String,
88    pub parent_collection: String,
89    pub chunk_collection: String,
90}
91
92impl Default for MongoStoreConfig {
93    fn default() -> Self {
94        Self {
95            uri: "mongodb://localhost:27017".to_string(),
96            database: "langchainrust".to_string(),
97            parent_collection: "parent_docs".to_string(),
98            chunk_collection: "chunks".to_string(),
99        }
100    }
101}
102
103impl MongoStoreConfig {
104    pub fn new(uri: impl Into<String>, database: impl Into<String>) -> Self {
105        Self {
106            uri: uri.into(),
107            database: database.into(),
108            parent_collection: "parent_docs".to_string(),
109            chunk_collection: "chunks".to_string(),
110        }
111    }
112
113    pub fn with_collections(mut self, parent: impl Into<String>, chunk: impl Into<String>) -> Self {
114        self.parent_collection = parent.into();
115        self.chunk_collection = chunk.into();
116        self
117    }
118}
119
120/// MongoDB ChunkedDocumentStore 实现
121pub struct MongoChunkedDocumentStore {
122    client: Client,
123    parent_collection: Collection<MongoParentDoc>,
124    chunk_collection: Collection<MongoChunkDoc>,
125}
126
127impl MongoChunkedDocumentStore {
128    pub async fn new(config: MongoStoreConfig) -> Result<Self, VectorStoreError> {
129        let client_options = ClientOptions::parse(&config.uri)
130            .await
131            .map_err(|e| VectorStoreError::ConnectionError(e.to_string()))?;
132
133        let client = Client::with_options(client_options)
134            .map_err(|e| VectorStoreError::ConnectionError(e.to_string()))?;
135
136        let db = client.database(&config.database);
137        let parent_collection = db.collection(&config.parent_collection);
138        let chunk_collection = db.collection(&config.chunk_collection);
139
140        Ok(Self {
141            client,
142            parent_collection,
143            chunk_collection,
144        })
145    }
146
147    pub async fn create_indexes(&self) -> Result<(), VectorStoreError> {
148        self.chunk_collection
149            .create_index(
150                mongodb::IndexModel::builder()
151                    .keys(doc! { "parent_id": 1 })
152                    .build(),
153                None,
154            )
155            .await
156            .map_err(|e| VectorStoreError::StorageError(e.to_string()))?;
157
158        Ok(())
159    }
160
161    pub fn client(&self) -> &Client {
162        &self.client
163    }
164}
165
166#[async_trait]
167impl ChunkedDocumentStoreTrait for MongoChunkedDocumentStore {
168    async fn add_parent_document(
169        &self,
170        document: Document,
171        chunk_size: usize,
172    ) -> Result<(String, Vec<String>), VectorStoreError> {
173        let parent_id = document
174            .id
175            .clone()
176            .unwrap_or_else(|| uuid::Uuid::new_v4().to_string());
177
178        let mongo_parent = MongoParentDoc {
179            id: parent_id.clone(),
180            content: document.content.clone(),
181            metadata: document.metadata.clone(),
182        };
183
184        self.parent_collection
185            .insert_one(mongo_parent, None)
186            .await
187            .map_err(|e| VectorStoreError::StorageError(e.to_string()))?;
188
189        let splitter = RecursiveCharacterSplitter::new(chunk_size, chunk_size / 10);
190        let chunks = splitter.split_text(&document.content);
191
192        let mut chunk_ids = Vec::new();
193
194        for (segment, chunk_content) in chunks.into_iter().enumerate() {
195            let chunk_id = format!("{}_{}", parent_id, segment);
196
197            let mongo_chunk = MongoChunkDoc {
198                chunk_id: chunk_id.clone(),
199                parent_id: parent_id.clone(),
200                content: chunk_content,
201                segment: segment as i32,
202                metadata: HashMap::new(),
203            };
204
205            self.chunk_collection
206                .insert_one(mongo_chunk, None)
207                .await
208                .map_err(|e| VectorStoreError::StorageError(e.to_string()))?;
209
210            chunk_ids.push(chunk_id);
211        }
212
213        Ok((parent_id, chunk_ids))
214    }
215
216    async fn add_parent_documents(
217        &self,
218        documents: Vec<Document>,
219        chunk_size: usize,
220    ) -> Result<Vec<(String, Vec<String>)>, VectorStoreError> {
221        let mut results = Vec::new();
222        for doc in documents {
223            let result = self.add_parent_document(doc, chunk_size).await?;
224            results.push(result);
225        }
226        Ok(results)
227    }
228
229    async fn get_parent_document(
230        &self,
231        parent_id: &str,
232    ) -> Result<Option<Document>, VectorStoreError> {
233        let result = self
234            .parent_collection
235            .find_one(doc! { "_id": parent_id }, None)
236            .await
237            .map_err(|e| VectorStoreError::StorageError(e.to_string()))?;
238
239        Ok(result.map(|m| m.into()))
240    }
241
242    async fn get_chunk(&self, chunk_id: &str) -> Result<Option<ChunkDocument>, VectorStoreError> {
243        let result = self
244            .chunk_collection
245            .find_one(doc! { "_id": chunk_id }, None)
246            .await
247            .map_err(|e| VectorStoreError::StorageError(e.to_string()))?;
248
249        Ok(result.map(|m| m.into()))
250    }
251
252    async fn get_chunk_document(
253        &self,
254        chunk_id: &str,
255    ) -> Result<Option<Document>, VectorStoreError> {
256        let chunk = self.get_chunk(chunk_id).await?;
257        Ok(chunk.map(|c| c.to_document()))
258    }
259
260    async fn get_chunks_for_parent(
261        &self,
262        parent_id: &str,
263    ) -> Result<Vec<ChunkDocument>, VectorStoreError> {
264        let options = mongodb::options::FindOptions::builder()
265            .sort(doc! { "segment": 1 })
266            .build();
267
268        let mut cursor = self
269            .chunk_collection
270            .find(doc! { "parent_id": parent_id }, options)
271            .await
272            .map_err(|e| VectorStoreError::StorageError(e.to_string()))?;
273
274        let mut chunks = Vec::new();
275        while cursor
276            .advance()
277            .await
278            .map_err(|e| VectorStoreError::StorageError(e.to_string()))?
279        {
280            let doc = cursor
281                .deserialize_current()
282                .map_err(|e| VectorStoreError::StorageError(e.to_string()))?;
283            chunks.push(doc.into());
284        }
285
286        Ok(chunks)
287    }
288
289    async fn get_chunk_documents_for_parent(
290        &self,
291        parent_id: &str,
292    ) -> Result<Vec<Document>, VectorStoreError> {
293        let chunks = self.get_chunks_for_parent(parent_id).await?;
294        Ok(chunks.into_iter().map(|c| c.to_document()).collect())
295    }
296
297    async fn delete_parent_document(&self, parent_id: &str) -> Result<(), VectorStoreError> {
298        self.chunk_collection
299            .delete_many(doc! { "parent_id": parent_id }, None)
300            .await
301            .map_err(|e| VectorStoreError::StorageError(e.to_string()))?;
302
303        self.parent_collection
304            .delete_one(doc! { "_id": parent_id }, None)
305            .await
306            .map_err(|e| VectorStoreError::StorageError(e.to_string()))?;
307
308        Ok(())
309    }
310
311    async fn parent_count(&self) -> usize {
312        self.parent_collection
313            .count_documents(doc! {}, None)
314            .await
315            .unwrap_or(0) as usize
316    }
317
318    async fn chunk_count(&self) -> usize {
319        self.chunk_collection
320            .count_documents(doc! {}, None)
321            .await
322            .unwrap_or(0) as usize
323    }
324
325    async fn get_all_chunks(&self) -> Result<Vec<ChunkDocument>, VectorStoreError> {
326        let mut cursor = self
327            .chunk_collection
328            .find(doc! {}, None)
329            .await
330            .map_err(|e| VectorStoreError::StorageError(e.to_string()))?;
331
332        let mut chunks = Vec::new();
333        while cursor
334            .advance()
335            .await
336            .map_err(|e| VectorStoreError::StorageError(e.to_string()))?
337        {
338            let doc = cursor
339                .deserialize_current()
340                .map_err(|e| VectorStoreError::StorageError(e.to_string()))?;
341            chunks.push(doc.into());
342        }
343
344        Ok(chunks)
345    }
346
347    async fn clear(&self) -> Result<(), VectorStoreError> {
348        self.parent_collection
349            .delete_many(doc! {}, None)
350            .await
351            .map_err(|e| VectorStoreError::StorageError(e.to_string()))?;
352
353        self.chunk_collection
354            .delete_many(doc! {}, None)
355            .await
356            .map_err(|e| VectorStoreError::StorageError(e.to_string()))?;
357
358        Ok(())
359    }
360
361    fn add_parent_document_blocking(
362        &self,
363        document: Document,
364        chunk_size: usize,
365    ) -> Result<(String, Vec<String>), VectorStoreError> {
366        tokio::task::block_in_place(|| {
367            tokio::runtime::Handle::current()
368                .block_on(self.add_parent_document(document, chunk_size))
369        })
370    }
371
372    fn get_parent_document_blocking(
373        &self,
374        parent_id: &str,
375    ) -> Result<Option<Document>, VectorStoreError> {
376        tokio::task::block_in_place(|| {
377            tokio::runtime::Handle::current().block_on(self.get_parent_document(parent_id))
378        })
379    }
380
381    fn get_chunk_blocking(
382        &self,
383        chunk_id: &str,
384    ) -> Result<Option<ChunkDocument>, VectorStoreError> {
385        tokio::task::block_in_place(|| {
386            tokio::runtime::Handle::current().block_on(self.get_chunk(chunk_id))
387        })
388    }
389
390    fn blocking_get_chunks_for_parent(
391        &self,
392        parent_id: &str,
393    ) -> Result<Vec<ChunkDocument>, VectorStoreError> {
394        tokio::task::block_in_place(|| {
395            tokio::runtime::Handle::current().block_on(self.get_chunks_for_parent(parent_id))
396        })
397    }
398}
399
400#[cfg(test)]
401mod tests {
402    use super::*;
403
404    #[test]
405    fn test_config_creation() {
406        let config = MongoStoreConfig::new("mongodb://localhost:27017", "test_db");
407        assert_eq!(config.uri, "mongodb://localhost:27017");
408        assert_eq!(config.database, "test_db");
409    }
410
411    #[test]
412    fn test_mongo_parent_doc_conversion() {
413        let doc = Document::new("test content").with_id("test_id");
414        let mongo: MongoParentDoc = doc.clone().into();
415        assert_eq!(mongo.id, "test_id");
416        assert_eq!(mongo.content, "test content");
417
418        let back: Document = mongo.into();
419        assert_eq!(back.content, "test content");
420    }
421
422    #[test]
423    fn test_mongo_chunk_doc_conversion() {
424        let chunk = ChunkDocument::new(
425            "chunk_0".to_string(),
426            "parent_1".to_string(),
427            "content".to_string(),
428            0,
429        );
430        let mongo: MongoChunkDoc = chunk.clone().into();
431        assert_eq!(mongo.chunk_id, "chunk_0");
432        assert_eq!(mongo.parent_id, "parent_1");
433
434        let back: ChunkDocument = mongo.into();
435        assert_eq!(back.chunk_id, "chunk_0");
436    }
437}