1use crate::hybrid_native::{FusionMethod, NativeHybridQuery, NativeHybridSearch};
5use crate::{Document, FilterOp, MetadataFilter, SearchResult, VectorStore, VectorStoreError};
6use async_trait::async_trait;
7use qdrant_client::{
8 qdrant::{
9 Condition, CreateCollectionBuilder, DeletePointsBuilder, Distance, Filter, Fusion, PointId,
10 PointStruct, PrefetchQuery, PrefetchQueryBuilder, Query, QueryPointsBuilder, Range,
11 ScoredPoint, UpsertPointsBuilder, VectorParamsBuilder,
12 },
13 Payload, Qdrant,
14};
15use serde_json::Value;
16use std::collections::HashMap;
17use std::sync::Arc;
18use uuid::Uuid;
19
20#[derive(Debug, Clone)]
22pub struct QdrantConfig {
23 pub url: String,
25 pub collection_name: String,
27 pub vector_size: usize,
29 pub distance: QdrantDistance,
31}
32
33#[derive(Debug, Clone, Copy)]
35pub enum QdrantDistance {
36 Cosine,
38 Euclid,
40 Dot,
42}
43
44impl From<QdrantDistance> for Distance {
45 fn from(dist: QdrantDistance) -> Self {
46 match dist {
47 QdrantDistance::Cosine => Distance::Cosine,
48 QdrantDistance::Euclid => Distance::Euclid,
49 QdrantDistance::Dot => Distance::Dot,
50 }
51 }
52}
53
54impl Default for QdrantConfig {
55 fn default() -> Self {
56 Self {
57 url: "http://localhost:6334".to_string(),
58 collection_name: "langchainrust".to_string(),
59 vector_size: 1536,
60 distance: QdrantDistance::Cosine,
61 }
62 }
63}
64
65impl QdrantConfig {
66 pub fn new(url: impl Into<String>, collection_name: impl Into<String>) -> Self {
68 Self {
69 url: url.into(),
70 collection_name: collection_name.into(),
71 ..Default::default()
72 }
73 }
74
75 pub fn with_vector_size(mut self, size: usize) -> Self {
77 self.vector_size = size;
78 self
79 }
80
81 pub fn with_distance(mut self, distance: QdrantDistance) -> Self {
83 self.distance = distance;
84 self
85 }
86}
87
88pub struct QdrantVectorStore {
90 client: Arc<Qdrant>,
91 config: QdrantConfig,
92}
93
94impl QdrantVectorStore {
95 pub async fn new(config: QdrantConfig) -> Result<Self, VectorStoreError> {
97 let client = Qdrant::from_url(&config.url).build().map_err(|e| {
98 VectorStoreError::ConnectionError(format!("failed to connect to Qdrant: {}", e))
99 })?;
100
101 let client = Arc::new(client);
102
103 let exists = client
104 .collection_exists(&config.collection_name)
105 .await
106 .map_err(|e| {
107 VectorStoreError::StorageError(format!("failed to check collection: {}", e))
108 })?;
109
110 if !exists {
111 client
112 .create_collection(
113 CreateCollectionBuilder::new(&config.collection_name).vectors_config(
114 VectorParamsBuilder::new(
115 config.vector_size as u64,
116 Distance::from(config.distance),
117 ),
118 ),
119 )
120 .await
121 .map_err(|e| {
122 VectorStoreError::StorageError(format!("failed to create collection: {}", e))
123 })?;
124 }
125
126 Ok(Self { client, config })
127 }
128
129 pub async fn from_env() -> Result<Self, VectorStoreError> {
131 let url =
132 std::env::var("QDRANT_URL").unwrap_or_else(|_| "http://localhost:6334".to_string());
133 let collection_name =
134 std::env::var("QDRANT_COLLECTION").unwrap_or_else(|_| "langchainrust".to_string());
135
136 Self::new(QdrantConfig::new(url, collection_name)).await
137 }
138
139 pub async fn delete_by_metadata(
141 &self,
142 key: &str,
143 value: &str,
144 ) -> Result<usize, VectorStoreError> {
145 let filter = Filter::must([Condition::matches(key, value.to_string())]);
146
147 let total = self.count().await as u64;
150 let matched = self
151 .client
152 .query(
153 QueryPointsBuilder::new(&self.config.collection_name)
154 .query(vec![0.0; self.config.vector_size])
155 .filter(filter.clone())
156 .limit(total.max(1))
157 .with_payload(false),
158 )
159 .await
160 .map_err(|e| {
161 VectorStoreError::StorageError(format!(
162 "failed to count matching points by metadata: {}",
163 e
164 ))
165 })?;
166
167 let deleted = matched.result.len();
168
169 if deleted > 0 {
170 self.client
171 .delete_points(
172 DeletePointsBuilder::new(&self.config.collection_name).points(filter),
173 )
174 .await
175 .map_err(|e| {
176 VectorStoreError::StorageError(format!(
177 "failed to delete points by metadata: {}",
178 e
179 ))
180 })?;
181 }
182
183 Ok(deleted)
184 }
185
186 fn build_query_builder(
191 &self,
192 query_embedding: &[f32],
193 k: usize,
194 filter: Option<&MetadataFilter>,
195 ) -> Result<QueryPointsBuilder, VectorStoreError> {
196 if query_embedding.len() != self.config.vector_size {
197 return Err(VectorStoreError::StorageError(format!(
198 "query vector dimension mismatch: expected {}, got {}",
199 self.config.vector_size,
200 query_embedding.len()
201 )));
202 }
203
204 let mut builder = QueryPointsBuilder::new(&self.config.collection_name)
205 .query(query_embedding.to_vec())
206 .limit(k as u64)
207 .with_payload(true);
208
209 if let Some(f) = filter {
210 builder = builder.filter(filter_to_qdrant(f)?);
211 }
212
213 Ok(builder)
214 }
215
216 async fn search_impl(
219 &self,
220 builder: QueryPointsBuilder,
221 ) -> Result<Vec<SearchResult>, VectorStoreError> {
222 let search_result = self
223 .client
224 .query(builder)
225 .await
226 .map_err(|e| VectorStoreError::StorageError(format!("search failed: {}", e)))?;
227
228 Ok(search_result
229 .result
230 .into_iter()
231 .map(scored_point_to_result)
232 .collect())
233 }
234}
235
236pub(crate) fn scored_point_to_result(scored_point: ScoredPoint) -> SearchResult {
242 let payload = scored_point.payload;
243
244 let content = payload
245 .get("content")
246 .and_then(|v| v.as_str())
247 .map(|s| s.as_str())
248 .unwrap_or("")
249 .to_string();
250
251 let id = payload
252 .get("doc_id")
253 .and_then(|v| v.as_str())
254 .map(|s| s.to_string());
255
256 let mut metadata = HashMap::new();
257 for (key, value) in &payload {
258 if key != "content" && key != "doc_id" {
259 if let Some(s) = value.as_str() {
260 metadata.insert(key.clone(), s.clone().into());
261 }
262 }
263 }
264
265 SearchResult {
266 document: Document {
267 content,
268 metadata,
269 id,
270 },
271 score: scored_point.score,
272 }
273}
274
275fn match_condition(key: &str, value: &Value) -> Result<Condition, VectorStoreError> {
286 match value {
287 Value::String(s) => Ok(Condition::matches(key, s.clone())),
288 Value::Bool(b) => Ok(Condition::matches(key, *b)),
289 Value::Number(n) => {
290 let int = n
291 .as_i64()
292 .or_else(|| n.as_f64().filter(|f| f.fract() == 0.0).map(|f| f as i64));
293 match int {
294 Some(i) => Ok(Condition::matches(key, i)),
295 None => Err(VectorStoreError::UnsupportedFilter(format!(
296 "Qdrant match condition requires an integer, string, or boolean value, got {n}"
297 ))),
298 }
299 }
300 other => Err(VectorStoreError::UnsupportedFilter(format!(
301 "Qdrant match condition requires a scalar value, got {other}"
302 ))),
303 }
304}
305
306fn field_to_condition(
312 key: &str,
313 op: FilterOp,
314 value: &Value,
315) -> Result<Condition, VectorStoreError> {
316 match op {
317 FilterOp::Eq => match_condition(key, value),
318 FilterOp::Ne => Ok(Condition::from(Filter::must_not([match_condition(
319 key, value,
320 )?]))),
321 FilterOp::Gt | FilterOp::Gte | FilterOp::Lt | FilterOp::Lte => {
322 let num = value.as_f64().ok_or_else(|| {
323 VectorStoreError::UnsupportedFilter(format!(
324 "Qdrant range condition requires a numeric value, got {value}"
325 ))
326 })?;
327 let mut range = Range::default();
328 match op {
329 FilterOp::Gt => range.gt = Some(num),
330 FilterOp::Gte => range.gte = Some(num),
331 FilterOp::Lt => range.lt = Some(num),
332 FilterOp::Lte => range.lte = Some(num),
333 _ => unreachable!(),
334 }
335 Ok(Condition::range(key, range))
336 }
337 FilterOp::In | FilterOp::Nin => {
338 let set = value.as_array().ok_or_else(|| {
339 VectorStoreError::UnsupportedFilter(format!(
340 "Qdrant {:?} condition requires an array value, got {value}",
341 op
342 ))
343 })?;
344 if set.is_empty() && op == FilterOp::In {
346 return Err(VectorStoreError::UnsupportedFilter(
347 "Qdrant In condition with an empty array cannot be expressed".to_string(),
348 ));
349 }
350 let conds: Result<Vec<Condition>, _> =
351 set.iter().map(|v| match_condition(key, v)).collect();
352 match op {
353 FilterOp::In => Ok(Condition::from(Filter::should(conds?))),
354 FilterOp::Nin => Ok(Condition::from(Filter::must_not(conds?))),
355 _ => unreachable!(),
356 }
357 }
358 }
359}
360
361fn to_condition(filter: &MetadataFilter) -> Result<Condition, VectorStoreError> {
368 match filter {
369 MetadataFilter::Field { key, op, value } => field_to_condition(key, *op, value),
370 MetadataFilter::And(items) => {
371 let conds: Result<Vec<Condition>, _> = items.iter().map(to_condition).collect();
372 Ok(Condition::from(Filter::must(conds?)))
373 }
374 MetadataFilter::Or(items) => {
375 let conds: Result<Vec<Condition>, _> = items.iter().map(to_condition).collect();
376 Ok(Condition::from(Filter::should(conds?)))
377 }
378 }
379}
380
381pub fn filter_to_qdrant(filter: &MetadataFilter) -> Result<Filter, VectorStoreError> {
386 Ok(Filter::must([to_condition(filter)?]))
387}
388
389#[async_trait]
390impl VectorStore for QdrantVectorStore {
391 async fn add_documents(
392 &self,
393 documents: Vec<Document>,
394 embeddings: Vec<Vec<f32>>,
395 ) -> Result<Vec<String>, VectorStoreError> {
396 if documents.len() != embeddings.len() {
397 return Err(VectorStoreError::StorageError(
398 "document count and embedding count mismatch".to_string(),
399 ));
400 }
401
402 if documents.is_empty() {
403 return Ok(Vec::new());
404 }
405
406 for embedding in &embeddings {
407 if embedding.len() != self.config.vector_size {
408 return Err(VectorStoreError::StorageError(format!(
409 "vector dimension mismatch: expected {}, got {}",
410 self.config.vector_size,
411 embedding.len()
412 )));
413 }
414 }
415
416 let mut ids = Vec::new();
417 let mut points = Vec::new();
418
419 for (doc, embedding) in documents.into_iter().zip(embeddings) {
420 let user_id = doc.id.clone().unwrap_or_else(|| Uuid::new_v4().to_string());
421
422 let internal_uuid = Uuid::new_v4();
424 let point_id = PointId::from(internal_uuid.to_string());
425
426 let mut payload = Payload::new();
427 payload.insert("content", doc.content.clone());
428 payload.insert("doc_id", user_id.clone()); for (key, value) in &doc.metadata {
431 payload.insert(key.clone(), value.clone());
432 }
433
434 let point = PointStruct::new(point_id, embedding, payload);
435 points.push(point);
436 ids.push(user_id);
437 }
438
439 self.client
440 .upsert_points(UpsertPointsBuilder::new(
441 &self.config.collection_name,
442 points,
443 ))
444 .await
445 .map_err(|e| {
446 VectorStoreError::StorageError(format!("failed to insert documents: {}", e))
447 })?;
448
449 Ok(ids)
450 }
451
452 async fn similarity_search(
453 &self,
454 query_embedding: &[f32],
455 k: usize,
456 ) -> Result<Vec<SearchResult>, VectorStoreError> {
457 let builder = self.build_query_builder(query_embedding, k, None)?;
458 self.search_impl(builder).await
459 }
460
461 async fn similarity_search_with_filter(
463 &self,
464 query_embedding: &[f32],
465 k: usize,
466 filter: Option<&MetadataFilter>,
467 ) -> Result<Vec<SearchResult>, VectorStoreError> {
468 let builder = self.build_query_builder(query_embedding, k, filter)?;
469 self.search_impl(builder).await
470 }
471
472 async fn get_document(&self, id: &str) -> Result<Option<Document>, VectorStoreError> {
473 let filter = Filter::must([Condition::matches("doc_id", id.to_string())]);
474
475 let results = self
476 .client
477 .query(
478 QueryPointsBuilder::new(&self.config.collection_name)
479 .query(vec![0.0; self.config.vector_size])
480 .filter(filter)
481 .limit(1)
482 .with_payload(true),
483 )
484 .await
485 .map_err(|e| {
486 VectorStoreError::StorageError(format!("failed to get document: {}", e))
487 })?;
488
489 if let Some(point) = results.result.first() {
490 let payload_map = point.payload.clone();
491
492 let content = payload_map
493 .get("content")
494 .and_then(|v| v.as_str())
495 .map(|s| s.as_str())
496 .unwrap_or("")
497 .to_string();
498
499 let doc_id = payload_map
500 .get("doc_id")
501 .and_then(|v| v.as_str())
502 .map(|s| s.to_string());
503
504 let mut metadata = HashMap::new();
505 for (key, value) in &payload_map {
506 if key != "content" && key != "doc_id" {
507 if let Some(s) = value.as_str() {
508 metadata.insert(key.clone(), s.clone().into());
509 }
510 }
511 }
512
513 Ok(Some(Document {
514 content,
515 metadata,
516 id: doc_id,
517 }))
518 } else {
519 Ok(None)
520 }
521 }
522
523 async fn get_embedding(&self, id: &str) -> Result<Option<Vec<f32>>, VectorStoreError> {
524 let filter = Filter::must([Condition::matches("doc_id", id.to_string())]);
525
526 let results = self
527 .client
528 .query(
529 QueryPointsBuilder::new(&self.config.collection_name)
530 .query(vec![0.0; self.config.vector_size])
531 .filter(filter)
532 .limit(1)
533 .with_payload(true),
534 )
535 .await
536 .map_err(|e| VectorStoreError::StorageError(format!("failed to get vector: {}", e)))?;
537
538 if let Some(point) = results.result.first() {
539 if let Some(vectors) = &point.vectors {
540 if let Some(qdrant_client::qdrant::vector_output::Vector::Dense(dense)) =
541 vectors.get_vector()
542 {
543 return Ok(Some(dense.data.clone()));
544 }
545 }
546 }
547 Ok(None)
548 }
549
550 async fn delete_document(&self, id: &str) -> Result<(), VectorStoreError> {
551 let filter = Filter::must([Condition::matches("doc_id", id.to_string())]);
552
553 self.client
554 .delete_points(DeletePointsBuilder::new(&self.config.collection_name).points(filter))
555 .await
556 .map_err(|e| {
557 VectorStoreError::StorageError(format!("failed to delete document: {}", e))
558 })?;
559
560 Ok(())
561 }
562
563 async fn count(&self) -> usize {
564 let info = self
565 .client
566 .collection_info(&self.config.collection_name)
567 .await;
568
569 info.map(|i| i.result.and_then(|r| r.points_count).unwrap_or(0) as usize)
570 .unwrap_or(0)
571 }
572
573 async fn clear(&self) -> Result<(), VectorStoreError> {
574 let collection_name = self.config.collection_name.clone();
575
576 self.client
577 .delete_collection(&collection_name)
578 .await
579 .map_err(|e| {
580 VectorStoreError::StorageError(format!("failed to delete collection: {}", e))
581 })?;
582
583 self.client
584 .create_collection(
585 CreateCollectionBuilder::new(&collection_name).vectors_config(
586 VectorParamsBuilder::new(
587 self.config.vector_size as u64,
588 Distance::from(self.config.distance),
589 ),
590 ),
591 )
592 .await
593 .map_err(|e| {
594 VectorStoreError::StorageError(format!("failed to recreate collection: {}", e))
595 })?;
596
597 Ok(())
598 }
599}
600
601#[cfg(test)]
602mod tests {
603 use super::*;
604
605 #[test]
606 fn test_config_default() {
607 let config = QdrantConfig::default();
608 assert_eq!(config.url, "http://localhost:6334");
609 assert_eq!(config.collection_name, "langchainrust");
610 assert_eq!(config.vector_size, 1536);
611 }
612
613 #[test]
614 fn test_config_builder() {
615 let config = QdrantConfig::new("http://custom:6334", "test_collection")
616 .with_vector_size(3072)
617 .with_distance(QdrantDistance::Euclid);
618
619 assert_eq!(config.url, "http://custom:6334");
620 assert_eq!(config.collection_name, "test_collection");
621 assert_eq!(config.vector_size, 3072);
622 assert!(matches!(config.distance, QdrantDistance::Euclid));
623 }
624
625 #[test]
627 fn test_filter_to_qdrant_eq() {
628 let f = filter_to_qdrant(&MetadataFilter::field("lang", FilterOp::Eq, "rust")).unwrap();
629 let expected = Filter::must([Condition::matches("lang", "rust".to_string())]);
630 assert_eq!(f, expected);
631 }
632
633 #[test]
635 fn test_filter_to_qdrant_ne_number() {
636 let f = filter_to_qdrant(&MetadataFilter::field("year", FilterOp::Ne, 2020.0)).unwrap();
637 let expected = Filter::must([Condition::from(Filter::must_not([Condition::matches(
638 "year", 2020_i64,
639 )]))]);
640 assert_eq!(f, expected);
641 }
642
643 #[test]
645 fn test_filter_to_qdrant_range() {
646 let f = filter_to_qdrant(&MetadataFilter::field("year", FilterOp::Gte, 2020)).unwrap();
647 let expected = Filter::must([Condition::range(
648 "year",
649 Range {
650 gte: Some(2020.0),
651 ..Default::default()
652 },
653 )]);
654 assert_eq!(f, expected);
655 }
656
657 #[test]
659 fn test_filter_to_qdrant_in_nin() {
660 let f =
661 filter_to_qdrant(&MetadataFilter::field("tag", FilterOp::In, vec!["a", "b"])).unwrap();
662 let expected = Filter::must([Condition::from(Filter::should([
663 Condition::matches("tag", "a".to_string()),
664 Condition::matches("tag", "b".to_string()),
665 ]))]);
666 assert_eq!(f, expected);
667
668 let f = filter_to_qdrant(&MetadataFilter::field("tag", FilterOp::Nin, vec!["a"])).unwrap();
669 let expected = Filter::must([Condition::from(Filter::must_not([Condition::matches(
670 "tag",
671 "a".to_string(),
672 )]))]);
673 assert_eq!(f, expected);
674 }
675
676 #[test]
678 fn test_filter_to_qdrant_and_or() {
679 let f = MetadataFilter::and(vec![
680 MetadataFilter::field("lang", FilterOp::Eq, "rust"),
681 MetadataFilter::or(vec![
682 MetadataFilter::field("year", FilterOp::Gte, 2020),
683 MetadataFilter::field("tag", FilterOp::In, vec!["ml"]),
684 ]),
685 ]);
686 let expected = Filter::must([Condition::from(Filter::must([
687 Condition::matches("lang", "rust".to_string()),
688 Condition::from(Filter::should([
689 Condition::range(
690 "year",
691 Range {
692 gte: Some(2020.0),
693 ..Default::default()
694 },
695 ),
696 Condition::from(Filter::should([Condition::matches(
697 "tag",
698 "ml".to_string(),
699 )])),
700 ])),
701 ]))]);
702 assert_eq!(filter_to_qdrant(&f).unwrap(), expected);
703 }
704
705 #[test]
707 fn test_filter_to_qdrant_unsupported() {
708 let float_eq = filter_to_qdrant(&MetadataFilter::field("score", FilterOp::Eq, 0.5));
710 assert!(matches!(
711 float_eq,
712 Err(VectorStoreError::UnsupportedFilter(_))
713 ));
714
715 let range_on_str = filter_to_qdrant(&MetadataFilter::field("year", FilterOp::Gt, "abc"));
717 assert!(matches!(
718 range_on_str,
719 Err(VectorStoreError::UnsupportedFilter(_))
720 ));
721
722 let empty_in = filter_to_qdrant(&MetadataFilter::field(
724 "tag",
725 FilterOp::In,
726 Vec::<String>::new(),
727 ));
728 assert!(matches!(
729 empty_in,
730 Err(VectorStoreError::UnsupportedFilter(_))
731 ));
732 }
733}
734
735#[async_trait]
740impl NativeHybridSearch for QdrantVectorStore {
741 fn supports_native_hybrid(&self) -> bool {
745 true
746 }
747
748 async fn native_hybrid_search(
749 &self,
750 query: &NativeHybridQuery,
751 ) -> Result<Vec<SearchResult>, VectorStoreError> {
752 let prefetch_limit = query.effective_prefetch_limit();
754 let prefetches: Vec<PrefetchQuery> = query
755 .query_vectors
756 .iter()
757 .map(|vector| {
758 PrefetchQueryBuilder::default()
759 .query(vector.clone())
760 .limit(prefetch_limit)
761 .build()
762 })
763 .collect();
764
765 let mut builder = QueryPointsBuilder::new(&self.config.collection_name)
766 .prefetch(prefetches)
767 .query(qdrant_client::qdrant::Query::new_fusion(Fusion::from(
768 query.fusion,
769 )))
770 .limit(query.limit as u64)
771 .with_payload(true);
772
773 if let Some(filter) = &query.filter {
774 builder = builder.filter(filter_to_qdrant(filter)?);
775 }
776
777 self.search_impl(builder).await
778 }
779}
780
781#[cfg(test)]
782mod hybrid_native_tests {
783 use super::*;
784 use crate::hybrid_native::fixtures::scored_point as fixture_point;
785
786 #[test]
789 fn scored_point_maps_to_search_result() {
790 let result = scored_point_to_result(fixture_point(1, 0.98, "rust doc", "docs"));
791 assert_eq!(result.document.content, "rust doc");
792 assert_eq!(result.document.id, None, "no doc_id payload -> no id");
793 assert_eq!(result.score, 0.98);
794 assert_eq!(
795 result
796 .document
797 .metadata
798 .get("source")
799 .and_then(|v| v.as_str()),
800 Some("docs")
801 );
802 assert!(
803 !result.document.metadata.contains_key("content"),
804 "content is not duplicated into metadata"
805 );
806 }
807
808 #[test]
810 fn doc_id_payload_becomes_document_id() {
811 let mut point = fixture_point(2, 0.9, "hello", "docs");
812 point.payload.insert(
813 "doc_id".to_string(),
814 qdrant_client::qdrant::Value::from("doc-42"),
815 );
816 let result = scored_point_to_result(point);
817 assert_eq!(result.document.id.as_deref(), Some("doc-42"));
818 }
819
820 #[tokio::test]
825 #[ignore = "requires a live Qdrant server (QDRANT_URL)"]
826 async fn native_hybrid_search_end_to_end() {
827 let url = std::env::var("QDRANT_URL").unwrap_or_else(|_| "http://localhost:6334".into());
828 let store = QdrantVectorStore::new(
829 crate::QdrantConfig::new(url, "lc-native-hybrid-test")
830 .with_vector_size(4)
831 .with_distance(crate::QdrantDistance::Dot),
832 )
833 .await
834 .expect("connect to Qdrant");
835
836 let docs = vec![
837 Document::new("alpha document"),
838 Document::new("beta document"),
839 Document::new("gamma document"),
840 ];
841 let embeddings = vec![
842 vec![1.0, 0.0, 0.0, 0.0],
843 vec![0.0, 1.0, 0.0, 0.0],
844 vec![0.0, 0.0, 1.0, 0.0],
845 ];
846 store.add_documents(docs, embeddings).await.unwrap();
847
848 let query =
849 NativeHybridQuery::new(vec![vec![1.0, 0.0, 0.0, 0.0], vec![0.9, 0.1, 0.0, 0.0]], 2)
850 .unwrap();
851 assert!(store.supports_native_hybrid());
852 let results = store.native_hybrid_search(&query).await.unwrap();
853 assert_eq!(results.len(), 2, "fused top-2");
854 assert_eq!(results[0].document.content, "alpha document");
855 }
856}