openframe-adapters-db-qdrant
Qdrant vector-store adapter for the OpenFrame Microservice Suite.
Part of the openframe-adapters monorepo. Implements BaseVectorStore[T]
(which extends BaseRepository[T]) from openframe-core using
qdrant-client's AsyncQdrantClient.
Installation
pip install openframe-adapters-db-qdrant
Required env var:
QDRANT_URL=http://localhost:6333
Quick start
Raw dict mode
from openframe.adapters.db.qdrant import QdrantSettings, QdrantVectorStore
settings = QdrantSettings() # reads QDRANT_URL from env
store = QdrantVectorStore(settings, collection="documents")
item = await store.get("abc-123") # dict | None
items, total = await store.list(10, 0) # ([dict, ...], int)
created = await store.create({"id": "abc-123", "vector": [0.1, 0.2, 0.3], "text": "hello"})
updated = await store.update({"id": "abc-123", "vector": [0.1, 0.2, 0.3], "text": "bye"})
deleted = await store.delete("abc-123") # bool
results = await store.search(query_vector=[0.1, 0.2, 0.3], k=5) # list[dict]
Typed domain mode
from dataclasses import dataclass
from qdrant_client.http.models import PointStruct
from openframe.adapters.db.qdrant import QdrantSettings, QdrantVectorStore
@dataclass
class Document:
id: str
vector: list[float]
text: str
class DocumentStore(QdrantVectorStore[Document]):
_collection = "documents"
def _point_to_entity(self, point) -> Document:
return Document(id=point.id, vector=point.vector, text=(point.payload or {}).get("text", ""))
def _entity_to_point(self, entity: Document) -> PointStruct:
return PointStruct(id=entity.id, vector=entity.vector, payload={"text": entity.text})
settings = QdrantSettings()
store = DocumentStore(settings)
doc: Document | None = await store.get("abc-123")
Wiring into an application
For a real service, wire QdrantPlugin (the BasePort-satisfying plugin
class) through ApplicationBootstrap.compose() from openframe-core. This
gives you proper lifecycle management — initialize() / health() /
shutdown() — for free, instead of constructing QdrantVectorStore
directly and managing the client yourself:
from openframe.core.runtime import ApplicationBootstrap
from openframe.core.ports import Capability
from openframe.adapters.db.qdrant import QdrantPlugin, QdrantSettings
settings = QdrantSettings() # reads QDRANT_URL from env
plugin = QdrantPlugin(settings, collection="documents")
async with ApplicationBootstrap.compose(plugin) as app:
store = app.get(Capability.SEARCH) # -> QdrantVectorStore
results = await store.search(query_vector=[0.1, 0.2, 0.3], k=5)
# client is closed automatically on exit (plugin.shutdown() ran)
compose() calls plugin.initialize() on entry and plugin.shutdown() on
exit, so the client is created, health-checked, and torn down without any
manual lifecycle code. Requires openframe-core>=3.3.
Reach for a subclassed ApplicationBootstrap (with a configure() method)
only when you need per-port config=/init_timeout= or conditional
registration order; use app.registry as an escape hatch for anything
neither tier covers. The QdrantVectorStore(settings, collection=...)
construction shown above under "Quick start" remains valid for tests,
scripts, or any context that doesn't need plugin lifecycle management.
Resilience (optional)
openframe-core>=3.4 ships openframe.core.resilience. Compose
CircuitBreakerProxy around the (optionally traced) store — never the
reverse, so a short-circuited call never produces a misleading adapter span
for a call that never reached Qdrant:
from openframe.core.resilience import CircuitBreakerProxy
from openframe.core.observability import TracingProxy
store = CircuitBreakerProxy(
TracingProxy(plugin.get_repository(), prefix="vectorstore.documents"),
failure_threshold=5,
reset_timeout=30.0,
)
No adapter code needs to change to support this — CircuitBreakerProxy
wraps from the outside, exactly like TracingProxy.
Configuration
All settings are read from environment variables.
| Env var | Type | Default | Description |
|---|---|---|---|
QDRANT_URL |
str |
required | Base URL of the Qdrant server (http://host:6333) |
QDRANT_API_KEY |
str | None |
None |
API key for Qdrant Cloud / secured instances |
QDRANT_PREFER_GRPC |
bool |
False |
Use the gRPC transport instead of REST |
QDRANT_HTTPS |
bool | None |
None |
Force TLS; None infers from QDRANT_URL |
CONNECTION_TIMEOUT |
float |
30.0 |
Connectivity-probe timeout (s) |
OPERATION_TIMEOUT |
float |
10.0 |
Per-operation timeout (s) |
MAX_RETRIES |
int |
3 |
Max retry attempts |
API mapping and caveats
QdrantVectorStore maps BaseVectorStore[T] onto Qdrant's
AsyncQdrantClient (native async, verified against the installed
qdrant-client — see connection.py's module docstring for how).
| Port method | Qdrant call |
|---|---|
get(id) |
client.retrieve(collection, ids=[id], with_vectors=True) |
list(limit, offset) |
client.scroll(...) + client.count(...) — see caveat below |
create(entity) |
client.upsert(collection, points=[PointStruct(...)]) |
update(entity) |
existence check via get(), then the same upsert() |
delete(id) |
existence check via get(), then client.delete(...) |
search(query_vector, k) |
client.query_points(collection, query=query_vector, limit=k) |
list() offset caveat: Qdrant's own scroll() offset is a point-id
cursor ("skip points with id less than this"), not an integer skip-count.
This adapter implements a compatibility shim — it scrolls
limit + offset points id-ascending from the start and slices the first
offset off in Python — to honour BaseRepository.list(limit, offset)'s
documented integer-offset contract. This is correctness-preserving for
test-sized and small collections but is not performant for a large
offset, since there is no server-side "skip N" cursor and every call
re-fetches and discards the skipped prefix. For efficient pagination over
a large collection, page by Qdrant's own id-cursor next_page_offset
directly via the raw driver (store.client / get_qdrant_client()), not
via this method's offset.
search() note: Qdrant 1.19's AsyncQdrantClient has no search()
method — it was replaced by the universal query endpoint query_points().
This adapter's search() is implemented against query_points(). The
similarity metric used is whatever metric the target collection was
created with (Qdrant stores the metric per-collection at creation time) —
this adapter does not create collections or choose a metric itself.
Health checks
QdrantVectorStore.health() never raises — it returns a PluginHealth
snapshot (PluginStatus.READY or PluginStatus.FAILED with a message)
based on a low-cost get_collections() call.
Exception hierarchy
All exceptions are AdapterError subclasses from openframe.core.exceptions.
Raw qdrant_client exceptions never escape the adapter.
| Situation | Exception |
|---|---|
| Cannot connect to Qdrant (transport-level failure) | AdapterConnectionError |
| Qdrant server error (5xx / unrecognised response) | AdapterConnectionError |
Invalid QDRANT_URL |
AdapterConfigurationError |
| Query failed (4xx — e.g. vector dimensionality mismatch) | AdapterQueryError |
| Operation exceeded timeout | AdapterTimeoutError |
See repository.py's _wrap_qdrant() for the full classification logic.
Development
# from the package directory
pip install -e ".[dev]"
pytest tests/ -v
Protocol conformance
from openframe.core.ports import BaseVectorStore, BaseRepository
store = QdrantVectorStore(settings, collection="documents")
assert isinstance(store, BaseVectorStore) # True — structural check
assert isinstance(store, BaseRepository) # True — BaseVectorStore extends it
No inheritance from either Protocol is required or used.
License
MIT
Metadata
Release files for openframe-adapters-db-qdrant 0.1.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| openframe_adapters_db_qdrant-0.1.0.tar.gz | 24.2 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| openframe_adapters_db_qdrant-0.1.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 43.3 kB
Release files / openframe_adapters_db_qdrant-0.1.0.tar.gz
| Download URL | openframe_adapters_db_qdrant-0.1.0.tar.gz |
|---|---|
| Size | 24.2 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
4818dd26ceb2a7182258fdd796828419618927761120298e1af6d558a60aa91a
|
|
BLAKE2b-256 checksum How to use checksums |
5ddda92d10ef6f7317c3b456692a9db8a5618d251c34eef073166519d6e50a63
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Release files / openframe_adapters_db_qdrant-0.1.0-py3-none-any.whl
| Download URL | openframe_adapters_db_qdrant-0.1.0-py3-none-any.whl |
|---|---|
| Size | 19.0 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
87a52c702826f9aeab6e0fcafa96e6f2364b3c822bc03a03c69771774658ed05
|
|
BLAKE2b-256 checksum How to use checksums |
d10d8ffc7f760a2ae66986ba21a0c31073c6c67731f8d2391ee81071cbb1d831
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|