1use std::collections::HashMap;
11use std::future::Future;
12use std::pin::Pin;
13use std::time::Duration;
14
15use crate::vector_store::BoxFuture;
16use qdrant_client::Qdrant;
17use qdrant_client::qdrant::vector_output::Vector as VectorVariant;
18use qdrant_client::qdrant::{
19 CreateCollectionBuilder, DeletePointsBuilder, Distance, Filter, GetPointsBuilder, PointId,
20 PointStruct, PointsIdsList, QueryPointsBuilder, ScoredPoint, ScrollPointsBuilder,
21 UpsertPointsBuilder, VectorParamsBuilder, value::Kind,
22};
23
24type QdrantResult<T> = Result<T, Box<qdrant_client::QdrantError>>;
25
26const DEFAULT_TIMEOUT: Duration = Duration::from_secs(10);
32
33#[derive(Clone)]
35pub struct QdrantOps {
36 client: Qdrant,
37 timeout: Duration,
38}
39
40impl std::fmt::Debug for QdrantOps {
41 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
42 f.debug_struct("QdrantOps")
43 .field("timeout", &self.timeout)
44 .finish_non_exhaustive()
45 }
46}
47
48impl QdrantOps {
49 pub fn new(url: &str, api_key: Option<&str>) -> QdrantResult<Self> {
62 let mut builder = Qdrant::from_url(url);
63 if let Some(key) = api_key.filter(|k| !k.trim().is_empty()) {
64 builder = builder.api_key(key.trim());
65 }
66 let client = builder.build().map_err(Box::new)?;
67 Ok(Self {
68 client,
69 timeout: DEFAULT_TIMEOUT,
70 })
71 }
72
73 #[must_use]
90 pub fn with_timeout(mut self, timeout: Duration) -> Self {
91 self.timeout = timeout;
92 self
93 }
94
95 #[must_use]
104 pub fn client(&self) -> &Qdrant {
105 &self.client
106 }
107
108 async fn timed<T>(
118 &self,
119 fut: Pin<Box<dyn Future<Output = Result<T, qdrant_client::QdrantError>> + Send + '_>>,
120 ) -> QdrantResult<T> {
121 match tokio::time::timeout(self.timeout, fut).await {
122 Ok(result) => result.map_err(Box::new),
123 Err(_) => Err(Box::new(qdrant_client::QdrantError::Io(
124 std::io::Error::new(
125 std::io::ErrorKind::TimedOut,
126 format!("Qdrant gRPC call exceeded {:?}", self.timeout),
127 ),
128 ))),
129 }
130 }
131
132 #[tracing::instrument(name = "memory.qdrant.ensure_collection", skip_all, err)]
145 pub async fn ensure_collection(&self, collection: &str, vector_size: u64) -> QdrantResult<()> {
146 if self
147 .timed(Box::pin(self.client.collection_exists(collection)))
148 .await?
149 {
150 let existing_size = self.get_collection_vector_size(collection).await?;
151 if existing_size == Some(vector_size) {
152 return Ok(());
153 }
154 tracing::warn!(
155 collection,
156 existing = ?existing_size,
157 required = vector_size,
158 "vector dimension mismatch — recreating collection (existing data will be lost)"
159 );
160 self.timed(Box::pin(self.client.delete_collection(collection)))
161 .await?;
162 }
163 self.timed(Box::pin(
164 self.client.create_collection(
165 CreateCollectionBuilder::new(collection)
166 .vectors_config(VectorParamsBuilder::new(vector_size, Distance::Cosine)),
167 ),
168 ))
169 .await?;
170 Ok(())
171 }
172
173 #[tracing::instrument(
197 name = "memory.qdrant.get_collection_vector_size",
198 skip_all,
199 err,
200 level = "debug"
201 )]
202 pub async fn get_collection_vector_size(&self, collection: &str) -> QdrantResult<Option<u64>> {
203 let info = self
204 .timed(Box::pin(self.client.collection_info(collection)))
205 .await?;
206 let size = info
207 .result
208 .and_then(|r| r.config)
209 .and_then(|cfg| cfg.params)
210 .and_then(|params| params.vectors_config)
211 .and_then(|vc| vc.config)
212 .and_then(|cfg| match cfg {
213 qdrant_client::qdrant::vectors_config::Config::Params(vp) => Some(vp.size),
214 qdrant_client::qdrant::vectors_config::Config::ParamsMap(_) => None,
216 });
217 Ok(size)
218 }
219
220 #[tracing::instrument(name = "memory.qdrant.collection_exists", skip_all, err)]
226 pub async fn collection_exists(&self, collection: &str) -> QdrantResult<bool> {
227 self.timed(Box::pin(self.client.collection_exists(collection)))
228 .await
229 }
230
231 #[tracing::instrument(name = "memory.qdrant.delete_collection", skip_all, err)]
237 pub async fn delete_collection(&self, collection: &str) -> QdrantResult<()> {
238 self.timed(Box::pin(self.client.delete_collection(collection)))
239 .await?;
240 Ok(())
241 }
242
243 #[tracing::instrument(name = "memory.qdrant.upsert", skip_all, err)]
249 pub async fn upsert(&self, collection: &str, points: Vec<PointStruct>) -> QdrantResult<()> {
250 self.timed(Box::pin(self.client.upsert_points(
251 UpsertPointsBuilder::new(collection, points).wait(true),
252 )))
253 .await?;
254 Ok(())
255 }
256
257 #[tracing::instrument(name = "memory.qdrant.search", skip_all, err)]
266 pub async fn search(
267 &self,
268 collection: &str,
269 vector: Vec<f32>,
270 limit: u64,
271 filter: Option<Filter>,
272 ) -> QdrantResult<Vec<ScoredPoint>> {
273 let mut builder = QueryPointsBuilder::new(collection)
274 .query(vector)
275 .limit(limit)
276 .with_payload(true);
277 if let Some(f) = filter {
278 builder = builder.filter(f);
279 }
280 let results = self.timed(Box::pin(self.client.query(builder))).await?;
281 Ok(results.result)
282 }
283
284 #[tracing::instrument(name = "memory.qdrant.delete_by_ids", skip_all, err)]
290 pub async fn delete_by_ids(&self, collection: &str, ids: Vec<PointId>) -> QdrantResult<()> {
291 if ids.is_empty() {
292 return Ok(());
293 }
294 self.timed(Box::pin(
295 self.client.delete_points(
296 DeletePointsBuilder::new(collection)
297 .points(PointsIdsList { ids })
298 .wait(true),
299 ),
300 ))
301 .await?;
302 Ok(())
303 }
304
305 #[tracing::instrument(name = "memory.qdrant.scroll_all", skip_all, err)]
313 pub async fn scroll_all(
314 &self,
315 collection: &str,
316 key_field: &str,
317 ) -> QdrantResult<HashMap<String, HashMap<String, String>>> {
318 let mut result = HashMap::new();
319 let mut offset: Option<PointId> = None;
320
321 loop {
322 let mut builder = ScrollPointsBuilder::new(collection)
323 .with_payload(true)
324 .with_vectors(false)
325 .limit(100);
326
327 if let Some(ref off) = offset {
328 builder = builder.offset(off.clone());
329 }
330
331 let response = self.timed(Box::pin(self.client.scroll(builder))).await?;
332
333 for point in &response.result {
334 let Some(key_val) = point.payload.get(key_field) else {
335 continue;
336 };
337 let Some(Kind::StringValue(key)) = &key_val.kind else {
338 continue;
339 };
340
341 let mut fields = HashMap::new();
342 for (k, val) in &point.payload {
343 if let Some(Kind::StringValue(s)) = &val.kind {
344 fields.insert(k.clone(), s.clone());
345 }
346 }
347 result.insert(key.clone(), fields);
348 }
349
350 match response.next_page_offset {
351 Some(next) => offset = Some(next),
352 None => break,
353 }
354 }
355
356 Ok(result)
357 }
358
359 #[tracing::instrument(name = "memory.qdrant.scroll_all_with_point_ids", skip_all, err)]
368 pub async fn scroll_all_with_point_ids(
369 &self,
370 collection: &str,
371 key_field: &str,
372 ) -> QdrantResult<Vec<(String, HashMap<String, String>)>> {
373 let mut result = Vec::new();
374 let mut offset: Option<PointId> = None;
375
376 loop {
377 let mut builder = ScrollPointsBuilder::new(collection)
378 .with_payload(true)
379 .with_vectors(false)
380 .limit(100);
381
382 if let Some(ref off) = offset {
383 builder = builder.offset(off.clone());
384 }
385
386 let response = self.timed(Box::pin(self.client.scroll(builder))).await?;
387
388 for point in &response.result {
389 let Some(key_val) = point.payload.get(key_field) else {
390 continue;
391 };
392 let Some(Kind::StringValue(_)) = &key_val.kind else {
393 continue;
394 };
395 let Some(point_id_str) = point_id_to_string(point.id.clone()) else {
396 continue;
397 };
398
399 let mut fields = HashMap::new();
400 for (k, val) in &point.payload {
401 if let Some(Kind::StringValue(s)) = &val.kind {
402 fields.insert(k.clone(), s.clone());
403 }
404 }
405 result.push((point_id_str, fields));
406 }
407
408 match response.next_page_offset {
409 Some(next) => offset = Some(next),
410 None => break,
411 }
412 }
413
414 Ok(result)
415 }
416
417 #[tracing::instrument(
427 name = "memory.qdrant.ensure_collection_with_quantization",
428 skip_all,
429 err
430 )]
431 pub async fn ensure_collection_with_quantization(
432 &self,
433 collection: &str,
434 vector_size: u64,
435 keyword_fields: &[&str],
436 ) -> Result<(), crate::VectorStoreError> {
437 use qdrant_client::qdrant::{
438 CreateFieldIndexCollectionBuilder, FieldType, ScalarQuantizationBuilder,
439 };
440 if self
441 .timed(Box::pin(self.client.collection_exists(collection)))
442 .await
443 .map_err(|e| crate::VectorStoreError::Collection(e.to_string()))?
444 {
445 let existing_size = self
446 .get_collection_vector_size(collection)
447 .await
448 .map_err(|e| crate::VectorStoreError::Collection(e.to_string()))?;
449 if existing_size == Some(vector_size) {
450 return Ok(());
451 }
452 tracing::warn!(
453 collection,
454 existing = ?existing_size,
455 required = vector_size,
456 "vector dimension mismatch — recreating collection (existing data will be lost)"
457 );
458 self.timed(Box::pin(self.client.delete_collection(collection)))
459 .await
460 .map_err(|e| crate::VectorStoreError::Collection(e.to_string()))?;
461 }
462 self.timed(Box::pin(
463 self.client.create_collection(
464 CreateCollectionBuilder::new(collection)
465 .vectors_config(VectorParamsBuilder::new(vector_size, Distance::Cosine))
466 .quantization_config(ScalarQuantizationBuilder::default()),
467 ),
468 ))
469 .await
470 .map_err(|e| crate::VectorStoreError::Collection(e.to_string()))?;
471
472 for field in keyword_fields {
473 self.timed(Box::pin(self.client.create_field_index(
474 CreateFieldIndexCollectionBuilder::new(collection, *field, FieldType::Keyword),
475 )))
476 .await
477 .map_err(|e| crate::VectorStoreError::Collection(e.to_string()))?;
478 }
479 Ok(())
480 }
481
482 pub fn json_to_payload(
488 value: serde_json::Value,
489 ) -> Result<HashMap<String, qdrant_client::qdrant::Value>, serde_json::Error> {
490 serde_json::from_value(value)
491 }
492}
493
494impl crate::vector_store::VectorStore for QdrantOps {
495 fn ensure_collection(
496 &self,
497 collection: &str,
498 vector_size: u64,
499 ) -> BoxFuture<'_, Result<(), crate::VectorStoreError>> {
500 let collection = collection.to_owned();
501 Box::pin(async move {
502 self.ensure_collection(&collection, vector_size)
503 .await
504 .map_err(|e| crate::VectorStoreError::Collection(e.to_string()))
505 })
506 }
507
508 fn collection_exists(
509 &self,
510 collection: &str,
511 ) -> BoxFuture<'_, Result<bool, crate::VectorStoreError>> {
512 let collection = collection.to_owned();
513 Box::pin(async move {
514 self.collection_exists(&collection)
515 .await
516 .map_err(|e| crate::VectorStoreError::Collection(e.to_string()))
517 })
518 }
519
520 fn delete_collection(
521 &self,
522 collection: &str,
523 ) -> BoxFuture<'_, Result<(), crate::VectorStoreError>> {
524 let collection = collection.to_owned();
525 Box::pin(async move {
526 self.delete_collection(&collection)
527 .await
528 .map_err(|e| crate::VectorStoreError::Collection(e.to_string()))
529 })
530 }
531
532 fn upsert(
533 &self,
534 collection: &str,
535 points: Vec<crate::VectorPoint>,
536 ) -> BoxFuture<'_, Result<(), crate::VectorStoreError>> {
537 let collection = collection.to_owned();
538 Box::pin(async move {
539 let qdrant_points: Vec<PointStruct> = points
540 .into_iter()
541 .map(|p| {
542 let payload: HashMap<String, qdrant_client::qdrant::Value> =
543 serde_json::from_value(serde_json::Value::Object(
544 p.payload.into_iter().collect(),
545 ))
546 .unwrap_or_default();
547 PointStruct::new(p.id, p.vector, payload)
548 })
549 .collect();
550 self.upsert(&collection, qdrant_points)
551 .await
552 .map_err(|e| crate::VectorStoreError::Upsert(e.to_string()))
553 })
554 }
555
556 fn search(
557 &self,
558 collection: &str,
559 vector: Vec<f32>,
560 limit: u64,
561 filter: Option<crate::VectorFilter>,
562 ) -> BoxFuture<'_, Result<Vec<crate::ScoredVectorPoint>, crate::VectorStoreError>> {
563 let collection = collection.to_owned();
564 Box::pin(async move {
565 let qdrant_filter = filter.map(vector_filter_to_qdrant);
566 let results = self
567 .search(&collection, vector, limit, qdrant_filter)
568 .await
569 .map_err(|e| crate::VectorStoreError::Search(e.to_string()))?;
570 Ok(results.into_iter().map(scored_point_to_vector).collect())
571 })
572 }
573
574 fn delete_by_ids(
575 &self,
576 collection: &str,
577 ids: Vec<String>,
578 ) -> BoxFuture<'_, Result<(), crate::VectorStoreError>> {
579 let collection = collection.to_owned();
580 Box::pin(async move {
581 let point_ids: Vec<PointId> = ids.into_iter().map(PointId::from).collect();
582 self.delete_by_ids(&collection, point_ids)
583 .await
584 .map_err(|e| crate::VectorStoreError::Delete(e.to_string()))
585 })
586 }
587
588 fn scroll_all(
589 &self,
590 collection: &str,
591 key_field: &str,
592 ) -> BoxFuture<'_, Result<HashMap<String, HashMap<String, String>>, crate::VectorStoreError>>
593 {
594 let collection = collection.to_owned();
595 let key_field = key_field.to_owned();
596 Box::pin(async move {
597 self.scroll_all(&collection, &key_field)
598 .await
599 .map_err(|e| crate::VectorStoreError::Scroll(e.to_string()))
600 })
601 }
602
603 fn scroll_all_with_point_ids(
604 &self,
605 collection: &str,
606 key_field: &str,
607 ) -> BoxFuture<'_, Result<crate::vector_store::ScrollWithIdsResult, crate::VectorStoreError>>
608 {
609 let collection = collection.to_owned();
610 let key_field = key_field.to_owned();
611 Box::pin(async move {
612 self.scroll_all_with_point_ids(&collection, &key_field)
613 .await
614 .map_err(|e| crate::VectorStoreError::Scroll(e.to_string()))
615 })
616 }
617
618 fn health_check(&self) -> BoxFuture<'_, Result<bool, crate::VectorStoreError>> {
619 use tracing::Instrument as _;
620 Box::pin(
621 async move {
622 match self.timed(Box::pin(self.client.health_check())).await {
623 Ok(_) => Ok(true),
624 Err(e) => {
625 tracing::warn!(err = %e, "health_check failed");
626 Err(crate::VectorStoreError::Collection(e.to_string()))
627 }
628 }
629 }
630 .instrument(tracing::debug_span!("memory.qdrant.health_check")),
631 )
632 }
633
634 fn create_keyword_indexes(
635 &self,
636 collection: &str,
637 fields: &[&str],
638 ) -> BoxFuture<'_, Result<(), crate::VectorStoreError>> {
639 use qdrant_client::qdrant::{CreateFieldIndexCollectionBuilder, FieldType};
640 use tracing::Instrument as _;
641 let collection = collection.to_owned();
642 let fields: Vec<String> = fields.iter().map(|f| (*f).to_owned()).collect();
643 Box::pin(
644 async move {
645 for field in &fields {
646 self.timed(Box::pin(self.client.create_field_index(
647 CreateFieldIndexCollectionBuilder::new(
648 &collection,
649 field.as_str(),
650 FieldType::Keyword,
651 ),
652 )))
653 .await
654 .map_err(|e| crate::VectorStoreError::Collection(e.to_string()))?;
655 }
656 Ok(())
657 }
658 .instrument(tracing::debug_span!("memory.qdrant.create_keyword_indexes")),
659 )
660 }
661
662 fn get_points(
663 &self,
664 collection: &str,
665 ids: Vec<String>,
666 ) -> BoxFuture<'_, Result<Vec<crate::VectorPoint>, crate::VectorStoreError>> {
667 use tracing::Instrument as _;
668 let collection = collection.to_owned();
669 Box::pin(
670 async move {
671 if ids.is_empty() {
672 return Ok(Vec::new());
673 }
674 let point_ids: Vec<PointId> = ids.into_iter().map(PointId::from).collect();
675 let response = self
676 .timed(Box::pin(
677 self.client.get_points(
678 GetPointsBuilder::new(&collection, point_ids)
679 .with_vectors(true)
680 .with_payload(true),
681 ),
682 ))
683 .await
684 .map_err(|e| {
685 tracing::error!(err = %e, "get_points failed");
686 crate::VectorStoreError::Search(e.to_string())
687 })?;
688
689 let mut result = Vec::with_capacity(response.result.len());
690 for point in response.result {
691 let Some(id_str) = point_id_to_string(point.id) else {
692 continue;
693 };
694 let vector = match point.vectors.and_then(|v| v.get_vector()) {
696 Some(VectorVariant::Dense(dv)) => dv.data,
697 _ => continue,
698 };
699 let payload: HashMap<String, serde_json::Value> = point
700 .payload
701 .into_iter()
702 .filter_map(|(k, v)| {
703 let json = qdrant_value_to_json(v.kind?)?;
704 Some((k, json))
705 })
706 .collect();
707 result.push(crate::VectorPoint {
708 id: id_str,
709 vector,
710 payload,
711 });
712 }
713 Ok(result)
714 }
715 .instrument(tracing::debug_span!("memory.qdrant.get_points")),
716 )
717 }
718}
719
720fn vector_filter_to_qdrant(filter: crate::VectorFilter) -> Filter {
721 let must: Vec<_> = filter
722 .must
723 .into_iter()
724 .map(field_condition_to_qdrant)
725 .collect();
726 let must_not: Vec<_> = filter
727 .must_not
728 .into_iter()
729 .map(field_condition_to_qdrant)
730 .collect();
731
732 let mut f = Filter::default();
733 if !must.is_empty() {
734 f.must = must;
735 }
736 if !must_not.is_empty() {
737 f.must_not = must_not;
738 }
739 f
740}
741
742fn field_condition_to_qdrant(cond: crate::FieldCondition) -> qdrant_client::qdrant::Condition {
743 match cond.value {
744 crate::FieldValue::Integer(v) => qdrant_client::qdrant::Condition::matches(cond.field, v),
745 crate::FieldValue::Text(v) => qdrant_client::qdrant::Condition::matches(cond.field, v),
746 }
747}
748
749fn point_id_to_string(pid: Option<qdrant_client::qdrant::PointId>) -> Option<String> {
753 match pid?.point_id_options? {
754 qdrant_client::qdrant::point_id::PointIdOptions::Uuid(u) => Some(u),
755 qdrant_client::qdrant::point_id::PointIdOptions::Num(n) => Some(n.to_string()),
756 }
757}
758
759fn qdrant_value_to_json(kind: Kind) -> Option<serde_json::Value> {
763 match kind {
764 Kind::StringValue(s) => Some(serde_json::Value::String(s)),
765 Kind::IntegerValue(i) => Some(serde_json::Value::Number(i.into())),
766 Kind::DoubleValue(d) => serde_json::Number::from_f64(d).map(serde_json::Value::Number),
767 Kind::BoolValue(b) => Some(serde_json::Value::Bool(b)),
768 _ => None,
769 }
770}
771
772fn scored_point_to_vector(point: ScoredPoint) -> crate::ScoredVectorPoint {
773 let payload: HashMap<String, serde_json::Value> = point
774 .payload
775 .into_iter()
776 .filter_map(|(k, v)| Some((k, qdrant_value_to_json(v.kind?)?)))
777 .collect();
778
779 let id = point_id_to_string(point.id).unwrap_or_default();
780
781 crate::ScoredVectorPoint {
782 id,
783 score: point.score,
784 payload,
785 }
786}
787
788#[cfg(test)]
789mod tests {
790 use super::*;
791
792 #[test]
793 fn new_valid_url() {
794 let ops = QdrantOps::new("http://localhost:6334", None);
795 assert!(ops.is_ok());
796 }
797
798 #[test]
799 fn new_invalid_url() {
800 let ops = QdrantOps::new("not a valid url", None);
801 assert!(ops.is_err());
802 }
803
804 #[test]
807 fn new_empty_api_key_is_treated_as_none() {
808 let result = QdrantOps::new("http://127.0.0.1:9999", Some(""));
809 assert!(result.is_ok(), "empty key must not cause a build error");
810 }
811
812 #[test]
814 fn new_whitespace_api_key_is_treated_as_none() {
815 let result = QdrantOps::new("http://127.0.0.1:9999", Some(" "));
816 assert!(
817 result.is_ok(),
818 "whitespace-only key must not cause a build error"
819 );
820 }
821
822 #[test]
824 fn new_with_api_key_constructs_successfully() {
825 let result = QdrantOps::new("http://127.0.0.1:9999", Some("valid-key"));
826 assert!(result.is_ok(), "valid key must not cause a build error");
827 }
828
829 #[test]
832 fn with_timeout_overrides_default() {
833 let ops = QdrantOps::new("http://localhost:6334", None).unwrap();
834 assert!(format!("{ops:?}").contains("10s"), "default must be 10s");
835
836 let ops = ops.with_timeout(Duration::from_secs(2));
837 assert!(
838 format!("{ops:?}").contains("2s"),
839 "with_timeout must override the default"
840 );
841 }
842
843 #[tokio::test]
846 async fn timed_returns_error_instead_of_hanging() {
847 let ops = QdrantOps::new("http://localhost:6334", None)
848 .unwrap()
849 .with_timeout(Duration::from_millis(10));
850
851 let never_resolves: Pin<
852 Box<dyn Future<Output = Result<(), qdrant_client::QdrantError>> + Send>,
853 > = Box::pin(std::future::pending());
854
855 let result = ops.timed(never_resolves).await;
856 assert!(result.is_err(), "must time out instead of hanging forever");
857 }
858
859 #[test]
860 fn debug_format() {
861 let ops = QdrantOps::new("http://localhost:6334", None).unwrap();
862 let dbg = format!("{ops:?}");
863 assert!(dbg.contains("QdrantOps"));
864 }
865
866 #[test]
867 fn json_to_payload_valid() {
868 let value = serde_json::json!({"key": "value", "num": 42});
869 let result = QdrantOps::json_to_payload(value);
870 assert!(result.is_ok());
871 }
872
873 #[test]
874 fn json_to_payload_empty() {
875 let result = QdrantOps::json_to_payload(serde_json::json!({}));
876 assert!(result.is_ok());
877 assert!(result.unwrap().is_empty());
878 }
879
880 #[test]
881 fn delete_by_ids_empty_is_ok_sync() {
882 let ops = QdrantOps::new("http://localhost:6334", None);
886 assert!(ops.is_ok());
887 }
888
889 #[tokio::test]
891 #[ignore = "requires a live Qdrant instance at localhost:6334"]
892 async fn ensure_collection_with_quantization_idempotent() {
893 let ops = QdrantOps::new("http://localhost:6334", None).unwrap();
894 let collection = "test_quant_idempotent";
895
896 let _ = ops.delete_collection(collection).await;
898
899 ops.ensure_collection_with_quantization(collection, 128, &["language", "file_path"])
901 .await
902 .unwrap();
903
904 assert!(ops.collection_exists(collection).await.unwrap());
905
906 ops.ensure_collection_with_quantization(collection, 128, &["language", "file_path"])
908 .await
909 .unwrap();
910
911 ops.delete_collection(collection).await.unwrap();
913 }
914
915 #[tokio::test]
917 #[ignore = "requires a live Qdrant instance at localhost:6334"]
918 async fn delete_by_ids_empty_no_network_call() {
919 let ops = QdrantOps::new("http://localhost:6334", None).unwrap();
920 let result = ops.delete_by_ids("nonexistent_collection", vec![]).await;
922 assert!(result.is_ok());
923 }
924
925 #[tokio::test]
927 #[ignore = "requires a live Qdrant instance at localhost:6334"]
928 async fn ensure_collection_idempotent_same_size() {
929 let ops = QdrantOps::new("http://localhost:6334", None).unwrap();
930 let collection = "test_ensure_idempotent";
931
932 let _ = ops.delete_collection(collection).await;
933
934 ops.ensure_collection(collection, 128).await.unwrap();
935 assert!(ops.collection_exists(collection).await.unwrap());
936
937 ops.ensure_collection(collection, 128).await.unwrap();
939 assert!(ops.collection_exists(collection).await.unwrap());
940
941 ops.delete_collection(collection).await.unwrap();
942 }
943
944 #[tokio::test]
949 #[ignore = "requires a live Qdrant instance at localhost:6334"]
950 async fn ensure_collection_recreates_on_dimension_mismatch() {
951 let ops = QdrantOps::new("http://localhost:6334", None).unwrap();
952 let collection = "test_dim_mismatch";
953
954 let _ = ops.delete_collection(collection).await;
955
956 ops.ensure_collection(collection, 128).await.unwrap();
958 assert_eq!(
959 ops.get_collection_vector_size(collection).await.unwrap(),
960 Some(128)
961 );
962
963 ops.ensure_collection(collection, 256).await.unwrap();
965 assert_eq!(
966 ops.get_collection_vector_size(collection).await.unwrap(),
967 Some(256),
968 "collection must have been recreated with the new dimension"
969 );
970
971 ops.delete_collection(collection).await.unwrap();
972 }
973
974 #[tokio::test]
978 #[ignore = "requires a live Qdrant instance at localhost:6334"]
979 async fn ensure_collection_with_quantization_recreates_on_dimension_mismatch() {
980 let ops = QdrantOps::new("http://localhost:6334", None).unwrap();
981 let collection = "test_quant_dim_mismatch";
982
983 let _ = ops.delete_collection(collection).await;
984
985 ops.ensure_collection_with_quantization(collection, 128, &["language"])
986 .await
987 .unwrap();
988 assert_eq!(
989 ops.get_collection_vector_size(collection).await.unwrap(),
990 Some(128)
991 );
992
993 ops.ensure_collection_with_quantization(collection, 384, &["language"])
995 .await
996 .unwrap();
997 assert_eq!(
998 ops.get_collection_vector_size(collection).await.unwrap(),
999 Some(384),
1000 "collection must have been recreated with the new dimension"
1001 );
1002
1003 ops.delete_collection(collection).await.unwrap();
1004 }
1005}