Qdrant on a VPS: High-Performance Vector Search Engine for AI Applications and Semantic Search

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.

Fast • Reliable • Affordable VPS - DO It Now!

Get top VPS hosting with VPS.DO’s fast, low-cost plans. Try risk-free with our 7-day no-questions-asked refund and start today!