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    pub fn new(
40        host: impl Into<String>,
41        collection_name: impl Into<String>,
42        vector_size: usize,
43    ) -> Self {
44        Self {
45            host: host.into(),
46            collection_name: collection_name.into(),
47            vector_size,
48            metadata: None,
49        }
50    }
51}
52
53/// ChromaDB 集合信息(从 API 返回解析)
54#[derive(Debug, Deserialize)]
55#[allow(dead_code)]
56struct ChromaCollection {
57    id: String,
58    name: String,
59    #[serde(default)]
60    metadata: Option<serde_json::Value>,
61}
62
63/// ChromaDB add 请求体
64#[derive(Debug, Serialize)]
65struct ChromaAddRequest {
66    ids: Vec<String>,
67    embeddings: Vec<Vec<f32>>,
68    documents: Vec<String>,
69    #[serde(skip_serializing_if = "Option::is_none")]
70    metadatas: Option<Vec<HashMap<String, String>>>,
71}
72
73/// ChromaDB query 请求体
74#[derive(Debug, Serialize)]
75struct ChromaQueryRequest {
76    query_embeddings: Vec<Vec<f32>>,
77    n_results: usize,
78    #[serde(skip_serializing_if = "Option::is_none")]
79    include: Option<Vec<String>>,
80}
81
82/// ChromaDB query 响应
83#[derive(Debug, Deserialize)]
84struct ChromaQueryResponse {
85    ids: Vec<Vec<String>>,
86    distances: Vec<Vec<f64>>,
87    documents: Vec<Vec<String>>,
88    #[serde(default)]
89    metadatas: Vec<Vec<Option<HashMap<String, String>>>>,
90}
91
92/// ChromaDB get 响应
93#[derive(Debug, Deserialize)]
94struct ChromaGetResponse {
95    ids: Vec<String>,
96    documents: Vec<Option<String>>,
97    #[serde(default)]
98    metadatas: Vec<Option<HashMap<String, String>>>,
99    embeddings: Option<Vec<Vec<f32>>>,
100}
101
102/// ChromaDB 向量存储
103///
104/// 通过 HTTP API 连接 ChromaDB 服务。
105///
106/// # 示例
107/// ```ignore
108/// use lc_vector_stores::ChromaDBVectorStore;
109///
110/// let store = ChromaDBVectorStore::new(
111///     ChromaDBConfig::new("http://localhost:8000", "my_collection", 384)
112/// ).await?;
113/// ```
114pub struct ChromaDBVectorStore {
115    config: ChromaDBConfig,
116    client: reqwest::Client,
117    collection_id: Option<String>,
118}
119
120impl ChromaDBVectorStore {
121    /// 创建 ChromaDB 向量存储并自动初始化集合
122    pub async fn new(config: ChromaDBConfig) -> Result<Self, VectorStoreError> {
123        let client = reqwest::Client::new();
124        let mut store = Self {
125            config,
126            client,
127            collection_id: None,
128        };
129        store.init_collection().await?;
130        Ok(store)
131    }
132
133    /// 初始化或获取集合
134    async fn init_collection(&mut self) -> Result<(), VectorStoreError> {
135        // 尝试获取已有集合
136        let url = format!(
137            "{}/api/v1/collections/{}",
138            self.config.host, self.config.collection_name
139        );
140        let response = self
141            .client
142            .get(&url)
143            .send()
144            .await
145            .map_err(|e| VectorStoreError::ConnectionError(e.to_string()))?;
146
147        if response.status().is_success() {
148            let collection: ChromaCollection = response
149                .json()
150                .await
151                .map_err(|e| VectorStoreError::StorageError(format!("解析集合信息失败: {}", e)))?;
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!("解析新集合信息失败: {}", e))
177            })?;
178            self.collection_id = Some(collection.id);
179            Ok(())
180        } else {
181            let text = response.text().await.unwrap_or_default();
182            Err(VectorStoreError::StorageError(format!(
183                "创建集合失败: {}",
184                text
185            )))
186        }
187    }
188
189    /// 获取集合 ID
190    fn get_collection_id(&self) -> Result<&str, VectorStoreError> {
191        self.collection_id
192            .as_deref()
193            .ok_or_else(|| VectorStoreError::StorageError("集合未初始化".to_string()))
194    }
195
196    /// 构建集合 API 基础 URL
197    fn collection_url(&self, endpoint: &str) -> Result<String, VectorStoreError> {
198        let cid = self.get_collection_id()?;
199        Ok(format!(
200            "{}/api/v1/collections/{}/{}",
201            self.config.host, cid, endpoint
202        ))
203    }
204}
205
206#[async_trait]
207impl VectorStore for ChromaDBVectorStore {
208    async fn add_documents(
209        &self,
210        documents: Vec<Document>,
211        embeddings: Vec<Vec<f32>>,
212    ) -> Result<Vec<String>, VectorStoreError> {
213        if documents.is_empty() {
214            return Ok(Vec::new());
215        }
216
217        let count = documents.len();
218        let ids: Vec<String> = (0..count)
219            .map(|i| {
220                documents[i]
221                    .id
222                    .clone()
223                    .unwrap_or_else(|| uuid::Uuid::new_v4().to_string())
224            })
225            .collect();
226
227        let contents: Vec<String> = documents.iter().map(|d| d.content.clone()).collect();
228        let metadatas: Vec<HashMap<String, String>> =
229            documents.iter().map(|d| d.metadata.clone()).collect();
230        let has_metadata = metadatas.iter().any(|m| !m.is_empty());
231
232        let request = ChromaAddRequest {
233            ids: ids.clone(),
234            embeddings,
235            documents: contents,
236            metadatas: if has_metadata { Some(metadatas) } else { None },
237        };
238
239        let url = self.collection_url("add")?;
240        let response = self
241            .client
242            .post(&url)
243            .json(&request)
244            .send()
245            .await
246            .map_err(|e| VectorStoreError::ConnectionError(e.to_string()))?;
247
248        if !response.status().is_success() {
249            let text = response.text().await.unwrap_or_default();
250            return Err(VectorStoreError::StorageError(format!(
251                "添加文档失败: {}",
252                text
253            )));
254        }
255
256        Ok(ids)
257    }
258
259    async fn similarity_search(
260        &self,
261        query_embedding: &[f32],
262        k: usize,
263    ) -> Result<Vec<SearchResult>, VectorStoreError> {
264        let request = ChromaQueryRequest {
265            query_embeddings: vec![query_embedding.to_vec()],
266            n_results: k,
267            include: Some(vec![
268                "documents".to_string(),
269                "distances".to_string(),
270                "metadatas".to_string(),
271            ]),
272        };
273
274        let url = self.collection_url("query")?;
275        let response = self
276            .client
277            .post(&url)
278            .json(&request)
279            .send()
280            .await
281            .map_err(|e| VectorStoreError::ConnectionError(e.to_string()))?;
282
283        if !response.status().is_success() {
284            let text = response.text().await.unwrap_or_default();
285            return Err(VectorStoreError::StorageError(format!(
286                "查询失败: {}",
287                text
288            )));
289        }
290
291        let query_result: ChromaQueryResponse = response
292            .json()
293            .await
294            .map_err(|e| VectorStoreError::StorageError(format!("解析查询结果失败: {}", e)))?;
295
296        let mut results = Vec::new();
297
298        // ChromaDB 返回嵌套数组(每个 query 一个结果集)
299        if let Some(doc_list) = query_result.documents.into_iter().next() {
300            let dist_list = query_result
301                .distances
302                .into_iter()
303                .next()
304                .unwrap_or_default();
305            let meta_list = query_result
306                .metadatas
307                .into_iter()
308                .next()
309                .unwrap_or_default();
310            let id_list = query_result.ids.into_iter().next().unwrap_or_default();
311
312            for (i, content) in doc_list.into_iter().enumerate() {
313                let score = dist_list.get(i).copied().unwrap_or(0.0);
314                // ChromaDB 返回的是 L2 距离,转换为相似度分数(1 / (1 + dist))
315                let similarity = 1.0 / (1.0 + score);
316                let metadata = meta_list
317                    .get(i)
318                    .unwrap_or(&None)
319                    .clone()
320                    .unwrap_or_default();
321                let doc_id = id_list.get(i).cloned();
322
323                results.push(SearchResult {
324                    document: Document {
325                        content,
326                        metadata,
327                        id: doc_id,
328                    },
329                    score: similarity as f32,
330                });
331            }
332        }
333
334        // 按相似度降序排序
335        results.sort_by(|a, b| {
336            b.score
337                .partial_cmp(&a.score)
338                .unwrap_or(std::cmp::Ordering::Equal)
339        });
340        Ok(results)
341    }
342
343    async fn get_document(&self, id: &str) -> Result<Option<Document>, VectorStoreError> {
344        let url = self.collection_url("get")?;
345        let body = json!({
346            "ids": [id],
347            "include": ["documents", "metadatas"]
348        });
349
350        let response = self
351            .client
352            .post(&url)
353            .json(&body)
354            .send()
355            .await
356            .map_err(|e| VectorStoreError::ConnectionError(e.to_string()))?;
357
358        if !response.status().is_success() {
359            return Ok(None);
360        }
361
362        let get_result: ChromaGetResponse = response
363            .json()
364            .await
365            .map_err(|e| VectorStoreError::StorageError(format!("解析文档失败: {}", e)))?;
366
367        if get_result.ids.is_empty() {
368            return Ok(None);
369        }
370
371        let content = get_result
372            .documents
373            .into_iter()
374            .next()
375            .flatten()
376            .unwrap_or_default();
377        let metadata = get_result
378            .metadatas
379            .into_iter()
380            .next()
381            .flatten()
382            .unwrap_or_default();
383
384        Ok(Some(Document {
385            content,
386            metadata,
387            id: Some(id.to_string()),
388        }))
389    }
390
391    async fn get_embedding(&self, id: &str) -> Result<Option<Vec<f32>>, VectorStoreError> {
392        let url = self.collection_url("get")?;
393        let body = json!({
394            "ids": [id],
395            "include": ["embeddings"]
396        });
397
398        let response = self
399            .client
400            .post(&url)
401            .json(&body)
402            .send()
403            .await
404            .map_err(|e| VectorStoreError::ConnectionError(e.to_string()))?;
405
406        if !response.status().is_success() {
407            return Ok(None);
408        }
409
410        let get_result: ChromaGetResponse = response
411            .json()
412            .await
413            .map_err(|e| VectorStoreError::StorageError(format!("解析文档失败: {}", e)))?;
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                "删除文档失败: {}",
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                "获取文档列表失败: {}",
498                text
499            )));
500        }
501
502        let get_result: ChromaGetResponse = response
503            .json()
504            .await
505            .map_err(|e| VectorStoreError::StorageError(format!("解析文档列表失败: {}", e)))?;
506
507        if get_result.ids.is_empty() {
508            return Ok(());
509        }
510
511        // 批量删除
512        let del_url = self.collection_url("delete")?;
513        let del_body = json!({
514            "ids": get_result.ids
515        });
516
517        let response = self
518            .client
519            .post(&del_url)
520            .json(&del_body)
521            .send()
522            .await
523            .map_err(|e| VectorStoreError::ConnectionError(e.to_string()))?;
524
525        if !response.status().is_success() {
526            let text = response.text().await.unwrap_or_default();
527            return Err(VectorStoreError::StorageError(format!(
528                "清空集合失败: {}",
529                text
530            )));
531        }
532
533        Ok(())
534    }
535}