1use 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#[derive(Debug, Clone)]
16pub struct ChromaDBConfig {
17 pub host: String,
19 pub collection_name: String,
21 pub vector_size: usize,
23 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(
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#[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#[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#[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#[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#[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
103pub struct ChromaDBVectorStore {
116 config: ChromaDBConfig,
117 client: reqwest::Client,
118 collection_id: Option<String>,
119}
120
121impl ChromaDBVectorStore {
122 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 async fn init_collection(&mut self) -> Result<(), VectorStoreError> {
136 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 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 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 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 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 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 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 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 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}