xyberos-qdrant 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,68 @@
1
+ Metadata-Version: 2.4
2
+ Name: xyberos-qdrant
3
+ Version: 0.1.0
4
+ Summary: Qdrant VectorStore plugin (RFC-0019, M4): hosted or in-memory Qdrant as an xyberos VectorStore
5
+ License: Apache-2.0
6
+ Keywords: xyberos,plugin,vector,qdrant,rag
7
+ Requires-Python: >=3.10
8
+ Description-Content-Type: text/markdown
9
+ Requires-Dist: xyberos>=1.0
10
+ Provides-Extra: vectors
11
+ Requires-Dist: qdrant-client; extra == "vectors"
12
+ Provides-Extra: test
13
+ Requires-Dist: pytest; extra == "test"
14
+
15
+ # xyberos-qdrant
16
+
17
+ **Qdrant `VectorStore` plugin — RFC-0019, M4.** Hosted or local [Qdrant](https://qdrant.tech)
18
+ as an `xyberos` `VectorStore`. Passes the same parity scenarios as the stdlib
19
+ `SqliteVectorStore` (the M4 Definition of Done).
20
+
21
+ ## Install
22
+
23
+ ```bash
24
+ pip install -e ./qdrant
25
+ pip install qdrant-client # or: pip install xyberos[vectors]
26
+ ```
27
+
28
+ ## Usage
29
+
30
+ ```python
31
+ from xyberos import create_app
32
+ from xyberos_qdrant import QdrantPlugin
33
+
34
+ app = create_app()
35
+ app.load_plugin(QdrantPlugin(location=":memory:")) # local, no infrastructure
36
+ # hosted: QdrantPlugin(url="https://...", api_key=os.getenv("QDRANT_API_KEY"))
37
+
38
+ store = app.resolve("vector_store")
39
+ store.upsert("ns", "doc-1", [1.0, 0.0, 0.0, 0.0], {"text": "alpha"})
40
+ hits = store.query("ns", [1.0, 0.0, 0.0, 0.0], top_k=3)
41
+ ```
42
+
43
+ Or configure through the environment (`QDRANT_URL`, `QDRANT_API_KEY`,
44
+ `QDRANT_DIM`) and let the `xyberos.plugins` entry point register it.
45
+
46
+ ## Design
47
+
48
+ - Each **namespace** maps to a Qdrant collection using **cosine** distance
49
+ (higher score = more relevant, matching the `VectorStore` contract).
50
+ - The client is imported **lazily** and a clear `ProviderError` is raised when
51
+ `qdrant-client` is missing.
52
+ - Arbitrary string ids are mapped to deterministic UUIDs (Qdrant point ids are
53
+ `int`/`UUID`); the original id round-trips through the payload, so the
54
+ contract's string ids hold exactly.
55
+
56
+ ## Tests
57
+
58
+ ```bash
59
+ pip install pytest
60
+ pytest tests/ # unit + parity + plugin tests
61
+ ```
62
+
63
+ Parity tests run against Qdrant's **in-memory local mode**, so they need no
64
+ server; they skip cleanly if `qdrant-client` is absent.
65
+
66
+ ## Ship location
67
+
68
+ `[vectors]` extra (`qdrant-client` already added to `pyproject.toml`).
@@ -0,0 +1,54 @@
1
+ # xyberos-qdrant
2
+
3
+ **Qdrant `VectorStore` plugin — RFC-0019, M4.** Hosted or local [Qdrant](https://qdrant.tech)
4
+ as an `xyberos` `VectorStore`. Passes the same parity scenarios as the stdlib
5
+ `SqliteVectorStore` (the M4 Definition of Done).
6
+
7
+ ## Install
8
+
9
+ ```bash
10
+ pip install -e ./qdrant
11
+ pip install qdrant-client # or: pip install xyberos[vectors]
12
+ ```
13
+
14
+ ## Usage
15
+
16
+ ```python
17
+ from xyberos import create_app
18
+ from xyberos_qdrant import QdrantPlugin
19
+
20
+ app = create_app()
21
+ app.load_plugin(QdrantPlugin(location=":memory:")) # local, no infrastructure
22
+ # hosted: QdrantPlugin(url="https://...", api_key=os.getenv("QDRANT_API_KEY"))
23
+
24
+ store = app.resolve("vector_store")
25
+ store.upsert("ns", "doc-1", [1.0, 0.0, 0.0, 0.0], {"text": "alpha"})
26
+ hits = store.query("ns", [1.0, 0.0, 0.0, 0.0], top_k=3)
27
+ ```
28
+
29
+ Or configure through the environment (`QDRANT_URL`, `QDRANT_API_KEY`,
30
+ `QDRANT_DIM`) and let the `xyberos.plugins` entry point register it.
31
+
32
+ ## Design
33
+
34
+ - Each **namespace** maps to a Qdrant collection using **cosine** distance
35
+ (higher score = more relevant, matching the `VectorStore` contract).
36
+ - The client is imported **lazily** and a clear `ProviderError` is raised when
37
+ `qdrant-client` is missing.
38
+ - Arbitrary string ids are mapped to deterministic UUIDs (Qdrant point ids are
39
+ `int`/`UUID`); the original id round-trips through the payload, so the
40
+ contract's string ids hold exactly.
41
+
42
+ ## Tests
43
+
44
+ ```bash
45
+ pip install pytest
46
+ pytest tests/ # unit + parity + plugin tests
47
+ ```
48
+
49
+ Parity tests run against Qdrant's **in-memory local mode**, so they need no
50
+ server; they skip cleanly if `qdrant-client` is absent.
51
+
52
+ ## Ship location
53
+
54
+ `[vectors]` extra (`qdrant-client` already added to `pyproject.toml`).
@@ -0,0 +1,27 @@
1
+ [build-system]
2
+ requires = ["setuptools>=61.0"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "xyberos-qdrant"
7
+ version = "0.1.0"
8
+ description = "Qdrant VectorStore plugin (RFC-0019, M4): hosted or in-memory Qdrant as an xyberos VectorStore"
9
+ readme = "README.md"
10
+ requires-python = ">=3.10"
11
+ license = {text = "Apache-2.0"}
12
+ dependencies = ["xyberos>=1.0"]
13
+ keywords = ["xyberos", "plugin", "vector", "qdrant", "rag"]
14
+
15
+ [project.optional-dependencies]
16
+ vectors = ["qdrant-client"]
17
+ test = ["pytest"]
18
+
19
+ [project.entry-points."xyberos.plugins"]
20
+ qdrant = "xyberos_qdrant.plugin:plugin"
21
+
22
+ [tool.setuptools]
23
+ packages = ["xyberos_qdrant"]
24
+
25
+ [tool.pytest.ini_options]
26
+ testpaths = ["tests"]
27
+ pythonpath = ["."]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,49 @@
1
+ """M4 parity: QdrantVectorStore must behave like SqliteVectorStore."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import pytest
6
+ from xyberos.vector import SqliteVectorStore
7
+
8
+ from xyberos_qdrant import QdrantVectorStore
9
+
10
+ QUERY = [1.0, 0.0, 0.0, 0.0]
11
+
12
+
13
+ def run_parity_scenarios(store) -> None:
14
+ """The full VectorStore contract; every adapter must pass these."""
15
+ store.upsert("ns", "a", [1.0, 0.0, 0.0, 0.0], {"text": "alpha"})
16
+ store.upsert("ns", "b", [0.8, 0.2, 0.0, 0.0], {"text": "beta"})
17
+ store.upsert("ns", "c", [0.5, 0.5, 0.0, 0.0], {"text": "gamma"})
18
+ store.upsert("other", "x", [1.0, 1.0, 1.0, 1.0], {"text": "other ns"})
19
+
20
+ hits = store.query("ns", QUERY, top_k=3)
21
+ assert [h.id for h in hits] == ["a", "b", "c"]
22
+ assert hits[0].payload["text"] == "alpha"
23
+ assert hits[0].score > hits[1].score > hits[2].score
24
+ assert len(store.query("ns", QUERY, top_k=1)) == 1
25
+
26
+ strict = store.query("ns", QUERY, top_k=3, threshold=0.99)
27
+ assert [h.id for h in strict] == ["a"]
28
+
29
+ store.delete("ns", "b")
30
+ assert [h.id for h in store.query("ns", QUERY, top_k=3)] == ["a", "c"]
31
+ assert [h.id for h in store.query("other", [1.0, 1.0, 1.0, 1.0], top_k=2)] == ["x"]
32
+ assert all(abs(h.score) < 1e-6 for h in store.query("ns", [0.0, 0.0, 0.0, 1.0], top_k=2))
33
+
34
+ store.upsert("ns", "a", [1.0, 0.0, 0.0, 0.0], {"text": "alpha-v2"})
35
+ assert store.query("ns", QUERY, top_k=1)[0].payload["text"] == "alpha-v2"
36
+
37
+ store.clear("ns")
38
+ assert store.query("ns", QUERY, top_k=3) == []
39
+ assert [h.id for h in store.query("other", [1.0, 1.0, 1.0, 1.0], top_k=1)] == ["x"]
40
+
41
+
42
+ def test_sqlite_reference() -> None:
43
+ """The stdlib reference passes the same scenarios the adapters must match."""
44
+ run_parity_scenarios(SqliteVectorStore(":memory:"))
45
+
46
+
47
+ def test_qdrant_parity():
48
+ pytest.importorskip("qdrant_client")
49
+ run_parity_scenarios(QdrantVectorStore(location=":memory:"))
@@ -0,0 +1,25 @@
1
+ """Tests for loading the qdrant plugin into a Xyberos app."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import pytest
6
+ from xyberos import create_app
7
+
8
+ from xyberos_qdrant import QdrantPlugin
9
+
10
+
11
+ def test_plugin_conforms_to_contract():
12
+ plugin = QdrantPlugin(location=":memory:")
13
+ assert plugin.name == "qdrant"
14
+ assert callable(plugin.register) and callable(plugin.unregister)
15
+ assert plugin.vector_store() is plugin.vector_store()
16
+
17
+
18
+ def test_plugin_registers_vector_store():
19
+ pytest.importorskip("qdrant_client")
20
+ app = create_app()
21
+ app.load_plugin(QdrantPlugin(location=":memory:"))
22
+ store = app.resolve("vector_store")
23
+ store.upsert("ns", "a", [1.0, 0.0, 0.0, 0.0], {"text": "alpha"})
24
+ assert store.query("ns", [1.0, 0.0, 0.0, 0.0], top_k=1)[0].payload["text"] == "alpha"
25
+ app.unload_plugin("qdrant")
@@ -0,0 +1,57 @@
1
+ """Unit tests for the QdrantVectorStore (in-memory local mode)."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import pytest
6
+
7
+ from xyberos_qdrant import QdrantVectorStore
8
+
9
+
10
+ @pytest.fixture()
11
+ def store():
12
+ pytest.importorskip("qdrant_client")
13
+ instance = QdrantVectorStore(location=":memory:")
14
+ return instance
15
+
16
+
17
+ def test_upsert_and_query(store):
18
+ store.upsert("ns", "a", [1.0, 0.0, 0.0], {"text": "alpha"})
19
+ store.upsert("ns", "b", [0.0, 1.0, 0.0], {"text": "beta"})
20
+ hits = store.query("ns", [1.0, 0.0, 0.0], top_k=2)
21
+ assert hits[0].id == "a"
22
+ assert hits[0].payload["text"] == "alpha"
23
+
24
+
25
+ def test_string_ids_round_trip(store):
26
+ store.upsert("ns", "document-1", [1.0, 0.0], {"text": "one"})
27
+ store.upsert("ns", "document-2", [0.0, 1.0], {"text": "two"})
28
+ hits = store.query("ns", [1.0, 0.0], top_k=2)
29
+ assert {h.id for h in hits} == {"document-1", "document-2"}
30
+ assert {h.payload["text"] for h in hits} == {"one", "two"}
31
+
32
+
33
+ def test_delete(store):
34
+ store.upsert("ns", "a", [1.0, 0.0, 0.0, 0.0])
35
+ store.upsert("ns", "b", [0.0, 1.0, 0.0, 0.0])
36
+ store.delete("ns", "a")
37
+ assert [h.id for h in store.query("ns", [1.0, 0.0, 0.0, 0.0], top_k=2)] == ["b"]
38
+
39
+
40
+ def test_clear(store):
41
+ store.upsert("ns", "a", [1.0, 0.0, 0.0, 0.0])
42
+ store.clear("ns")
43
+ assert store.query("ns", [1.0, 0.0, 0.0, 0.0]) == []
44
+
45
+
46
+ def test_missing_namespace_returns_empty(store):
47
+ assert store.query("missing", [1.0, 0.0, 0.0, 0.0]) == []
48
+
49
+
50
+ def test_provider_error_when_client_missing(monkeypatch):
51
+ import importlib.util
52
+
53
+ if importlib.util.find_spec("qdrant_client"):
54
+ pytest.skip("qdrant-client is installed")
55
+ store = QdrantVectorStore(url="http://localhost:6333")
56
+ with pytest.raises(Exception):
57
+ store.upsert("ns", "a", [1.0, 0.0, 0.0])
@@ -0,0 +1,6 @@
1
+ """Qdrant VectorStore plugin (RFC-0019, M4)."""
2
+
3
+ from .plugin import QdrantPlugin
4
+ from .vector import QdrantVectorStore
5
+
6
+ __all__ = ["QdrantPlugin", "QdrantVectorStore"]
@@ -0,0 +1,65 @@
1
+ """Qdrant plugin entry point (RFC-0019, M4)."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import os
6
+ from typing import Any
7
+
8
+ from xyberos.contracts import Plugin, VectorStore
9
+
10
+ from .vector import QdrantVectorStore
11
+
12
+
13
+ class QdrantPlugin(Plugin):
14
+ """Registers a :class:`QdrantVectorStore` as the app's ``vector_store``.
15
+
16
+ Configuration (in priority order): explicit constructor args, then
17
+ ``QDRANT_URL`` / ``QDRANT_API_KEY`` / ``QDRANT_DIM`` env vars. With no
18
+ configuration it defaults to Qdrant's in-memory local mode, which is
19
+ harmless for development and testing.
20
+ """
21
+
22
+ def __init__(
23
+ self,
24
+ *,
25
+ url: str | None = None,
26
+ api_key: str | None = None,
27
+ location: str | None = None,
28
+ path: str | None = None,
29
+ dim: int | None = None,
30
+ client: Any | None = None,
31
+ ) -> None:
32
+ self._url = url or os.getenv("QDRANT_URL")
33
+ self._api_key = api_key or os.getenv("QDRANT_API_KEY")
34
+ self._location = location
35
+ self._path = path
36
+ raw_dim = dim if dim is not None else os.getenv("QDRANT_DIM")
37
+ self._dim = int(raw_dim) if raw_dim else None
38
+ self._client = client
39
+ self._store: QdrantVectorStore | None = None
40
+
41
+ @property
42
+ def name(self) -> str:
43
+ return "qdrant"
44
+
45
+ def vector_store(self) -> QdrantVectorStore:
46
+ if self._store is None:
47
+ self._store = QdrantVectorStore(
48
+ url=self._url,
49
+ api_key=self._api_key,
50
+ location=self._location or (":memory:" if not self._url and not self._path else None),
51
+ path=self._path,
52
+ dim=self._dim,
53
+ client=self._client,
54
+ )
55
+ return self._store
56
+
57
+ def register(self, kernel: object) -> None:
58
+ kernel.register("vector_store", self.vector_store(), replace=True)
59
+
60
+ def unregister(self, kernel: object) -> None:
61
+ pass
62
+
63
+
64
+ #: Auto-discovered by ``app.load_entry_points()``.
65
+ plugin = QdrantPlugin()
@@ -0,0 +1,193 @@
1
+ """Qdrant-backed :class:`~xyberos.contracts.VectorStore` (lazy ``qdrant-client``).
2
+
3
+ Each namespace maps to a Qdrant collection using cosine distance. Supports
4
+ hosted Qdrant (``url`` + optional ``api_key``) and local modes (``location=
5
+ ":memory:"`` or a ``path``). The client is imported lazily on first use and a
6
+ clear :class:`~xyberos.exceptions.provider.ProviderError` is raised when
7
+ ``qdrant-client`` is missing (``pip install xyberos[vectors]``).
8
+
9
+ Arbitrary string ids are mapped to deterministic UUIDs (Qdrant point ids are
10
+ ``int`` or ``UUID``); the original id is preserved in the payload and restored
11
+ on query, so the :class:`VectorStore` contract (string ids) holds exactly.
12
+ """
13
+
14
+ from __future__ import annotations
15
+
16
+ import importlib
17
+ import uuid
18
+ from collections.abc import Mapping, Sequence
19
+ from typing import Any
20
+
21
+ from xyberos.contracts.vector import ScoredHit, VectorStore
22
+ from xyberos.exceptions.provider import ProviderError
23
+
24
+ _DEFAULT_DIM = 384
25
+ _ID_NAMESPACE = uuid.UUID("6ba7b810-9dad-11d1-80b4-00c04fd430c8") # DNS namespace
26
+
27
+
28
+ def _require_qdrant() -> Any:
29
+ try:
30
+ importlib.import_module("qdrant_client") # noqa: F401
31
+ models = importlib.import_module("qdrant_client.models")
32
+ except ImportError as exc:
33
+ raise ProviderError(
34
+ "the 'qdrant-client' package is required; install it with "
35
+ "'pip install xyberos[vectors]'"
36
+ ) from exc
37
+ return models
38
+
39
+
40
+ class QdrantVectorStore(VectorStore):
41
+ """A :class:`VectorStore` backed by Qdrant (hosted or local)."""
42
+
43
+ def __init__(
44
+ self,
45
+ *,
46
+ url: str | None = None,
47
+ api_key: str | None = None,
48
+ location: str | None = None,
49
+ path: str | None = None,
50
+ dim: int | None = None,
51
+ prefer_grpc: bool = False,
52
+ client: Any | None = None,
53
+ ) -> None:
54
+ if dim is not None and dim <= 0:
55
+ raise ValueError("dim must be a positive integer")
56
+ self._url = url
57
+ self._api_key = api_key
58
+ self._location = location
59
+ self._path = path
60
+ self._dim = dim
61
+ self._prefer_grpc = prefer_grpc
62
+ self._client = client
63
+ self._known: set[str] = set()
64
+
65
+ # -- lifecycle ----------------------------------------------------------
66
+
67
+ def start(self) -> None:
68
+ """Connect to Qdrant (kernel lifecycle hook)."""
69
+ self._get_client()
70
+
71
+ def stop(self) -> None:
72
+ """Release the client (kernel lifecycle hook)."""
73
+ self._client = None
74
+ self._known.clear()
75
+
76
+ # -- VectorStore contract -----------------------------------------------
77
+
78
+ def upsert(
79
+ self,
80
+ namespace: str,
81
+ id: str,
82
+ vector: Sequence[float],
83
+ payload: Mapping[str, Any] | None = None,
84
+ ) -> None:
85
+ models = _require_qdrant()
86
+ client = self._get_client()
87
+ collection = self._ensure_collection(client, namespace, dim=len(vector), models=models)
88
+ stored = dict(payload or {})
89
+ stored["_id"] = str(id)
90
+ point = models.PointStruct(
91
+ id=self._point_id(id),
92
+ vector=[float(value) for value in vector],
93
+ payload=stored,
94
+ )
95
+ client.upsert(collection, points=[point])
96
+
97
+ def query(
98
+ self,
99
+ namespace: str,
100
+ vector: Sequence[float],
101
+ *,
102
+ top_k: int = 5,
103
+ threshold: float | None = None,
104
+ ) -> list[ScoredHit]:
105
+ client = self._get_client()
106
+ if not self._collection_exists(client, namespace):
107
+ return []
108
+ result = client.query_points(
109
+ collection_name=namespace,
110
+ query=[float(value) for value in vector],
111
+ limit=max(top_k, 1),
112
+ with_payload=True,
113
+ )
114
+ hits: list[ScoredHit] = []
115
+ for point in result.points:
116
+ score = float(point.score)
117
+ if threshold is not None and score < threshold:
118
+ continue
119
+ payload = dict(point.payload or {})
120
+ stored_id = payload.pop("_id", None)
121
+ hits.append(
122
+ ScoredHit(
123
+ id=str(stored_id) if stored_id is not None else str(point.id),
124
+ score=score,
125
+ payload=payload or None,
126
+ )
127
+ )
128
+ return hits
129
+
130
+ def delete(self, namespace: str, id: str) -> None:
131
+ models = _require_qdrant()
132
+ client = self._get_client()
133
+ if not self._collection_exists(client, namespace):
134
+ return
135
+ client.delete(
136
+ collection_name=namespace,
137
+ points_selector=models.PointIdsList(points=[self._point_id(id)]),
138
+ )
139
+
140
+ def clear(self, namespace: str) -> None:
141
+ client = self._get_client()
142
+ if not self._collection_exists(client, namespace):
143
+ return
144
+ client.delete_collection(collection_name=namespace)
145
+ self._known.discard(namespace)
146
+
147
+ # -- helpers ------------------------------------------------------------
148
+
149
+ def _get_client(self) -> Any:
150
+ if self._client is not None:
151
+ return self._client
152
+ QdrantClient = importlib.import_module("qdrant_client").QdrantClient
153
+
154
+ if self._location is not None or self._path is not None:
155
+ self._client = QdrantClient(location=self._location, path=self._path)
156
+ else:
157
+ self._client = QdrantClient(
158
+ url=self._url,
159
+ api_key=self._api_key,
160
+ prefer_grpc=self._prefer_grpc,
161
+ )
162
+ return self._client
163
+
164
+ @staticmethod
165
+ def _point_id(id: str) -> Any:
166
+ """Map an arbitrary string id to an int or UUID (Qdrant point id)."""
167
+ if isinstance(id, int):
168
+ return id
169
+ text = str(id)
170
+ if text.isdigit():
171
+ return int(text)
172
+ return uuid.uuid5(_ID_NAMESPACE, text)
173
+
174
+ def _ensure_collection(self, client: Any, namespace: str, *, dim: int, models: Any) -> str:
175
+ if namespace in self._known:
176
+ return namespace
177
+ if not self._collection_exists(client, namespace):
178
+ client.create_collection(
179
+ collection_name=namespace,
180
+ vectors_config=models.VectorParams(
181
+ size=self._dim or dim,
182
+ distance=models.Distance.COSINE,
183
+ ),
184
+ )
185
+ self._known.add(namespace)
186
+ return namespace
187
+
188
+ def _collection_exists(self, client: Any, namespace: str) -> bool:
189
+ try:
190
+ client.get_collection(collection_name=namespace)
191
+ return True
192
+ except Exception:
193
+ return False
@@ -0,0 +1,68 @@
1
+ Metadata-Version: 2.4
2
+ Name: xyberos-qdrant
3
+ Version: 0.1.0
4
+ Summary: Qdrant VectorStore plugin (RFC-0019, M4): hosted or in-memory Qdrant as an xyberos VectorStore
5
+ License: Apache-2.0
6
+ Keywords: xyberos,plugin,vector,qdrant,rag
7
+ Requires-Python: >=3.10
8
+ Description-Content-Type: text/markdown
9
+ Requires-Dist: xyberos>=1.0
10
+ Provides-Extra: vectors
11
+ Requires-Dist: qdrant-client; extra == "vectors"
12
+ Provides-Extra: test
13
+ Requires-Dist: pytest; extra == "test"
14
+
15
+ # xyberos-qdrant
16
+
17
+ **Qdrant `VectorStore` plugin — RFC-0019, M4.** Hosted or local [Qdrant](https://qdrant.tech)
18
+ as an `xyberos` `VectorStore`. Passes the same parity scenarios as the stdlib
19
+ `SqliteVectorStore` (the M4 Definition of Done).
20
+
21
+ ## Install
22
+
23
+ ```bash
24
+ pip install -e ./qdrant
25
+ pip install qdrant-client # or: pip install xyberos[vectors]
26
+ ```
27
+
28
+ ## Usage
29
+
30
+ ```python
31
+ from xyberos import create_app
32
+ from xyberos_qdrant import QdrantPlugin
33
+
34
+ app = create_app()
35
+ app.load_plugin(QdrantPlugin(location=":memory:")) # local, no infrastructure
36
+ # hosted: QdrantPlugin(url="https://...", api_key=os.getenv("QDRANT_API_KEY"))
37
+
38
+ store = app.resolve("vector_store")
39
+ store.upsert("ns", "doc-1", [1.0, 0.0, 0.0, 0.0], {"text": "alpha"})
40
+ hits = store.query("ns", [1.0, 0.0, 0.0, 0.0], top_k=3)
41
+ ```
42
+
43
+ Or configure through the environment (`QDRANT_URL`, `QDRANT_API_KEY`,
44
+ `QDRANT_DIM`) and let the `xyberos.plugins` entry point register it.
45
+
46
+ ## Design
47
+
48
+ - Each **namespace** maps to a Qdrant collection using **cosine** distance
49
+ (higher score = more relevant, matching the `VectorStore` contract).
50
+ - The client is imported **lazily** and a clear `ProviderError` is raised when
51
+ `qdrant-client` is missing.
52
+ - Arbitrary string ids are mapped to deterministic UUIDs (Qdrant point ids are
53
+ `int`/`UUID`); the original id round-trips through the payload, so the
54
+ contract's string ids hold exactly.
55
+
56
+ ## Tests
57
+
58
+ ```bash
59
+ pip install pytest
60
+ pytest tests/ # unit + parity + plugin tests
61
+ ```
62
+
63
+ Parity tests run against Qdrant's **in-memory local mode**, so they need no
64
+ server; they skip cleanly if `qdrant-client` is absent.
65
+
66
+ ## Ship location
67
+
68
+ `[vectors]` extra (`qdrant-client` already added to `pyproject.toml`).
@@ -0,0 +1,14 @@
1
+ README.md
2
+ pyproject.toml
3
+ tests/test_parity.py
4
+ tests/test_plugin.py
5
+ tests/test_vector.py
6
+ xyberos_qdrant/__init__.py
7
+ xyberos_qdrant/plugin.py
8
+ xyberos_qdrant/vector.py
9
+ xyberos_qdrant.egg-info/PKG-INFO
10
+ xyberos_qdrant.egg-info/SOURCES.txt
11
+ xyberos_qdrant.egg-info/dependency_links.txt
12
+ xyberos_qdrant.egg-info/entry_points.txt
13
+ xyberos_qdrant.egg-info/requires.txt
14
+ xyberos_qdrant.egg-info/top_level.txt
@@ -0,0 +1,2 @@
1
+ [xyberos.plugins]
2
+ qdrant = xyberos_qdrant.plugin:plugin
@@ -0,0 +1,7 @@
1
+ xyberos>=1.0
2
+
3
+ [test]
4
+ pytest
5
+
6
+ [vectors]
7
+ qdrant-client
@@ -0,0 +1 @@
1
+ xyberos_qdrant