Coverage for src / qdrant_loader_core / config / capabilities.py: 94%

32 statements  

« prev     ^ index     » next       coverage.py v7.13.5, created at 2026-06-11 09:34 +0000

1"""Qdrant collection vector-schema capabilities. 

2 

3A Pydantic model describing what a live Qdrant collection supports, plus a 

4parser that converts a fetched ``CollectionInfo`` into that model. I/O is the 

5caller's responsibility — we don't wrap ``client.get_collection`` here. 

6""" 

7 

8from __future__ import annotations 

9 

10from typing import Any 

11 

12from pydantic import BaseModel, ConfigDict 

13 

14from .sparse import SparseRuntimeConfig 

15 

16 

17class CollectionVectorCapabilities(BaseModel): 

18 """What a live Qdrant collection supports — derived from CollectionInfo.""" 

19 

20 model_config = ConfigDict(frozen=True) 

21 

22 has_named_dense: bool = False 

23 has_sparse: bool = False 

24 

25 @property 

26 def hybrid_ready(self) -> bool: 

27 """True when the collection has both a named dense vector and a sparse vector.""" 

28 return self.has_named_dense and self.has_sparse 

29 

30 

31def parse_collection_capabilities( 

32 info: Any, runtime: SparseRuntimeConfig 

33) -> CollectionVectorCapabilities: 

34 """Inspect a Qdrant ``CollectionInfo`` against the runtime config. 

35 

36 Resilient to both dict-shaped and Pydantic-model-shaped ``vectors`` / 

37 ``sparse_vectors`` fields, which differ across qdrant-client versions. 

38 """ 

39 params = getattr(getattr(info, "config", None), "params", None) 

40 vectors = getattr(params, "vectors", None) 

41 sparse_vectors = getattr(params, "sparse_vectors", None) 

42 

43 has_named_dense = runtime.dense_vector_name in _normalise_vector_map(vectors) 

44 has_sparse = runtime.sparse_vector_name in _normalise_vector_map(sparse_vectors) 

45 

46 return CollectionVectorCapabilities( 

47 has_named_dense=has_named_dense, has_sparse=has_sparse 

48 ) 

49 

50 

51def _normalise_vector_map(value: object) -> dict[str, Any]: 

52 """Coerce qdrant-client ``vectors`` / ``sparse_vectors`` field to a plain dict. 

53 

54 Older client versions return plain dicts; newer ones may return Pydantic 

55 models. Anything that isn't a mapping and doesn't expose ``model_dump`` / 

56 ``dict`` is treated as empty. 

57 """ 

58 if isinstance(value, dict): 

59 return value 

60 for attr in ("model_dump", "dict"): 

61 dumper = getattr(value, attr, None) 

62 if not callable(dumper): 

63 continue 

64 try: 

65 dumped = dumper() 

66 except Exception: 

67 return {} 

68 if isinstance(dumped, dict): 

69 return dumped 

70 return {}