A compact playground for building high-quality search over documentation with Qdrant. The project takes docs-style JSON, chunks it, creates dense, sparse, and ColBERT vectors, and compares hybrid retrieval quality with repeatable evaluation scripts.
It is meant to make search experiments quick to repeat: swap an embedder, adjust chunking, rebuild a collection, then run the same evaluation set to see what actually improved.
- Load docs-style JSON from
datasets/. - Chunk document text with LlamaIndex splitters and Hugging Face tokenizers.
- Create dense, sparse, and ColBERT vectors with FastEmbed, FlagEmbedding, or the Jina + miniCOIL setup.
- Upload vectors and payloads into a Qdrant collection.
- Embed the user query with the same vector types.
- Run hybrid search in Qdrant and return ranked results.
- Evaluate the same flow with recall, MRR, and latency metrics.
- Chunking controls how much context each vector represents; larger chunks keep more context, while smaller chunks can improve precise matches.
- Dense vectors capture semantic similarity, so related wording can match even when the exact query terms are missing.
- Sparse vectors capture lexical matches, which helps preserve keyword precision for names, API terms, settings, and error messages.
- ColBERT vectors keep token-level representations and compare them late in the search flow, giving stronger reranking than a single document vector alone.
- RRF merges the dense, sparse, and ColBERT result lists without needing their raw scores to be on the same scale.
- Payload indexes make filters such as
tags,page_url, andsection_urlefficient during search.
Install the Python dependencies:
pip install -r requirements.txtRun Qdrant locally with persistent storage:
docker run --name qdrant -p 6333:6333 -p 6334:6334 \
-e QDRANT__SERVICE__MAX_REQUEST_SIZE_MB=128 \
-v "$(pwd)/qdrant_storage:/qdrant/storage:z" \
qdrant/qdrantThe qdrant_storage/ mount keeps collections across container restarts. The
128 MB request limit gives more room for large hybrid upserts, especially when
using ColBERT-style multi-vectors.
- Run the main search flow:
python qdrant_search.pyqdrant_search.py creates or reuses a collection, chunks the configured dataset,
embeds the chunks, uploads them to Qdrant, and runs a sample hybrid search.
- Compare chunking strategies:
python scripts/compare_chunking.pycompare_chunking.py prints fixed, sentence, and semantic chunks for one sample
document.
- Evaluate retrieval quality:
python scripts/evaluate_search.pyevaluate_search.py runs the eval queries and reports recall, MRR, and latency.
qdrant_search.pycontainsTextChunkerandQdrantSearchManager.qdrant_embedder.pycontains FastEmbed, BGE-M3, and Jina document embedders.qdrant_config.pycontains model ids, dataset configs, and vector/chunk config helpers.scripts/compare_chunking.pycompares fixed, sentence, and semantic chunking.scripts/evaluate_search.pyevaluates recall, MRR, and latency.datasets/contains the sample and generated dataset files.
- Qdrant stores vectors, payloads, indexes, and runs hybrid retrieval.
- FastEmbed provides the MiniLM dense model, BM25/miniCOIL sparse models, and ColBERT model.
- FlagEmbedding runs BGE-M3 for dense, sparse, and ColBERT-style vectors.
- Hugging Face and Sentence Transformers provide model/tokenizer loading for chunking and embeddings.
- LlamaIndex provides sentence and semantic chunking utilities.
DocumentEmbedderuses FastEmbed models for dense, sparse, and ColBERT vectors.BGEM3DocumentEmbedderuses FlagEmbedding with BGE-M3 for dense, sparse, and ColBERT vectors from one model.JinaDocumentEmbedderuses Jina v5 for dense vectors, miniCOIL for sparse vectors, and FastEmbed ColBERT.- Vector dimensions are read from the active embedder and passed into the Qdrant collection config.
sentenceis the default strategy and preserves sentence boundaries with overlap.fixedsplits by token count and is mostly useful for debugging.semanticuses embedding similarity to group nearby text.- Chunk sizes live in
chunking_config(), with separate defaults for MiniLM, BGE-M3, and Jina v5.
Datasets are configured in DATASET_CONFIGS. Each entry points to a JSON file,
declares the expected entry count, and defines payload indexes for Qdrant. The
docs datasets currently index page_url, section_url, tags, and
breadcrumbs.
Example entry:
{
"page_title": "Collections and Points with Python client",
"section_title": "Collection lifecycle",
"page_url": "/documentation/concepts/collections-and-points/python/local-docker/",
"section_url": "/documentation/concepts/collections-and-points/python/local-docker/#collection-lifecycle",
"breadcrumbs": [
"Documentation",
"Concepts",
"Collections and Points",
"Python client",
"Collection lifecycle"
],
"chunk_text": "Collection lifecycle explains how collection design is usually modeled when Qdrant backs product search on local Docker. ...",
"prev_section_text": "",
"next_section_text": "Point identifiers and payloads explains how collection design is usually modeled when Qdrant backs product search on local Docker. ...",
"tags": [
"collections",
"local-docker",
"payload",
"points",
"product-search",
"python",
"vector-search"
]
}The evaluation script checks whether each query retrieves its expected documentation URL in the top results. It reports recall, MRR, and latency for the configured collection.
Change embedding_name in scripts/evaluate_search.py to compare
all_minilm, bge_m3, and jina_v5 runs.
- Recall@10 measures whether the expected URL or section anchor appears in
the top 10 results. A score of
0.8means the system finds the correct answer 80% of the time. - MRR@10 measures how early the correct result appears. Rank 1 scores
1.0, rank 2 scores0.5, and a miss in the top 10 scores0. - Latency P50/P95 measures response time. P50 is the median query latency, while P95 captures slower tail latency that affects user experience.
Example BGE-M3 run:
Collection 'docs_search_bge_m3' ready (status=green, points=5400, segments=6).
Evaluation: queries=25, limit=10, candidate_limit=100, fusion=rrf, hnsw_ef=None
rank=1 1585.1 ms how to configure HNSW graph construction for better recall
rank=1 498.9 ms combine dense and sparse results with reciprocal rank fusion
rank=6 368.1 ms ColBERT token level vectors for reranking
rank=1 225.6 ms RAG context assembly and answer grounding
Summary: recall@10=1.000, mrr@10=0.910, p50=328.0 ms, p95=1507.7 ms
- BGE-M3 creates much larger vectors than the MiniLM/FastEmbed setup, so keep upload batches small.
- Jina v5 is available as an experimental mixed setup with Jina dense, miniCOIL sparse, and FastEmbed ColBERT.
- The active collection, dataset, embedder, and chunking profile are selected in each script's
__main__block. - Use a new collection name when changing vector dimensions, otherwise Qdrant will reject incompatible uploads.
- The datasets are synthetic and were created with ChatGPT for search experiments.
- The large
qdrant_docs_100kdataset uses Git LFS; install Git LFS and rungit lfs pullafter cloning if the file is not downloaded automatically.
- Qdrant fundamentals: Qdrant Essentials
- Dense embedding model: all-MiniLM-L6-v2
- Jina dense embedding model: jina-embeddings-v5 text small retrieval
- Sparse embedding model: miniCOIL v1
- Semantic chunking: LlamaIndex Semantic Double Merging Chunking
- BGE-M3 embedding library: FlagEmbedding