Skip to main content

lc_vector_stores/
chromadb.rs

1// lc-vector-stores/src/chromadb.rs
2//! ChromaDB 向量存储实现(HTTP API)
3//!
4//! 使用 ChromaDB 的 REST API 进行向量存储和检索。
5//! 支持连接远程 ChromaDB 服务(docker run -p 8000:8000 chromadb/chroma)。
6
7use async_trait::async_trait;
8use serde::{Deserialize, Serialize};
9use serde_json::json;
10use std::collections::HashMap;
11
12use crate::{Document, SearchResult, VectorStore, VectorStoreError};
13
14/// ChromaDB 配置
15#[derive(Debug, Clone)]
16pub struct ChromaDBConfig {
17    /// ChromaDB 服务地址,默认为 http://localhost:8000
18    pub host: String,
19    /// 集合名称
20    pub collection_name: String,
21    /// 向量维度
22    pub vector_size: usize,
23    /// 集合元数据(可选)
24    pub metadata: Option<HashMap<String, String>>,
25}
26
27impl Default for ChromaDBConfig {
28    fn default() -> Self {
29        Self {
30            host: "http://localhost:8000".to_string(),
31            collection_name: "langchainrust".to_string(),
32            vector_size: 1536,
33            metadata: None,
34        }
35    }
36}
37
38impl ChromaDBConfig {
39    /// 创建新的 ChromaDB 配置
40    pub fn new(
41        host: impl Into<String>,
42        collection_name: impl Into<String>,
43        vector_size: usize,
44    ) -> Self {
45        Self {
46            host: host.into(),
47            collection_name: collection_name.into(),
48            vector_size,
49            metadata: None,
50        }
51    }
52}
53
54/// ChromaDB 集合信息(从 API 返回解析)
55#[derive(Debug, Deserialize)]
56#[allow(dead_code)]
57struct ChromaCollection {
58    id: String,
59    name: String,
60    #[serde(default)]
61    metadata: Option<serde_json::Value>,
62}
63
64/// ChromaDB add 请求体
65#[derive(Debug, Serialize)]
66struct ChromaAddRequest {
67    ids: Vec<String>,
68    embeddings: Vec<Vec<f32>>,
69    documents: Vec<String>,
70    #[serde(skip_serializing_if = "Option::is_none")]
71    metadatas: Option<Vec<HashMap<String, serde_json::Value>>>,
72}
73
74/// ChromaDB query 请求体
75#[derive(Debug, Serialize)]
76struct ChromaQueryRequest {
77    query_embeddings: Vec<Vec<f32>>,
78    n_results: usize,
79    #[serde(skip_serializing_if = "Option::is_none")]
80    include: Option<Vec<String>>,
81}
82
83/// ChromaDB query 响应
84#[derive(Debug, Deserialize)]
85struct ChromaQueryResponse {
86    ids: Vec<Vec<String>>,
87    distances: Vec<Vec<f64>>,
88    documents: Vec<Vec<String>>,
89    #[serde(default)]
90    metadatas: Vec<Vec<Option<HashMap<String, serde_json::Value>>>>,
91}
92
93/// ChromaDB get 响应
94#[derive(Debug, Deserialize)]
95struct ChromaGetResponse {
96    ids: Vec<String>,
97    documents: Vec<Option<String>>,
98    #[serde(default)]
99    metadatas: Vec<Option<HashMap<String, serde_json::Value>>>,
100    embeddings: Option<Vec<Vec<f32>>>,
101}
102
103/// ChromaDB 向量存储
104///
105/// 通过 HTTP API 连接 ChromaDB 服务。
106///
107/// # 示例
108/// ```ignore
109/// use lc_vector_stores::ChromaDBVectorStore;
110///
111/// let store = ChromaDBVectorStore::new(
112///     ChromaDBConfig::new("http://localhost:8000", "my_collection", 384)
113/// ).await?;
114/// ```
115pub struct ChromaDBVectorStore {
116    config: ChromaDBConfig,
117    client: reqwest::Client,
118    collection_id: Option<String>,
119}
120
121impl ChromaDBVectorStore {
122    /// 创建 ChromaDB 向量存储并自动初始化集合
123    pub async fn new(config: ChromaDBConfig) -> Result<Self, VectorStoreError> {
124        let client = reqwest::Client::new();
125        let mut store = Self {
126            config,
127            client,
128            collection_id: None,
129        };
130        store.init_collection().await?;
131        Ok(store)
132    }
133
134    /// 初始化或获取集合
135    async fn init_collection(&mut self) -> Result<(), VectorStoreError> {
136        // 尝试获取已有集合
137        let url = format!(
138            "{}/api/v1/collections/{}",
139            self.config.host, self.config.collection_name
140        );
141        let response = self
142            .client
143            .get(&url)
144            .send()
145            .await
146            .map_err(|e| VectorStoreError::ConnectionError(e.to_string()))?;
147
148        if response.status().is_success() {
149            let collection: ChromaCollection = response.json().await.map_err(|e| {
150                VectorStoreError::StorageError(format!("failed to parse collection info: {}", e))
151            })?;
152            self.collection_id = Some(collection.id);
153            return Ok(());
154        }
155
156        // 集合不存在,创建新集合
157        let create_url = format!("{}/api/v1/collections", self.config.host);
158        let mut body = json!({
159            "name": self.config.collection_name,
160        });
161
162        if let Some(ref meta) = self.config.metadata {
163            body["metadata"] = serde_json::to_value(meta).unwrap_or(json!({}));
164        }
165
166        let response = self
167            .client
168            .post(&create_url)
169            .json(&body)
170            .send()
171            .await
172            .map_err(|e| VectorStoreError::ConnectionError(e.to_string()))?;
173
174        if response.status().is_success() {
175            let collection: ChromaCollection = response.json().await.map_err(|e| {
176                VectorStoreError::StorageError(format!(
177                    "failed to parse new collection info: {}",
178                    e
179                ))
180            })?;
181            self.collection_id = Some(collection.id);
182            Ok(())
183        } else {
184            let text = response.text().await.unwrap_or_default();
185            Err(VectorStoreError::StorageError(format!(
186                "failed to create collection: {}",
187                text
188            )))
189        }
190    }
191
192    /// 获取集合 ID
193    fn get_collection_id(&self) -> Result<&str, VectorStoreError> {
194        self.collection_id.as_deref().ok_or_else(|| {
195            VectorStoreError::StorageError("collection is not initialized".to_string())
196        })
197    }
198
199    /// 构建集合 API 基础 URL
200    fn collection_url(&self, endpoint: &str) -> Result<String, VectorStoreError> {
201        let cid = self.get_collection_id()?;
202        Ok(format!(
203            "{}/api/v1/collections/{}/{}",
204            self.config.host, cid, endpoint
205        ))
206    }
207}
208
209#[async_trait]
210impl VectorStore for ChromaDBVectorStore {
211    async fn add_documents(
212        &self,
213        documents: Vec<Document>,
214        embeddings: Vec<Vec<f32>>,
215    ) -> Result<Vec<String>, VectorStoreError> {
216        if documents.is_empty() {
217            return Ok(Vec::new());
218        }
219
220        let count = documents.len();
221        let ids: Vec<String> = (0..count)
222            .map(|i| {
223                documents[i]
224                    .id
225                    .clone()
226                    .unwrap_or_else(|| uuid::Uuid::new_v4().to_string())
227            })
228            .collect();
229
230        let contents: Vec<String> = documents.iter().map(|d| d.content.clone()).collect();
231        let metadatas: Vec<HashMap<String, serde_json::Value>> =
232            documents.iter().map(|d| d.metadata.clone()).collect();
233        let has_metadata = metadatas.iter().any(|m| !m.is_empty());
234
235        let request = ChromaAddRequest {
236            ids: ids.clone(),
237            embeddings,
238            documents: contents,
239            metadatas: if has_metadata { Some(metadatas) } else { None },
240        };
241
242        let url = self.collection_url("add")?;
243        let response = self
244            .client
245            .post(&url)
246            .json(&request)
247            .send()
248            .await
249            .map_err(|e| VectorStoreError::ConnectionError(e.to_string()))?;
250
251        if !response.status().is_success() {
252            let text = response.text().await.unwrap_or_default();
253            return Err(VectorStoreError::StorageError(format!(
254                "failed to add documents: {}",
255                text
256            )));
257        }
258
259        Ok(ids)
260    }
261
262    async fn similarity_search(
263        &self,
264        query_embedding: &[f32],
265        k: usize,
266    ) -> Result<Vec<SearchResult>, VectorStoreError> {
267        let request = ChromaQueryRequest {
268            query_embeddings: vec![query_embedding.to_vec()],
269            n_results: k,
270            include: Some(vec![
271                "documents".to_string(),
272                "distances".to_string(),
273                "metadatas".to_string(),
274            ]),
275        };
276
277        let url = self.collection_url("query")?;
278        let response = self
279            .client
280            .post(&url)
281            .json(&request)
282            .send()
283            .await
284            .map_err(|e| VectorStoreError::ConnectionError(e.to_string()))?;
285
286        if !response.status().is_success() {
287            let text = response.text().await.unwrap_or_default();
288            return Err(VectorStoreError::StorageError(format!(
289                "query failed: {}",
290                text
291            )));
292        }
293
294        let query_result: ChromaQueryResponse = response.json().await.map_err(|e| {
295            VectorStoreError::StorageError(format!("failed to parse query results: {}", e))
296        })?;
297
298        let mut results = Vec::new();
299
300        // ChromaDB 返回嵌套数组(每个 query 一个结果集)
301        if let Some(doc_list) = query_result.documents.into_iter().next() {
302            let dist_list = query_result
303                .distances
304                .into_iter()
305                .next()
306                .unwrap_or_default();
307            let meta_list = query_result
308                .metadatas
309                .into_iter()
310                .next()
311                .unwrap_or_default();
312            let id_list = query_result.ids.into_iter().next().unwrap_or_default();
313
314            for (i, content) in doc_list.into_iter().enumerate() {
315                let score = dist_list.get(i).copied().unwrap_or(0.0);
316                // ChromaDB 返回的是 L2 距离,转换为相似度分数(1 / (1 + dist))
317                let similarity = 1.0 / (1.0 + score);
318                let metadata = meta_list
319                    .get(i)
320                    .unwrap_or(&None)
321                    .clone()
322                    .unwrap_or_default();
323                let doc_id = id_list.get(i).cloned();
324
325                results.push(SearchResult {
326                    document: Document {
327                        content,
328                        metadata,
329                        id: doc_id,
330                    },
331                    score: similarity as f32,
332                });
333            }
334        }
335
336        // 按相似度降序排序
337        results.sort_by(|a, b| {
338            b.score
339                .partial_cmp(&a.score)
340                .unwrap_or(std::cmp::Ordering::Equal)
341        });
342        Ok(results)
343    }
344
345    async fn get_document(&self, id: &str) -> Result<Option<Document>, VectorStoreError> {
346        let url = self.collection_url("get")?;
347        let body = json!({
348            "ids": [id],
349            "include": ["documents", "metadatas"]
350        });
351
352        let response = self
353            .client
354            .post(&url)
355            .json(&body)
356            .send()
357            .await
358            .map_err(|e| VectorStoreError::ConnectionError(e.to_string()))?;
359
360        if !response.status().is_success() {
361            return Ok(None);
362        }
363
364        let get_result: ChromaGetResponse = response.json().await.map_err(|e| {
365            VectorStoreError::StorageError(format!("failed to parse document: {}", e))
366        })?;
367
368        if get_result.ids.is_empty() {
369            return Ok(None);
370        }
371
372        let content = get_result
373            .documents
374            .into_iter()
375            .next()
376            .flatten()
377            .unwrap_or_default();
378        let metadata = get_result
379            .metadatas
380            .into_iter()
381            .next()
382            .flatten()
383            .unwrap_or_default();
384
385        Ok(Some(Document {
386            content,
387            metadata,
388            id: Some(id.to_string()),
389        }))
390    }
391
392    async fn get_embedding(&self, id: &str) -> Result<Option<Vec<f32>>, VectorStoreError> {
393        let url = self.collection_url("get")?;
394        let body = json!({
395            "ids": [id],
396            "include": ["embeddings"]
397        });
398
399        let response = self
400            .client
401            .post(&url)
402            .json(&body)
403            .send()
404            .await
405            .map_err(|e| VectorStoreError::ConnectionError(e.to_string()))?;
406
407        if !response.status().is_success() {
408            return Ok(None);
409        }
410
411        let get_result: ChromaGetResponse = response.json().await.map_err(|e| {
412            VectorStoreError::StorageError(format!("failed to parse document: {}", e))
413        })?;
414
415        if let Some(embeddings) = get_result.embeddings {
416            Ok(embeddings.into_iter().next())
417        } else {
418            Ok(None)
419        }
420    }
421
422    async fn delete_document(&self, id: &str) -> Result<(), VectorStoreError> {
423        let url = self.collection_url("delete")?;
424        let body = json!({
425            "ids": [id]
426        });
427
428        let response = self
429            .client
430            .post(&url)
431            .json(&body)
432            .send()
433            .await
434            .map_err(|e| VectorStoreError::ConnectionError(e.to_string()))?;
435
436        if !response.status().is_success() {
437            let text = response.text().await.unwrap_or_default();
438            return Err(VectorStoreError::StorageError(format!(
439                "failed to delete document: {}",
440                text
441            )));
442        }
443
444        Ok(())
445    }
446
447    async fn count(&self) -> usize {
448        let url = match self.collection_url("count") {
449            Ok(u) => u,
450            Err(e) => {
451                log::warn!("ChromaDB count() failed to build URL: {}", e);
452                return 0;
453            }
454        };
455
456        let response = self.client.post(&url).send().await;
457        match response {
458            Ok(resp) => {
459                if resp.status().is_success() {
460                    match resp.json::<usize>().await {
461                        Ok(count) => count,
462                        Err(e) => {
463                            log::warn!("ChromaDB count() failed to parse response: {}", e);
464                            0
465                        }
466                    }
467                } else {
468                    log::warn!("ChromaDB count() request failed with non-success status");
469                    0
470                }
471            }
472            Err(e) => {
473                log::warn!("ChromaDB count() request error: {}", e);
474                0
475            }
476        }
477    }
478
479    async fn clear(&self) -> Result<(), VectorStoreError> {
480        // 获取所有文档 ID 后批量删除
481        let get_url = self.collection_url("get")?;
482        let body = json!({
483            "include": []
484        });
485
486        let response = self
487            .client
488            .post(&get_url)
489            .json(&body)
490            .send()
491            .await
492            .map_err(|e| VectorStoreError::ConnectionError(e.to_string()))?;
493
494        if !response.status().is_success() {
495            let text = response.text().await.unwrap_or_default();
496            return Err(VectorStoreError::StorageError(format!(
497                "failed to fetch document list: {}",
498                text
499            )));
500        }
501
502        let get_result: ChromaGetResponse = response.json().await.map_err(|e| {
503            VectorStoreError::StorageError(format!("failed to parse document list: {}", e))
504        })?;
505
506        if get_result.ids.is_empty() {
507            return Ok(());
508        }
509
510        // 批量删除
511        let del_url = self.collection_url("delete")?;
512        let del_body = json!({
513            "ids": get_result.ids
514        });
515
516        let response = self
517            .client
518            .post(&del_url)
519            .json(&del_body)
520            .send()
521            .await
522            .map_err(|e| VectorStoreError::ConnectionError(e.to_string()))?;
523
524        if !response.status().is_success() {
525            let text = response.text().await.unwrap_or_default();
526            return Err(VectorStoreError::StorageError(format!(
527                "failed to clear collection: {}",
528                text
529            )));
530        }
531
532        Ok(())
533    }
534}