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
« prev ^ index » next coverage.py v7.13.5, created at 2026-06-11 09:34 +0000
1"""Qdrant collection vector-schema capabilities.
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"""
8from __future__ import annotations
10from typing import Any
12from pydantic import BaseModel, ConfigDict
14from .sparse import SparseRuntimeConfig
17class CollectionVectorCapabilities(BaseModel):
18 """What a live Qdrant collection supports — derived from CollectionInfo."""
20 model_config = ConfigDict(frozen=True)
22 has_named_dense: bool = False
23 has_sparse: bool = False
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
31def parse_collection_capabilities(
32 info: Any, runtime: SparseRuntimeConfig
33) -> CollectionVectorCapabilities:
34 """Inspect a Qdrant ``CollectionInfo`` against the runtime config.
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)
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)
46 return CollectionVectorCapabilities(
47 has_named_dense=has_named_dense, has_sparse=has_sparse
48 )
51def _normalise_vector_map(value: object) -> dict[str, Any]:
52 """Coerce qdrant-client ``vectors`` / ``sparse_vectors`` field to a plain dict.
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 {}