A Qdrant-first ODM for typed models, schema sync, and vector search.
Project description
📦 Qdrant ODM (v0.3.2)
qdrant-odm is a Qdrant-first ODM for building production-grade vector search systems.
Why qdrant-odm?
Working with raw Qdrant is powerful, but it gets verbose as your project grows.
Typical pain points:
- Repeating payload field names as raw strings
- Manually managing collection and index setup
- Writing filters in low-level Qdrant syntax
- Mixing schema, query, and repository logic in application code
qdrant-odm keeps Qdrant native, but gives you a more structured way to work with it.
Before: raw Qdrant client
from uuid import uuid4
from qdrant_client import AsyncQdrantClient
from qdrant_client.http import models as qm
client = AsyncQdrantClient(url="http://localhost:6333")
await client.create_collection(
collection_name="documents",
vectors_config={
"content_dense": qm.VectorParams(size=3072, distance=qm.Distance.COSINE)
}
)
await client.create_payload_index(
collection_name="documents",
field_name="title",
field_schema=qm.PayloadSchemaType.KEYWORD
)
await client.upsert(
collection_name="documents",
points=[
qm.PointStruct(
id=str(uuid4()),
vector={"content_dense": [0.1] * 3072},
payload={
"title": "Qdrant ODM",
"category": "tech",
"page": 1,
},
)
],
)
results = await client.query_points(
collection_name="documents",
query=[0.1] * 3072,
using="content_dense",
query_filter=qm.Filter(
must=[
qm.FieldCondition(
key="category",
match=qm.MatchValue(value="tech")
),
qm.FieldCondition(
key="page",
range=qm.Range(gte=1)
),
]
),
limit=10,
)
After: qdrant-odm
from uuid import UUID, uuid4
from qdrant_odm import (
QdrantModel,
PayloadField,
VectorField,
QdrantODM,
QdrantRepository,
SearchQuery,
)
class Document(QdrantModel):
__collection__ = "documents"
id: UUID
title: str = PayloadField(index="keyword")
category: str = PayloadField(index="keyword")
page: int = PayloadField(index="integer")
dense = VectorField(name="content_dense", size=3072)
odm = QdrantODM(client)
await odm.sync_schema(Document)
repo = QdrantRepository(client, Document)
await repo.upsert(
Document(
id=uuid4(),
title="Qdrant ODM",
category="tech",
page=1,
),
vectors={
"content_dense": [0.1] * 3072,
},
)
results = await repo.search(
SearchQuery(
vector=[0.1] * 3072,
using="content_dense",
filter=(Document.category == "tech") & (Document.page >= 1),
limit=10,
)
)
What changes?
- Schema is defined once in the model
- Index configuration lives next to fields
- Filters use Python expressions instead of raw payload strings
- Repositories keep CRUD and search logic consistent
- Schema sync reduces collection setup boilerplate
🚀 Features
- Declarative schema (Pydantic-based)
- Explicit payload indexing with fine-grained control
- Collection modes (global / multitenant)
- Safe schema sync (diff → plan → sync)
- Python-native filter DSL
- Async repository abstraction
- Hybrid retrieval (dense + sparse with RRF)
- Batch optimized operations
🚀 Installation
Installation
pip install qdrant-odmx
Development
git clone https://github.com/yourname/qdrant-odmx
cd qdrant-odmx
pip install -e ".[dev]"
🧠 Architecture Overview
Model → Metadata → SchemaManager → Qdrant
↘ Query DSL → Compiler → Filter
↘ Repository → CRUD / Search
📌 Model Definition
Basic Model
from uuid import UUID
from datetime import datetime
from qdrant_odm import QdrantModel, PayloadField, VectorField
class Document(QdrantModel):
__collection__ = "documents"
id: UUID
title: str = PayloadField(index="keyword")
created_at: datetime = PayloadField(index="datetime")
dense = VectorField(name="content_dense", size=3072)
📌 Collection Modes
Global (default)
__collection_config__ = CollectionConfig(mode="global")
Multitenant
from qdrant_odm import CollectionConfig, KeywordIndexOptions
__collection_config__ = CollectionConfig(mode="multitenant")
tenant_id: str = PayloadField(
index="keyword",
keyword=KeywordIndexOptions(is_tenant=True)
)
Rules
- Exactly ONE tenant index
- Must be keyword
- Must set
is_tenant=True
📌 Vector Definition
VectorField(
name="content_dense",
size=3072,
distance="Cosine"
)
Supported distances
- Cosine
- Euclid
- Dot
- Manhattan
📌 Payload Index Options
from qdrant_odm import IntegerIndexOptions
page: int = PayloadField(
index="integer",
integer=IntegerIndexOptions(
lookup=True,
range=True,
on_disk=True
)
)
Supported:
- keyword
- integer
- float
- bool
- geo
- datetime
- text
- uuid
🔍 Query DSL
(Document.category == "law") & (Document.page >= 2)
Supports:
- == != > >= < <=
- in_ / not_in
- is_null / is_not_null
- &, |, ~
📦 Repository
repo = QdrantRepository(client, Document)
CRUD
await repo.get(id)
await repo.delete(id)
await repo.exists(id)
Batch
await repo.upsert_many([...])
Scroll
page = await repo.scroll()
🔍 Search
Dense
await repo.search(SearchQuery(...))
Sparse
SparseVectorInput(indices=[...], values=[...])
Hybrid
await repo.search_hybrid(HybridSearchQuery(...))
Uses RRF internally.
🧬 Schema Sync
Diff
await odm.schema.diff(Model)
Dry Run
await odm.schema.dry_run(Model)
Sync
await odm.sync_schema(Model)
⚠️ Behavior Rules
Safe Sync
Will NOT modify:
- vector schema
- index type
- index options
Only:
- create collection
- create missing indexes
Payload Behavior
- upsert → model fields only
- set_payload → free form
Filtering
- Works without index
- Slower without index
⚡ Performance Tips
- Always index filter fields
- Use batch operations
- Use scroll for large datasets
- Use hybrid search for recall boost
🔥 Summary
- Qdrant-native ODM
- strict schema guarantees
- multitenant ready
- production-ready async design
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file qdrant_odmx-0.3.2.tar.gz.
File metadata
- Download URL: qdrant_odmx-0.3.2.tar.gz
- Upload date:
- Size: 26.7 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
cf4dc497c19047ba13f0b90de9161a825105e5bbf845e3b56f0b4d4bd206db3a
|
|
| MD5 |
e9eb7917ed70103fc0d2ade2d2e604cd
|
|
| BLAKE2b-256 |
eaa45698918a891f67a90ea7f70deb1f6c29d968cfb35799f888ced4b1256185
|
Provenance
The following attestation bundles were made for qdrant_odmx-0.3.2.tar.gz:
Publisher:
publish.yml on Jeung-SeongYeon/qdrant-odm
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
qdrant_odmx-0.3.2.tar.gz -
Subject digest:
cf4dc497c19047ba13f0b90de9161a825105e5bbf845e3b56f0b4d4bd206db3a - Sigstore transparency entry: 1356325030
- Sigstore integration time:
-
Permalink:
Jeung-SeongYeon/qdrant-odm@749aafd193b06a77ba7e715b6654e1f048f7857a -
Branch / Tag:
refs/tags/v0.3.2 - Owner: https://github.com/Jeung-SeongYeon
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@749aafd193b06a77ba7e715b6654e1f048f7857a -
Trigger Event:
release
-
Statement type:
File details
Details for the file qdrant_odmx-0.3.2-py3-none-any.whl.
File metadata
- Download URL: qdrant_odmx-0.3.2-py3-none-any.whl
- Upload date:
- Size: 33.8 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
c0949c8fdc4dd2589f85e52b7cfcb1783a7eab0d2ee924280131a593bd502a97
|
|
| MD5 |
f8c0930ba56624df3484075290247852
|
|
| BLAKE2b-256 |
efcbcc4075b60270506ef29ea02b4eea9d3d4bca7ac2e7f3d2454fd8aaf148a6
|
Provenance
The following attestation bundles were made for qdrant_odmx-0.3.2-py3-none-any.whl:
Publisher:
publish.yml on Jeung-SeongYeon/qdrant-odm
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
qdrant_odmx-0.3.2-py3-none-any.whl -
Subject digest:
c0949c8fdc4dd2589f85e52b7cfcb1783a7eab0d2ee924280131a593bd502a97 - Sigstore transparency entry: 1356325045
- Sigstore integration time:
-
Permalink:
Jeung-SeongYeon/qdrant-odm@749aafd193b06a77ba7e715b6654e1f048f7857a -
Branch / Tag:
refs/tags/v0.3.2 - Owner: https://github.com/Jeung-SeongYeon
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@749aafd193b06a77ba7e715b6654e1f048f7857a -
Trigger Event:
release
-
Statement type: