What Qdrant actually is
Qdrant is an open-source vector database written in Rust that stores high-dimensional vectors alongside a JSON payload for each point. The Rust implementation matters more than the marketing suggests, because vector search is memory bound and Rust gives you predictable latency without a garbage collector pausing the process mid-query.
Under the hood, Qdrant uses the HNSW (Hierarchical Navigable Small World) algorithm as its primary index, with optional quantization layers (scalar, product, and binary) that trade a small amount of recall for major reductions in RAM. You talk to it over REST or gRPC, the Python and Rust clients are first-class, and there is a managed cloud version if you do not want to operate the cluster yourself.
What separates Qdrant from a bare FAISS wrapper is the payload system. Every vector carries a structured JSON document, and Qdrant can filter on that payload before, during, or after the vector search. This sounds unremarkable until you build a real retrieval pipeline and realize most of the interesting business logic lives in the filter, not the similarity score.
It is not a general-purpose database. You would not put your transactional records in it. Treat it as a specialized index for similarity search with structured metadata, and the rest of the architecture decisions become clearer.
Setup and authentication
The fastest path is Docker. The image ships with both the REST endpoint on 6333 and the gRPC endpoint on 6334 exposed, and a small web UI lives at the root of the REST port for poking around.
Pull and run the latest image with a persistent volume so your data survives container restarts. The bind mount points to /qdrant/storage inside the container, which is where collections, indexes, and the write-ahead log live.
For production, you would front this with a reverse proxy, set QDRANT__SERVICE__API_KEY in the environment, and disable the dashboard or protect it with a separate auth layer. The API key check is enforced on the gRPC and REST ports and is the only thing standing between your indexes and the open internet.
On the client side, the Python package installs cleanly with pip. The QdrantClient constructor takes a URL and an API key. For local Docker it is usually http://localhost:6333 with no key. For Qdrant Cloud you paste the cluster URL and the key from the dashboard, and the same client object works against either target.
If you are on Rust, go, or Java, the same endpoints are available. The HTTP shape is small enough that a curl-based smoke test is usually the first thing to wire up in CI.
First working example
Here is a concrete, runnable flow that exercises the three things you will do ninety percent of the time: create a collection, upsert points with payloads, and run a filtered search.
Start by connecting the client and creating a collection sized for 384-dimensional vectors, which is the output dimension of common sentence-transformer models. Set the distance metric to Cosine if you are comparing semantic embeddings, which is the default mental model most teams have. If you are comparing normalized image embeddings, Cosine and Dot give the same ranking. Use Euclidean only when the absolute distance in the original space carries meaning.
Next, build a list of points where each point carries an id, a vector, and a payload with whatever structured fields matter to your domain. A realistic payload might be a document id, the source URL, the section title, and a tenant identifier for multi-tenant filtering.
Upsert the points in a single batch call. Qdrant accepts batches in the thousands per call, and the client handles serialization for you.
Now the interesting part. Run a search with a query vector and a payload filter. The filter syntax is a small DSL with must, should, and must_not clauses, plus field-level operators like match, range, and geo. A typical filter for a multi-tenant RAG system is a tenant match combined with a created_at range, and Qdrant will only score vectors that pass the filter, which is much faster than retrieving everything and filtering in application code.
The response is a list of scored points ordered by similarity. Each point carries the score, the payload, and the original id, which is usually enough to hydrate a response with one extra lookup against your source-of-truth database.
If you get results back and the IDs match what you inserted, you have a working retrieval pipeline. Everything else in this guide is refinement.
Key settings that matter
Most teams ship with the defaults and never touch these. That works until it does not.
The distance metric is the first decision and it is permanent per collection. Cosine is the safe default for text embeddings. Dot product is faster because it skips the normalization step and is the right choice when you control the embedding model and can guarantee unit-norm vectors. Euclidean is what you want when distance in the original embedding space carries semantic meaning, which is rare.
HNSW has two parameters that matter. m controls the number of bidirectional links per node, and ef_construct controls the search beam width during indexing. Higher values give better recall at the cost of memory and indexing time. The defaults are tuned for general workloads. If you are indexing millions of vectors and seeing poor recall, raise ef_construct first and m second. If you are memory-constrained, lower them and accept a small recall hit.
Quantization is the single biggest lever for cost. Scalar quantization reduces each vector to int8 with minimal recall loss and roughly four times memory reduction. Product quantization is more aggressive and requires picking the right number of subvectors. Binary quantization is the most aggressive and is appropriate when you only need rough similarity ordering. You can enable quantization on collection creation or alter it later on an existing collection, but switching the type requires a reindex.
Payload indexing is the second-biggest lever. By default Qdrant scans payloads at query time, which is fine for small collections. The moment a payload field appears in most of your filters, build a payload index on it. The build is incremental and runs in the background.
Sharding and replication are cluster-level decisions. A single-node deployment is fine up to a few million vectors in the size range most teams use. Beyond that, increase the shard count to spread the index across nodes, and set the replication factor to at least two for durability.
On-disk payload storage is worth knowing about for large payloads. By default, payloads live in memory. Setting on_disk=true moves them to disk with a small latency cost, which is often the right trade when you are storing long document chunks but only ever returning a snippet.
Where it shines
Filtered hybrid search is the headline use case. The combination of vector similarity plus structured filters in a single query is hard to replicate without bolting Postgres and FAISS together, and that combination is brittle at scale.
Recommendation systems fit well, especially when you have user vectors, item vectors, and rich item metadata. The payload filter lets you exclude items the user has already seen, restrict to in-stock inventory, or boost recent content, all within a single ranked result set.
Multi-tenant SaaS applications are a natural fit because tenant identifiers in the payload become a hard filter, and you can guarantee isolation at query time without separate indexes per tenant.
Image and audio similarity at scale is the other strong use case. The Rust core handles large batch inserts and concurrent reads well, and the gRPC interface removes HTTP overhead for high-QPS workloads. Teams running reverse image search or audio fingerprinting against tens of millions of items report consistent sub-50ms p95 latencies on appropriately sized clusters.
Versioned document search where each document has a created_at, version, and status field is a quiet win. The payload DSL handles range and match conditions, and you can combine them with vector similarity without writing any application-side orchestration.
Where it fails
Exact-match lookups are not what Qdrant is for. If your query is an id lookup, use a key-value store. Vector search always ranks a neighborhood, even when one of the neighbors is the exact match, and the overhead is wasted for that use case.
Memory cost is the operational tax. HNSW indexes are RAM-resident for best performance, and a million 768-dimensional vectors at full precision is in the gigabyte range, which is in line with other vector databases at this tier. Quantization cuts this dramatically but adds operational complexity.
There is no built-in BM25 text search in the open-source version, and adding it on top means a second system. The cloud tier has been adding hybrid features, and the Qdrant team has been rolling out sparse vector support, so this is changing, but if you need production-grade lexical + semantic hybrid today, plan for a second store and a re-ranker.
Cold-start latency on a freshly loaded collection is real. The HNSW graph warms up over the first few hundred queries and reaches steady state after that. Pre-warming strategies exist but cost memory.
Single-node deployments have an upper limit in the tens of millions of vectors for typical embedding sizes. Once you outgrow that, you are doing cluster work, and cluster work has its own learning curve around shard placement and replication.
The client API has evolved quickly, and examples you find online may use deprecated patterns. Pin your client version and read the changelog before upgrading.
Practical workflow pattern
Local development starts with the Docker image and a single-node configuration. Use a small embedding model, seed a couple hundred vectors, and confirm your filter logic is correct before scaling up. The web UI at the REST root is genuinely useful for inspecting a collection and running ad-hoc queries while debugging.
Staging should mirror production sizing as closely as possible, with the same shard count and replication factor. Vector indexes behave differently at 10,000 points than at 10 million, and bugs in filter logic or recall issues only show up at scale. Use a synthetic dataset generator that matches your embedding distribution, or replay a sampled subset of production traffic.
The indexing pipeline in production is usually a worker consuming from a queue. Embed documents as they are created or updated, batch them into groups of a few hundred, and upsert into Qdrant. Idempotency matters, so use stable point ids derived from your source-of-truth id, not auto-incremented values.
Backups are snapshot-based. Qdrant can create a snapshot of a collection on demand, and you should ship those snapshots to object storage on a schedule. Restoring is a matter of uploading the snapshot and pointing a new collection at it.
Monitoring should track p50, p95, and p99 query latency, recall against a known answer set, queue depth on the indexing side, and memory pressure on each node. The REST API exposes telemetry and integrates with Prometheus, which is the path most teams take.
The decision of self-hosted versus cloud usually comes down to operations capacity. Self-hosted gives you full control and lower per-vector cost at scale. Cloud removes the cluster work and gives you managed backups and versioning, at a higher unit cost. For a first deployment, cloud is usually the right call, and self-hosting is worth revisiting when monthly spend justifies the operational investment.
Enterprise DNA put together a free field guide on exactly this: the full Claude ecosystem, Claude Code, and how to roll agents out without breaking things. Get the guide.