Qdrant on a VPS: High-Performance Vector Search Engine for AI Applications and Semantic Search
Qdrant is a purpose-built vector search engine — not a PostgreSQL extension, but a dedicated high-performance database optimized exclusively for vector similarity search. While pgvector integrates with existing PostgreSQL, Qdrant provides higher throughput, richer filtering, payload storage alongside vectors, named collections, and a REST + gRPC API purpose-built for vector operations. It’s the right choice when vector search is your primary workload rather than one feature among many.
Qdrant vs pgvector: When to Choose Qdrant
| Factor | Qdrant | pgvector |
|---|---|---|
| Primary use case | Vector search is the main workload | Vector search alongside SQL data |
| Query speed | Higher throughput (Rust, optimized) | Good, bounded by PostgreSQL |
| Filtering | Rich payload filtering during search | SQL WHERE clause |
| Multiple vectors | Yes (named vector spaces per point) | One vector column per row |
| Payload storage | JSON payload stored with vectors | Regular table columns |
| Existing PostgreSQL | Separate service | Extension in existing DB |
| RAM footprint | Low (Rust, configurable) | Shared with PostgreSQL |
Step 1: Install Qdrant with Docker
<code">mkdir -p /opt/qdrant/storage && cd /opt/qdrant nano docker-compose.yml
<code">version: '3.8'
services:
qdrant:
image: qdrant/qdrant:latest
container_name: qdrant
restart: always
ports:
- "127.0.0.1:6333:6333" # REST API
- "127.0.0.1:6334:6334" # gRPC API
volumes:
- ./storage:/qdrant/storage
environment:
QDRANT__SERVICE__API_KEY: ${QDRANT_API_KEY}
configs:
- source: qdrant_config
target: /qdrant/config/production.yaml
configs:
qdrant_config:
content: |
service:
host: 0.0.0.0
http_port: 6333
grpc_port: 6334
api_key: ${QDRANT_API_KEY}
storage:
storage_path: /qdrant/storage
# Performance tuning
hnsw_index:
m: 16
ef_construct: 100
full_scan_threshold: 10000
<code"># Generate a strong API key
echo "QDRANT_API_KEY=$(openssl rand -hex 32)" > .env
chmod 600 .env
docker compose up -d
docker compose logs -f qdrant # Wait for "Qdrant HTTP listening on 6333"
# Verify
curl http://localhost:6333/health # {"title":"qdrant - vector search engine","version":"..."}
Step 2: Nginx Reverse Proxy (Secure Access)
<code">sudo nano /etc/nginx/sites-available/qdrant
<code">server {
listen 443 ssl http2;
server_name qdrant.yourdomain.com;
ssl_certificate /etc/letsencrypt/live/qdrant.yourdomain.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/qdrant.yourdomain.com/privkey.pem;
# Qdrant API key provides auth — add extra IP restriction if needed
location / {
proxy_pass http://127.0.0.1:6333;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_read_timeout 300s;
client_max_body_size 100M;
}
}
<code">sudo certbot --nginx -d qdrant.yourdomain.com sudo ln -s /etc/nginx/sites-available/qdrant /etc/nginx/sites-enabled/ sudo systemctl reload nginx
Step 3: Create a Collection
<code"># Create a collection for document embeddings (OpenAI text-embedding-3-small = 1536 dims)
curl -X PUT http://localhost:6333/collections/documents \
-H "api-key: YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"vectors": {
"size": 1536,
"distance": "Cosine"
},
"hnsw_config": {
"m": 16,
"ef_construct": 100,
"full_scan_threshold": 10000
},
"on_disk_payload": true
}'
# For multi-vector collections (e.g., title + body embeddings separately):
curl -X PUT http://localhost:6333/collections/articles \
-H "api-key: YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"vectors": {
"title": {"size": 1536, "distance": "Cosine"},
"body": {"size": 1536, "distance": "Cosine"}
}
}'
Step 4: Insert Points (Vectors + Payload)
<code">pip install qdrant-client openai
<code">from qdrant_client import QdrantClient, models
from qdrant_client.models import PointStruct, VectorParams, Distance
import openai
import uuid
# Initialize clients
qdrant = QdrantClient(
url="http://localhost:6333",
api_key="YOUR_API_KEY",
)
openai_client = openai.OpenAI(api_key="YOUR_OPENAI_KEY")
def embed(text: str) -> list[float]:
"""Generate OpenAI embedding."""
response = openai_client.embeddings.create(
model="text-embedding-3-small",
input=text,
)
return response.data[0].embedding
# Insert documents
documents = [
{"title": "VPS Security Guide", "content": "How to secure your VPS with UFW...", "category": "security"},
{"title": "Docker on Ubuntu", "content": "Installing Docker on Ubuntu 24.04...", "category": "devops"},
{"title": "PostgreSQL Optimization", "content": "Tuning PostgreSQL for performance...", "category": "database"},
]
points = []
for doc in documents:
embedding = embed(f"{doc['title']} {doc['content']}")
points.append(PointStruct(
id=str(uuid.uuid4()),
vector=embedding,
payload={
"title": doc["title"],
"content": doc["content"],
"category": doc["category"],
}
))
qdrant.upsert(collection_name="documents", points=points)
print(f"Inserted {len(points)} documents")
Step 5: Semantic Search
<code">def semantic_search(
query: str,
collection: str = "documents",
top_k: int = 5,
filter_category: str = None,
) -> list[dict]:
"""Search for semantically similar documents."""
query_vector = embed(query)
# Optional metadata filter
query_filter = None
if filter_category:
query_filter = models.Filter(
must=[
models.FieldCondition(
key="category",
match=models.MatchValue(value=filter_category),
)
]
)
results = qdrant.search(
collection_name=collection,
query_vector=query_vector,
query_filter=query_filter,
limit=top_k,
with_payload=True, # Include stored metadata
score_threshold=0.7, # Only return results above this similarity score
)
return [
{
"id": r.id,
"score": r.score,
"title": r.payload.get("title"),
"content": r.payload.get("content"),
"category": r.payload.get("category"),
}
for r in results
]
# Usage
results = semantic_search("how to harden linux server security")
for r in results:
print(f"{r['score']:.3f} — {r['title']}")
# With category filter
security_results = semantic_search(
"database performance",
filter_category="database",
top_k=3,
)
Step 6: Recommendation System
<code"># Find items similar to a known good item
similar = qdrant.recommend(
collection_name="documents",
positive=["point_id_of_liked_article"], # IDs of items the user liked
negative=["point_id_of_disliked_article"], # IDs of items the user disliked
limit=10,
with_payload=True,
)
# Find similar but exclude already seen
similar = qdrant.recommend(
collection_name="documents",
positive=["point_id_1", "point_id_2"],
filter=models.Filter(
must_not=[
models.HasIdCondition(has_id=["seen_id_1", "seen_id_2"])
]
),
limit=5,
)
Step 7: REST API Examples
<code"># Search via REST (for non-Python clients)
curl -X POST http://localhost:6333/collections/documents/points/search \
-H "api-key: YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"vector": [0.1, 0.2, ...],
"limit": 5,
"with_payload": true,
"filter": {
"must": [
{"key": "category", "match": {"value": "security"}}
]
},
"score_threshold": 0.7
}'
# Count points in collection
curl http://localhost:6333/collections/documents \
-H "api-key: YOUR_API_KEY"
# List all collections
curl http://localhost:6333/collections \
-H "api-key: YOUR_API_KEY"
# Delete points by filter (e.g., delete outdated content)
curl -X POST http://localhost:6333/collections/documents/points/delete \
-H "api-key: YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"filter": {
"must": [{"key": "outdated", "match": {"value": true}}]
}
}'
Memory Configuration for VPS
<code"># Qdrant memory modes for different VPS sizes: # production.yaml: # For 2 GB VPS — store vectors on disk, use RAM only for HNSW index storage: on_disk_vectors: true # Vectors stored on NVMe, loaded on demand storage_path: /qdrant/storage # For 4+ GB VPS — keep vectors in RAM for faster search storage: on_disk_vectors: false # Vectors in RAM (much faster) # Performance impact: on_disk_vectors=true is ~2-5× slower for search # but enables millions of vectors on a small VPS
Getting Started
Qdrant’s Rust implementation is memory-efficient — with on_disk_vectors: true, a 1 GB RAM VPS handles millions of vectors by keeping only the HNSW index in memory. For latency-sensitive applications, a 4 GB Ubuntu VPS at VPS.DO with vectors in RAM provides sub-millisecond search across hundreds of thousands of embeddings. NVMe storage is especially important for on-disk vector mode — Qdrant’s I/O patterns benefit from NVMe random read performance.
Conclusion
Qdrant on a VPS delivers a purpose-built vector search engine with high throughput, rich payload filtering, named vector spaces, and REST + gRPC APIs — optimized specifically for AI applications where vector search is the primary workload. For semantic search, recommendation systems, duplicate detection, and RAG pipelines that need more performance and flexibility than pgvector provides, self-hosted Qdrant on a VPS replaces managed vector database services (Pinecone at $70+/month, Weaviate Cloud) at infrastructure cost only.