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, 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 存储配置
84#[derive(Debug, Clone)]
85pub struct MongoStoreConfig {
86    /// MongoDB 连接地址
87    pub uri: String,
88    /// 数据库名称
89    pub database: String,
90    /// 父文档集合名称
91    pub parent_collection: String,
92    /// 分块集合名称
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    /// 使用连接地址和数据库名创建配置,集合名使用默认值。
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    /// 设置父文档集合和分块集合的名称。
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 实现
127pub struct MongoChunkedDocumentStore {
128    client: Client,
129    parent_collection: Collection<MongoParentDoc>,
130    chunk_collection: Collection<MongoChunkDoc>,
131}
132
133impl MongoChunkedDocumentStore {
134    /// 根据配置连接 MongoDB 并创建存储实例。
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    /// 在分块集合上创建 `parent_id` 索引以加速查询。
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    /// 返回底层 MongoDB 客户端引用。
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        self.parent_collection
194            .insert_one(mongo_parent, None)
195            .await
196            .map_err(|e| VectorStoreError::StorageError(e.to_string()))?;
197
198        let splitter = RecursiveCharacterSplitter::new(chunk_size, chunk_size / 10);
199        let chunks = splitter.split_text(&document.content);
200
201        let mut chunk_ids = Vec::new();
202
203        for (segment, chunk_content) in chunks.into_iter().enumerate() {
204            let chunk_id = format!("{}_{}", parent_id, segment);
205
206            let mongo_chunk = MongoChunkDoc {
207                chunk_id: chunk_id.clone(),
208                parent_id: parent_id.clone(),
209                content: chunk_content,
210                segment: segment as i32,
211                metadata: HashMap::new(),
212            };
213
214            self.chunk_collection
215                .insert_one(mongo_chunk, None)
216                .await
217                .map_err(|e| VectorStoreError::StorageError(e.to_string()))?;
218
219            chunk_ids.push(chunk_id);
220        }
221
222        Ok((parent_id, chunk_ids))
223    }
224
225    async fn add_parent_documents(
226        &self,
227        documents: Vec<Document>,
228        chunk_size: usize,
229    ) -> Result<Vec<(String, Vec<String>)>, VectorStoreError> {
230        let mut results = Vec::new();
231        for doc in documents {
232            let result = self.add_parent_document(doc, chunk_size).await?;
233            results.push(result);
234        }
235        Ok(results)
236    }
237
238    async fn get_parent_document(
239        &self,
240        parent_id: &str,
241    ) -> Result<Option<Document>, VectorStoreError> {
242        let result = self
243            .parent_collection
244            .find_one(doc! { "_id": parent_id }, None)
245            .await
246            .map_err(|e| VectorStoreError::StorageError(e.to_string()))?;
247
248        Ok(result.map(|m| m.into()))
249    }
250
251    async fn get_chunk(&self, chunk_id: &str) -> Result<Option<ChunkDocument>, VectorStoreError> {
252        let result = self
253            .chunk_collection
254            .find_one(doc! { "_id": chunk_id }, None)
255            .await
256            .map_err(|e| VectorStoreError::StorageError(e.to_string()))?;
257
258        Ok(result.map(|m| m.into()))
259    }
260
261    async fn get_chunk_document(
262        &self,
263        chunk_id: &str,
264    ) -> Result<Option<Document>, VectorStoreError> {
265        let chunk = self.get_chunk(chunk_id).await?;
266        Ok(chunk.map(|c| c.to_document()))
267    }
268
269    async fn get_chunks_for_parent(
270        &self,
271        parent_id: &str,
272    ) -> Result<Vec<ChunkDocument>, VectorStoreError> {
273        let options = mongodb::options::FindOptions::builder()
274            .sort(doc! { "segment": 1 })
275            .build();
276
277        let mut cursor = self
278            .chunk_collection
279            .find(doc! { "parent_id": parent_id }, options)
280            .await
281            .map_err(|e| VectorStoreError::StorageError(e.to_string()))?;
282
283        let mut chunks = Vec::new();
284        while cursor
285            .advance()
286            .await
287            .map_err(|e| VectorStoreError::StorageError(e.to_string()))?
288        {
289            let doc = cursor
290                .deserialize_current()
291                .map_err(|e| VectorStoreError::StorageError(e.to_string()))?;
292            chunks.push(doc.into());
293        }
294
295        Ok(chunks)
296    }
297
298    async fn get_chunk_documents_for_parent(
299        &self,
300        parent_id: &str,
301    ) -> Result<Vec<Document>, VectorStoreError> {
302        let chunks = self.get_chunks_for_parent(parent_id).await?;
303        Ok(chunks.into_iter().map(|c| c.to_document()).collect())
304    }
305
306    async fn delete_parent_document(&self, parent_id: &str) -> Result<(), VectorStoreError> {
307        self.chunk_collection
308            .delete_many(doc! { "parent_id": parent_id }, None)
309            .await
310            .map_err(|e| VectorStoreError::StorageError(e.to_string()))?;
311
312        self.parent_collection
313            .delete_one(doc! { "_id": parent_id }, None)
314            .await
315            .map_err(|e| VectorStoreError::StorageError(e.to_string()))?;
316
317        Ok(())
318    }
319
320    async fn parent_count(&self) -> usize {
321        self.parent_collection
322            .count_documents(doc! {}, None)
323            .await
324            .unwrap_or(0) as usize
325    }
326
327    async fn chunk_count(&self) -> usize {
328        self.chunk_collection
329            .count_documents(doc! {}, None)
330            .await
331            .unwrap_or(0) as usize
332    }
333
334    async fn get_all_chunks(&self) -> Result<Vec<ChunkDocument>, VectorStoreError> {
335        let mut cursor = self
336            .chunk_collection
337            .find(doc! {}, None)
338            .await
339            .map_err(|e| VectorStoreError::StorageError(e.to_string()))?;
340
341        let mut chunks = Vec::new();
342        while cursor
343            .advance()
344            .await
345            .map_err(|e| VectorStoreError::StorageError(e.to_string()))?
346        {
347            let doc = cursor
348                .deserialize_current()
349                .map_err(|e| VectorStoreError::StorageError(e.to_string()))?;
350            chunks.push(doc.into());
351        }
352
353        Ok(chunks)
354    }
355
356    async fn clear(&self) -> Result<(), VectorStoreError> {
357        self.parent_collection
358            .delete_many(doc! {}, None)
359            .await
360            .map_err(|e| VectorStoreError::StorageError(e.to_string()))?;
361
362        self.chunk_collection
363            .delete_many(doc! {}, None)
364            .await
365            .map_err(|e| VectorStoreError::StorageError(e.to_string()))?;
366
367        Ok(())
368    }
369
370    fn add_parent_document_blocking(
371        &self,
372        document: Document,
373        chunk_size: usize,
374    ) -> Result<(String, Vec<String>), VectorStoreError> {
375        tokio::task::block_in_place(|| {
376            tokio::runtime::Handle::current()
377                .block_on(self.add_parent_document(document, chunk_size))
378        })
379    }
380
381    fn get_parent_document_blocking(
382        &self,
383        parent_id: &str,
384    ) -> Result<Option<Document>, VectorStoreError> {
385        tokio::task::block_in_place(|| {
386            tokio::runtime::Handle::current().block_on(self.get_parent_document(parent_id))
387        })
388    }
389
390    fn get_chunk_blocking(
391        &self,
392        chunk_id: &str,
393    ) -> Result<Option<ChunkDocument>, VectorStoreError> {
394        tokio::task::block_in_place(|| {
395            tokio::runtime::Handle::current().block_on(self.get_chunk(chunk_id))
396        })
397    }
398
399    fn blocking_get_chunks_for_parent(
400        &self,
401        parent_id: &str,
402    ) -> Result<Vec<ChunkDocument>, VectorStoreError> {
403        tokio::task::block_in_place(|| {
404            tokio::runtime::Handle::current().block_on(self.get_chunks_for_parent(parent_id))
405        })
406    }
407}
408
409#[cfg(test)]
410mod tests {
411    use super::*;
412
413    #[test]
414    fn test_config_creation() {
415        let config = MongoStoreConfig::new("mongodb://localhost:27017", "test_db");
416        assert_eq!(config.uri, "mongodb://localhost:27017");
417        assert_eq!(config.database, "test_db");
418    }
419
420    #[test]
421    fn test_mongo_parent_doc_conversion() {
422        let doc = Document::new("test content").with_id("test_id");
423        let mongo: MongoParentDoc = doc.clone().into();
424        assert_eq!(mongo.id, "test_id");
425        assert_eq!(mongo.content, "test content");
426
427        let back: Document = mongo.into();
428        assert_eq!(back.content, "test content");
429    }
430
431    #[test]
432    fn test_mongo_chunk_doc_conversion() {
433        let chunk = ChunkDocument::new(
434            "chunk_0".to_string(),
435            "parent_1".to_string(),
436            "content".to_string(),
437            0,
438        );
439        let mongo: MongoChunkDoc = chunk.clone().into();
440        assert_eq!(mongo.chunk_id, "chunk_0");
441        assert_eq!(mongo.parent_id, "parent_1");
442
443        let back: ChunkDocument = mongo.into();
444        assert_eq!(back.chunk_id, "chunk_0");
445    }
446}