My hands-on experience with Qdrant and Docling (and Ollama)
Source: Dev.to
Using Qdrant for the first time!
Qdrant is an open‑source, high‑performance vector database and similarity‑search engine. It stores, indexes, and searches high‑dimensional vectors, which are essential for modern AI applications such as Retrieval‑Augmented Generation (RAG) systems, recommendation engines, and image recognition. Built in Rust for speed and memory efficiency, Qdrant can be deployed both in the cloud and on‑premises.
Having evaluated various RAG solutions, I wanted to test Qdrant’s advanced metadata filtering and hybrid‑search capabilities. I used the practical RAG pipeline example from the Docling documentation to spin up a local instance and run a hands‑on performance test.
What is Docling?
Docling is an open‑source library that simplifies document processing and parsing across many formats, including advanced PDF understanding. Key features include:
- Parsing of PDF, DOCX, PPTX, XLSX, HTML, WAV, MP3, VTT, images (PNG, TIFF, JPEG, …) and more
- Advanced PDF understanding: page layout, reading order, tables, code, formulas, image classification, etc.
- Unified
DoclingDocumentrepresentation - Export to Markdown, HTML, DocTags, lossless JSON
- Local execution for sensitive data and air‑gapped environments
- Plug‑and‑play integrations with LangChain, LlamaIndex, Crew AI, Haystack
- Extensive OCR support for scanned PDFs and images
- Support for several Visual Language Models (GraniteDocling)
- Audio support with Automatic Speech Recognition (ASR) models
- MCP server for connecting to any agent
- Simple CLI
Deploying Qdrant locally
The quick‑start page is:
The following Docker (or Podman) commands set up Qdrant in minutes:
docker pull qdrant/qdrant
mkdir -p qdrant_storage
docker run -p 6333:6333 -p 6334:6334 \
-v "$(pwd)/qdrant_storage:/qdrant/storage:z" \
qdrant/qdrant
When the container starts you’ll see something like:
_ _
__ _| |_ __ __ _ _ __ | |_
/ _` | __/ _` | '__/ _` | __|
| (_| | || (_| | | | (_| | |_
\__,_|\__\__,_|_| \__,_|\__|
Version: 1.16.2, build: d2834de0
Access web UI at http://localhost:6333/dashboard
REST API: http://localhost:6333
GRPC API: http://localhost:6334
Sample Python application
The code below demonstrates basic Qdrant operations: creating a collection, upserting points, and performing both plain and filtered nearest‑neighbor searches.
# qdrant_app.py
from qdrant_client import QdrantClient
from qdrant_client.models import (
Distance,
VectorParams,
PointStruct,
Filter,
FieldCondition,
MatchValue,
UpdateStatus,
)
import time
# 1. Client initialization
print("1. Initializing Qdrant client...")
client = QdrantClient(url="http://localhost:6333")
# 2. Collection setup
COLLECTION_NAME = "city_vectors"
VECTOR_SIZE = 4
DISTANCE_METRIC = Distance.DOT
# Ensure a clean start
try:
client.delete_collection(collection_name=COLLECTION_NAME)
print(f" Collection '{COLLECTION_NAME}' deleted (if it existed).")
except Exception:
pass
print(f" Creating collection '{COLLECTION_NAME}'...")
client.create_collection(
collection_name=COLLECTION_NAME,
vectors_config=VectorParams(size=VECTOR_SIZE, distance=DISTANCE_METRIC),
)
print(" Collection created successfully.")
# 3. Upsert points
print("\n2. Upserting data points...")
points_to_insert = [
PointStruct(id=1, vector=[0.05, 0.61, 0.76, 0.74], payload={"city": "Berlin", "population": 3.7}),
PointStruct(id=2, vector=[0.19, 0.81, 0.75, 0.11], payload={"city": "London", "population": 8.9}),
PointStruct(id=3, vector=[0.36, 0.55, 0.47, 0.94], payload={"city": "Moscow", "population": 12.6}),
PointStruct(id=4, vector=[0.18, 0.01, 0.85, 0.80], payload={"city": "New York", "population": 8.4}),
PointStruct(id=5, vector=[0.24, 0.18, 0.22, 0.44], payload={"city": "Beijing", "population": 21.5}),
PointStruct(id=6, vector=[0.35, 0.08, 0.11, 0.44], payload={"city": "Mumbai", "population": 20.4}),
]
operation_info = client.upsert(
collection_name=COLLECTION_NAME,
wait=True,
points=points_to_insert,
)
print(f" Upsert operation status: {operation_info.status.name}")
if operation_info.status == UpdateStatus.COMPLETED:
time.sleep(1) # ensure index is ready
print(f" Successfully inserted {len(points_to_insert)} points.")
# 4. Plain nearest‑neighbors search
QUERY_VECTOR_1 = [0.2, 0.1, 0.9, 0.7]
print(f"\n3. Performing nearest neighbors search for query vector: {QUERY_VECTOR_1}")
search_result_1 = client.query_points(
collection_name=COLLECTION_NAME,
query=QUERY_VECTOR_1,
with_payload=True,
limit=3,
).points
print(" Top 3 closest points:")
for result in search_result_1:
print(f" - Score: {result.score:.4f}, City: {result.payload.get('city')}, Vector ID: {result.id}")
# 5. Filtered nearest‑neighbors search (only London)
QUERY_VECTOR_2 = [0.2, 0.1, 0.9, 0.7]
print(f"\n4. Performing filtered search (city='London') for query vector: {QUERY_VECTOR_2}")
search_result_2 = client.query_points(
collection_name=COLLECTION_NAME,
query=QUERY_VECTOR_2,
query_filter=Filter(
must=[FieldCondition(key="city", match=MatchValue(value="London"))]
),
with_payload=True,
limit=3,
).points
print(" Closest point filtered by city='London':")
for result in search_result_2:
print(f" - Score: {result.score:.4f}, City: {result.payload.get('city')}, Vector ID: {result.id}")
Running the example
python qdrant_app.py
Typical output:
1. Initializing Qdrant client...
Collection 'city_vectors' deleted (if it existed).
Creating collection 'city_vectors'...
Collection created successfully.
2. Upserting data points...
Upsert operation status: COMPLETED
Successfully inserted 6 points.
3. Performing nearest neighbors search for query vector: [0.2, 0.1, 0.9, 0.7]
Top 3 closest points:
- Score: 1.3620, City: New York, Vector ID: 4
- Score: 1.2730, City: Berlin, Vector ID: 1
- Score: 1.2080, City: Moscow, Vector ID: 3
4. Performing filtered search (city='London') for query vector: [0.2, 0.1, 0.9, 0.7]
Closest point filtered by city='London':
- Score: 0.8710, City: London, Vector ID: 2