GICT knowledge base
Vector Databases Explained: A Tested, Private Qdrant Lab
Published
A vector database stores numerical representations of data and retrieves nearby vectors. It can support document search or an AI assistant’s retrieval step, but it is neither required by every AI application nor a guarantee that an answer is correct. This guide explains the trade-offs and provides a small, tested Qdrant lab.
Revised and locally tested October 14, 2026: Qdrant 1.19.2, Docker Engine 29.2.1 and Python 3.12.14. The test used synthetic vectors, not customer data. It verified API behavior, filtering and dimension checks, not production throughput or semantic answer quality.
What vectors do, and what they do not do
An embedding model converts a document or question into a vector. The database compares vectors using the configured distance or similarity metric. Use the same embedding model and compatible preprocessing for documents and queries; equal vector length alone does not make two models compatible.
Cosine similarity measures direction, dot product also depends on magnitude, and Euclidean distance measures separation. Choose the metric expected by the embedding model. A cosine score is not a probability that a passage answers the question, and thresholds from one dataset do not automatically transfer to another.
More dimensions do not, by themselves, establish a better model. Evaluate relevant and irrelevant queries against your own documents. Exact keyword or structured lookup may be better for account IDs, error codes or invoice numbers.
Do you need a separate database?
If you already operate PostgreSQL, investigate pgvector, an extension supporting vector similarity search. It is not automatically installed in every PostgreSQL service. Verify availability, supported versions and index behavior with your administrator.
A separate Qdrant service can be useful when its API, filtering and operational model suit your application. Compare the cost of another service, backups and monitoring against your existing database. There is no universal document-count boundary at which one product becomes the correct choice.
Start a private Qdrant lab
Use a trusted machine with Docker Compose and an unused local port 16333. Create a dedicated lab directory and put the following in compose.yaml. The resource limits bound this tiny test; they are not production sizing recommendations.
services:
qdrant:
image: qdrant/qdrant:v1.19.2
restart: "no"
ports:
- "127.0.0.1:16333:6333"
volumes:
- vector_data:/qdrant/storage
environment:
QDRANT__TELEMETRY_DISABLED: "true"
mem_limit: 1g
cpus: 1
volumes:
vector_data:
docker compose -p tips-vector-lab up -d
curl --fail http://127.0.0.1:16333/readyz
In our test, readiness returned HTTP 200 with all shards are ready, not the JSON body claimed by the earlier article. The host’s port 16333 maps to Qdrant’s HTTP API and dashboard on container port 6333. This lab does not publish gRPC port 6334.
For a VPS, keep the binding on loopback and use an SSH tunnel from your workstation:
ssh -N -L 16333:127.0.0.1:16333 USER@VPS_HOST
Replace the account and hostname. An existing local process must not already occupy that port. The optional dashboard is then at http://127.0.0.1:16333/dashboard. Qdrant is unauthenticated in this isolated lab: loopback limits network exposure but does not protect it from other users or processes on the same host. Follow Qdrant’s security guidance before production use, including authentication, network restrictions and protected transport. Use an updated Docker Engine; older releases have localhost publishing caveats.
Test a query with an exact project filter
Place this standard-library Python example in vector_api.py and run python3 vector_api.py. It creates a uniquely named collection, so it does not overwrite an existing one.
import json
from urllib.error import HTTPError
from urllib.request import Request, urlopen
import uuid
BASE = "http://127.0.0.1:16333"
def api(method, path, data=None):
body = None if data is None else json.dumps(data).encode("utf-8")
request = Request(BASE + path, data=body, method=method,
headers={"Content-Type": "application/json"})
with urlopen(request, timeout=30) as response:
return json.load(response)
def run_demo():
collection = "tips_vectors_" + uuid.uuid4().hex
path = "/collections/" + collection
api("PUT", path, {"vectors": {"size": 3, "distance": "Cosine"}})
api("PUT", path + "/points?wait=true", {"points": [
{"id": 1, "vector": [1.0, 0.0, 0.0], "payload": {"project": "alpha"}},
{"id": 2, "vector": [0.0, 1.0, 0.0], "payload": {"project": "alpha"}},
{"id": 3, "vector": [1.0, 0.0, 0.0], "payload": {"project": "beta"}}
]})
result = api("POST", path + "/points/query", {
"query": [1.0, 0.0, 0.0], "limit": 3, "with_payload": True,
"filter": {"must": [{"key": "project", "match": {"value": "alpha"}}]}
})
points = result["result"]["points"]
assert [point["id"] for point in points] == [1, 2]
print("Collection:", collection)
print("Filtered point IDs:", [point["id"] for point in points])
return collection, points
if __name__ == "__main__":
run_demo()
The expected filtered IDs are [1, 2]. Point 3 has the same vector as point 1, but belongs to project beta and must be excluded. These hand-written three-dimensional vectors demonstrate API mechanics only. They are not embeddings of the project names and do not prove semantic understanding.
The filter uses exact equality. A value such as /projects/api/ is not automatically a prefix match for every file below that directory. Store an explicit project identifier or design a suitable indexed field. See the Query API and filter reference.
Measure before tuning
Approximate nearest-neighbor indexes trade search work, memory and recall. Measure recall@k by comparing approximate top-k results with an exact-search baseline on the same dataset and filters. Separately measure whether those results actually answer the user’s question; nearest-vector recall and answer relevance are different tests.
Record collection size, dimensions, index settings, payload filters, hardware, concurrency and latency percentiles. This article does not establish a universal QPS, minimum RAM or similarity threshold. The previous unsupported performance figures have been withdrawn.
Before relying on persistent data, plan and test a restore into a separate instance using the Qdrant snapshot documentation. A Docker volume surviving a restart is not a backup.
Stop or remove this lab
docker compose -p tips-vector-lab down
This stops the lab while retaining its named volume. Only when that volume contains disposable test data, use docker compose -p tips-vector-lab down --volumes to delete it. Run cleanup from the lab directory with the correct Compose file, never against an unrelated project.
For a real embedding example, continue with the document retrieval lab. For hosting-account tools rather than document search, see the separate InterServer MCP Server guide.
Related guides
How to Secure the Ollama API with Nginx
Protect a private Ollama API with Nginx authentication and restricted routes. Verify allowed requests, denied management calls and direct-port isolation.
AIHow to Set Up an LLM Gateway with LiteLLM
Configure a LiteLLM gateway with private administration, virtual keys and model restrictions. Test allowed requests and understand budget limitations.
AIHow to Self-Host Karakeep with AI Tagging on a VPS
Self-host Karakeep with local Ollama tagging. Restrict first-user setup, disable public registration and verify collection, search and suggested tags.
AIHow to Categorize Support Tickets with a Local AI Model
Suggest support-ticket categories from a CSV using a CPU-only AI model. Keep inference local, preserve ticket IDs, and review uncertain classifications.
