Qdrant, an open source vector database you can run yourself
Qdrant stores vectors and searches by similarity much like its competitors, with two differences. It is written in Rust, so a single instance handles heavy traffic without tuning, and the Apache 2.0 licence lets you run it on your own server at no cost and with no feature restrictions.
Who it suits
Choosing between a hosted vector database and one on your own server rarely comes down to price. It comes down to where the data may live and who has time to maintain it.
Qdrant wins in three situations. When data cannot leave the company, because it concerns health, finance, or falls under internal policy. When you need precise metadata filtering, since here it is a first class mechanism rather than an add on. When the dataset is large enough that per operation billing in a hosted service stops being predictable.
It loses wherever nobody wants to maintain another database. The container needs upgrading, the disk needs monitoring, backups need making and checking. A team without that appetite ships faster on Pinecone or on the Postgres they already run.
Up and running in a minute
docker run -p 6333:6333 -p 6334:6334 \
-v $(pwd)/qdrant_storage:/qdrant/storage \
qdrant/qdrantAt localhost:6333/dashboard you get an interface for browsing collections, which when diagnosing results often beats querying the API.
from qdrant_client import QdrantClient
from qdrant_client.models import Distance, VectorParams, PointStruct
client = QdrantClient(url="http://localhost:6333")
client.create_collection(
collection_name="documentation",
vectors_config=VectorParams(size=1536, distance=Distance.COSINE),
)
client.upsert(
collection_name="documentation",
points=[
PointStruct(
id=1,
vector=embedding,
payload={"section": "returns", "language": "en", "version": 3},
)
],
)You assign point identifiers yourself, and they should derive from the chunk's origin, for example a document id plus a part number. Reprocessing a document then overwrites the old vectors instead of creating duplicates, and removing a document at the source maps cleanly into the database without guessing which points belonged to it.
The payload field is an arbitrary JSON object attached to a vector. Keep everything you want to filter on in it, plus an identifier that lets you fetch the full text from your own database.
Filtering, the strong suit here
In many databases, metadata filtering is bolted onto vector search and, under narrow conditions, can return fewer results than you asked for. Qdrant treats the filter as part of the index, so a query with a strict condition still returns a full set of hits.
from qdrant_client.models import Filter, FieldCondition, MatchValue, Range
results = client.query_points(
collection_name="documentation",
query=query_embedding,
limit=4,
query_filter=Filter(
must=[
FieldCondition(key="language", match=MatchValue(value="en")),
FieldCondition(key="version", range=Range(gte=3)),
]
),
).pointsThat has a concrete product consequence. A multi tenant application where every query must narrow to one customer works correctly here even with a thousand customers and a thin slice of data. Just add an index on the field you filter by most often, since without one large collections slow down under filtering.
Quantization, or fitting ten times more in
A 1536 dimensional vector stored as floating point numbers takes around six kilobytes. A million such vectors is six gigabytes of memory, and memory is the most expensive line on the bill.
Quantization converts those numbers into smaller representations. The scalar variant cuts size fourfold with minimal quality loss. The binary variant reduces each value to a single bit, giving thirty two fold compression, but it demands a quality check on your own data, because for some embedding models the loss is noticeable.
from qdrant_client.models import ScalarQuantization, ScalarQuantizationConfig, ScalarType
client.create_collection(
collection_name="documentation",
vectors_config=VectorParams(size=1536, distance=Distance.COSINE),
quantization_config=ScalarQuantization(
scalar=ScalarQuantizationConfig(type=ScalarType.INT8, always_ram=True)
),
)The practice runs like this: enable scalar quantization by default, since it costs almost nothing in quality. Reach for binary only once the dataset passes a few million vectors, and always measure accuracy before and after. The vendor rates the free cloud tier at roughly one million 768 dimensional vectors, which is around half a million at 1536 dimensions, and many times more with binary quantization, which illustrates the scale of the difference.
Hybrid search
Semantic similarity alone misses precise queries. A question about an error code or a version number will find chunks on a similar topic, not necessarily the one you need.
Qdrant supports sparse vectors alongside dense ones, so a single query merges meaning based and keyword based retrieval. Results from both channels fuse inside one query, without hitting two systems and stitching lists together in the application.
Something has to compute that sparse vector, and it usually means a second model in the pipeline alongside the embedding model. BGE sidesteps that, since one model returns a dense and a sparse representation in a single pass, and its licence lets you run it yourself with no per token charge. The cost is operating your own model serving, so at low traffic an external service comes out cheaper.
from qdrant_client.models import Prefetch, FusionQuery, Fusion
results = client.query_points(
collection_name="documentation",
prefetch=[
Prefetch(query=dense_vector, using="dense", limit=20),
Prefetch(query=sparse_vector, using="sparse", limit=20),
],
query=FusionQuery(fusion=Fusion.RRF),
limit=4,
).pointsBefore implementing it, check whether the problem exists at all. A set of twenty real queries with the document each should return will show whether vector search alone suffices. For technical documentation it usually does not, for descriptive text it often does.
What it costs
| Option | Cost | What you get |
|---|---|---|
| Self hosting | price of the server | Full functionality, Apache 2.0 licence, no limits |
| Cloud, free plan | 0 USD | 1 GB memory, 0.5 vCPU, 4 GB disk, no card |
| Cloud, 2 GB cluster | roughly 26 USD per month | One node, 0.5 vCPU, 8 GB disk, hourly billing |
| Cloud, 16 GB cluster | roughly 137 USD per month | One node, 2 vCPU, 64 GB disk, typical production |
| Hybrid and premium plans | custom quote | Your infrastructure, vendor managed operations, availability guarantee |
The prices in the table come from the vendor's calculator for North Virginia and are approximate, because they depend on the region. The same 4 GB node costs 0.0467 USD an hour there and 0.0745 USD in São Paulo, more than half again as much. There is no single rate across regions in this service, so always price the region where the cluster will actually stand.
When sizing a cluster, remember too that memory decides rather than vector count as such, and that the vectors are not the whole story. A million 1536 dimensional vectors take about six gigabytes raw, yet the vendor's calculator budgets ten gigabytes of memory for that set, because the index graph and room for field indexes come on top. So the cluster needs to be in the twelve to sixteen gigabyte range rather than eight. The same dataset with scalar quantization drops far lower and fits a considerably cheaper variant. That is the first calculation to do before choosing a plan, rather than after the first invoice.
The self hosting bill is only apparently lower. On top of the machine price, add upgrades, monitoring, backups, and the time of whoever responds to an incident. With one collection and modest traffic, the free cloud plan beats your own server. At tens of millions of vectors the ratio flips.
Multi tenancy, or not mixing customer data
An application serving many customers needs certainty that one customer's query never reaches another's data. Qdrant offers three approaches, and they differ more than first impressions suggest.
A separate collection per customer isolates most strongly, but every collection carries its own memory overhead. At five customers that is no problem, at five hundred it eats resources and slows instance startup.
A shared collection with a filter on a customer identifier scales best and is the recommended arrangement. One condition applies: the filter has to be imposed server side, from the identity in the token, rather than from a parameter supplied by the client. A mistake here is the most serious class of bug in multi tenant applications.
from qdrant_client.models import PayloadSchemaType
client.create_payload_index(
collection_name="documentation",
field_name="customer_id",
field_schema=PayloadSchemaType.KEYWORD,
)An index on the customer identifier field is mandatory here rather than optional. Without it, every query scans the whole dataset before narrowing, and response time grows with customer count.
The third approach shards by customer key in cluster mode. One customer's data then lands physically on a designated node, which helps with data location requirements, for instance when some customers demand processing inside Europe only.
When removing a customer from the system, deleting points by filter is a single operation, which matters when a personal data deletion request arrives.
Qdrant against the alternatives
| Solution | Strength | Weakness | Pick it when |
|---|---|---|---|
| Qdrant | Filtering, quantization, unrestricted self hosted edition | Maintenance falls on you when self hosting | Sensitive data, multi tenancy, large datasets |
| Pinecone | Zero maintenance, steady latency | Minimum charge, no local edition | Team with no time for infrastructure |
| pgvector on Supabase | One database for everything, transactions alongside data | Index tuning on larger datasets | Up to a million vectors, data already in Postgres |
| Chroma | Simplest start, good for prototypes | Fewer options at scale | Prototype and local work |
It also pays to check whether the project already runs a database that can handle this. Postgres with a vector extension covers most use cases up to a million vectors and saves an entire maintenance layer.
Migrating between these is not expensive as long as you keep the same embedding model. Changing the model is far costlier, since it requires recomputing the entire dataset.
Running a production instance
Three things deserve setting up on day one. API key authentication, because by default the instance is open, and exposing it on a public address without a key ends exactly as you would expect. Backups through collection snapshots, taken automatically and test restored once a quarter. Memory monitoring, since exceeding available memory with an in RAM index ends with the process being killed.
Under heavier traffic, move to cluster mode with replication. A collection split into shards and replicated survives the loss of one node, but that requires deliberate configuration and does not switch itself on.
When importing large datasets, batch points a few hundred at a time rather than sending them one by one. The difference in loading a million vectors is measured in hours, not percentages.
Keep gRPC in mind too. The REST interface is more convenient for testing, while at high write volume gRPC delivers noticeably better throughput and should handle data import.
Index tuning and the speed against accuracy trade
Vector search is approximate. The database does not compare a query against every vector, it walks a neighbourhood graph, and that graph's parameters decide whether an answer arrives in five milliseconds or fifty, and how many valid hits it skips along the way.
Two parameters shape that trade. The first sets how many neighbours each graph node remembers as the index is built. A higher value raises accuracy and memory usage, and lengthens index construction. The second sets how widely the database explores the graph for a given query, and that one changes without a rebuild.
from qdrant_client.models import SearchParams
results = client.query_points(
collection_name="documentation",
query=query_embedding,
limit=4,
search_params=SearchParams(hnsw_ef=128, exact=False),
).pointsA practical tuning order runs like this. Start from the defaults and measure accuracy on a set of real queries. If results hold, change nothing, since the defaults are sensible for most datasets. If hits are missing, raise the per query search width and measure again before reaching for an index rebuild.
Separately, know about exact mode, which compares the query against every vector and gives a reference answer. It is unfit for production because it is slow, but it serves beautifully as a baseline: run it over twenty queries and check how many hits approximate mode loses under your settings.
Common mistakes
The first is no index on fields used in filters. Without one, filtering a large collection scans linearly and response time grows with dataset size.
The second is dumping full document text into the payload. That field increases collection size and memory usage, while an identifier for fetching the text from your own database usually suffices.
The third is random point identifiers. Reprocessing a document then creates duplicates instead of overwriting old vectors, so the model receives two contradictory versions of the same clause.
The fourth is exposing an instance without authentication. The default configuration requires no key, so it has to be enabled deliberately before the first deployment.
The fifth is turning on binary quantization without measuring. Thirty two fold compression sounds excellent, but with some embedding models it degrades accuracy enough that the system stops answering sensibly.
FAQ
Is Qdrant free?
The open source edition under Apache 2.0 is free, commercially included, with no feature restrictions, and runs on your own server. The cloud has a free plan with one gigabyte of memory and no expiry date, but an unused free cluster is suspended after a week and deleted after four weeks of inactivity, so it is no good as a dormant spare. Paid plans bill hourly for allocated resources.
Qdrant or Pinecone?
Choose Qdrant when you need a local edition, precise filtering, or control over cost on a large dataset. Choose Pinecone when you want to maintain nothing and accept the minimum charge. At moderate scale both deliver comparable retrieval quality.
How much memory does a million vectors need?
A million 1536 dimensional vectors without compression takes around six gigabytes. Scalar quantization brings that to roughly one and a half gigabytes, binary to around two hundred megabytes, though the latter needs a quality check on your own data.
Can I combine vector search with keyword search?
Yes, through sparse vectors and result fusion inside a single query. That solves the familiar problem of queries containing error codes, version numbers, and proper nouns, which semantic similarity alone fails to find.
How do I integrate Qdrant with a Node or Python application?
Official libraries exist for Python, TypeScript, Rust, Go, and Java, and LangChain and LlamaIndex ship ready made integrations. Route calls from the server, since the API key must never reach the browser.
Documentation sits at qdrant.tech, and the source code in the GitHub repository.