1use async_trait::async_trait;
8use lc_core::http::{BoundedResponse, HttpClient, HttpError, RequestOptions};
9use serde::{Deserialize, Serialize};
10use serde_json::json;
11use std::collections::HashMap;
12
13use crate::{Document, FilterOp, MetadataFilter, SearchResult, VectorStore, VectorStoreError};
14
15const CHROMA_CLEAR_PAGE_SIZE: usize = 500;
23
24#[derive(Debug, Clone)]
26pub struct ChromaDBConfig {
27 pub host: String,
29 pub collection_name: String,
31 pub vector_size: usize,
33 pub metadata: Option<HashMap<String, String>>,
35}
36
37impl Default for ChromaDBConfig {
38 fn default() -> Self {
39 Self {
40 host: "http://localhost:8000".to_string(),
41 collection_name: "langchainrust".to_string(),
42 vector_size: 1536,
43 metadata: None,
44 }
45 }
46}
47
48impl ChromaDBConfig {
49 pub fn new(
51 host: impl Into<String>,
52 collection_name: impl Into<String>,
53 vector_size: usize,
54 ) -> Self {
55 Self {
56 host: host.into(),
57 collection_name: collection_name.into(),
58 vector_size,
59 metadata: None,
60 }
61 }
62}
63
64#[derive(Debug, Deserialize)]
66#[allow(dead_code)]
67struct ChromaCollection {
68 id: String,
69 name: String,
70 #[serde(default)]
71 metadata: Option<serde_json::Value>,
72}
73
74#[derive(Debug, Serialize)]
76struct ChromaAddRequest {
77 ids: Vec<String>,
78 embeddings: Vec<Vec<f32>>,
79 documents: Vec<String>,
80 #[serde(skip_serializing_if = "Option::is_none")]
81 metadatas: Option<Vec<HashMap<String, serde_json::Value>>>,
82}
83
84#[derive(Debug, Serialize)]
86struct ChromaQueryRequest {
87 query_embeddings: Vec<Vec<f32>>,
88 n_results: usize,
89 #[serde(skip_serializing_if = "Option::is_none")]
90 include: Option<Vec<String>>,
91 #[serde(rename = "where", skip_serializing_if = "Option::is_none")]
93 where_filter: Option<serde_json::Value>,
94}
95
96#[derive(Debug, Deserialize)]
98struct ChromaQueryResponse {
99 ids: Vec<Vec<String>>,
100 distances: Vec<Vec<f64>>,
101 documents: Vec<Vec<String>>,
102 #[serde(default)]
103 metadatas: Vec<Vec<Option<HashMap<String, serde_json::Value>>>>,
104}
105
106#[derive(Debug, Deserialize)]
108struct ChromaGetResponse {
109 ids: Vec<String>,
110 documents: Vec<Option<String>>,
111 #[serde(default)]
112 metadatas: Vec<Option<HashMap<String, serde_json::Value>>>,
113 embeddings: Option<Vec<Vec<f32>>>,
114}
115
116#[derive(Debug, Deserialize)]
123struct ChromaClearPage {
124 #[serde(default)]
125 ids: Option<Vec<String>>,
126}
127
128pub struct ChromaDBVectorStore {
141 config: ChromaDBConfig,
142 http: HttpClient,
143 collection_id: Option<String>,
144}
145
146impl ChromaDBVectorStore {
147 pub async fn new(config: ChromaDBConfig) -> Result<Self, VectorStoreError> {
149 let http = HttpClient::api().build().map_err(|e| {
153 VectorStoreError::ConfigError(format!("failed to build HTTP client: {e}"))
154 })?;
155 let mut store = Self {
156 config,
157 http,
158 collection_id: None,
159 };
160 store.init_collection().await?;
161 Ok(store)
162 }
163
164 fn map_http_error(prefix: &str, err: HttpError) -> VectorStoreError {
168 match err {
169 HttpError::Status { status, body } => {
170 VectorStoreError::StorageError(format!("{prefix}: HTTP {status}: {body}"))
171 }
172 other => VectorStoreError::ConnectionError(format!("{prefix}: {other}")),
173 }
174 }
175
176 async fn init_collection(&mut self) -> Result<(), VectorStoreError> {
178 let url = format!(
180 "{}/api/v1/collections/{}",
181 self.config.host, self.config.collection_name
182 );
183 let response = self
184 .http
185 .get(&url)
186 .await
187 .map_err(|e| Self::map_http_error("ChromaDB init error", e))?;
188
189 if response.status.is_success() {
190 let collection: ChromaCollection =
191 serde_json::from_str(&response.body).map_err(|e| {
192 VectorStoreError::StorageError(format!(
193 "failed to parse collection info: {}",
194 e
195 ))
196 })?;
197 self.collection_id = Some(collection.id);
198 return Ok(());
199 }
200
201 let create_url = format!("{}/api/v1/collections", self.config.host);
203 let mut body = json!({
204 "name": self.config.collection_name,
205 });
206
207 if let Some(ref meta) = self.config.metadata {
208 body["metadata"] = serde_json::to_value(meta).unwrap_or(json!({}));
209 }
210
211 let response = self
212 .http
213 .post_json_with(&create_url, &body, RequestOptions::new())
214 .await
215 .map_err(|e| Self::map_http_error("ChromaDB create error", e))?;
216
217 if response.status.is_success() {
218 let collection: ChromaCollection =
219 serde_json::from_str(&response.body).map_err(|e| {
220 VectorStoreError::StorageError(format!(
221 "failed to parse new collection info: {}",
222 e
223 ))
224 })?;
225 self.collection_id = Some(collection.id);
226 Ok(())
227 } else {
228 Err(VectorStoreError::StorageError(format!(
229 "failed to create collection: {}",
230 response.body
231 )))
232 }
233 }
234
235 async fn post_json(
240 &self,
241 url: &str,
242 body: &serde_json::Value,
243 prefix: &str,
244 ) -> Result<BoundedResponse, VectorStoreError> {
245 self.http
246 .post_json_with(url, body, RequestOptions::new())
247 .await
248 .map_err(|e| Self::map_http_error(prefix, e))
249 }
250
251 fn get_collection_id(&self) -> Result<&str, VectorStoreError> {
253 self.collection_id.as_deref().ok_or_else(|| {
254 VectorStoreError::StorageError("collection is not initialized".to_string())
255 })
256 }
257
258 fn collection_url(&self, endpoint: &str) -> Result<String, VectorStoreError> {
260 let cid = self.get_collection_id()?;
261 Ok(format!(
262 "{}/api/v1/collections/{}/{}",
263 self.config.host, cid, endpoint
264 ))
265 }
266
267 fn query_request(
269 query_embedding: &[f32],
270 k: usize,
271 filter: Option<&MetadataFilter>,
272 ) -> ChromaQueryRequest {
273 ChromaQueryRequest {
274 query_embeddings: vec![query_embedding.to_vec()],
275 n_results: k,
276 include: Some(vec![
277 "documents".to_string(),
278 "distances".to_string(),
279 "metadatas".to_string(),
280 ]),
281 where_filter: filter.map(filter_to_chroma),
282 }
283 }
284
285 async fn query_impl(
287 &self,
288 request: ChromaQueryRequest,
289 ) -> Result<Vec<SearchResult>, VectorStoreError> {
290 let url = self.collection_url("query")?;
291 let body = serde_json::to_value(&request).map_err(|e| {
292 VectorStoreError::StorageError(format!("failed to serialize query: {e}"))
293 })?;
294 let response = self.post_json(&url, &body, "ChromaDB query failed").await?;
295
296 let query_result: ChromaQueryResponse =
297 serde_json::from_str(&response.body).map_err(|e| {
298 VectorStoreError::StorageError(format!("failed to parse query results: {}", e))
299 })?;
300
301 let mut results = Vec::new();
302
303 if let Some(doc_list) = query_result.documents.into_iter().next() {
305 let dist_list = query_result
306 .distances
307 .into_iter()
308 .next()
309 .unwrap_or_default();
310 let meta_list = query_result
311 .metadatas
312 .into_iter()
313 .next()
314 .unwrap_or_default();
315 let id_list = query_result.ids.into_iter().next().unwrap_or_default();
316
317 for (i, content) in doc_list.into_iter().enumerate() {
318 let score = dist_list.get(i).copied().unwrap_or(0.0);
319 let similarity = 1.0 / (1.0 + score);
321 let metadata = meta_list
322 .get(i)
323 .unwrap_or(&None)
324 .clone()
325 .unwrap_or_default();
326 let doc_id = id_list.get(i).cloned();
327
328 results.push(SearchResult {
329 document: Document {
330 content,
331 metadata,
332 id: doc_id,
333 },
334 score: similarity as f32,
335 });
336 }
337 }
338
339 results.sort_by(|a, b| {
341 b.score
342 .partial_cmp(&a.score)
343 .unwrap_or(std::cmp::Ordering::Equal)
344 });
345 Ok(results)
346 }
347
348 async fn fetch_clear_page(&self) -> Result<Vec<String>, VectorStoreError> {
354 let get_url = self.collection_url("get")?;
355 let body = json!({
356 "include": [],
357 "limit": CHROMA_CLEAR_PAGE_SIZE
358 });
359 let response = self
360 .post_json(&get_url, &body, "ChromaDB clear fetch failed")
361 .await?;
362 let page: ChromaClearPage = serde_json::from_str(&response.body).map_err(|e| {
363 VectorStoreError::StorageError(format!("failed to parse document list: {}", e))
364 })?;
365 Ok(page.ids.unwrap_or_default())
366 }
367}
368
369#[async_trait]
370impl VectorStore for ChromaDBVectorStore {
371 async fn add_documents(
372 &self,
373 documents: Vec<Document>,
374 embeddings: Vec<Vec<f32>>,
375 ) -> Result<Vec<String>, VectorStoreError> {
376 if documents.is_empty() {
377 return Ok(Vec::new());
378 }
379
380 let count = documents.len();
381 let ids: Vec<String> = (0..count)
382 .map(|i| {
383 documents[i]
384 .id
385 .clone()
386 .unwrap_or_else(|| uuid::Uuid::new_v4().to_string())
387 })
388 .collect();
389
390 let contents: Vec<String> = documents.iter().map(|d| d.content.clone()).collect();
391 let metadatas: Vec<HashMap<String, serde_json::Value>> =
392 documents.iter().map(|d| d.metadata.clone()).collect();
393 let has_metadata = metadatas.iter().any(|m| !m.is_empty());
394
395 let request = ChromaAddRequest {
396 ids: ids.clone(),
397 embeddings,
398 documents: contents,
399 metadatas: if has_metadata { Some(metadatas) } else { None },
400 };
401
402 let url = self.collection_url("add")?;
403 let body = serde_json::to_value(&request).map_err(|e| {
404 VectorStoreError::StorageError(format!("failed to serialize add request: {e}"))
405 })?;
406 let _ = self.post_json(&url, &body, "ChromaDB add failed").await?;
407 Ok(ids)
408 }
409
410 async fn similarity_search(
411 &self,
412 query_embedding: &[f32],
413 k: usize,
414 ) -> Result<Vec<SearchResult>, VectorStoreError> {
415 let request = Self::query_request(query_embedding, k, None);
416 self.query_impl(request).await
417 }
418
419 async fn similarity_search_with_filter(
421 &self,
422 query_embedding: &[f32],
423 k: usize,
424 filter: Option<&MetadataFilter>,
425 ) -> Result<Vec<SearchResult>, VectorStoreError> {
426 let request = Self::query_request(query_embedding, k, filter);
427 self.query_impl(request).await
428 }
429
430 async fn get_document(&self, id: &str) -> Result<Option<Document>, VectorStoreError> {
431 let url = self.collection_url("get")?;
432 let body = json!({
433 "ids": [id],
434 "include": ["documents", "metadatas"]
435 });
436
437 let response = self
438 .http
439 .post_json_with(&url, &body, RequestOptions::new())
440 .await;
441 let response = match response {
442 Err(HttpError::Status { .. }) => return Ok(None),
444 Err(e) => return Err(Self::map_http_error("ChromaDB get_document error", e)),
445 Ok(r) => r,
446 };
447
448 let get_result: ChromaGetResponse = serde_json::from_str(&response.body).map_err(|e| {
449 VectorStoreError::StorageError(format!("failed to parse document: {}", e))
450 })?;
451
452 if get_result.ids.is_empty() {
453 return Ok(None);
454 }
455
456 let content = get_result
457 .documents
458 .into_iter()
459 .next()
460 .flatten()
461 .unwrap_or_default();
462 let metadata = get_result
463 .metadatas
464 .into_iter()
465 .next()
466 .flatten()
467 .unwrap_or_default();
468
469 Ok(Some(Document {
470 content,
471 metadata,
472 id: Some(id.to_string()),
473 }))
474 }
475
476 async fn get_embedding(&self, id: &str) -> Result<Option<Vec<f32>>, VectorStoreError> {
477 let url = self.collection_url("get")?;
478 let body = json!({
479 "ids": [id],
480 "include": ["embeddings"]
481 });
482
483 let response = self
484 .http
485 .post_json_with(&url, &body, RequestOptions::new())
486 .await;
487 let response = match response {
488 Err(HttpError::Status { .. }) => return Ok(None),
490 Err(e) => return Err(Self::map_http_error("ChromaDB get_embedding error", e)),
491 Ok(r) => r,
492 };
493
494 let get_result: ChromaGetResponse = serde_json::from_str(&response.body).map_err(|e| {
495 VectorStoreError::StorageError(format!("failed to parse document: {}", e))
496 })?;
497
498 if let Some(embeddings) = get_result.embeddings {
499 Ok(embeddings.into_iter().next())
500 } else {
501 Ok(None)
502 }
503 }
504
505 async fn delete_document(&self, id: &str) -> Result<(), VectorStoreError> {
506 let url = self.collection_url("delete")?;
507 let body = json!({
508 "ids": [id]
509 });
510
511 let _ = self
512 .post_json(&url, &body, "ChromaDB delete failed")
513 .await?;
514 Ok(())
515 }
516
517 async fn count(&self) -> usize {
518 let url = match self.collection_url("count") {
519 Ok(u) => u,
520 Err(e) => {
521 log::warn!("ChromaDB count() failed to build URL: {}", e);
522 return 0;
523 }
524 };
525
526 let response = self
527 .http
528 .post_json_with(&url, &serde_json::json!({}), RequestOptions::new())
529 .await;
530 match response {
531 Ok(resp) => match serde_json::from_str::<usize>(&resp.body) {
532 Ok(count) => count,
533 Err(e) => {
534 log::warn!("ChromaDB count() failed to parse response: {}", e);
535 0
536 }
537 },
538 Err(e) => {
539 log::warn!("ChromaDB count() request failed: {}", e);
540 0
541 }
542 }
543 }
544
545 async fn clear(&self) -> Result<(), VectorStoreError> {
546 const MAX_ROUNDS: usize = 10_000;
552 for _round in 0..MAX_ROUNDS {
553 let ids = self.fetch_clear_page().await?;
554 if ids.is_empty() {
555 return Ok(());
556 }
557
558 let del_url = self.collection_url("delete")?;
559 let del_body = json!({ "ids": ids });
560 let _ = self
561 .post_json(&del_url, &del_body, "ChromaDB clear delete failed")
562 .await?;
563 }
564
565 Err(VectorStoreError::StorageError(format!(
566 "failed to clear collection after {MAX_ROUNDS} delete rounds"
567 )))
568 }
569}
570
571pub fn filter_to_chroma(filter: &MetadataFilter) -> serde_json::Value {
578 fn op_str(op: FilterOp) -> &'static str {
579 match op {
580 FilterOp::Eq => "$eq",
581 FilterOp::Ne => "$ne",
582 FilterOp::Gt => "$gt",
583 FilterOp::Gte => "$gte",
584 FilterOp::Lt => "$lt",
585 FilterOp::Lte => "$lte",
586 FilterOp::In => "$in",
587 FilterOp::Nin => "$nin",
588 }
589 }
590 match filter {
591 MetadataFilter::Field { key, op, value } => {
592 serde_json::json!({ key.clone(): { op_str(*op): value.clone() } })
593 }
594 MetadataFilter::And(filters) => {
595 let items: Vec<serde_json::Value> = filters.iter().map(filter_to_chroma).collect();
596 serde_json::json!({ "$and": items })
597 }
598 MetadataFilter::Or(filters) => {
599 let items: Vec<serde_json::Value> = filters.iter().map(filter_to_chroma).collect();
600 serde_json::json!({ "$or": items })
601 }
602 }
603}
604
605#[cfg(test)]
606mod tests {
607 use super::*;
608 use tokio::io::{AsyncReadExt, AsyncWriteExt};
609 use tokio::net::TcpListener;
610
611 async fn spawn_loopback(
614 handler: impl Fn(&str, &str) -> (u16, String) + Send + Sync + 'static,
615 ) -> String {
616 use std::sync::Arc;
617 let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
618 let addr = listener.local_addr().unwrap();
619 let handler = Arc::new(handler);
620 tokio::spawn(async move {
621 loop {
622 let Ok((mut socket, _)) = listener.accept().await else {
623 break;
624 };
625 let handler = handler.clone();
626 tokio::spawn(async move {
627 let mut raw = Vec::new();
628 let mut buf = [0u8; 4096];
629 let (head_end, len) = loop {
630 if let Some(pos) = raw.windows(4).position(|w| w == b"\r\n\r\n") {
631 let text = String::from_utf8_lossy(&raw).to_string();
632 let len = text
633 .lines()
634 .find_map(|l| {
635 let lower = l.to_ascii_lowercase();
636 lower
637 .strip_prefix("content-length:")
638 .map(|v| v.trim().to_string())
639 })
640 .and_then(|v| v.parse::<usize>().ok())
641 .unwrap_or(0);
642 break (pos + 4, len);
643 }
644 let n = socket.read(&mut buf).await.unwrap_or(0);
645 if n == 0 {
646 break (raw.len(), 0);
647 }
648 raw.extend_from_slice(&buf[..n]);
649 };
650 while raw.len() < head_end + len {
651 let n = socket.read(&mut buf).await.unwrap_or(0);
652 if n == 0 {
653 break;
654 }
655 raw.extend_from_slice(&buf[..n]);
656 }
657 let text = String::from_utf8_lossy(&raw).to_string();
658 let path = text.split_whitespace().nth(1).unwrap_or("/").to_string();
659 let (status, resp_body) = handler(&path, &text);
660 let reason = if status == 200 { "OK" } else { "Error" };
661 let head = format!(
662 "HTTP/1.1 {status} {reason}\r\nContent-Type: application/json\r\nContent-Length: {}\r\nConnection: close\r\n\r\n",
663 resp_body.len()
664 );
665 let _ = socket.write_all(head.as_bytes()).await;
666 let _ = socket.write_all(resp_body.as_bytes()).await;
667 let _ = socket.shutdown().await;
668 });
669 }
670 });
671 format!("http://{}", addr)
672 }
673
674 fn store_with_collection(base: &str, cid: &str) -> ChromaDBVectorStore {
677 let http = HttpClient::api().build().expect("build http client");
678 ChromaDBVectorStore {
679 config: ChromaDBConfig::new(base, "col", 3),
680 http,
681 collection_id: Some(cid.to_string()),
682 }
683 }
684
685 #[tokio::test]
688 async fn test_clear_drains_in_pages() {
689 use std::sync::{Arc, Mutex};
690
691 let gets = Arc::new(Mutex::new(0u32));
692 let deletes = Arc::new(Mutex::new(0u32));
693 let gets_h = gets.clone();
694 let deletes_h = deletes.clone();
695
696 let base = spawn_loopback(move |path, request| {
697 if path.ends_with("/get") {
698 *gets_h.lock().unwrap() += 1;
699 let body = if *gets_h.lock().unwrap() == 1 {
701 r#"{"ids":["a","b","c"],"documents":null,"metadatas":null,"embeddings":null}"#
702 .to_string()
703 } else {
704 r#"{"ids":null,"documents":null,"metadatas":null,"embeddings":null}"#
705 .to_string()
706 };
707 let raw = request.to_string();
709 assert!(
710 raw.to_ascii_lowercase()
711 .contains("content-type: application/json"),
712 "clear /get must carry a JSON body: {raw}"
713 );
714 (200, body)
715 } else if path.ends_with("/delete") {
716 *deletes_h.lock().unwrap() += 1;
717 (200, String::new())
718 } else {
719 (404, "not found".to_string())
720 }
721 })
722 .await;
723
724 let store = store_with_collection(&base, "cid");
725 store.clear().await.unwrap();
726 assert_eq!(*gets.lock().unwrap(), 2, "two get rounds expected");
727 assert_eq!(*deletes.lock().unwrap(), 1, "one bulk delete expected");
729 }
730
731 #[tokio::test]
734 async fn test_clear_noop_when_empty() {
735 use std::sync::{Arc, Mutex};
736 let gets = Arc::new(Mutex::new(0u32));
737 let deletes = Arc::new(Mutex::new(0u32));
738 let gets_h = gets.clone();
739 let deletes_h = deletes.clone();
740
741 let base = spawn_loopback(move |path, _request| {
742 if path.ends_with("/get") {
743 *gets_h.lock().unwrap() += 1;
744 (
745 200,
746 r#"{"ids":null,"documents":null,"metadatas":null}"#.to_string(),
747 )
748 } else if path.ends_with("/delete") {
749 *deletes_h.lock().unwrap() += 1;
750 (200, String::new())
751 } else {
752 (404, "not found".to_string())
753 }
754 })
755 .await;
756
757 let store = store_with_collection(&base, "cid");
758 store.clear().await.unwrap();
759 assert_eq!(*gets.lock().unwrap(), 1);
760 assert_eq!(*deletes.lock().unwrap(), 0);
761 }
762
763 #[test]
765 fn test_filter_to_chroma_field() {
766 assert_eq!(
767 filter_to_chroma(&MetadataFilter::field("lang", FilterOp::Eq, "rust")),
768 serde_json::json!({ "lang": { "$eq": "rust" } })
769 );
770 assert_eq!(
771 filter_to_chroma(&MetadataFilter::field("year", FilterOp::Lt, 2020)),
772 serde_json::json!({ "year": { "$lt": 2020 } })
773 );
774 }
775
776 #[test]
778 fn test_filter_to_chroma_and_or() {
779 let f = MetadataFilter::or(vec![
780 MetadataFilter::field("lang", FilterOp::Eq, "python"),
781 MetadataFilter::and(vec![
782 MetadataFilter::field("lang", FilterOp::Eq, "rust"),
783 MetadataFilter::field("tags", FilterOp::In, vec!["ml"]),
784 ]),
785 ]);
786 assert_eq!(
787 filter_to_chroma(&f),
788 serde_json::json!({
789 "$or": [
790 { "lang": { "$eq": "python" } },
791 { "$and": [
792 { "lang": { "$eq": "rust" } },
793 { "tags": { "$in": ["ml"] } }
794 ]}
795 ]
796 })
797 );
798 }
799
800 #[test]
802 fn test_query_request_no_filter() {
803 let req = ChromaDBVectorStore::query_request(&[1.0, 2.0], 3, None);
804 assert!(req.where_filter.is_none());
805 let v = serde_json::to_value(&req).unwrap();
806 assert!(v.get("where").is_none());
807 assert_eq!(v["n_results"], 3);
808 }
809
810 #[test]
812 fn test_query_request_with_filter() {
813 let f = MetadataFilter::field("lang", FilterOp::Eq, "rust");
814 let req = ChromaDBVectorStore::query_request(&[1.0, 2.0], 3, Some(&f));
815 let v = serde_json::to_value(&req).unwrap();
816 assert_eq!(v["where"], serde_json::json!({ "lang": { "$eq": "rust" } }));
817 }
818
819 #[tokio::test]
825 #[ignore = "requires a running Chroma (set CHROMA_URL)"]
826 async fn clear_removes_all_pages_live() {
827 let Ok(host) = std::env::var("CHROMA_URL") else {
828 eprintln!("CHROMA_URL not set; skipping live Chroma clear-paging");
829 return;
830 };
831 let collection = format!("clear_paging_{}", std::process::id());
832 let store = ChromaDBVectorStore::new(ChromaDBConfig::new(host, collection, 8))
833 .await
834 .expect("connect to live Chroma");
835 store.clear().await.expect("clear before seeding");
836
837 let docs: Vec<Document> = (0..1250)
839 .map(|i| Document::new(format!("doc {i}")))
840 .collect();
841 let embeddings: Vec<Vec<f32>> = (0..1250).map(|i| vec![i as f32; 8]).collect();
842 let _ = store
843 .add_documents(docs, embeddings)
844 .await
845 .expect("seed live Chroma");
846 let seeded = store.count().await;
847 assert!(seeded >= 1250, "expected all seeded docs, got {seeded}");
848
849 store.clear().await.expect("live clear");
850 assert_eq!(store.count().await, 0, "clear must remove across all pages");
851 }
852}