Skip to main content

A Qdrant-first ODM for typed models, schema sync, and vector search.

Project description

📦 Qdrant ODM (v0.3.0)

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


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

qdrant_odmx-0.3.1.tar.gz (26.2 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

qdrant_odmx-0.3.1-py3-none-any.whl (33.8 kB view details)

Uploaded Python 3

File details

Details for the file qdrant_odmx-0.3.1.tar.gz.

File metadata

  • Download URL: qdrant_odmx-0.3.1.tar.gz
  • Upload date:
  • Size: 26.2 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.11.13

File hashes

Hashes for qdrant_odmx-0.3.1.tar.gz
Algorithm Hash digest
SHA256 9a55fa684d11da7e5e2e5e20a159b48c96fdb32b8dcd8f71a5d88857ce314534
MD5 cbf55423423862305ff9be1dd631d8b0
BLAKE2b-256 43b88b1ed7ad65acb3d4bc1bdce2d17abd039a9af2408a5049be26e07dda719b

See more details on using hashes here.

File details

Details for the file qdrant_odmx-0.3.1-py3-none-any.whl.

File metadata

  • Download URL: qdrant_odmx-0.3.1-py3-none-any.whl
  • Upload date:
  • Size: 33.8 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.11.13

File hashes

Hashes for qdrant_odmx-0.3.1-py3-none-any.whl
Algorithm Hash digest
SHA256 b110dff233dc17392e91fb5d65e26b01a1a87d41ba75f20c4a05cb08860b69de
MD5 84fd2d68f868d8b837f44252b225798c
BLAKE2b-256 3b7282fa3e357258bca346a9c13333338b2aac01f2c037268c790aa156ab1ca0

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page