This commit is contained in:
Dylan Couzon
2026-09-09 21:32:28 -04:00
parent 6ec807a54e
commit 45ca828f2c
54 changed files with 1676 additions and 432 deletions
@@ -32,6 +32,8 @@ jobs:
curl -sf http://localhost:1314/ >/dev/null && break curl -sf http://localhost:1314/ >/dev/null && break
sleep 1 sleep 1
done done
- name: Learn Navigation Check
run: python3 automation/check-learn.py --public qdrant-landing/public
- name: Internal Links Check - name: Internal Links Check
id: lychee id: lychee
uses: lycheeverse/lychee-action@e7477775783ea5526144ba13e8db5eec57747ce8 # v2.9.0 uses: lycheeverse/lychee-action@e7477775783ea5526144ba13e8db5eec57747ce8 # v2.9.0
+140
View File
@@ -0,0 +1,140 @@
#!/usr/bin/env python3
"""Check a Hugo build of Learn: python3 automation/check-learn.py --public PATH."""
import argparse
import hashlib
from html.parser import HTMLParser
import json
from pathlib import Path
import re
from urllib.parse import urlsplit, unquote
class Page(HTMLParser):
def __init__(self, text):
super().__init__()
self.links, self.ids, self.guides = set(), set(), set()
self.examples, self.neighbors, self.redirect = 0, {}, None
self.feed(text)
def handle_starttag(self, tag, attrs):
attr = dict(attrs)
if attr.get('id'):
self.ids.add(attr['id'])
if 'data-example' in attr:
self.examples += 1
if tag == 'a' and attr.get('href'):
href = attr['href']
self.links.add(href)
if 'data-guide-link' in attr:
self.guides.add(urlsplit(href).path)
if attr.get('rel') in ('prev', 'next'):
self.neighbors[attr['rel']] = urlsplit(href).path
if tag == 'meta' and attr.get('http-equiv', '').lower() == 'refresh':
self.redirect = urlsplit(attr.get('content', '').split('url=', 1)[-1]).path
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument('--public', type=Path, required=True)
args = parser.parse_args()
root = Path(__file__).resolve().parents[1]
manifest = json.loads((root / 'contributing/guide-sources.json').read_text())
errors, cache = [], {}
def page(route):
route = urlsplit(route).path
if route not in cache:
path = args.public / route.strip('/') / 'index.html'
if not path.exists():
errors.append(f'Missing page: {route}')
cache[route] = Page(path.read_text() if path.exists() else '')
return cache[route]
# Compare preserved guide text with the original source hash, allowing routing and presentation changes.
for entry in manifest['guides']:
source = root / entry['guide']
body = source.read_text().split('---', 2)[2]
original_title = '# ' + entry['title']
# Source documentation already contains its H1; migrated articles receive one for the docs layout.
if entry['added_title']:
body = body.replace(original_title, '', 1)
body = re.sub(r'{{< /?read-more >}}', '', body)
for other in manifest['guides']:
old = '/' + other['source'].split('content/', 1)[1].removesuffix('.md') + '/'
body = body.replace(old, other['url'])
digest = hashlib.sha256(re.sub(r'\s+', ' ', body).strip().encode()).hexdigest()
if digest != entry['body_sha256']:
errors.append(f'Guide source text changed: {source}')
if (root / entry['source']).exists():
errors.append(f'Duplicate source: {entry["source"]}')
if page(entry['url']).redirect:
errors.append(f'Guide must render its content: {entry["url"]}')
if '/articles/' in entry['source']:
old = '/articles/' + Path(entry['source']).stem + '/'
if page(old).redirect != entry['url']:
errors.append(f'Article redirect missing: {old}')
routes = {entry['url'] for entry in manifest['guides']}
for section, count in [('search-quality', 2), ('search-tuning', 8), ('production-patterns', 3)]:
route = '/documentation/' + section + '/'
links = {urlsplit(link).path for link in page(route).links}
expected = {e['url'] for e in manifest['guides'] if Path(e['guide']).parent.name == section}
if len(expected) != count or not expected <= links or page(route).guides != routes:
errors.append(f'Guide category has incorrect membership: {section}')
for landing in ['/learn/', '/documentation/guides/']:
if route not in {urlsplit(link).path for link in page(landing).links}:
errors.append(f'{landing} omits {route}')
series = [e['url'] for e in manifest['guides'] if '/search-tuning/' in e['url']]
for index, route in enumerate(series):
expected = {}
if index:
expected['prev'] = series[index - 1]
if index + 1 < len(series):
expected['next'] = series[index + 1]
if page(route).neighbors != expected:
errors.append(f'Series order incorrect: {route}')
for link in page(route).links:
target = urlsplit(link)
if target.path in series and target.fragment and unquote(target.fragment) not in page(target.path).ids:
errors.append(f'Broken series anchor: {link}')
entries = re.findall(r'^- page: (\S+)', (root / 'qdrant-landing/data/examples.yaml').read_text(), re.M)
catalog = page('/learn/examples/')
if catalog.examples != len(entries) or len(set(entries)) != len(entries):
errors.append('Catalog must show each registered tutorial exactly once')
if not {'example-query', 'example-goal', 'example-stack'} <= catalog.ids:
errors.append('Catalog filters are missing')
for route in entries:
route = route.lower()
if page(route).redirect:
errors.append(f'Tutorial source was replaced: {route}')
if route not in {urlsplit(link).path for link in catalog.links}:
errors.append(f'Tutorial missing from catalog: {route}')
if (args.public / 'learn/examples/index.md').read_text().count('[Open Example]') != len(entries):
errors.append('Markdown catalog differs from HTML catalog')
# Cascade rules suppress archive bodies in HTML, Markdown, and discovery indexes.
index = (root / 'qdrant-landing/content/articles/_index.md').read_text()
retired = re.search(r'path: /articles/\{([^}]+)\}', index)[1].split(',')
discovery = '\n'.join((args.public / name).read_text() for name in ['sitemap.xml', 'llms.txt', 'articles/index.md'])
for slug in retired:
source = root / 'qdrant-landing/content/articles' / (slug + '.md')
if not source.exists():
source = root / 'qdrant-landing/content/articles' / slug / '_index.md'
fm = source.read_text().split('---', 2)[1]
explicit = re.search(r'^slug:\s*(\S+)', fm, re.M)
route = '/articles/' + re.sub(r'[^\w-]', '', explicit[1].strip('"\'').lower() if explicit else slug) + '/'
path = args.public / route.strip('/')
if re.search(r'^draft: true\s*$', fm, re.M):
if (path / 'index.html').exists():
errors.append(f'Archived draft published: {route}')
continue
if page(route).redirect != '/articles/' or not (path / 'index.md').exists() or (path / 'index.md').read_text().strip() != '# Articles\n\nBrowse current Qdrant articles in [Articles](/articles/index.md).':
errors.append(f'Archive body exposed: {route}')
if route in discovery:
errors.append(f'Archive listed in discovery: {route}')
for slug in ['search-quality', 'embedding-research', 'qdrant-internals', 'production-ops']:
category = page('/articles/' + slug + '/')
if category.redirect:
errors.append(f'Active article category redirected: {slug}')
if errors:
raise SystemExit('\n'.join(errors))
print(f'PASS: {len(routes)} preserved guides; {len(series)} ordered series parts; {len(entries)} original tutorials; archive bodies excluded.')
+31
View File
@@ -0,0 +1,31 @@
# Maintain Learn
Learn has four resources: Guides, Tutorials & Examples, Courses, and Articles. Keep each piece in one source file and use the collections to make it discoverable.
## Tutorials & Examples
`qdrant-landing/data/examples.yaml` is the catalog. Each entry identifies an existing tutorial page, its goal, and its stack. Optional keywords improve search. Selected notebook and repository links appear in `resources`.
Hugo reads each title and description from the source tutorial. The catalog links to that page; it does not move or copy the tutorial. Both the HTML and Markdown catalog use the same entries. The browser filters the rendered cards without a separate search service.
Adding a tutorial requires one catalog entry. Use existing goal and stack labels where they fit. The build fails if the source page is missing or a selected resource URL no longer appears in the source tutorial.
## Guides
The three guide sections use `learning_kind: guides` and `partition: learn`. Topic cards and sidebar entries derive from their contents. Public URLs can remain stable through `url`, while `aliases` preserve former article URLs after a move.
The tuning series uses `guide_series: true` on its six pages. Their weights determine the order, numbered cards, and previous/next links. Standalone design guides remain outside that sequence.
`contributing/guide-sources.json` records the existing source for each migrated guide. Its hashes protect the preserved source text while allowing the routing and presentation changes recorded there. Review technical revisions separately from navigation changes.
## Articles
An article's `category` remains its normal topic field. The Articles index contains the few compatibility mappings needed for the four public topics. Authors can use Search Quality, Embedding Research, Qdrant Internals, or Production Ops directly for new articles.
The index also contains the archive cascade. It makes the listed pages redirect, excludes them from discovery, and suppresses their bodies in HTML and Markdown. Their original source files remain untouched. Apply retirement rules there instead of editing each archived article.
## Verify Changes
Build Hugo, then run `python3 automation/check-learn.py --public qdrant-landing/public`. The check covers preserved guide text, category membership, series navigation, catalog links, redirects, and archive exclusions. The existing redirect audit remains part of CI.
Check search, filters, Clear Filters, and a narrow-screen layout in the preview before submitting navigation changes.
+109
View File
@@ -0,0 +1,109 @@
{
"baseline": "0490e1e38cde035eacece7ca185e5ff44bbca32e",
"guides": [
{
"source": "qdrant-landing/content/documentation/improve-search/retrieval-relevance.md",
"guide": "qdrant-landing/content/documentation/search-quality/retrieval-relevance.md",
"url": "/documentation/improve-search/retrieval-relevance/",
"title": "Measuring Retrieval Relevance",
"body_sha256": "4809cf786abb14b818800e002d2b505bb8ce80fad66df477f1ec513fd02b2926",
"added_title": false
},
{
"source": "qdrant-landing/content/documentation/improve-search/pipeline-output-quality.md",
"guide": "qdrant-landing/content/documentation/search-quality/pipeline-output-quality.md",
"url": "/documentation/improve-search/pipeline-output-quality/",
"title": "Evaluating Pipeline Output Quality",
"body_sha256": "2673b05d442d7adbf6ffc98c3cebd2658fd41a533b6776f98f74c7a0d399defa",
"added_title": false
},
{
"source": "qdrant-landing/content/documentation/improve-search/query-decomposition.md",
"guide": "qdrant-landing/content/documentation/search-tuning/query-decomposition.md",
"url": "/documentation/improve-search/query-decomposition/",
"title": "Query Decomposition for Multi-Hop Questions",
"body_sha256": "2d7a8721d8d796706d4d7c4eea9a582fa890dcf8c673ad3f757fd9126b859178",
"added_title": false
},
{
"source": "qdrant-landing/content/articles/how-to-choose-an-embedding-model.md",
"guide": "qdrant-landing/content/documentation/search-tuning/choose-embedding-model.md",
"url": "/documentation/search-quality/choose-embedding-model/",
"title": "How to Choose an Embedding Model: Evaluation & Tradeoffs",
"body_sha256": "45db72823411381133ce3f6ef2ec103c0d6068315a7c6de27baf2ce37488506d",
"added_title": true
},
{
"source": "qdrant-landing/content/articles/multitenancy.md",
"guide": "qdrant-landing/content/documentation/production-patterns/multitenant-search.md",
"url": "/documentation/production-patterns/multitenant-search/",
"title": "How to Implement Multitenancy and Custom Sharding in Qdrant",
"body_sha256": "47b7150fdb34474e743884ea4977a0bd1889dd1edf184e450056bf277b75e6df",
"added_title": false
},
{
"source": "qdrant-landing/content/articles/bulk-uploads-in-qdrant.md",
"guide": "qdrant-landing/content/documentation/production-patterns/bulk-data-import.md",
"url": "/documentation/production-patterns/bulk-data-import/",
"title": "Bulk Uploading Data to Qdrant",
"body_sha256": "11c1166ab98a23f5cac181e485bd55c5178ef7a1376e4cdee7333e4d1ba72de6",
"added_title": true
},
{
"source": "qdrant-landing/content/articles/memory-tiers-in-qdrant-what-to-use-and-when.md",
"guide": "qdrant-landing/content/documentation/production-patterns/memory-tiers.md",
"url": "/documentation/production-patterns/memory-tiers/",
"title": "Memory Tiers in Qdrant: What to Use and When",
"body_sha256": "e28a3be091a91e2675033086b05e03788bf944ea9894a8159c5bcadb9c9f52b5",
"added_title": true
},
{
"source": "qdrant-landing/content/articles/hybrid-search.md",
"guide": "qdrant-landing/content/documentation/search-tuning/hybrid-search.md",
"url": "/documentation/search-tuning/hybrid-search/",
"title": "Hybrid Search in Qdrant",
"body_sha256": "de19a6ef0520904a1e7b9b63f4ba663b82b110683730a1b015d72543da71cb3b",
"added_title": true
},
{
"source": "qdrant-landing/content/articles/before-tuning-a-qdrant-collection.md",
"guide": "qdrant-landing/content/documentation/search-tuning/before-tuning-a-qdrant-collection.md",
"url": "/documentation/search-tuning/before-tuning-a-qdrant-collection/",
"title": "What to Check Before Tuning a Qdrant Collection",
"body_sha256": "16b896e07cea553faced53467a2d1d1530a6b17ae716546a3b4db9f8121f0d82",
"added_title": false
},
{
"source": "qdrant-landing/content/articles/candidate-depth.md",
"guide": "qdrant-landing/content/documentation/search-tuning/candidate-depth.md",
"url": "/documentation/search-tuning/candidate-depth/",
"title": "Candidate Depth: How Much Retrieval Is Enough?",
"body_sha256": "4a62dc32ef582437e87cbb5ed4f6839d99173dada631a3aeca82515a8566a4e0",
"added_title": false
},
{
"source": "qdrant-landing/content/articles/how-to-tune-hybrid-search.md",
"guide": "qdrant-landing/content/documentation/search-tuning/how-to-tune-hybrid-search.md",
"url": "/documentation/search-tuning/how-to-tune-hybrid-search/",
"title": "How to Tune Hybrid Search in Qdrant",
"body_sha256": "ac5ff44ad1e7ec60ae96f27e9511e782eb461fb7ca532b1f01271b6200f434f3",
"added_title": true
},
{
"source": "qdrant-landing/content/articles/when-a-reranker-is-worth-it.md",
"guide": "qdrant-landing/content/documentation/search-tuning/when-a-reranker-is-worth-it.md",
"url": "/documentation/search-tuning/when-a-reranker-is-worth-it/",
"title": "When Is a Reranker Worth It?",
"body_sha256": "08b380373ea6efb9350a4ba49a59cf05d6f5a8c13e3d6bb4adc41a47eb7878c8",
"added_title": false
},
{
"source": "qdrant-landing/content/articles/when-your-collection-outgrows-ram.md",
"guide": "qdrant-landing/content/documentation/search-tuning/when-your-collection-outgrows-ram.md",
"url": "/documentation/search-tuning/when-your-collection-outgrows-ram/",
"title": "When Your Collection Outgrows RAM",
"body_sha256": "ebca984ca16a8a307e35af82318bf2eb0649cdb606bf96a104dd0bf6f83d8253",
"added_title": true
}
]
}
+9 -3
View File
@@ -136,8 +136,14 @@ disableKinds = ["taxonomy", "term"]
id = 'G-NZYW2651NE' id = 'G-NZYW2651NE'
[server] [server]
[server.headers] # Local previews must show the same navigation after content moves.
[[server.headers]]
for = '/get_anonymous_id/**' for = '/get_anonymous_id/**'
[[server.headers.values]] [server.headers.values]
Cache-Control = 'no-store'
Content-Security-Policy = 'frame-ancestors https://localhost:3000' Content-Security-Policy = 'frame-ancestors https://localhost:3000'
X-Frame-Options = 'ALLOW-FROM https://localhost:3000' X-Frame-Options = 'ALLOW-FROM https://localhost:3000'
[[server.headers]]
for = '/**'
[server.headers.values]
Cache-Control = 'no-store'
+17 -1
View File
@@ -1,7 +1,7 @@
--- ---
title: Qdrant Articles title: Qdrant Articles
page_title: Articles about Vector Search page_title: Articles about Vector Search
short_description: "Long-form articles on vector search, RAG, quantization, hybrid retrieval, and Qdrant internals from the engineering team." short_description: Long-form articles on vector search, RAG, quantization, hybrid retrieval, and Qdrant internals from the engineering team.
description: Articles about vector search and similarity larning related topics. Latest updates on Qdrant vector search engine. description: Articles about vector search and similarity larning related topics. Latest updates on Qdrant vector search engine.
section_title: Check out our latest publications section_title: Check out our latest publications
subtitle: Check out our latest publications subtitle: Check out our latest publications
@@ -10,4 +10,20 @@ partition: learn
learnButton: Learn More learnButton: Learn More
isMainPage: true isMainPage: true
toc_start_level: 2 toc_start_level: 2
cascade:
- _target:
path: /articles/{agentic-builders-guide,agentic-rag,batch-vector-search-with-qdrant,binary-quantization,binary-quantization-openai,cars-recognition,core-concepts,data-exploration,data-privacy,dataset-quality,dedicated-service,demos-and-tutorials,detecting-coffee-anomalies,discovery-search,distance-based-exploration,embedding-recycler,faq-question-answering,fastembed,food-discovery-demo,indexing-optimization,langchain-integration,mastering-search,memory-consumption,metric-learning-tips,modern-sparse-neural-retrieval,neural-search-tutorial,product-quantization,qa-with-cohere-and-qdrant,rag-and-agents,rag-is-dead,rapid-rag-optimization-with-qdrant-and-quotient,search-as-you-type,search-feedback-loop,semantic-cache-ai-data-retrieval,serverless,storing-multiple-vectors-per-object-in-qdrant,triplet-loss,vector-search-filtering,vector-search-production,vector-search-resource-optimization,vector-similarity-beyond-search,what-are-embeddings,what-is-a-vector-database,what-is-quantization,what-is-rag-in-ai}
layout: redirect
redirect_to: /articles/
hideFromList: true
sitemapExclude: true
build:
list: never
render: always
publishResources: false
category_aliases:
mastering-search: embedding-research
category_overrides:
/articles/dedicated-vector-search/: qdrant-internals
/articles/sparse-vectors/: embedding-research
--- ---
@@ -74,5 +74,4 @@ build:
- [Essential Examples](/documentation/tutorials-build-essentials/index.md) — Hands-on tutorials for agentic RAG, multimodal search, data ingestion, and automation integrations. - [Essential Examples](/documentation/tutorials-build-essentials/index.md) — Hands-on tutorials for agentic RAG, multimodal search, data ingestion, and automation integrations.
- [Build Prototypes](/documentation/examples/index.md) — End-to-end code samples for RAG pipelines, hybrid search, multitenancy, recommendations, and multimodal search. - [Build Prototypes](/documentation/examples/index.md) — End-to-end code samples for RAG pipelines, hybrid search, multitenancy, recommendations, and multimodal search.
- [Improve Search](/documentation/improve-search/index.md) — Techniques for improving retrieval relevance and pipeline output quality.
- [Practice Datasets](/documentation/datasets/index.md) — Ready-made Qdrant snapshots of public datasets you can import and explore without the embedding step. - [Practice Datasets](/documentation/datasets/index.md) — Ready-made Qdrant snapshots of public datasets you can import and explore without the embedding step.
@@ -0,0 +1,55 @@
---
title: Guides
short_description: Find practical guidance on search evaluation, embedding model selection, multitenancy, bulk uploads, and memory placement.
description: Explore practical Qdrant guides for evaluating search quality, choosing embedding models, and planning multitenancy, bulk uploads, and memory use.
partition: learn
learning_kind: guides
breadcrumb: false
hideTOC: true
expandSidebar: true
slug: guides
hideInSidebar: true
build:
render: always
content:
- partial: documentation/banners/banner-a
title: Build Better Search
description: Practical guidance for evaluating and tuning search, choosing models, and planning how your application grows.
linkDescription: Start with the guide that matches your next decision.
cloudButton:
text: Explore Search Evaluation
url: /documentation/search-quality/
localButton:
text: Explore Production & Performance
url: /documentation/production-patterns/
- partial: documentation/guides/topics
- partial: documentation/sections/cards-section
title: Start with a Practical Guide
description: Work through a decision you can apply to your own search system.
cardsPartial: documentation/cards/docs-cards
cards:
- title: 'How to Choose an Embedding Model: Evaluation & Tradeoffs'
description: Compare relevance, language support, and serving cost before you rebuild document vectors.
icon:
src: /icons/outline/vectors-blue.svg
alt: ''
link:
text: Compare Models
url: /documentation/search-quality/choose-embedding-model/
- title: How to Implement Multitenancy and Custom Sharding in Qdrant
description: Choose shared collections, tenant filters, and shard placement as customer workloads grow.
icon:
src: /icons/outline/cloud-cog-teal.svg
alt: ''
link:
text: Plan Tenant Growth
url: /documentation/production-patterns/multitenant-search/
- title: Bulk Uploading Data to Qdrant
description: Plan batching, parallel uploads, sharding, and indexing for large datasets.
icon:
src: /icons/outline/refresh-cw-purple.svg
alt: ''
link:
text: Plan Your Import
url: /documentation/production-patterns/bulk-data-import/
---
@@ -1,15 +0,0 @@
---
title: Improve Search
weight: 1450
partition: ecosystem
---
# Improve Search
*Embedding choice, chunking strategies, and retrieval evaluation using Python ecosystem tools.*
| Tutorial | Objective | Stack | Time | Level |
| :--- | :--- | :--- | :--- | :--- |
| [Measuring Retrieval Relevance](/documentation/improve-search/retrieval-relevance/) | Build a labeled golden set and score retrieval relevance with ranx. | <span class="pill">Python</span> | 40m | <span class="text-yellow">Intermediate</span> |
| [Evaluating Pipeline Output Quality](/documentation/improve-search/pipeline-output-quality/) | Score a RAG pipeline with Ragas and isolate retrieval vs generation failures. | <span class="pill">Python</span> | 45m | <span class="text-yellow">Intermediate</span> |
| [Query Decomposition for Multi-Hop Questions](/documentation/improve-search/query-decomposition/) | Answer multi-hop questions by retrieving in steps, with an LLM asking the next sub-question. | <span class="pill">Python</span> | 15m | <span class="text-yellow">Intermediate</span> |
@@ -7,7 +7,6 @@ aliases:
- overview - overview
- orientation - orientation
- concepts - concepts
- guides
partition: develop partition: develop
--- ---
@@ -0,0 +1,34 @@
---
title: Production & Performance
short_description: Plan multitenancy, bulk uploads, and memory placement as your Qdrant application and vector collection grow.
description: Plan multitenancy, bulk uploads, and memory placement as your Qdrant application and vector collection grow.
partition: learn
learning_kind: guides
weight: 150
hideTOC: true
breadcrumb: false
guide_icon: /icons/outline/cloud-cog-teal.svg
related:
- /documentation/manage-data/multitenancy/
- /documentation/manage-data/bulk-upload/
- /documentation/ops-optimization/read-write-contention/
- /documentation/capacity-planning/
content:
- partial: documentation/banners/banner-a
title: Production & Performance
description: Plan multitenancy, bulk uploads, and memory placement as your Qdrant application and vector collection grow.
linkDescription: Choose the pattern that matches your workload and its constraints.
cloudButton:
text: Serve Many Tenants
url: /documentation/production-patterns/multitenant-search/
localButton:
text: Plan a Data Import
url: /documentation/production-patterns/bulk-data-import/
- partial: documentation/guides/guide-cards
section: /documentation/production-patterns/
worked_examples:
- /documentation/tutorials-search-engineering/index-dynamic-payloads/
- /documentation/tutorials-search-engineering/branch-aware-search/
- /documentation/tutorials-operations/embedding-model-migration/
---
@@ -1,23 +1,29 @@
--- ---
title: "Bulk Uploading Data to Qdrant" title: Bulk Uploading Data to Qdrant
short_description: "Plan bulk uploads in Qdrant at scale: batching, parallelization, sharding, payload indexes, quantization, and on-disk storage." short_description: 'Plan bulk uploads in Qdrant at scale: batching, parallelization, sharding, payload indexes, quantization, and on-disk storage.'
description: "Plan bulk uploads in Qdrant: batching, parallelization, sharding, payload indexes, quantization, and on-disk storage." description: 'Plan bulk uploads in Qdrant: batching, parallelization, sharding, payload indexes, quantization, and on-disk storage.'
preview_dir: /articles_data/bulk-uploads-in-qdrant/preview preview_dir: /articles_data/bulk-uploads-in-qdrant/preview
social_preview_image: /articles_data/bulk-uploads-in-qdrant/preview/social_preview.jpg social_preview_image: /articles_data/bulk-uploads-in-qdrant/preview/social_preview.jpg
weight: 35 weight: 35
author: John Kupchanko author: John Kupchanko
author_link: https://github.com/jkupchanko author_link: https://github.com/jkupchanko
keywords: keywords:
- bulk upload - bulk upload
- vector database - vector database
- batching - batching
- quantization - quantization
- sharding - sharding
category: production-ops date: 2026-07-14 00:00:00+00:00
date: 2026-07-14T00:00:00.000Z
draft: false draft: false
partition: learn
learning_kind: guides
url: /documentation/production-patterns/bulk-data-import/
aliases:
- /articles/bulk-uploads-in-qdrant/
--- ---
# Bulk Uploading Data to Qdrant
## Why Bulk Uploading Matters ## Why Bulk Uploading Matters
When you start using Qdrant at scale, one of the first challenges you may run into is uploading large amounts of data efficiently. Small uploads are usually straightforward, but bulk ingestion introduces a different set of concerns. As millions of vectors, payloads, and indexes are written into a collection, the system has to manage memory usage, disk writes, background optimization, and search availability at the same time. When you start using Qdrant at scale, one of the first challenges you may run into is uploading large amounts of data efficiently. Small uploads are usually straightforward, but bulk ingestion introduces a different set of concerns. As millions of vectors, payloads, and indexes are written into a collection, the system has to manage memory usage, disk writes, background optimization, and search availability at the same time.
@@ -1,23 +1,29 @@
--- ---
title: "Memory Tiers in Qdrant: What to Use and When" title: 'Memory Tiers in Qdrant: What to Use and When'
short_description: "A guide to choosing a Qdrant memory tier layout as your collection grows." short_description: A guide to choosing a Qdrant memory tier layout as your collection grows.
description: "Which Qdrant memory tier layout to use and when, and why, backed by benchmarks." description: Which Qdrant memory tier layout to use and when, and why, backed by benchmarks.
social_preview_image: /articles_data/memory-tiers-in-qdrant-what-to-use-and-when/preview/social_preview.jpg social_preview_image: /articles_data/memory-tiers-in-qdrant-what-to-use-and-when/preview/social_preview.jpg
preview_dir: /articles_data/memory-tiers-in-qdrant-what-to-use-and-when/preview preview_dir: /articles_data/memory-tiers-in-qdrant-what-to-use-and-when/preview
author: Clelia Bertelli author: Clelia Bertelli
author_link: https://qdrant.tech author_link: https://qdrant.tech
date: 2026-08-28T10:00:00+02:00 date: 2026-08-28 10:00:00+02:00
draft: false draft: false
keywords: keywords:
- memory tiers - memory tiers
- caching - caching
- disk - disk
- scaling - scaling
- benchmark - benchmark
category: production-ops
weight: 8 weight: 8
partition: learn
learning_kind: guides
url: /documentation/production-patterns/memory-tiers/
aliases:
- /articles/memory-tiers-in-qdrant-what-to-use-and-when/
--- ---
# Memory Tiers in Qdrant: What to Use and When
A growing vector collection eventually outgrows the RAM it started with: Qdrant handles that by letting you assign dense vectors, the HNSW graph, quantized vectors, payloads, and payload indexes each to whichever memory tier that structure supports, instead of forcing one RAM-versus-disk trade-off onto the whole collection. A growing vector collection eventually outgrows the RAM it started with: Qdrant handles that by letting you assign dense vectors, the HNSW graph, quantized vectors, payloads, and payload indexes each to whichever memory tier that structure supports, instead of forcing one RAM-versus-disk trade-off onto the whole collection.
This article will give you practical guidance over which combination of tiers and quantization to reach for at each stage of a collection's growth, and the reasons behind the choice. This article will give you practical guidance over which combination of tiers and quantization to reach for at each stage of a collection's growth, and the reasons behind the choice.
@@ -122,6 +128,10 @@ A story built only on point count, where more data always means a worse tail, do
## Adjacent Work ## Adjacent Work
{{< read-more >}}
- [Memory tiers documentation](/documentation/ops-configuration/memory-tiers/): the full set of tier and quantization options per structure. - [Memory tiers documentation](/documentation/ops-configuration/memory-tiers/): the full set of tier and quantization options per structure.
- [Storage documentation](/documentation/manage-data/storage/): how collections, segments, and storage structures fit together on disk. - [Storage documentation](/documentation/manage-data/storage/): how collections, segments, and storage structures fit together on disk.
- [qdrant-labs/memory-tiers-explained](https://github.com/qdrant-labs/memory-tiers-explained): the benchmark code and raw results behind the guidance in this piece. - [qdrant-labs/memory-tiers-explained](https://github.com/qdrant-labs/memory-tiers-explained): the benchmark code and raw results behind the guidance in this piece.
{{< /read-more >}}
@@ -1,20 +1,24 @@
--- ---
title: "How to Implement Multitenancy and Custom Sharding in Qdrant" title: How to Implement Multitenancy and Custom Sharding in Qdrant
short_description: "Explore how Qdrant's multitenancy and custom sharding streamline machine-learning operations, enhancing scalability and data security." short_description: Explore how Qdrant's multitenancy and custom sharding streamline machine-learning operations, enhancing scalability and data security.
description: "Discover how multitenancy and custom sharding in Qdrant can streamline your machine-learning operations. Learn how to scale efficiently and manage data securely." description: Discover how multitenancy and custom sharding in Qdrant can streamline your machine-learning operations. Learn how to scale efficiently and manage data securely.
social_preview_image: /articles_data/multitenancy/preview/social_preview.jpg social_preview_image: /articles_data/multitenancy/preview/social_preview.jpg
preview_dir: /articles_data/multitenancy/preview preview_dir: /articles_data/multitenancy/preview
small_preview_image: /articles_data/multitenancy/icon.svg small_preview_image: /articles_data/multitenancy/icon.svg
weight: 60 weight: 60
author: David Myriel author: David Myriel
date: 2024-02-06T13:21:00.000Z date: 2024-02-06 13:21:00+00:00
draft: false draft: false
keywords: keywords:
- multitenancy - multitenancy
- custom sharding - custom sharding
- multiple partitions - multiple partitions
- vector database - vector database
category: production-ops partition: learn
learning_kind: guides
url: /documentation/production-patterns/multitenant-search/
aliases:
- /articles/multitenancy/
--- ---
# Scaling Your Machine Learning Setup: The Power of Multitenancy and Custom Sharding in Qdrant # Scaling Your Machine Learning Setup: The Power of Multitenancy and Custom Sharding in Qdrant
@@ -0,0 +1,32 @@
---
title: Search Evaluation
short_description: Measure retrieval relevance and pipeline output quality to establish a baseline and decide whether a search change helps.
description: Evaluate Qdrant retrieval relevance and pipeline output quality with labeled queries, repeatable measurements, and checks for meaningful improvements.
partition: learn
learning_kind: guides
weight: 100
hideTOC: true
breadcrumb: false
guide_icon: /icons/outline/search-blue.svg
related:
- /documentation/search/
- /articles/search-quality/
content:
- partial: documentation/banners/banner-a
title: Search Evaluation
description: Build an evaluation baseline and measure whether your search system produces useful results.
linkDescription: Choose the evaluation method that matches the result you need to judge.
cloudButton:
text: Measure Retrieval Relevance
url: /documentation/improve-search/retrieval-relevance/
localButton:
text: Evaluate Pipeline Output
url: /documentation/improve-search/pipeline-output-quality/
- partial: documentation/guides/guide-cards
section: /documentation/search-quality/
aliases:
- /documentation/improve-search/
worked_examples:
- /documentation/tutorials-search-engineering/ann-recall/
---
@@ -2,8 +2,12 @@
title: Evaluating Pipeline Output Quality title: Evaluating Pipeline Output Quality
weight: 7 weight: 7
aliases: aliases:
- /documentation/tutorials/retrieval-quality-pipeline-output/ - /documentation/tutorials/retrieval-quality-pipeline-output/
partition: ecosystem partition: learn
learning_kind: guides
url: /documentation/improve-search/pipeline-output-quality/
short_description: Separate retrieval failures from generation failures and evaluate whether your full pipeline produces supported, useful answers.
description: Evaluate retrieval and generation separately to identify why a Qdrant search pipeline returns an unsupported answer or misses the information users need.
--- ---
# Evaluating Pipeline Output Quality # Evaluating Pipeline Output Quality
@@ -2,8 +2,12 @@
title: Measuring Retrieval Relevance title: Measuring Retrieval Relevance
weight: 6 weight: 6
aliases: aliases:
- /documentation/tutorials/retrieval-quality-golden-set/ - /documentation/tutorials/retrieval-quality-golden-set/
partition: ecosystem partition: learn
learning_kind: guides
url: /documentation/improve-search/retrieval-relevance/
short_description: Build a labeled query set and measure whether retrieved documents answer users' questions, with query-level relevance metrics.
description: Measure Qdrant retrieval relevance with labeled queries, document IDs, and ranking metrics to compare search configurations on your own data.
--- ---
# Measuring Retrieval Relevance # Measuring Retrieval Relevance
@@ -0,0 +1,25 @@
---
title: Search Design & Tuning
short_description: Choose embedding models and retrieval strategies, then tune candidate depth, fusion, reranking, and memory against your search goals.
description: 'Design and tune Qdrant search: choose embeddings and retrieval strategies, then evaluate changes to candidate depth, fusion, reranking, and memory.'
partition: learn
learning_kind: guides
weight: 125
hideTOC: true
breadcrumb: false
guide_icon: /icons/outline/speedometer-blue.svg
content:
- partial: documentation/banners/banner-a
title: Search Design & Tuning
description: Choose a search approach, then use evaluation results to decide what to change.
linkDescription: Start with a design decision or follow the complete retrieval tuning series.
cloudButton:
text: Choose an Embedding Model
url: /documentation/search-quality/choose-embedding-model/
localButton:
text: Start the Tuning Series
url: /documentation/search-tuning/hybrid-search/
- partial: documentation/guides/guide-cards
section: /documentation/search-tuning/
guide_series_title: Tune Your Retrieval Pipeline
---
@@ -1,21 +1,25 @@
--- ---
title: "What to Check Before Tuning a Qdrant Collection" title: What to Check Before Tuning a Qdrant Collection
short_description: "Seven collection settings that degrade retrieval without an error, the order to try changes in, and how many labeled queries a gain needs." short_description: Seven collection settings that degrade retrieval without an error, the order to try changes in, and how many labeled queries a gain needs.
description: "Audit a Qdrant collection: find the settings that degrade retrieval silently, choose the cheapest next change, and size a labeled query set." description: 'Audit a Qdrant collection: find the settings that degrade retrieval silently, choose the cheapest next change, and size a labeled query set.'
preview_dir: /articles_data/before-tuning-a-qdrant-collection/preview preview_dir: /articles_data/before-tuning-a-qdrant-collection/preview
social_preview_image: /articles_data/before-tuning-a-qdrant-collection/preview/social_preview.jpg social_preview_image: /articles_data/before-tuning-a-qdrant-collection/preview/social_preview.jpg
weight: -214 weight: 120
author: Dylan Couzon author: Dylan Couzon
author_link: https://www.linkedin.com/in/dcouzon/ author_link: https://www.linkedin.com/in/dcouzon/
date: 2026-08-20T00:00:00+03:00 date: 2026-08-20 00:00:00+03:00
draft: false draft: false
keywords: keywords:
- retrieval tuning - retrieval tuning
- search relevance - search relevance
- nDCG - nDCG
- labeled query set - labeled query set
- Qdrant collection audit - Qdrant collection audit
category: search-quality partition: learn
learning_kind: guides
aliases:
- /articles/before-tuning-a-qdrant-collection/
guide_series: true
--- ---
Before you change a setting, decide what better retrieval means for your workload. The right document at rank one, more candidates for a reranker, lower latency, and a smaller memory footprint each favor different settings, so pick your goal first. If your labeled queries can't detect the improvement you're chasing, you won't be able to tell whether a change helped. Before you change a setting, decide what better retrieval means for your workload. The right document at rank one, more candidates for a reranker, lower latency, and a smaller memory footprint each favor different settings, so pick your goal first. If your labeled queries can't detect the improvement you're chasing, you won't be able to tell whether a change helped.
@@ -30,7 +34,7 @@ Every query first retrieves candidates, then ranks them. In dense-only search, o
_The hybrid pipeline and the settings each stage owns. Dense-only search uses the dense prefetch path on its own, so `limit` and `hnsw_ef` are its only settings here._ _The hybrid pipeline and the settings each stage owns. Dense-only search uses the dense prefetch path on its own, so `limit` and `hnsw_ef` are its only settings here._
If you run dense-only search and exact keywords are missing from results, hybrid search is the first change to test. [Tuning hybrid search](/articles/how-to-tune-hybrid-search/) covers the request shape, what the second prefetch costs, and how to check that fusion beats either prefetch on your labels. If you run dense-only search and exact keywords are missing from results, hybrid search is the first change to test. [Tuning hybrid search](/documentation/search-tuning/how-to-tune-hybrid-search/) covers the request shape, what the second prefetch costs, and how to check that fusion beats either prefetch on your labels.
Before you tune: Before you tune:
@@ -44,12 +48,12 @@ Start with the failure mode, not the config reference. The table maps each sympt
| What You See | First Check | Read Next | | What You See | First Check | Read Next |
|---|---|---| |---|---|---|
| You cannot separate a gain from noise | Build labeled queries, choose a metric, and calculate an interval | This article | | You cannot separate a gain from noise | Build labeled queries, choose a metric, and calculate an interval | This article |
| Relevant documents do not appear | Measure whether candidate depth is limiting recall | [Candidate Depth: How Much Retrieval Is Enough?](/articles/candidate-depth/) | | Relevant documents do not appear | Measure whether candidate depth is limiting recall | [Candidate Depth: How Much Retrieval Is Enough?](/documentation/search-tuning/candidate-depth/) |
| Keywords, identifiers, SKUs, or error codes do not match | Add a sparse prefetch and measure fusion against each prefetch alone | [How to Tune Hybrid Search in Qdrant](/articles/how-to-tune-hybrid-search/) | | Keywords, identifiers, SKUs, or error codes do not match | Add a sparse prefetch and measure fusion against each prefetch alone | [How to Tune Hybrid Search in Qdrant](/documentation/search-tuning/how-to-tune-hybrid-search/) |
| Relevant documents are present but misordered | For hybrid search, tune fusion. If the candidate list needs another ranking stage, test a reranker | [How to Tune Hybrid Search in Qdrant](/articles/how-to-tune-hybrid-search/), [When Is a Reranker Worth It?](/articles/when-a-reranker-is-worth-it/) | | Relevant documents are present but misordered | For hybrid search, tune fusion. If the candidate list needs another ranking stage, test a reranker | [How to Tune Hybrid Search in Qdrant](/documentation/search-tuning/how-to-tune-hybrid-search/), [When Is a Reranker Worth It?](/documentation/search-tuning/when-a-reranker-is-worth-it/) |
| Results repeat near-duplicates | Test maximal marginal relevance. If chunks from one document fill the page, use grouping | [When Is a Reranker Worth It?](/articles/when-a-reranker-is-worth-it/) | | Results repeat near-duplicates | Test maximal marginal relevance. If chunks from one document fill the page, use grouping | [When Is a Reranker Worth It?](/documentation/search-tuning/when-a-reranker-is-worth-it/) |
| Search misses its p95 target | Measure the cost of candidate depth before adding another retrieval stage | [Candidate Depth: How Much Retrieval Is Enough?](/articles/candidate-depth/) | | Search misses its p95 target | Measure the cost of candidate depth before adding another retrieval stage | [Candidate Depth: How Much Retrieval Is Enough?](/documentation/search-tuning/candidate-depth/) |
| The collection no longer fits in RAM | Test memory placement and rescoring | [When Your Collection Outgrows RAM](/articles/when-your-collection-outgrows-ram/) | | The collection no longer fits in RAM | Test memory placement and rescoring | [When Your Collection Outgrows RAM](/documentation/search-tuning/when-your-collection-outgrows-ram/) |
## How to Read These Measurements ## How to Read These Measurements
@@ -213,7 +217,7 @@ The more labeled queries you evaluate, the more precise the measured gain. Acros
The label count you need depends primarily on effect size and query-to-query variation, not collection size alone. The label count you need depends primarily on effect size and query-to-query variation, not collection size alone.
In our measurements, [fusion settings](/articles/how-to-tune-hybrid-search/) moved `nDCG@10` by 0.012 to 0.038, gains from tuning an already-working collection rather than rebuilding the retrieval pipeline. In our measurements, [fusion settings](/documentation/search-tuning/how-to-tune-hybrid-search/) moved `nDCG@10` by 0.012 to 0.038, gains from tuning an already-working collection rather than rebuilding the retrieval pipeline.
Fifty labeled queries were enough for the larger gains: the 0.038 gain had an interval excluding zero in 93% of draws, while gains under 0.02 cleared that bar in 7% to 38%. Treat small movement as unresolved until you have the labels to measure it. Fifty labeled queries were enough for the larger gains: the 0.038 gain had an interval excluding zero in 93% of draws, while gains under 0.02 cleared that bar in 7% to 38%. Treat small movement as unresolved until you have the labels to measure it.
@@ -227,4 +231,4 @@ If you compare separately rebuilt indexes, check top-10 agreement across two bui
## Start with One Change ## Start with One Change
Record the current relevance metric and p95 latency for a representative query set. Choose one low-cost change from the symptom table, validate it on fresh queries, and keep it only if the gain survives. Once you have that baseline, [Candidate Depth: How Much Retrieval Is Enough?](/articles/candidate-depth/) shows how to test whether retrieval depth is the constraint. Record the current relevance metric and p95 latency for a representative query set. Choose one low-cost change from the symptom table, validate it on fresh queries, and keep it only if the gain survives. Once you have that baseline, [Candidate Depth: How Much Retrieval Is Enough?](/documentation/search-tuning/candidate-depth/) shows how to test whether retrieval depth is the constraint.
@@ -1,24 +1,28 @@
--- ---
title: "Candidate Depth: How Much Retrieval Is Enough?" title: 'Candidate Depth: How Much Retrieval Is Enough?'
short_description: "Raising candidate depth raises the best score a later ranking stage could reach, but default fusion barely used that extra room." short_description: Raising candidate depth raises the best score a later ranking stage could reach, but default fusion barely used that extra room.
description: "Set candidate depth and hnsw_ef in Qdrant, measure the gap between your ranking and a perfect one, and balance the trade-offs." description: Set candidate depth and hnsw_ef in Qdrant, measure the gap between your ranking and a perfect one, and balance the trade-offs.
preview_dir: /articles_data/candidate-depth/preview preview_dir: /articles_data/candidate-depth/preview
social_preview_image: /articles_data/candidate-depth/preview/social_preview.jpg social_preview_image: /articles_data/candidate-depth/preview/social_preview.jpg
weight: -213 weight: 130
author: Dylan Couzon author: Dylan Couzon
author_link: https://www.linkedin.com/in/dcouzon/ author_link: https://www.linkedin.com/in/dcouzon/
date: 2026-08-21T00:00:00+03:00 date: 2026-08-21 00:00:00+03:00
draft: false draft: false
keywords: keywords:
- candidate depth - candidate depth
- hnsw_ef - hnsw_ef
- scalar quantization - scalar quantization
- memory tiers - memory tiers
- HNSW tuning - HNSW tuning
category: search-quality partition: learn
learning_kind: guides
aliases:
- /articles/candidate-depth/
guide_series: true
--- ---
Before you tune candidate depth, use the [pre-tuning checks](/articles/before-tuning-a-qdrant-collection/) to verify index state and set a labeled baseline. Everything below measures against that baseline. Before you tune candidate depth, use the [pre-tuning checks](/documentation/search-tuning/before-tuning-a-qdrant-collection/) to verify index state and set a labeled baseline. Everything below measures against that baseline.
Candidate depth is the number of candidates a retrieval stage passes to a later ranking stage. It matters only when a later stage can use the extra candidates. In hybrid search, every `prefetch` carries its own `limit`, and a [multi-stage query](/documentation/search/hybrid-queries/#multi-stage-queries) that nests one prefetch inside another sets a depth at each level. In dense-only or sparse-only search, it is the number of candidates you pass to a reranker or other downstream stage. Candidate depth is the number of candidates a retrieval stage passes to a later ranking stage. It matters only when a later stage can use the extra candidates. In hybrid search, every `prefetch` carries its own `limit`, and a [multi-stage query](/documentation/search/hybrid-queries/#multi-stage-queries) that nests one prefetch inside another sets a depth at each level. In dense-only or sparse-only search, it is the number of candidates you pass to a reranker or other downstream stage.
@@ -34,7 +38,7 @@ Candidate depth is the number of candidates a retrieval stage passes to a later
## More Candidates Can Raise the Best Possible Score ## More Candidates Can Raise the Best Possible Score
Start by measuring the gap between the candidates you retrieved and the order your pipeline returns them in. Use your [labeled query set](/articles/before-tuning-a-qdrant-collection/#make-sure-your-labels-can-detect-a-gain) to score the candidate set as if it were ordered perfectly. That is the best possible score any later ranking of those candidates could reach. Compare it with the current score from the same queries. In these hybrid measurements, the current score is fusion's `nDCG@10` over the same candidates. `nDCG@10` grades the top 10 results and gives more credit to relevant documents near the top. Start by measuring the gap between the candidates you retrieved and the order your pipeline returns them in. Use your [labeled query set](/documentation/search-tuning/before-tuning-a-qdrant-collection/#make-sure-your-labels-can-detect-a-gain) to score the candidate set as if it were ordered perfectly. That is the best possible score any later ranking of those candidates could reach. Compare it with the current score from the same queries. In these hybrid measurements, the current score is fusion's `nDCG@10` over the same candidates. `nDCG@10` grades the top 10 results and gives more credit to relevant documents near the top.
Suppose a query retrieves three relevant documents, and fusion ranks them 4, 30, and 180. The current score sees only the one at rank 4, since the other two sit outside the top 10 it grades. The best possible score reorders those same candidates and puts all three at the top. No later ranking stage could do better with the candidates that were retrieved. Suppose a query retrieves three relevant documents, and fusion ranks them 4, 30, and 180. The current score sees only the one at rank 4, since the other two sit outside the top 10 it grades. The best possible score reorders those same candidates and puts all three at the top. No later ranking stage could do better with the candidates that were retrieved.
@@ -56,9 +60,9 @@ _The full sweep behind the table. The best possible score climbs at every depth
The best possible score change rises with corpus size across these five, from 5,183 documents on SciFact to 100,000 on DBPedia-entity, while the current score change stays flat. Size and domain move together here, so re-measure the gap as your own collection grows. The best possible score change rises with corpus size across these five, from 5,183 documents on SciFact to 100,000 on DBPedia-entity, while the current score change stays flat. Size and domain move together here, so re-measure the gap as your own collection grows.
With Qdrant's default [RRF](/documentation/search/hybrid-queries/#reciprocal-rank-fusion-rrf), the top ranks in each `prefetch` contribute far more to the fused score than the tail. Raising `limit` can add candidates without changing the top 10, or replace a more relevant result. The fused score is not always higher at greater depth: CodeSearchNet peaks at `limit=200` and is lower at 500, and DBPedia-entity peaks at 50. Other fusion methods can rank those candidates differently. [Fusion tuning](/articles/how-to-tune-hybrid-search/) shows how to test them on your labels. With Qdrant's default [RRF](/documentation/search/hybrid-queries/#reciprocal-rank-fusion-rrf), the top ranks in each `prefetch` contribute far more to the fused score than the tail. Raising `limit` can add candidates without changing the top 10, or replace a more relevant result. The fused score is not always higher at greater depth: CodeSearchNet peaks at `limit=200` and is lower at 500, and DBPedia-entity peaks at 50. Other fusion methods can rank those candidates differently. [Fusion tuning](/documentation/search-tuning/how-to-tune-hybrid-search/) shows how to test them on your labels.
Start `limit` around 100 to 200, then test larger values on your own labels. A [reranker](/articles/when-a-reranker-is-worth-it/) can use the added candidates, and a [Formula Query](/documentation/search/hybrid-queries/#custom-scoring-with-a-formula-query) can rescore those same candidates from payload fields. Start `limit` around 100 to 200, then test larger values on your own labels. A [reranker](/documentation/search-tuning/when-a-reranker-is-worth-it/) can use the added candidates, and a [Formula Query](/documentation/search/hybrid-queries/#custom-scoring-with-a-formula-query) can rescore those same candidates from payload fields.
Raising `limit` adds retrieval work. If a reranker follows, it also increases the number of candidates the reranker scores. In our single-shard tests, raising `limit` from 10 to 500 increased median latency by 37% to 43%. These results establish the direction, not a portable ratio. Measure the change under your own p95 budget, concurrency, and shard fan-out. Raising `limit` adds retrieval work. If a reranker follows, it also increases the number of candidates the reranker scores. In our single-shard tests, raising `limit` from 10 to 500 increased median latency by 37% to 43%. These results establish the direction, not a portable ratio. Measure the change under your own p95 budget, concurrency, and shard fan-out.
@@ -122,7 +126,7 @@ for ef in (16, 64, 128, 256, 512):
On these five datasets, raising `hnsw_ef` through 16, 64, 128, and 512 at depth 200 moved fused `nDCG@10` by at most 0.0022. Relevant-document recall in the candidate union moved by at most 0.0040. Median latency rose between 4% and 49% across the five hybrid requests at prefetch `limit=200`. When the graph is already saturated, the wider search budget is close to pure cost. On these five datasets, raising `hnsw_ef` through 16, 64, 128, and 512 at depth 200 moved fused `nDCG@10` by at most 0.0022. Relevant-document recall in the candidate union moved by at most 0.0040. Median latency rose between 4% and 49% across the five hybrid requests at prefetch `limit=200`. When the graph is already saturated, the wider search budget is close to pure cost.
On your collection, choose the lowest `hnsw_ef` that reaches your recall target inside your latency budget. When recall is flat from the first value, keep `hnsw_ef` where it is and confirm that Qdrant has built an HNSW graph. Qdrant builds that graph after a segment passes the default `indexing_threshold`, and smaller segments use exhaustive search where `hnsw_ef` has no effect. The [pre-tuning checks](/articles/before-tuning-a-qdrant-collection/) show how to confirm the graph exists. On your collection, choose the lowest `hnsw_ef` that reaches your recall target inside your latency budget. When recall is flat from the first value, keep `hnsw_ef` where it is and confirm that Qdrant has built an HNSW graph. Qdrant builds that graph after a segment passes the default `indexing_threshold`, and smaller segments use exhaustive search where `hnsw_ef` has no effect. The [pre-tuning checks](/documentation/search-tuning/before-tuning-a-qdrant-collection/) show how to confirm the graph exists.
Saturation is a property of your own graph. These collections held at most 100,000 documents, built in one batch, unfiltered and unquantized. We ran the same check on the full 4,635,922-document DBPedia-entity collection, and it returned 0.957 of the exact top 10: about 4% of the true nearest neighbors never came back. Saturation is a property of your own graph. These collections held at most 100,000 documents, built in one batch, unfiltered and unquantized. We ran the same check on the full 4,635,922-document DBPedia-entity collection, and it returned 0.957 of the exact top 10: about 4% of the true nearest neighbors never came back.
@@ -146,10 +150,10 @@ This measurement covers int8 scalar quantization on one shard at 5,000 and 100,0
Compare quantization with cutting `limit`. Dropping depth from 500 to 10 removed 27% to 30% of median latency in our runs and left the footprint where it was. Int8 quantization stored the vectors at one-quarter the size and moved fused `nDCG@10` by at most 0.0001 in either direction. Of the two, quantization is the one that shrinks what the vectors need in RAM. Compare quantization with cutting `limit`. Dropping depth from 500 to 10 removed 27% to 30% of median latency in our runs and left the footprint where it was. Int8 quantization stored the vectors at one-quarter the size and moved fused `nDCG@10` by at most 0.0001 in either direction. Of the two, quantization is the one that shrinks what the vectors need in RAM.
Once the collection outgrows RAM, the question stops being how many candidates to fetch and becomes which structures stay resident. [Memory placement and rescoring](/articles/when-your-collection-outgrows-ram/) measures that boundary on 4.6 million vectors and explains the placement rules. Once the collection outgrows RAM, the question stops being how many candidates to fetch and becomes which structures stay resident. [Memory placement and rescoring](/documentation/search-tuning/when-your-collection-outgrows-ram/) measures that boundary on 4.6 million vectors and explains the placement rules.
## What to Tune Next ## What to Tune Next
The gap between the best possible score and the current score tells you whether the next experiment should focus on ranking or retrieval. A large gap means relevant candidates are present but not ranked highly enough. In hybrid search, test fusion settings; in any pipeline with a downstream stage, test whether a reranker can recover the gap. A small gap means ranking is already close to the best the candidate set allows, so improve the candidates instead. The gap between the best possible score and the current score tells you whether the next experiment should focus on ranking or retrieval. A large gap means relevant candidates are present but not ranked highly enough. In hybrid search, test fusion settings; in any pipeline with a downstream stage, test whether a reranker can recover the gap. A small gap means ranking is already close to the best the candidate set allows, so improve the candidates instead.
Next, if you use hybrid search, [tune fusion over the candidates you already retrieve](/articles/how-to-tune-hybrid-search/). Next, if you use hybrid search, [tune fusion over the candidates you already retrieve](/documentation/search-tuning/how-to-tune-hybrid-search/).
@@ -1,17 +1,23 @@
--- ---
title: "How to Choose an Embedding Model: Evaluation & Tradeoffs" title: 'How to Choose an Embedding Model: Evaluation & Tradeoffs'
short_description: "There is no one-size-fits-all solution when it comes to embedding models. Learn how to choose the right one for your use case." short_description: There is no one-size-fits-all solution when it comes to embedding models. Learn how to choose the right one for your use case.
description: "Building proper search requires selecting the right embedding model for your specific use case. This guide helps you navigate the selection process based on performance, cost, and other practical considerations." description: Building proper search requires selecting the right embedding model for your specific use case. This guide helps you navigate the selection process based on performance, cost, and other practical considerations.
preview_dir: /articles_data/how-to-choose-an-embedding-model/preview preview_dir: /articles_data/how-to-choose-an-embedding-model/preview
social_preview_image: /articles_data/how-to-choose-an-embedding-model/preview/social_preview.jpg social_preview_image: /articles_data/how-to-choose-an-embedding-model/preview/social_preview.jpg
author: Kacper Łukawski author: Kacper Łukawski
author_link: https://www.kacperlukawski.com author_link: https://www.kacperlukawski.com
date: 2025-07-15T00:00:00.000Z date: 2025-07-15 00:00:00+00:00
category: core-concepts
draft: false draft: false
weight: 10 weight: 10
partition: learn
learning_kind: guides
url: /documentation/search-quality/choose-embedding-model/
aliases:
- /articles/how-to-choose-an-embedding-model/
--- ---
# How to Choose an Embedding Model: Evaluation & Tradeoffs
No matter if you are just beginning your journey in the world of vector search, or you are a seasoned practitioner, you No matter if you are just beginning your journey in the world of vector search, or you are a seasoned practitioner, you
have probably wondered how to choose the right embedding model to achieve the best search quality. There are some have probably wondered how to choose the right embedding model to achieve the best search quality. There are some
public benchmarks, such as [MTEB](https://huggingface.co/spaces/mteb/leaderboard), that can help you narrow down the public benchmarks, such as [MTEB](https://huggingface.co/spaces/mteb/leaderboard), that can help you narrow down the
@@ -1,24 +1,30 @@
--- ---
title: "How to Tune Hybrid Search in Qdrant" title: How to Tune Hybrid Search in Qdrant
short_description: "Tune hybrid search with RRF or DBSF, choose k from relevance labels, and learn why weights are pairs instead of ratios." short_description: Tune hybrid search with RRF or DBSF, choose k from relevance labels, and learn why weights are pairs instead of ratios.
description: "Tune hybrid search fusion in Qdrant: choose between RRF and DBSF, set the constant k from your relevance labels, and get weights right." description: 'Tune hybrid search fusion in Qdrant: choose between RRF and DBSF, set the constant k from your relevance labels, and get weights right.'
preview_dir: /articles_data/how-to-tune-hybrid-search/preview preview_dir: /articles_data/how-to-tune-hybrid-search/preview
social_preview_image: /articles_data/how-to-tune-hybrid-search/preview/social_preview.jpg social_preview_image: /articles_data/how-to-tune-hybrid-search/preview/social_preview.jpg
weight: -211 weight: 140
author: Dylan Couzon author: Dylan Couzon
author_link: https://www.linkedin.com/in/dcouzon/ author_link: https://www.linkedin.com/in/dcouzon/
date: 2026-08-22T00:00:00+03:00 date: 2026-08-22 00:00:00+03:00
draft: false draft: false
keywords: keywords:
- hybrid search tuning - hybrid search tuning
- reciprocal rank fusion - reciprocal rank fusion
- RRF k parameter - RRF k parameter
- fusion weights - fusion weights
- DBSF - DBSF
category: search-quality partition: learn
learning_kind: guides
aliases:
- /articles/how-to-tune-hybrid-search/
guide_series: true
--- ---
Before you tune fusion, use the [pre-tuning checks](/articles/before-tuning-a-qdrant-collection/) to verify index state and set a labeled baseline. # How to Tune Hybrid Search in Qdrant
Before you tune fusion, use the [pre-tuning checks](/documentation/search-tuning/before-tuning-a-qdrant-collection/) to verify index state and set a labeled baseline.
Hybrid search retrieves dense and sparse candidate lists, then fuses them into one ranking. The dense prefetch finds similar meaning; the sparse prefetch finds matching keywords. Fusion reorders the candidates the prefetches return, so a document missing from both lists cannot appear in the result. Hybrid search retrieves dense and sparse candidate lists, then fuses them into one ranking. The dense prefetch finds similar meaning; the sparse prefetch finds matching keywords. Fusion reorders the candidates the prefetches return, so a document missing from both lists cannot appear in the result.
@@ -29,7 +35,7 @@ Before tuning, compare dense retrieval, sparse retrieval, and default [Reciproca
Qdrant defaults to `k=2`. The original RRF paper uses 60, which maps to `k=61` in Qdrant's formula. That gap is what most of this article is about. Qdrant defaults to `k=2`. The original RRF paper uses 60, which maps to `k=61` in Qdrant's formula. That gap is what most of this article is about.
<aside role="status"> <aside role="status">
<strong>Note:</strong> The measurements in this article use five public datasets chosen to vary in corpus size, document and query shape, and relevance task, so read these fusion deltas as directional. They range from 5,183 to 100,000 documents. Each collection was built in one batch on one shard, unquantized and unfiltered, with <code>all-MiniLM-L6-v2</code> for dense retrieval, Qdrant's core BM25 for sparse retrieval, and 200 candidates from each prefetch. A graph shaped by continuous upserts and optimizer merges can return a different ranking. Latency medians come from one Qdrant container on an idle laptop, one request at a time, so re-measure under your own p95 budget, concurrency, and shard fan-out. <a href="/articles/before-tuning-a-qdrant-collection/">Building a labeled set</a> explains how the winning configuration was rechecked on held-out queries. <strong>Note:</strong> The measurements in this article use five public datasets chosen to vary in corpus size, document and query shape, and relevance task, so read these fusion deltas as directional. They range from 5,183 to 100,000 documents. Each collection was built in one batch on one shard, unquantized and unfiltered, with <code>all-MiniLM-L6-v2</code> for dense retrieval, Qdrant's core BM25 for sparse retrieval, and 200 candidates from each prefetch. A graph shaped by continuous upserts and optimizer merges can return a different ranking. Latency medians come from one Qdrant container on an idle laptop, one request at a time, so re-measure under your own p95 budget, concurrency, and shard fan-out. <a href="/documentation/search-tuning/before-tuning-a-qdrant-collection/">Building a labeled set</a> explains how the winning configuration was rechecked on held-out queries.
</aside> </aside>
`Over the Better One` is default RRF's `nDCG@10` minus the better individual prefetch. `Second Prefetch Cost` is the median latency the second prefetch adds over the dense prefetch alone. `Over the Better One` is default RRF's `nDCG@10` minus the better individual prefetch. `Second Prefetch Cost` is the median latency the second prefetch adds over the dense prefetch alone.
@@ -58,7 +64,7 @@ RRF ignores score scale, so a cosine similarity and a BM25 score combine without
## Compare RRF and DBSF on Your Labels ## Compare RRF and DBSF on Your Labels
Use your [labeled query set](/articles/before-tuning-a-qdrant-collection/#make-sure-your-labels-can-detect-a-gain) to compare RRF and DBSF over the same prefetches. Run RRF at `k=2` and equal weights, then run DBSF. Use your [labeled query set](/documentation/search-tuning/before-tuning-a-qdrant-collection/#make-sure-your-labels-can-detect-a-gain) to compare RRF and DBSF over the same prefetches. Run RRF at `k=2` and equal weights, then run DBSF.
Both queries read the same two candidate lists, so connect once and build the prefetches once. The prefetches must use the models the collection was indexed with. Both queries read the same two candidate lists, so connect once and build the prefetches once. The prefetches must use the models the collection was indexed with.
@@ -99,7 +105,7 @@ dbsf_response = client.query_points(
``` ```
<aside role="status"> <aside role="status">
In a multi-shard collection, each shard applies its own prefetch <code>limit</code>. With root-level fusion, Qdrant combines those candidates across shards. A larger limit can expose more candidates to fusion, but it also adds retrieval work and candidates for a downstream reranker. Fusion nested inside a prefetch runs per shard, and DBSF rescales against the score distribution of each shard's own candidates. The <a href="/articles/candidate-depth/">candidate depth guide</a> explains how to set the limit. In a multi-shard collection, each shard applies its own prefetch <code>limit</code>. With root-level fusion, Qdrant combines those candidates across shards. A larger limit can expose more candidates to fusion, but it also adds retrieval work and candidates for a downstream reranker. Fusion nested inside a prefetch runs per shard, and DBSF rescales against the score distribution of each shard's own candidates. The <a href="/documentation/search-tuning/candidate-depth/">candidate depth guide</a> explains how to set the limit.
</aside> </aside>
On three of these five datasets, DBSF scored higher than default RRF by a margin whose 95% interval excludes zero. SciFact's 0.0148 gain and ArguAna's 0.0045 loss both cross zero, so those two datasets are inconclusive. On three of these five datasets, DBSF scored higher than default RRF by a margin whose 95% interval excludes zero. SciFact's 0.0148 gain and ArguAna's 0.0045 loss both cross zero, so those two datasets are inconclusive.
@@ -162,7 +168,7 @@ A weight of 0.0 keeps every document from that prefetch and scores each one 0.0.
## Confirm the Selected Configuration on Held-Out Queries ## Confirm the Selected Configuration on Held-Out Queries
A configuration can score best on the queries used to select it and still fail on held-out queries. Run both checks from [the pre-tuning article](/articles/before-tuning-a-qdrant-collection/): a bootstrap interval on per-query gain, and a split between selection and held-out queries. Ship a configuration when its interval excludes zero and its selected gain holds on the held-out half. A configuration can score best on the queries used to select it and still fail on held-out queries. Run both checks from [the pre-tuning article](/documentation/search-tuning/before-tuning-a-qdrant-collection/): a bootstrap interval on per-query gain, and a split between selection and held-out queries. Ship a configuration when its interval excludes zero and its selected gain holds on the held-out half.
On SciFact's 300 queries, nothing we tried had a 95% interval that excluded zero, including DBSF's 0.0148 gain. Across 200 random splits, a selected fusion configuration kept 67% to 95% of its gain on held-out queries. Keeping the default is a real answer, and it was the right one on one of our five datasets. On SciFact's 300 queries, nothing we tried had a 95% interval that excluded zero, including DBSF's 0.0148 gain. Across 200 random splits, a selected fusion configuration kept 67% to 95% of its gain on held-out queries. Keeping the default is a real answer, and it was the right one on one of our five datasets.
@@ -176,4 +182,4 @@ Each step is cheap enough to run in a single session.
4. Sweep a few weight pairs at that `k`. 4. Sweep a few weight pairs at that `k`.
5. Validate the winner on held-out queries before shipping. 5. Validate the winner on held-out queries before shipping.
Next, if a downstream model could improve the ranking of your retrieved candidates, [test whether a reranker is worth its cost](/articles/when-a-reranker-is-worth-it/). Next, if a downstream model could improve the ranking of your retrieved candidates, [test whether a reranker is worth its cost](/documentation/search-tuning/when-a-reranker-is-worth-it/).
@@ -1,23 +1,29 @@
--- ---
title: "Hybrid Search in Qdrant" title: Hybrid Search in Qdrant
short_description: "Run dense and sparse retrieval together: the queries each one gets wrong, what the second index costs, and how to tell if it helped." short_description: 'Run dense and sparse retrieval together: the queries each one gets wrong, what the second index costs, and how to tell if it helped.'
description: "Decide whether to add hybrid search in Qdrant: the queries dense and sparse retrieval each get wrong, and how to measure the gain." description: 'Decide whether to add hybrid search in Qdrant: the queries dense and sparse retrieval each get wrong, and how to measure the gain.'
preview_dir: /articles_data/hybrid-search/preview preview_dir: /articles_data/hybrid-search/preview
social_preview_image: /articles_data/hybrid-search/preview/social_preview.jpg social_preview_image: /articles_data/hybrid-search/preview/social_preview.jpg
weight: -215 weight: 110
author: Dylan Couzon author: Dylan Couzon
author_link: https://www.linkedin.com/in/dcouzon/ author_link: https://www.linkedin.com/in/dcouzon/
date: 2026-08-24T09:00:00+03:00 date: 2026-08-24 09:00:00+03:00
draft: false draft: false
keywords: keywords:
- hybrid search - hybrid search
- sparse vectors - sparse vectors
- BM25 - BM25
- reciprocal rank fusion - reciprocal rank fusion
- search relevance - search relevance
category: search-quality partition: learn
learning_kind: guides
aliases:
- /articles/hybrid-search/
guide_series: true
--- ---
# Hybrid Search in Qdrant
A search result can look plausible and still be wrong. Dense retrieval can return a document on the right topic but miss an exact identifier copied into the query. Sparse retrieval can miss a relevant document when the query describes it with terms the corpus doesn't use. Either way, your logs record a successful query. A search result can look plausible and still be wrong. Dense retrieval can return a document on the right topic but miss an exact identifier copied into the query. Sparse retrieval can miss a relevant document when the query describes it with terms the corpus doesn't use. Either way, your logs record a successful query.
Hybrid search runs dense and sparse retrieval over the same query, then merges their result lists. Dense retrieval adds semantic similarity, so paraphrases can rank together. Sparse retrieval adds weighted term matching for exact words and identifiers. Hybrid search runs dense and sparse retrieval over the same query, then merges their result lists. Dense retrieval adds semantic similarity, so paraphrases can rank together. Sparse retrieval adds weighted term matching for exact words and identifiers.
@@ -65,7 +71,7 @@ Formula Queries serve a different purpose: they rescore retrieved candidates wit
Fusion only reorders. It works on the union of what the two prefetches returned, so a document neither one found cannot appear anywhere in the results. Fusion only reorders. It works on the union of what the two prefetches returned, so a document neither one found cannot appear anywhere in the results.
If a relevant document falls below a prefetch cutoff, increasing one or both prefetch limits can expose it to fusion. A larger limit adds retrieval work, and it does not help if the retrievers still miss the document at greater depth. [Candidate depth](/articles/candidate-depth/) explains how to test the limits, and the [hybrid query documentation](/documentation/search/hybrid-queries/) covers how prefetches feed fusion. If a relevant document falls below a prefetch cutoff, increasing one or both prefetch limits can expose it to fusion. A larger limit adds retrieval work, and it does not help if the retrievers still miss the document at greater depth. [Candidate depth](/documentation/search-tuning/candidate-depth/) explains how to test the limits, and the [hybrid query documentation](/documentation/search/hybrid-queries/) covers how prefetches feed fusion.
![A collection drawn as a field of documents with two overlapping oval regions over it. Documents inside the left oval are red and labeled dense prefetch, documents inside the right oval are blue and labeled sparse prefetch, documents in the overlap are dark, and roughly a third of the documents sit outside both ovals in pale grey. A note reading candidate union passed to fusion points into the retrieved region.](/articles_data/hybrid-search/candidate-boundary.png) ![A collection drawn as a field of documents with two overlapping oval regions over it. Documents inside the left oval are red and labeled dense prefetch, documents inside the right oval are blue and labeled sparse prefetch, documents in the overlap are dark, and roughly a third of the documents sit outside both ovals in pale grey. A note reading candidate union passed to fusion points into the retrieved region.](/articles_data/hybrid-search/candidate-boundary.png)
@@ -142,9 +148,9 @@ Run the same labeled queries with dense retrieval, sparse retrieval, and fusion.
First, check whether fusion beats both retrievers. Review the queries where their rankings differ, then see whether the wins and losses cluster around important query types in your workload. First, check whether fusion beats both retrievers. Review the queries where their rankings differ, then see whether the wins and losses cluster around important query types in your workload.
If one retriever finds relevant results that fusion ranks too low, tune the fusion method or weights. [How to Tune Hybrid Search](/articles/how-to-tune-hybrid-search/) covers those settings. If both retrievers miss a result, fusion has no candidate to promote. If one retriever finds relevant results that fusion ranks too low, tune the fusion method or weights. [How to Tune Hybrid Search](/documentation/search-tuning/how-to-tune-hybrid-search/) covers those settings. If both retrievers miss a result, fusion has no candidate to promote.
Recheck the winning setup on held-out queries. [Building a labeled set](/articles/before-tuning-a-qdrant-collection/) covers query selection and held-out evaluation. Recheck the winning setup on held-out queries. [Building a labeled set](/documentation/search-tuning/before-tuning-a-qdrant-collection/) covers query selection and held-out evaluation.
Dense retrieval may already cover much of a natural-language-only workload, but query shape alone cannot tell you whether hybrid search will help. Dense retrieval may already cover much of a natural-language-only workload, but query shape alone cannot tell you whether hybrid search will help.
@@ -152,5 +158,5 @@ Keep the sparse retriever when the relevance gain justifies its measured indexin
## What to Test Next ## What to Test Next
- **Add more stages.** [Multi-stage queries](/documentation/search/hybrid-queries/#multi-stage-queries) retrieve with a cheap representation and rescore with an expensive one. Cross-encoder reranking puts the query and chunk into a model together. [When Is a Reranker Worth It?](/articles/when-a-reranker-is-worth-it/) compares that approach with a tuned first stage. - **Add more stages.** [Multi-stage queries](/documentation/search/hybrid-queries/#multi-stage-queries) retrieve with a cheap representation and rescore with an expensive one. Cross-encoder reranking puts the query and chunk into a model together. [When Is a Reranker Worth It?](/documentation/search-tuning/when-a-reranker-is-worth-it/) compares that approach with a tuned first stage.
- **Tune what you have.** The fusion method, the RRF constant, and the per-retriever weights can all move relevance without adding a stage. [How to Tune Hybrid Search](/articles/how-to-tune-hybrid-search/) measures each one across the same five datasets. - **Tune what you have.** The fusion method, the RRF constant, and the per-retriever weights can all move relevance without adding a stage. [How to Tune Hybrid Search](/documentation/search-tuning/how-to-tune-hybrid-search/) measures each one across the same five datasets.
@@ -1,9 +1,11 @@
--- ---
title: Query Decomposition for Multi-Hop Questions title: Query Decomposition for Multi-Hop Questions
short_description: "Answer multi-hop questions by retrieving in steps: an LLM asks each follow-up sub-question, then fuse the per-hop results with RRF." short_description: 'Answer multi-hop questions by retrieving in steps: an LLM asks each follow-up sub-question, then fuse the per-hop results with RRF.'
description: "Answer multi-hop questions in Qdrant: decompose the query into retrieval steps, let an LLM ask each follow-up, and fuse results with RRF." description: 'Answer multi-hop questions in Qdrant: decompose the query into retrieval steps, let an LLM ask each follow-up, and fuse results with RRF.'
weight: 8 weight: 20
partition: ecosystem partition: learn
learning_kind: guides
url: /documentation/improve-search/query-decomposition/
--- ---
# Query Decomposition for Multi-Hop Questions # Query Decomposition for Multi-Hop Questions
@@ -1,40 +1,44 @@
--- ---
title: "When Is a Reranker Worth It?" title: When Is a Reranker Worth It?
short_description: "Rerank 10 candidates, compare with your tuned first stage on held-out queries, and raise the count only after the win holds." short_description: Rerank 10 candidates, compare with your tuned first stage on held-out queries, and raise the count only after the win holds.
description: "Test whether a cross-encoder reranker beats your tuned first stage in Qdrant, then choose the model and candidate count from measured results." description: Test whether a cross-encoder reranker beats your tuned first stage in Qdrant, then choose the model and candidate count from measured results.
preview_dir: /articles_data/when-a-reranker-is-worth-it/preview preview_dir: /articles_data/when-a-reranker-is-worth-it/preview
social_preview_image: /articles_data/when-a-reranker-is-worth-it/preview/social_preview.jpg social_preview_image: /articles_data/when-a-reranker-is-worth-it/preview/social_preview.jpg
weight: -210 weight: 150
author: Dylan Couzon author: Dylan Couzon
author_link: https://www.linkedin.com/in/dcouzon/ author_link: https://www.linkedin.com/in/dcouzon/
date: 2026-08-23T00:00:00+03:00 date: 2026-08-23 00:00:00+03:00
draft: false draft: false
keywords: keywords:
- cross-encoder reranker - cross-encoder reranker
- reranking - reranking
- MMR - MMR
- search relevance - search relevance
- FastEmbed - FastEmbed
category: search-quality partition: learn
learning_kind: guides
aliases:
- /articles/when-a-reranker-is-worth-it/
guide_series: true
--- ---
Before you tune a reranker, use the [pre-tuning checks](/articles/before-tuning-a-qdrant-collection/) to verify index state and set a labeled baseline. Before you tune a reranker, use the [pre-tuning checks](/documentation/search-tuning/before-tuning-a-qdrant-collection/) to verify index state and set a labeled baseline.
Your candidate list can already contain documents your ranking never shows. Score those candidates as if they were perfectly ordered, then compare that with the score your pipeline returns today. The gap between the two is everything a better ranking stage could recover, so measure it before you reach for a model. Use `nDCG@10`, which grades the top 10 results and gives more credit to relevant documents near the top. Your candidate list can already contain documents your ranking never shows. Score those candidates as if they were perfectly ordered, then compare that with the score your pipeline returns today. The gap between the two is everything a better ranking stage could recover, so measure it before you reach for a model. Use `nDCG@10`, which grades the top 10 results and gives more credit to relevant documents near the top.
A wide gap means a better order is worth chasing. The deepest count measured here was 200 candidates. At that count, the `nDCG@10` gap ran from 0.247 to 0.487 across the five datasets, and [candidate depth](/articles/candidate-depth/) shows how to measure it on your own collection. A wide gap means a better order is worth chasing. The deepest count measured here was 200 candidates. At that count, the `nDCG@10` gap ran from 0.247 to 0.487 across the five datasets, and [candidate depth](/documentation/search-tuning/candidate-depth/) shows how to measure it on your own collection.
Reranking covers several model families. This article measures cross-encoders, which read the query and candidate together as a single sequence. A classification head returns one relevance score for the pair. Joint reading lets the model capture token interactions that separately encoded query and document vectors miss. Reranking covers several model families. This article measures cross-encoders, which read the query and candidate together as a single sequence. A classification head returns one relevance score for the pair. Joint reading lets the model capture token interactions that separately encoded query and document vectors miss.
Every candidate takes a forward pass at query time, which rules a cross-encoder out as a first stage and keeps it in the reranking slot. Late interaction models rerank from stored vectors instead, and the last section covers where they fit. Every candidate takes a forward pass at query time, which rules a cross-encoder out as a first stage and keeps it in the reranking slot. Late interaction models rerank from stored vectors instead, and the last section covers where they fit.
<aside role="status"> <aside role="status">
<strong>Note:</strong> The measurements in this article use five public datasets chosen to vary in corpus size, document and query shape, and relevance task, so read these reranker deltas as directional. They range from 5,183 to 100,000 documents, and each collection was built in one batch on one shard, unquantized and unfiltered, with <code>all-MiniLM-L6-v2</code> for dense retrieval and Qdrant's core BM25 for sparse retrieval. Four cross-encoders reranked the fused candidates at counts from 10 through 200, scored on 200 queries per dataset. <a href="/articles/before-tuning-a-qdrant-collection/#check-the-winner-on-fresh-queries">Held-out validation</a> explains the split, and <a href="/articles/before-tuning-a-qdrant-collection/#make-sure-your-labels-can-detect-a-gain">building a labeled set</a> explains the labels. <strong>Note:</strong> The measurements in this article use five public datasets chosen to vary in corpus size, document and query shape, and relevance task, so read these reranker deltas as directional. They range from 5,183 to 100,000 documents, and each collection was built in one batch on one shard, unquantized and unfiltered, with <code>all-MiniLM-L6-v2</code> for dense retrieval and Qdrant's core BM25 for sparse retrieval. Four cross-encoders reranked the fused candidates at counts from 10 through 200, scored on 200 queries per dataset. <a href="/documentation/search-tuning/before-tuning-a-qdrant-collection/#check-the-winner-on-fresh-queries">Held-out validation</a> explains the split, and <a href="/documentation/search-tuning/before-tuning-a-qdrant-collection/#make-sure-your-labels-can-detect-a-gain">building a labeled set</a> explains the labels.
</aside> </aside>
## Test a Reranker in Three Steps ## Test a Reranker in Three Steps
1. Establish the baseline the reranker has to beat: [tuned fusion](/articles/how-to-tune-hybrid-search/) if you run hybrid search, your current ranking if you run dense-only or sparse-only. Confirm the documents your labels mark relevant reach the candidate list. A reranker only reorders what it receives; missing documents are a [candidate depth](/articles/candidate-depth/) or retrieval problem. 1. Establish the baseline the reranker has to beat: [tuned fusion](/documentation/search-tuning/how-to-tune-hybrid-search/) if you run hybrid search, your current ranking if you run dense-only or sparse-only. Confirm the documents your labels mark relevant reach the candidate list. A reranker only reorders what it receives; missing documents are a [candidate depth](/documentation/search-tuning/candidate-depth/) or retrieval problem.
2. Rerank 10 candidates with the model you would actually serve, since model choice moved our results more than any other setting. Read its model card first for languages, domains, and context window. [Reranking with FastEmbed](/documentation/fastembed/fastembed-rerankers/) shows the cross-encoder workflow and the available models. Compare the result with the first-stage baseline on held-out labeled queries. 2. Rerank 10 candidates with the model you would actually serve, since model choice moved our results more than any other setting. Read its model card first for languages, domains, and context window. [Reranking with FastEmbed](/documentation/fastembed/fastembed-rerankers/) shows the cross-encoder workflow and the available models. Compare the result with the first-stage baseline on held-out labeled queries.
3. Raise the candidate count only if the reranker wins. Measure throughput on your document lengths before making it part of the serving path. 3. Raise the candidate count only if the reranker wins. Measure throughput on your document lengths before making it part of the serving path.
@@ -79,7 +83,7 @@ order = sorted(range(len(fused)), key=lambda i: scores[i], reverse=True)
reranked = [fused[i] for i in order] reranked = [fused[i] for i in order]
``` ```
You now have two orderings of the same 10 candidates. Score both with the `nDCG@10` function from the [pre-tuning article](/articles/before-tuning-a-qdrant-collection/#make-sure-your-labels-can-detect-a-gain). You now have two orderings of the same 10 candidates. Score both with the `nDCG@10` function from the [pre-tuning article](/documentation/search-tuning/before-tuning-a-qdrant-collection/#make-sure-your-labels-can-detect-a-gain).
```python ```python
# relevance holds this query's labels, keyed by doc_id. # relevance holds this query's labels, keyed by doc_id.
@@ -93,7 +97,7 @@ Run that over your labeled queries, average the per-query difference, then check
Compare the reranker against the strongest first stage you can build. Qdrant's default reciprocal rank fusion (RRF) is already a solid baseline, and fusion tuned on your own labels is stronger. A reranker measured against the default can look like a win that tuning would have delivered for far less work at query time. Compare the reranker against the strongest first stage you can build. Qdrant's default reciprocal rank fusion (RRF) is already a solid baseline, and fusion tuned on your own labels is stronger. A reranker measured against the default can look like a win that tuning would have delivered for far less work at query time.
So [tune fusion](/articles/how-to-tune-hybrid-search/) first, then make that tuned ranking the number the reranker has to beat on held-out labeled queries. So [tune fusion](/documentation/search-tuning/how-to-tune-hybrid-search/) first, then make that tuned ranking the number the reranker has to beat on held-out labeled queries.
Each row in the following table reports the best of four cross-encoders on that dataset. The deltas show the `nDCG@10` change over default RRF and over fusion tuned on the same candidates. `MiniLM-L-6`, `MiniLM-L-12`, and `bge-reranker-base` truncate each pair at 512 tokens. `jina-reranker-v2` reads up to 1024 and was trained on a broader mix, including code. Each row in the following table reports the best of four cross-encoders on that dataset. The deltas show the `nDCG@10` change over default RRF and over fusion tuned on the same candidates. `MiniLM-L-6`, `MiniLM-L-12`, and `bge-reranker-base` truncate each pair at 512 tokens. `jina-reranker-v2` reads up to 1024 and was trained on a broader mix, including code.
@@ -107,7 +111,7 @@ The held-out column is the one that decides. It holds the share of 200 split-hal
| CodeSearchNet | `jina-reranker-v2` @ 200 | +0.169 | +0.135 | yes, 100% | | CodeSearchNet | `jina-reranker-v2` @ 200 | +0.169 | +0.135 | yes, 100% |
| DBPedia-entity | `jina-reranker-v2` @ 200 | +0.137 | +0.115 | yes, 100% | | DBPedia-entity | `jina-reranker-v2` @ 200 | +0.137 | +0.115 | yes, 100% |
Ship a reranker gain only when it survives held-out validation. The two confirmed wins here held in 100% of the split-half draws, while the three unconfirmed results held in under half of them. When a positive result fails the split, add queries or keep fusion, and use [the label-count table in the pre-tuning article](/articles/before-tuning-a-qdrant-collection/#make-sure-your-labels-can-detect-a-gain) to size that confirmation. Ship a reranker gain only when it survives held-out validation. The two confirmed wins here held in 100% of the split-half draws, while the three unconfirmed results held in under half of them. When a positive result fails the split, add queries or keep fusion, and use [the label-count table in the pre-tuning article](/documentation/search-tuning/before-tuning-a-qdrant-collection/#make-sure-your-labels-can-detect-a-gain) to size that confirmation.
Keep fusion when the reranker loses to the tuned first stage. WANDS gained +0.039 over default RRF and still lost to fusion tuned on the same candidates. Keep fusion when the reranker loses to the tuned first stage. WANDS gained +0.039 over default RRF and still lost to fusion tuned on the same candidates.
@@ -158,7 +162,7 @@ The table shows CPU throughput for the four [FastEmbed cross-encoders](/document
Document length explains each range. DBPedia-entity has short entity abstracts, while SciFact has full paper abstracts. Document length explains each range. DBPedia-entity has short entity abstracts, while SciFact has full paper abstracts.
Weigh those rates against the held-out gain. At 100 candidates, one CPU process spends between half a second and five seconds per query with the three smaller models, where the second prefetch behind [tuned fusion](/articles/how-to-tune-hybrid-search/) added 0.6 to 1.5 ms in the same setup. The 10-candidate test itself stays fast even on CPU, at 47 to 156 ms per query with the smallest model, so run it before you plan any serving work. Weigh those rates against the held-out gain. At 100 candidates, one CPU process spends between half a second and five seconds per query with the three smaller models, where the second prefetch behind [tuned fusion](/documentation/search-tuning/how-to-tune-hybrid-search/) added 0.6 to 1.5 ms in the same setup. The 10-candidate test itself stays fast even on CPU, at 47 to 156 ms per query with the smallest model, so run it before you plan any serving work.
Pick the model on fit rather than size. `bge-reranker-base` and `jina-reranker-v2` are nearly the same size, and only the second ever beat tuned fusion. Training data and context window separated them. Pick the model on fit rather than size. `bge-reranker-base` and `jina-reranker-v2` are nearly the same size, and only the second ever beat tuned fusion. Training data and context window separated them.
@@ -185,6 +189,6 @@ Test a [late interaction model](/documentation/fastembed/fastembed-colbert/) whe
## What to Tune Next ## What to Tune Next
After a win, the work moves to throughput, where the candidate count you can serve decides how much of the gain survives. After a loss, the gap is still there and the candidates are what to change, so revisit retrieval and [candidate depth](/articles/candidate-depth/) before adding another ranking stage. After a win, the work moves to throughput, where the candidate count you can serve decides how much of the gain survives. After a loss, the gap is still there and the candidates are what to change, so revisit retrieval and [candidate depth](/documentation/search-tuning/candidate-depth/) before adding another ranking stage.
Next, if memory is the constraint, [measure what memory placement and rescoring add to query latency](/articles/when-your-collection-outgrows-ram/). Next, if memory is the constraint, [measure what memory placement and rescoring add to query latency](/documentation/search-tuning/when-your-collection-outgrows-ram/).
@@ -1,23 +1,29 @@
--- ---
title: "When Your Collection Outgrows RAM" title: When Your Collection Outgrows RAM
short_description: "Keep the quantized copy in RAM and the original vectors on disk, then measure what rescoring reads back on your own deployment." short_description: Keep the quantized copy in RAM and the original vectors on disk, then measure what rescoring reads back on your own deployment.
description: "Set quantization and memory placement in Qdrant once a collection outgrows RAM: what the rescoring disk read costs and what quality it recovers." description: 'Set quantization and memory placement in Qdrant once a collection outgrows RAM: what the rescoring disk read costs and what quality it recovers.'
preview_dir: /articles_data/when-your-collection-outgrows-ram/preview preview_dir: /articles_data/when-your-collection-outgrows-ram/preview
social_preview_image: /articles_data/when-your-collection-outgrows-ram/preview/social_preview.jpg social_preview_image: /articles_data/when-your-collection-outgrows-ram/preview/social_preview.jpg
weight: -209 weight: 160
author: Dylan Couzon author: Dylan Couzon
author_link: https://www.linkedin.com/in/dcouzon/ author_link: https://www.linkedin.com/in/dcouzon/
date: 2026-08-24T00:00:00+03:00 date: 2026-08-24 00:00:00+03:00
draft: false draft: false
keywords: keywords:
- memory tiers - memory tiers
- quantization - quantization
- rescoring - rescoring
- oversampling - oversampling
- TurboQuant - TurboQuant
category: search-quality partition: learn
learning_kind: guides
aliases:
- /articles/when-your-collection-outgrows-ram/
guide_series: true
--- ---
# When Your Collection Outgrows RAM
Once a collection no longer fits in RAM, the kernel evicts vector pages, and the next query waits on a disk read to get them back. Quantization buys that memory back. Qdrant keeps a compressed copy of each dense vector in RAM and moves the full-precision originals to disk. Once a collection no longer fits in RAM, the kernel evicts vector pages, and the next query waits on a disk read to get them back. Quantization buys that memory back. Qdrant keeps a compressed copy of each dense vector in RAM and moves the full-precision originals to disk.
[TurboQuant](/documentation/manage-data/quantization/#turboquant-quantization) is the method measured here. It rotates each vector before compressing it, which spreads the error evenly across coordinates, and its `bits` parameter sets the depth from `bits4` down to `bits1`. Start at `bits4`, a good default for many workloads at eight times compression. [TurboQuant](/documentation/manage-data/quantization/#turboquant-quantization) is the method measured here. It rotates each vector before compressing it, which spreads the error evenly across coordinates, and its `bits` parameter sets the depth from `bits4` down to `bits1`. Start at `bits4`, a good default for many workloads at eight times compression.
@@ -49,7 +55,7 @@ The extra 50% covers metadata, indexes, point versions, and temporary segments c
Qdrant [recommends pinning the quantized copy with `cold` originals](/documentation/manage-data/quantization/#memory-and-speed-tuning) to shrink the footprint while keeping search fast. The following two sections measure what that pairing costs in disk reads and what rescoring recovers. Qdrant [recommends pinning the quantized copy with `cold` originals](/documentation/manage-data/quantization/#memory-and-speed-tuning) to shrink the footprint while keeping search fast. The following two sections measure what that pairing costs in disk reads and what rescoring recovers.
Step 4 needs a [labeled set](/articles/before-tuning-a-qdrant-collection/). Compare `nDCG@k` with `k` set to the number of results you return, pick the configuration on one part of the set, then confirm it on queries that took no part in the selection. Use `Recall@k` against exact search to explain a loss. Step 4 needs a [labeled set](/documentation/search-tuning/before-tuning-a-qdrant-collection/). Compare `nDCG@k` with `k` set to the number of results you return, pick the configuration on one part of the set, then confirm it on queries that took no part in the selection. Use `Recall@k` against exact search to explain a loss.
## Rescoring Adds the Disk Read ## Rescoring Adds the Disk Read
@@ -182,7 +188,7 @@ First, compute the exact dense top `k` once for a representative sample of your
Then run your existing dense prefetch with each `rescore` and `oversampling` variant, changing nothing else. `Recall@k` against the exact result shows what quantization changed in the dense prefetch. Without labels, that check and the latency numbers still stand on their own. Then run your existing dense prefetch with each `rescore` and `oversampling` variant, changing nothing else. `Recall@k` against the exact result shows what quantization changed in the dense prefetch. Without labels, that check and the latency numbers still stand on their own.
For hybrid search, keep the prefetches, [fusion settings](/articles/how-to-tune-hybrid-search/), and filters your service already uses, then compare the final `nDCG@k`. For hybrid search, keep the prefetches, [fusion settings](/documentation/search-tuning/how-to-tune-hybrid-search/), and filters your service already uses, then compare the final `nDCG@k`.
### Self-Hosted ### Self-Hosted
@@ -198,6 +204,6 @@ Keep the first configuration that meets your held-out `nDCG@k` and latency targe
On a multi-shard hybrid collection, rerun the full request on your deployed shard layout once the dense-vector placements are set. Each shard runs the prefetch and rescoring against its own data. On a multi-shard hybrid collection, rerun the full request on your deployed shard layout once the dense-vector placements are set. Each shard runs the prefetch and rescoring against its own data.
With a `limit` of 200 and `oversampling` 1, rescoring can read up to 200 original vectors per shard, or up to 2,400 across 12 shards. [Candidate depth](/articles/candidate-depth/) covers how to set the limit that total scales with. With a `limit` of 200 and `oversampling` 1, rescoring can read up to 200 original vectors per shard, or up to 2,400 across 12 shards. [Candidate depth](/documentation/search-tuning/candidate-depth/) covers how to set the limit that total scales with.
If you do not have a labeled query set yet, [What to Check Before Tuning a Qdrant Collection](/articles/before-tuning-a-qdrant-collection/) covers how to build one. If you do not have a labeled query set yet, [What to Check Before Tuning a Qdrant Collection](/documentation/search-tuning/before-tuning-a-qdrant-collection/) covers how to build one.
+90 -95
View File
@@ -1,6 +1,6 @@
--- ---
title: "Qdrant Learn Portal" title: Qdrant Learn Portal
description: "Tutorials, Courses, Articles" description: Grow your search engineering skills with practical Qdrant guides, working examples, structured courses, and research on retrieval and engine internals.
hideTOC: true hideTOC: true
breadcrumb: false breadcrumb: false
partition: learn partition: learn
@@ -8,101 +8,96 @@ feedback: false
build: build:
render: always render: always
cascade: cascade:
- build: - build:
list: local list: local
publishResources: false publishResources: false
render: never render: never
content: content:
# - partial: documentation/banners/banner-b - partial: documentation/banners/banner-a
# title: Welcome to Qdrant Learn title: Grow as a Search Engineer
# description: Learn Portal description: Make better search decisions, adapt a working example, or understand how Qdrant works beneath the API.
# image: linkDescription: Choose the resource that answers your question today.
# src: /img/dev-portal-cloud/dev-portal-cloud-hero.png cloudButton:
# alt: Qdrant Course text: Explore Practical Guides
# startedButton: url: /documentation/guides/
# text: Start Learning localButton:
# url: /courses/essentials text: Start with Qdrant Essentials
- partial: documentation/sections/cards-section url: /course/essentials/
title: Learn - partial: documentation/sections/cards-section
description: Master vector search with Qdrant through comprehensive documentation, structured courses, and hands-on tutorials. title: Choose How to Learn
cardsPartial: documentation/cards/docs-cards description: Each resource serves a different purpose. Start with the one that fits your task.
cards: cardsPartial: documentation/cards/docs-cards
- id: 1 cardsPerRow: 2
image: cards:
src: /img/dev-portal-learn/articles.png - title: Guides
alt: Articles description: Evaluate search quality, choose embedding models, and plan how your Qdrant application grows.
title: Articles link:
description: In-depth technical documentation covering vector search concepts, system architecture, and advanced techniques. url: /documentation/guides/
text: Find Practical Guidance
list: image:
title: "Featured articles:" src: /img/dev-portal-learn/articles.png
elements: alt: ''
- Data Exploration with Qdrant's Distance Matrix API - title: Tutorials & Examples
- Why Vector Search Needs a Dedicated Database description: Open working code and walkthroughs, then adapt the implementation to your data and application stack.
- Semantic Search As You Type link:
link: url: /learn/examples/
url: /articles/ text: Browse Tutorials & Examples
text: Browse Articles image:
- id: 2 src: /img/dev-portal-learn/tutorials.png
image: alt: ''
src: /img/dev-portal-learn/courses.png - title: Courses
alt: Courses description: Build your understanding through structured lessons and exercises, starting with Qdrant Essentials.
title: Courses link:
description: Structured learning paths with progressive difficulty levels, from beginner fundamentals to advanced implementations. url: /course/
list: text: Explore Courses
title: "Available topics:" image:
elements: src: /img/dev-portal-learn/courses.png
- "Beginner: Vector search basics" alt: ''
- "Intermediate: Advanced querying" - title: Articles
- "Advanced: Production optimization" description: Examine retrieval experiments and the mechanisms behind Qdrant indexing, storage, and search.
link:
link: url: /articles/
url: /course/ text: Explore Articles
text: View Courses image:
- id: 3 src: /img/dev-portal-learn/articles.png
image: alt: ''
src: /img/dev-portal-learn/tutorials.png - partial: documentation/sections/cards-section
alt: Tutorials title: Start with Your Task
title: Tutorials description: Take a direct path to a common search engineering task.
description: Step-by-step guides and video content for hands-on learning with practical examples and real-world applications. cardsPartial: documentation/cards/docs-cards
list: cardsPerRow: 2
title: "Tutorial categories:" cards:
elements: - title: Build Your First Search
- "Search Engineering" description: Start with a small semantic search application and adapt it to your own data.
- "Operations and Scale" link:
- "Develop and Implement" url: /documentation/tutorials-basics/search-beginners/
text: Open the Example
link: - title: Evaluate Search Quality
url: /documentation/tutorials-lp-overview/ description: Choose an evaluation baseline before changing embeddings, retrieval, or ranking.
text: Explore Tutorials link:
- partial: documentation/sections/cards-section url: /documentation/search-quality/
title: Quickstart text: Explore Search Evaluation
description: - title: Prepare for Production
cardsPartial: documentation/cards/docs-cards description: Plan tenant growth and large imports around the workload you need to serve.
cardsPerRow: 2 link:
cards: url: /documentation/production-patterns/
- id: 1 text: Explore Production & Performance
icon: - title: Design and Tune Search
src: /icons/outline/rocket-blue.svg description: Choose embeddings and retrieval strategies, then follow the tuning series to test improvements.
alt: Rocket icon link:
title: New to Vector Search? url: /documentation/search-tuning/
description: Start with our beginner-friendly exercises on vector embeddings and basic concepts. text: Explore Search Design & Tuning
link:
text: Start Learning
url: /documentation/tutorials-basics/
- id: 2
icon:
src: /icons/outline/hacker-purple.svg
alt: Hacker icon
title: Ready to Build?
description: Build out practical projects using our example prototypes and integration guides.
link:
text: View Examples
url: /documentation/examples/
--- ---
# Learn # Learn
- **[Articles](/articles/index.md)**: Long-form technical pieces covering vector search concepts, system architecture, RAG pipelines, quantization, hybrid retrieval, and Qdrant internals. Choose Guides for practical decisions, Tutorials & Examples for an implementation, Courses for structured study, or Articles for new evidence and engine mechanisms.
- **[Courses](/course/index.md)**: Structured, self-paced learning paths through Qdrant Academy. Two courses are currently available: *Qdrant Essentials* (9–12 hours) and *Multi-Vector Search* (4–6 hours), with beginner, intermediate, and advanced courses planned. Free, with certification.
- **[Tutorials](/documentation/tutorials-lp-overview/index.md)**: Step-by-step guides organized into five categories: Basic, Search Engineering, Operations & Scale, Develop & Implement, and Migrate to Qdrant. ## Read More
- [Guides](/documentation/guides/)
- [Tutorials & Examples](/learn/examples/)
- [Courses](/course/)
- [Articles](/articles/)
Start with the resource that answers your current question.
+25
View File
@@ -0,0 +1,25 @@
---
title: Tutorials & Examples
layout: examples
description: Find Qdrant tutorials and examples by goal and stack. Open working code for search, RAG, filtering, and operations, then follow the steps relevant to your application.
short_description: Find tutorials and examples by goal and stack, open the available notebooks, and adapt the implementation to your search application.
partition: learn
weight: 310
hideTOC: true
breadcrumb: false
feedback: false
build:
render: always
content:
- partial: documentation/banners/banner-a
title: Find a Tutorial or Example
description: Search, RAG, recommendations, and operations, with code and walkthroughs you can adapt.
linkDescription: Choose your goal and stack, then open the example or its available notebook.
cloudButton:
text: Build Your First Search
url: /documentation/tutorials-basics/search-beginners/
localButton:
text: Browse Tutorials & Examples
url: '#example-library'
- partial: documentation/examples/catalog
---
+309
View File
@@ -0,0 +1,309 @@
- page: /documentation/tutorials-basics/search-beginners/
goal: Get Started
stack:
- Python
- Cloud Inference
resources:
- label: Open Notebook
url: https://githubtocolab.com/qdrant/examples/blob/master/semantic-search-in-5-minutes/semantic_search_in_5_minutes.ipynb
- page: /documentation/tutorials-basics/search-beginners-local/
goal: Get Started
stack:
- Python
- Sentence Transformers
- page: /documentation/tutorials-basics/cloud-inference-hybrid-search/
goal: Search Quality
stack:
- Python
- Cloud Inference
- page: /documentation/tutorials-develop/hybrid-search-fastembed/
goal: Search Quality
stack:
- Python
- FastEmbed
- FastAPI
resources:
- label: View Code
url: https://github.com/qdrant/qdrant_demo/
- page: /documentation/tutorials-basics/reranking-hybrid-search/
goal: Search Quality
stack:
- Python
- FastEmbed
- page: /documentation/tutorials-develop/neural-search/
goal: Get Started
stack:
- Python
- FastAPI
resources:
- label: Open Notebook
url: https://colab.research.google.com/drive/1kPktoudAP8Tu8n8l-iVMOQhVmHkWV_L9?usp=sharing
- page: /documentation/tutorials-develop/code-search/
goal: Search Quality
stack:
- Python
- FastEmbed
resources:
- label: Open Notebook
url: https://colab.research.google.com/github/qdrant/examples/blob/master/code-search/code-search.ipynb
- page: /documentation/tutorials-basics/huggingface-datasets/
goal: Data & Filtering
stack:
- Python
- Hugging Face
- page: /documentation/tutorials-develop/async-api/
goal: Operations
stack:
- Python
- page: /documentation/tutorials-search-engineering/ann-recall/
goal: Search Quality
stack:
- Python
- Web UI
description: Compare approximate search with exact results, inspect ANN recall in the Web UI, and add a repeatable Python check.
keywords: evaluation index quality hnsw approximate exact recall
- page: /documentation/tutorials-search-engineering/branch-aware-search/
goal: Data & Filtering
stack:
- Python
keywords: versioned documents branches filters inherited data
- page: /documentation/tutorials-search-engineering/index-dynamic-payloads/
goal: Data & Filtering
stack:
- Python
keywords: arbitrary dynamic attributes filters payload schema
- page: /documentation/tutorials-search-engineering/multi-representation-search/
goal: Search Quality
stack:
- Python
- FastEmbed
description: Combine title, summary, and chunk vectors in one retrieval pipeline, then compare the effect of each representation.
resources:
- label: Open Notebook
url: https://githubtocolab.com/qdrant/examples/blob/master/multi-representation-search/multi-representation-search.ipynb
- page: /documentation/tutorials-search-engineering/pdf-retrieval-at-scale/
goal: Multimodal Search
stack:
- Python
- ColPali
resources:
- label: Open Notebook
url: https://githubtocolab.com/qdrant/examples/blob/master/pdf-retrieval-at-scale/ColPali_ColQwen2_Tutorial.ipynb
- page: /documentation/tutorials-search-engineering/using-multivector-representations/
goal: Search Quality
stack:
- Python
- FastEmbed
- page: /documentation/tutorials-search-engineering/using-relevance-feedback/
goal: Search Quality
stack:
- Python
resources:
- label: Open Notebook
url: https://githubtocolab.com/qdrant/examples/blob/master/using-relevance-feedback/Customizing_Relevance_Feedback.ipynb
- page: /documentation/tutorials-search-engineering/static-embeddings/
goal: Search Quality
stack:
- Python
- page: /documentation/tutorials-search-engineering/turbo4-multivector-search/
goal: Search Quality
stack:
- Python
resources:
- label: Open Notebook
url: https://githubtocolab.com/qdrant/examples/blob/master/multivector-turbo4/Multivector_Turbo4.ipynb
- page: /documentation/tutorials-search-engineering/collaborative-filtering/
goal: Recommendations
stack:
- Python
resources:
- label: Open Notebook
url: https://githubtocolab.com/qdrant/examples/blob/master/collaborative-filtering/collaborative-filtering.ipynb
- page: /documentation/tutorials-operations/embedding-model-migration/
goal: Operations
stack:
- Python
keywords: replace change embedding models migration serving traffic
- page: /documentation/tutorials-operations/create-snapshot/
goal: Operations
stack:
- Python
- page: /documentation/tutorials-operations/blue-green-deployment/
goal: Operations
stack:
- Python
- page: /documentation/tutorials-operations/gpu-accelerated-hnsw-indexing/
goal: Operations
stack:
- Python
- Qdrant Cloud
resources:
- label: Open Notebook
url: https://githubtocolab.com/qdrant/examples/blob/master/gpu-accelerated-hnsw-indexing/Gpu_Accelerated_HNSW_Indexing.ipynb
- page: /documentation/tutorials-operations/incremental-embedding-updates/
goal: Data & Filtering
stack:
- Python
resources:
- label: Open Notebook
url: https://githubtocolab.com/qdrant/examples/blob/master/temporal-data-drift/sync_raw_data_to_embeddings.ipynb
- page: /documentation/tutorials-operations/large-scale-search/
goal: Operations
stack:
- Python
resources:
- label: View Code
url: https://github.com/qdrant/laion-400m-benchmark
- page: /documentation/tutorials-operations/migration/
goal: Operations
stack:
- Migration Tool
- page: /documentation/tutorials-operations/prevent-unoptimized-usage/
goal: Operations
stack:
- Python
resources:
- label: Open Notebook
url: https://githubtocolab.com/qdrant/examples/blob/master/prevent_unoptimized_usage/prevent_unoptimized.ipynb
- page: /documentation/tutorials-operations/secure-qdrant/
goal: Operations
stack:
- Docker
- page: /documentation/tutorials-operations/time-based-sharding/
goal: Data & Filtering
stack:
- Python
- page: /documentation/tutorials-build-essentials/rag-deepseek/
goal: RAG & Agents
stack:
- Python
- DeepSeek
resources:
- label: View Notebook
url: https://github.com/qdrant/examples/blob/master/rag-with-qdrant-deepseek/deepseek-qdrant.ipynb
- page: /documentation/tutorials-build-essentials/qdrant-n8n/
goal: RAG & Agents
stack:
- n8n
- page: /documentation/tutorials-build-essentials/multimodal-search/
goal: Multimodal Search
stack:
- Python
- LlamaIndex
resources:
- label: Open Notebook
url: https://githubtocolab.com/qdrant/examples/blob/master/multimodal-search/Multimodal_Search_with_LlamaIndex.ipynb
- page: /documentation/tutorials-build-essentials/data-ingestion-beginners/
goal: Data & Filtering
stack:
- Python
- LangChain
- AWS
- page: /documentation/tutorials-build-essentials/agentic-rag-camelai-discord/
goal: RAG & Agents
stack:
- Python
- CAMEL-AI
resources:
- label: Open Notebook
url: https://colab.research.google.com/drive/1Ymqzm6ySoyVOekY7fteQBCFCXYiYyHxw#scrollTo=QQZXwzqmNfaS
- page: /documentation/tutorials-build-essentials/video-anomaly-edge-part-1/
goal: Multimodal Search
stack:
- Qdrant Edge
- Twelve Labs
resources:
- label: View Code
url: https://github.com/qdrant/video-anomaly-edge
- page: /documentation/tutorials-build-essentials/video-anomaly-edge-part-2/
goal: Multimodal Search
stack:
- Qdrant Edge
- Twelve Labs
resources:
- label: View Code
url: https://github.com/qdrant/video-anomaly-edge
- page: /documentation/tutorials-build-essentials/video-anomaly-edge-part-3/
goal: Multimodal Search
stack:
- Qdrant Edge
- Twelve Labs
resources:
- label: View Code
url: https://github.com/qdrant/video-anomaly-edge
- page: /documentation/examples/graphrag-qdrant-neo4j/
goal: RAG & Agents
stack:
- Python
- Neo4j
resources:
- label: View Code
url: https://github.com/qdrant/examples/blob/master/graphrag_neo4j/graphrag.py
- page: /documentation/examples/hybrid-search-llamaindex-jinaai/
goal: RAG & Agents
stack:
- Python
- LlamaIndex
- Jina
resources:
- label: Open Notebook
url: https://githubtocolab.com/infoslack/qdrant-example/blob/main/HC-demo/HC-DO-LlamaIndex-Jina-v2.ipynb
keywords: PDF documents manuals LlamaIndex Jina RAG
- page: /documentation/examples/Qdrant-DSPy-medicalbot/
goal: RAG & Agents
stack:
- Python
- DSPy
resources:
- label: View Notebook
url: https://github.com/qdrant/examples/blob/master/DSPy-medical-bot/medical_bot_DSPy_Qdrant.ipynb
- page: /documentation/examples/cohere-rag-connector/
goal: RAG & Agents
stack:
- Python
- Cohere
- page: /documentation/examples/natural-language-search-oracle-cloud-infrastructure-cohere-langchain/
goal: RAG & Agents
stack:
- LangChain
- Cohere
- Oracle Cloud
- page: /documentation/examples/rag-chatbot-red-hat-openshift-haystack/
goal: RAG & Agents
stack:
- Haystack
- OpenShift
- page: /documentation/examples/rag-chatbot-scaleway/
goal: RAG & Agents
stack:
- LangChain
- OpenAI
- Scaleway
resources:
- label: View Notebook
url: https://github.com/qdrant/examples/blob/langchain-lcel-rag/langchain-lcel-rag/Langchain-LCEL-RAG-Demo.ipynb
- page: /documentation/examples/rag-chatbot-vultr-dspy-ollama/
goal: RAG & Agents
stack:
- DSPy
- Ollama
- Vultr
- page: /documentation/examples/rag-contract-management-stackit-aleph-alpha/
goal: RAG & Agents
stack:
- Aleph Alpha
- STACKIT
- page: /documentation/examples/rag-customer-support-cohere-airbyte-aws/
goal: RAG & Agents
stack:
- Cohere
- Airbyte
- AWS
- page: /documentation/examples/recommendation-system-ovhcloud/
goal: Recommendations
stack:
- Python
- OVHcloud
resources:
- label: View Notebook
url: https://github.com/infoslack/qdrant-example/blob/main/HC-demo/HC-OVH.ipynb
@@ -3,7 +3,7 @@
> Use this file to discover all available pages: https://qdrant.tech/llms.txt > Use this file to discover all available pages: https://qdrant.tech/llms.txt
{{- $content := printf "# %s\n\n" .Title -}} {{- $content := printf "# %s\n\n" .Title -}}
{{- range .RegularPages.ByPublishDate.Reverse -}} {{- range (partial "documentation/articles/list" .) -}}
{{- $content = printf "%s- [%s](%s)\n" $content .Title .RelPermalink -}} {{- $content = printf "%s- [%s](%s)\n" $content .Title .RelPermalink -}}
{{- end -}} {{- end -}}
{{- $content = replaceRE `\]\((/[^):]*/)([\)#?])` `](https://qdrant.tech${1}index.md${2}` $content -}} {{- $content = replaceRE `\]\((/[^):]*/)([\)#?])` `](https://qdrant.tech${1}index.md${2}` $content -}}
@@ -0,0 +1,3 @@
# Articles
Browse current Qdrant articles in [Articles](/articles/index.md).
@@ -0,0 +1,16 @@
# Tutorials & Examples
Find an implementation by goal and stack. Open the example for its prerequisites and procedure, or use its available code.
{{ range site.Data.examples }}
{{ $page := site.GetPage .page }}
## {{ .title | default $page.Title }}
{{ .description | default $page.Params.short_description | default $page.Description }}
Goal: {{ .goal }}. Stack: {{ delimit .stack ", " }}.
[Open Example]({{ $page.RelPermalink }}index.md){{ range .resources }} | [{{ .label }}]({{ .url }}){{ end }}
{{ end }}
Choose an example that matches your data, stack, and operating requirements.
@@ -0,0 +1 @@
{{ .Inner }}
+5 -5
View File
@@ -76,14 +76,14 @@
/documentation/tutorials-develop/bulk-upload/* /documentation/manage-data/bulk-upload/:splat 301 /documentation/tutorials-develop/bulk-upload/* /documentation/manage-data/bulk-upload/:splat 301
# Articles category reorganization (technical articles taxonomy) # Articles category reorganization (technical articles taxonomy)
/articles/vector-search-manuals/ /articles/mastering-search/ 301 /articles/vector-search-manuals/ /articles/search-quality/ 301
/articles/machine-learning/ /articles/embedding-research/ 301 /articles/machine-learning/ /articles/embedding-research/ 301
/articles/ecosystem/ /articles/demos-and-tutorials/ 301 /articles/ecosystem/ /articles/ 301
/articles/practicle-examples/ /articles/demos-and-tutorials/ 301 /articles/practicle-examples/ /articles/ 301
/articles/rag-and-genai/ /articles/rag-and-agents/ 301 /articles/rag-and-genai/ /articles/ 301
# Unpublished indexing-optimization article superseded by bulk-uploads-in-qdrant # Unpublished indexing-optimization article superseded by bulk-uploads-in-qdrant
/articles/indexing-optimization/ /articles/bulk-uploads-in-qdrant/ 301 /articles/indexing-optimization/ /documentation/production-patterns/bulk-data-import/ 301
# ACORN blog post converted into an internals article # ACORN blog post converted into an internals article
/blog/filtered-vector-search-acorn/ /articles/filtered-vector-search-acorn/ 301 /blog/filtered-vector-search-acorn/ /articles/filtered-vector-search-acorn/ 301
@@ -2,6 +2,7 @@
@import 'partials/theme-switch'; @import 'partials/theme-switch';
@import 'partials/documentation/documentation-menu'; @import 'partials/documentation/documentation-menu';
@import 'partials/documentation/docs-card'; @import 'partials/documentation/docs-card';
@import 'partials/documentation/learning';
@import 'partials/documentation/documentation'; @import 'partials/documentation/documentation';
@import 'partials/documentation/documentation-article'; @import 'partials/documentation/documentation-article';
@import 'partials/documentation/docs'; @import 'partials/documentation/docs';
@@ -2,6 +2,55 @@
@use 'sass:math'; @use 'sass:math';
.docs-articles { .docs-articles {
&__topics {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: $spacer * 0.75;
margin-top: $spacer * 1.5;
> span {
color: $neutral-70;
width: 100%;
}
a {
padding: $spacer * 0.5 $spacer;
border: pxToRem(1) solid $neutral-30;
border-radius: $spacer * 2;
color: $neutral-90;
font-size: pxToRem(14);
&:hover,
&[aria-current='page'] {
border-color: $primary-50;
color: $primary-50;
}
}
}
&__guide-callout {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: $spacer;
padding: $spacer * 1.5;
border: pxToRem(1) solid $neutral-30;
border-radius: $spacer * 0.5;
> div { flex: 1 1 100%; }
}
.post-type-badge {
display: inline-block;
font-size: pxToRem(11);
padding: $spacer * 0.2 $spacer * 0.6;
border: pxToRem(1) solid $neutral-50;
border-radius: $spacer;
margin-bottom: $spacer * 0.5;
color: $neutral-70;
}
&__title { &__title {
margin-bottom: math.div($spacer, 2); margin-bottom: math.div($spacer, 2);
font-size: $spacer * 1.5; font-size: $spacer * 1.5;
@@ -17,8 +66,11 @@
&__blocks { &__blocks {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: $spacer * 5; gap: $spacer * 3;
padding-bottom: $spacer * 3; padding-bottom: $spacer * 3;
.docs-core__hero { margin-bottom: 0; }
> .docs-articles__topics { margin-top: 0; }
} }
&__block { &__block {
@@ -148,6 +200,21 @@
} }
[data-theme='light'] & { [data-theme='light'] & {
&__topics a,
&__topics > span,
.post-type-badge {
color: $neutral-30;
border-color: $neutral-70;
}
&__topics a:hover,
&__topics a[aria-current='page'] {
color: $primary-50;
border-color: $primary-50;
}
&__guide-callout { border-color: $neutral-90; }
&__title { &__title {
color: $neutral-30; color: $neutral-30;
} }
@@ -0,0 +1,56 @@
.example-library {
scroll-margin-top: 7rem;
[hidden] { display: none !important; }
&__filters {
display: flex;
flex-wrap: wrap;
align-items: end;
gap: 1rem;
margin: 1.5rem 0;
> div { flex: 1 1 12rem; }
label { display: block; margin-bottom: .5rem; }
input, select {
width: 100%;
min-height: 2.75rem;
padding: .65rem;
border: 1px solid $neutral-50;
border-radius: .5rem;
background: $neutral-10;
color: $neutral-98;
color-scheme: dark;
}
:focus-visible { outline: 2px solid $secondary-blue-50; outline-offset: 3px; }
}
.docs-card__title a { color: inherit; }
&__stack { font-size: .875rem; margin: 0; }
&__actions { display: flex; gap: 1rem; flex-wrap: wrap; margin-top: auto; }
[data-theme='light'] & {
input, select { background: $neutral-98; color: $neutral-10; color-scheme: light; }
}
}
.guide-read-more { margin-bottom: 1.5rem; }
// Leave room for the disclosure arrow when a guide topic wraps.
.guide-topic > summary > a { padding-right: 2.5rem; }
.guide-series-navigation {
border-top: 1px solid $neutral-50;
margin-top: 2rem;
padding-top: 1rem;
&__links {
display: flex;
flex-wrap: wrap;
gap: 1rem 2rem;
a { flex: 1 1 15rem; }
span { display: block; font-size: .875rem; color: $neutral-70; }
}
}
.guide-series-label {
list-style: none;
margin: .75rem 0 .25rem;
font-size: .75rem;
font-weight: 600;
color: $neutral-70;
}
@@ -0,0 +1,61 @@
const library = document.querySelector('.example-library');
if (library) {
const form = library.querySelector('form');
const query = form.elements.namedItem('q');
const goal = form.elements.namedItem('goal');
const stack = form.elements.namedItem('stack');
const cards = [...library.querySelectorAll('[data-example]')];
const count = library.querySelector('[data-example-count]');
const empty = library.querySelector('[data-example-empty]');
function readURL() {
const params = new URLSearchParams(window.location.search);
query.value = params.get('q') || '';
for (const select of [goal, stack]) {
const value = params.get(select.name) || '';
select.value = [...select.options].some(option => option.value === value) ? value : '';
}
}
function filter(updateURL = true) {
const terms = query.value.toLowerCase().trim().split(/\s+/).filter(Boolean);
let visible = 0;
cards.forEach(card => {
const match = (!goal.value || card.dataset.goal === goal.value)
&& (!stack.value || card.dataset.stack.split('|').includes(stack.value))
&& terms.every(term => card.dataset.search.includes(term));
card.hidden = !match;
if (match) visible += 1;
});
count.textContent = `${visible} ${visible === 1 ? 'result' : 'results'}`;
empty.hidden = visible !== 0;
document.querySelectorAll('[data-example-nav-goal]').forEach(item => {
const active = item.dataset.exampleNavGoal === goal.value;
item.classList.toggle('active', active);
const link = item.querySelector('a');
if (active) link.setAttribute('aria-current', 'location');
else link.removeAttribute('aria-current');
});
if (updateURL) {
const url = new URL(window.location.href);
for (const control of [query, goal, stack]) {
const value = control.value.trim();
if (value) url.searchParams.set(control.name, value);
else url.searchParams.delete(control.name);
}
window.history.replaceState(null, '', url);
}
}
form.hidden = false;
readURL();
filter(false);
form.addEventListener('submit', event => { event.preventDefault(); filter(); });
form.addEventListener('input', () => filter());
form.addEventListener('change', () => filter());
form.addEventListener('reset', () => {
query.value = goal.value = stack.value = '';
filter();
});
window.addEventListener('popstate', () => { readURL(); filter(false); });
}
@@ -0,0 +1,11 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="robots" content="noindex">
<title>Articles - Qdrant</title>
<link rel="canonical" href="{{ .Params.redirect_to | absURL }}">
<meta http-equiv="refresh" content="0; url={{ .Params.redirect_to | absURL }}">
</head>
<body><p>Continue to <a href="{{ .Params.redirect_to }}">Articles</a>.</p></body>
</html>
@@ -7,3 +7,5 @@
{{ $old := "<table>" }} {{ $old := "<table>" }}
{{ $new := printf "<table class=\"%s\">" "table mb-5" }} {{ $new := printf "<table class=\"%s\">" "table mb-5" }}
{{ $contentWithWrappedTables | replaceRE $old $new | safeHTML }} {{ $contentWithWrappedTables | replaceRE $old $new | safeHTML }}
{{ if eq .Params.learning_kind "guides" }}{{ partial "documentation/guides/series-navigation" . }}{{ end }}
@@ -0,0 +1,19 @@
{{ $page := .page }}
<article class="{{ .column | default "col-12 col-lg-4" }} post post-sm">
<a class="post-link" href="{{ $page.RelPermalink }}">
{{ with $page.Params.preview_dir }}
<div class="post-preview"><img src="{{ . }}/preview.jpg" alt="" loading="lazy" /></div>
{{ end }}
{{ with partial "documentation/articles/topic" $page }}
{{ with site.GetPage (printf "/articles/%s/" .) }}
<span class="post-type-badge">{{ .Title }}</span>
{{ end }}
{{ end }}
<h3 class="post-title">{{ $page.Title }}</h3>
<p class="post-description">{{ $page.Params.short_description | default $page.Description }}</p>
<div class="post-about">
<p>{{ $page.Params.author }}</p>
<p>{{ time.Format "January 2, 2006" $page.Date }}</p>
</div>
</a>
</article>
@@ -0,0 +1,22 @@
<div class="docs-articles docs-articles__list">
{{ partial "documentation/breadcrumbs" . }}
<h1 class="docs-articles__title">{{ .Title }}</h1>
<p class="docs-articles__description">{{ .Description }}</p>
<nav class="docs-articles__topics" aria-label="Article Topics">
<a href="/articles/">All Articles</a>
{{ range (site.GetPage "/articles").Sections.ByWeight }}
{{ if and .Params.isCategoryPage (not .Params.article_collection) (not .Params.hideFromList) }}
<a href="{{ .RelPermalink }}" {{ if eq $.Params.category .Params.category }}aria-current="page"{{ end }}>{{ .Title }}</a>
{{ end }}
{{ end }}
</nav>
{{ $paginator := .Paginate (partial "documentation/articles/list" .) 14 }}
<div class="docs-articles__posts row gx-4 gy-5">
{{ range $paginator.Pages }}
{{ partial "documentation/articles/card" (dict "page" . "column" "col-12 col-lg-6") }}
{{ end }}
</div>
{{ if gt $paginator.TotalPages 1 }}
<div class="docs-articles__pagination">{{ partial "pagination" $paginator }}</div>
{{ end }}
</div>
@@ -0,0 +1,49 @@
<div class="docs-articles docs-articles__blocks">
<div class="docs-core">
{{ partial "documentation/banners/banner-a" (dict "title" "Articles" "description" "Explore search quality, embedding research, Qdrant internals, and production operations." "linkDescription" "Read experiments, mechanisms, and engineering decisions from the Qdrant team." "cloudButton" (dict "text" "Browse Articles" "url" "#article-topics") "localButton" (dict "text" "Explore Guides" "url" "/documentation/guides/")) }}
</div>
<nav id="article-topics" class="docs-articles__topics" aria-label="Article Topics">
<span>Browse by Topic</span>
{{ range (site.GetPage "/articles").Sections.ByWeight }}
{{ if and .Params.isCategoryPage (not .Params.article_collection) (not .Params.hideFromList) }}
<a href="{{ .RelPermalink }}">{{ .Title }}</a>
{{ end }}
{{ end }}
</nav>
{{ range slice "/articles/search-quality" "/articles/embedding-research" "/articles/qdrant-internals" "/articles/production-ops" }}
{{ with site.GetPage . }}
<section class="docs-articles__block">
<div class="docs-articles__block-header">
<div>
<h2 class="docs-articles__title">{{ .Title }}</h2>
<p class="docs-articles__description">{{ .Description }}</p>
</div>
<a href="{{ .RelPermalink }}" class="docs-articles__block-link button button_outlined button_sm">Explore {{ .Title }}</a>
</div>
<div class="docs-articles__posts row gy-4">
{{ range first 3 (partial "documentation/articles/list" .) }}
{{ partial "documentation/articles/card" (dict "page" .) }}
{{ end }}
</div>
</section>
{{ end }}
{{ end }}
<aside class="docs-articles__guide-callout">
<div>
<h2 class="docs-articles__title">Put It into Practice</h2>
<p class="docs-articles__description">Fix a search problem or compare configurations in Guides. Use a worked example when you need code to adapt.</p>
</div>
<a class="button button_contained button_sm" href="/documentation/guides/">Explore Guides</a>
<a class="button button_outlined button_sm" href="/learn/examples/">Explore Tutorials &amp; Examples</a>
</aside>
<section class="docs-articles__block">
<div class="docs-articles__block-header">
<div><h2 class="docs-articles__title">Recently Published</h2><p class="docs-articles__description">The latest research and engineering explanations from the team.</p></div>
</div>
<div class="docs-articles__posts row gy-4">
{{ range first 3 (partial "documentation/articles/list" .) }}
{{ partial "documentation/articles/card" (dict "page" .) }}
{{ end }}
</div>
</section>
</div>
@@ -0,0 +1,10 @@
{{ $pages := slice }}
{{ range (site.GetPage "/articles").RegularPages }}
{{ $topic := partial "documentation/articles/topic" . }}
{{ if and $topic (not .Draft) (not .Params.hideFromList) }}
{{ if or (not $.Params.category) (eq $.Params.category $topic) }}
{{ $pages = $pages | append . }}
{{ end }}
{{ end }}
{{ end }}
{{ return (sort $pages "PublishDate" "desc") }}
@@ -0,0 +1,6 @@
{{ $settings := (site.GetPage "/articles").Params }}
{{ $topic := .Params.category | default "" }}
{{ with index $settings.category_aliases $topic }}{{ $topic = . }}{{ end }}
{{ with index $settings.category_overrides .RelPermalink }}{{ $topic = . }}{{ end }}
{{ if not (in (slice "search-quality" "embedding-research" "qdrant-internals" "production-ops") $topic) }}{{ $topic = "" }}{{ end }}
{{ return $topic }}
@@ -1,4 +1,24 @@
<ul class="docs-breadcrumbs"> <ul class="docs-breadcrumbs">
{{ if eq .Params.learning_kind "guides" }}
<li class="docs-breadcrumbs__crumb"><a href="/learn/">Learn</a></li>
<li class="docs-breadcrumbs__crumb-separator"></li>
<li class="docs-breadcrumbs__crumb"><a href="/documentation/guides/">Guides</a></li>
<li class="docs-breadcrumbs__crumb-separator"></li>
{{ if eq .Parent.Params.learning_kind "guides" }}
<li class="docs-breadcrumbs__crumb"><a href="{{ .Parent.RelPermalink }}">{{ .Parent.Title }}</a></li>
<li class="docs-breadcrumbs__crumb-separator"></li>
{{ end }}
{{ else if eq .Section "articles" }}
<li class="docs-breadcrumbs__crumb"><a href="/learn/">Learn</a></li>
<li class="docs-breadcrumbs__crumb-separator"></li>
<li class="docs-breadcrumbs__crumb"><a href="/articles/">Articles</a></li>
<li class="docs-breadcrumbs__crumb-separator"></li>
{{ else if and (eq (partial "get-partition.html" (dict "page" .)) "learn") (eq .Section "documentation") }}
<li class="docs-breadcrumbs__crumb"><a href="/learn/">Learn</a></li>
<li class="docs-breadcrumbs__crumb-separator"></li>
<li class="docs-breadcrumbs__crumb"><a href="/learn/examples/">Tutorials &amp; Examples</a></li>
<li class="docs-breadcrumbs__crumb-separator"></li>
{{ else }}
{{ $rellink := "" }} {{ $rellink := "" }}
{{ $paths := (split .RelPermalink "/") }} {{ $paths := (split .RelPermalink "/") }}
{{ $len := sub (len $paths) 2 }} {{ $len := sub (len $paths) 2 }}
@@ -11,5 +31,6 @@
<li class="docs-breadcrumbs__crumb-separator"></li> <li class="docs-breadcrumbs__crumb-separator"></li>
{{ end }} {{ end }}
{{ end }} {{ end }}
{{ end }}
<li class="docs-breadcrumbs__crumb">{{ .Params.title | safeHTML }}</li> <li class="docs-breadcrumbs__crumb">{{ .Params.title | safeHTML }}</li>
</ul> </ul>
@@ -11,148 +11,10 @@
<!-- todo: update this and styles to support both types of content on the same page--> <!-- todo: update this and styles to support both types of content on the same page-->
<div class="documentation__content-wrapper"> <div class="documentation__content-wrapper">
{{ if eq .Section "articles" }} {{ if eq .Section "articles" }}
{{ $subject := site.GetPage "articles" }}
{{ $currentNode := .Params }}
{{ if .Params.isMainPage }} {{ if .Params.isMainPage }}
<div class="docs-articles docs-articles__blocks"> {{ partial "documentation/articles/hub" . }}
{{/* Latest section: the 3 most recent (non-hidden, categorized) articles */}}
{{ $latest := first 0 site.Pages }}
{{ range $subject.Pages }}
{{ if and (not .Params.isCategoryPage) (not .Params.hideFromList) .Params.category }}
{{ $latest = $latest | append . }}
{{ end }}
{{ end }}
{{ $latest = $latest.ByPublishDate.Reverse }}
{{ if gt (len $latest) 0 }}
<div class="docs-articles__block docs-articles__block_featured">
<div class="docs-articles__block-header">
<div>
<h4 class="docs-articles__title">Latest Articles</h4>
<p class="docs-articles__description">The most recent publications from the Qdrant team.</p>
</div>
</div>
<div class="docs-articles__posts">
<div class="row gy-4">
{{ range first 3 $latest }}
<article class="col-12 col-lg-4 post post-sm">
<a class="post-link" href="{{ .RelPermalink }}">
<div class="post-preview">
<img src="{{ .Params.preview_dir }}/preview.jpg" alt="Preview" />
</div>
<h6 class="post-title">{{ .Params.title }}</h6>
<p class="post-description">{{ .Params.description }}</p>
<div class="post-about">
<p>{{ .Params.author }}</p>
<p>{{ time.Format "January 02, 2006" .Date }}</p>
</div>
</a>
</article>
{{ end }}
</div>
</div>
</div>
{{ end }}
{{ range $subject.Pages }}
{{ if .Params.isCategoryPage }}
<div class="docs-articles__block">
<div class="docs-articles__block-header">
<div>
<h4 class="docs-articles__title">{{ .Params.title }}</h4>
<p class="docs-articles__description">{{ .Params.description }}</p>
</div>
<a href="{{ .Params.url }}" class="docs-articles__block-link button button_outlined button_sm">
{{ $currentNode.learnButton }}
</a>
</div>
{{ $category := .Params.category }}
<div class="docs-articles__posts">
<div class="row gy-4">
{{ $list := first 0 site.Pages }}
{{ range $subject.Pages }}
{{ if and (eq .Params.category $category) (not .Params.isCategoryPage) (not .Params.hideFromList) }}
{{ $list = $list | append . }}
{{ end }}
{{ end }}
{{ $all := $list.ByPublishDate.Reverse }}
{{ range first 3 $all }}
<article class="col-12 col-lg-4 post post-sm">
<a class="post-link" href="{{ .RelPermalink }}">
<div class="post-preview">
<img src="{{ .Params.preview_dir }}/preview.jpg" alt="Preview" />
</div>
<h6 class="post-title">{{ .Params.title }}</h6>
<p class="post-description">{{ .Params.description }}</p>
<div class="post-about">
<p>{{ .Params.author }}</p>
<p>{{ time.Format "January 02, 2006" .Date }}</p>
</div>
</a>
</article>
{{ end }}
</div>
</div>
</div>
{{ end }}
{{ end }}
</div>
{{ else if .Params.isCategoryPage }} {{ else if .Params.isCategoryPage }}
<div class="docs-articles docs-articles__list"> {{ partial "documentation/articles/category" . }}
{{ partial "documentation/breadcrumbs" . }}
<h4 class="docs-articles__title">{{ .Params.title }}</h4>
<p class="docs-articles__description">{{ .Params.description }}</p>
{{ $category := .Params.category }}
<div class="docs-articles__posts">
<div class="row gx-4 gy-5">
{{ $list := first 0 site.Pages }}
{{ range $subject.Pages }}
{{ if and (eq .Params.category $category) (not .Params.isCategoryPage) (not .Params.hideFromList) }}
{{ $list = $list | append . }}
{{ end }}
{{ end }}
{{ $all := $list.ByPublishDate.Reverse }}
{{ $paginator := .Paginate $all 14 }}
{{ range $paginator.Pages }}
<article class="col-12 col-lg-6 post post-md">
<a class="post-link" href="{{ .RelPermalink }}">
<div class="post-preview">
<img src="{{ .Params.preview_dir }}/preview.jpg" alt="Preview" />
</div>
<h6 class="post-title">{{ .Params.title }}</h6>
<p class="post-description">{{ .Params.description }}</p>
<div class="post-about">
<p>{{ .Params.author }}</p>
<p>{{ time.Format "January 02, 2006" .Date }}</p>
</div>
</a>
</article>
{{ end }}
</div>
</div>
<div class="row">
<div class="docs-articles__pagination col-12">
{{ if gt $paginator.TotalPages 1 }}
{{ partial "pagination" $paginator }}
{{ end }}
</div>
</div>
</div>
{{ else }} {{ else }}
<div class="documentation__article-wrapper"> <div class="documentation__article-wrapper">
{{ partial "documentation/breadcrumbs" . }} {{ partial "documentation/breadcrumbs" . }}
@@ -160,8 +22,8 @@
<article class="documentation-article"> <article class="documentation-article">
<div class="documentation-article__header"> <div class="documentation-article__header">
{{ $firstUrlElement := index (split .RelPermalink "/") 1 }} {{ $url := "articles" }}
{{ $url := path.Join $firstUrlElement .Params.category }} {{ with partial "documentation/articles/topic" . }}{{ $url = printf "articles/%s" . }}{{ end }}
<a href="/{{ $url }}/" class="documentation-article__header-link"> <a href="/{{ $url }}/" class="documentation-article__header-link">
@@ -0,0 +1,60 @@
{{ $goals := slice }}
{{ $stacks := slice }}
{{ range site.Data.examples }}
{{ $goals = $goals | append .goal }}
{{ $stacks = $stacks | append .stack }}
{{ end }}
<section class="example-library" id="example-library" aria-labelledby="examples-title">
<h2 id="examples-title">Browse Tutorials &amp; Examples</h2>
<p>Choose the result you want, then filter the collection by your stack.</p>
<form class="example-library__filters" role="search" aria-label="Find a Tutorial or Example" hidden>
<div>
<label for="example-query">Search This Collection</label>
<input id="example-query" name="q" type="search" placeholder="Try recall, PDF, or filters" autocomplete="off">
</div>
<div>
<label for="example-goal">Goal</label>
<select id="example-goal" name="goal">
<option value="">All Goals</option>
{{ range sort (uniq $goals) }}<option value="{{ . }}">{{ . }}</option>{{ end }}
</select>
</div>
<div>
<label for="example-stack">Stack</label>
<select id="example-stack" name="stack">
<option value="">All Stacks</option>
{{ range sort (uniq $stacks) }}<option value="{{ . }}">{{ . }}</option>{{ end }}
</select>
</div>
<button type="reset" class="button button_outlined button_sm">Clear Filters</button>
</form>
<p data-example-count role="status" aria-live="polite" aria-atomic="true">{{ len site.Data.examples }} results</p>
<p data-example-empty hidden>No tutorials or examples match these filters. Try a broader term or clear the filters.</p>
<div class="row g-4">
{{ range site.Data.examples }}
{{ $entry := . }}
{{ $page := site.GetPage .page }}
{{ if not $page }}{{ errorf "Unknown example page %s" .page }}{{ end }}
{{ $title := .title | default $page.Title }}
{{ $description := .description | default $page.Params.short_description | default $page.Description }}
{{ if not $description }}{{ errorf "Missing example description: %s" .page }}{{ end }}
<div class="col-12 col-lg-6" data-example data-goal="{{ .goal }}" data-stack="{{ delimit .stack "|" }}" data-search="{{ printf "%s %s %s %s %s" $title $description .goal (delimit .stack " ") (.keywords | default "") | lower }}">
<article class="docs-card example-library__card">
<span class="docs-card__tag">{{ .goal }}</span>
<h3 class="docs-card__title"><a href="{{ $page.RelPermalink }}">{{ $title }}</a></h3>
<p class="docs-card__description">{{ $description }}</p>
<p class="example-library__stack">{{ delimit .stack " · " }}</p>
<div class="example-library__actions">
<a href="{{ $page.RelPermalink }}" class="link link_sm">Open Example<span class="visually-hidden">: {{ $title }}</span></a>
{{ range .resources }}
{{ if not (strings.Contains $page.RawContent .url) }}{{ errorf "Example resource is no longer linked from %s: %s" $entry.page .url }}{{ end }}
<a href="{{ .url }}" class="link link_sm">{{ .label }}<span class="visually-hidden">: {{ $title }}</span></a>
{{ end }}
</div>
</article>
</div>
{{ end }}
</div>
</section>
{{ $script := resources.Get "js/example-library.js" | js.Build | minify | fingerprint }}
<script src="{{ $script.RelPermalink }}" defer></script>
@@ -0,0 +1,44 @@
{{ $section := site.GetPage .section }}
{{ $standalone := where $section.RegularPages.ByWeight "Params.guide_series" "ne" true }}
{{ $series := where $section.RegularPages.ByWeight "Params.guide_series" true }}
{{ $groups := slice (dict "title" "Find Your Guide" "description" "Find practical guidance for the search problem or design decision you are working on." "pages" $standalone "series" false) }}
{{ if $series }}
{{ $groups = $groups | append (dict "title" $section.Params.guide_series_title "description" "Follow the complete series in order, or choose the part that matches your next decision." "pages" $series "series" true) }}
{{ end }}
{{ range $groups }}
{{ $isSeries := .series }}
{{ $cards := slice }}
{{ range $index, $page := .pages }}
{{ $tag := .Params.guide_kind }}
{{ if $isSeries }}{{ $tag = printf "Part %d" (add $index 1) }}{{ end }}
{{ $cards = $cards | append (dict "title" .Title "description" .Params.short_description "tag" $tag "icon" (dict "src" $section.Params.guide_icon "alt" "") "link" (dict "text" "Open Guide" "url" .RelPermalink)) }}
{{ end }}
{{ if $cards }}
{{ partial "documentation/sections/cards-section" (dict "title" .title "description" .description "cardsPartial" "documentation/cards/docs-cards" "cardsPerRow" (cond (eq (len $cards) 2) 2 3) "cards" $cards) }}
{{ end }}
{{ end }}
{{ $examples := slice }}
{{ range $section.Params.worked_examples }}
{{ $url := . }}
{{ with site.GetPage . }}
{{ $description := .Params.short_description }}
{{ range where site.Data.examples "page" $url }}
{{ $description = .description | default $description }}
{{ end }}
{{ $examples = $examples | append (dict "title" .Title "description" $description "link" (dict "text" "Open Example" "url" .RelPermalink)) }}
{{ else }}{{ errorf "Unknown worked example %s" . }}{{ end }}
{{ end }}
{{ if $examples }}
{{ partial "documentation/sections/cards-section" (dict "title" "Apply These Techniques" "description" "Use a worked example to put the guidance into practice." "cardsPartial" "documentation/cards/docs-cards" "cards" $examples "button" (dict "text" "Browse Tutorials & Examples" "url" "/learn/examples/")) }}
{{ end }}
{{ $references := slice }}
{{ range $section.Params.related }}
{{ with site.GetPage . }}
{{ $references = $references | append (dict "title" .Title "description" .Params.short_description "link" (dict "text" "Explore Documentation" "url" .RelPermalink)) }}
{{ else }}
{{ errorf "Unknown related page %s in %s" . $section.Path }}
{{ end }}
{{ end }}
{{ if $references }}
{{ partial "documentation/sections/cards-section" (dict "title" "Read More" "description" "Use the full reference when you need configuration details or an operating procedure." "cardsPartial" "documentation/cards/docs-cards" "cards" $references) }}
{{ end }}
@@ -0,0 +1,19 @@
{{ if and (eq .Params.learning_kind "guides") .Params.guide_series }}
{{ $current := . }}
{{ $pages := where .Parent.RegularPages.ByWeight "Params.guide_series" true }}
{{ range $index, $page := $pages }}
{{ if eq $page $current }}
<nav class="guide-series-navigation" aria-label="Series Navigation">
<p><a href="{{ $current.Parent.RelPermalink }}">{{ $current.Parent.Params.guide_series_title }}</a> · Part {{ add $index 1 }} of {{ len $pages }}</p>
<div class="guide-series-navigation__links">
{{ if gt $index 0 }}
{{ with index $pages (sub $index 1) }}<a rel="prev" href="{{ .RelPermalink }}"><span>Previous</span>{{ .Title }}</a>{{ end }}
{{ end }}
{{ if lt (add $index 1) (len $pages) }}
{{ with index $pages (add $index 1) }}<a rel="next" href="{{ .RelPermalink }}"><span>Next</span>{{ .Title }}</a>{{ end }}
{{ end }}
</div>
</nav>
{{ end }}
{{ end }}
{{ end }}
@@ -0,0 +1,7 @@
{{ $cards := slice }}
{{ range (site.GetPage "/documentation").Sections.ByWeight }}
{{ if eq .Params.learning_kind "guides" }}
{{ $cards = $cards | append (dict "title" .Title "description" .Params.short_description "icon" (dict "src" .Params.guide_icon "alt" "") "link" (dict "text" "Explore Guides" "url" .RelPermalink)) }}
{{ end }}
{{ end }}
{{ partial "documentation/sections/cards-section" (dict "title" "Grow Your Search Engineering Skills" "description" "Learn the tradeoffs, apply a pattern, and test it against the needs of your application." "cardsPartial" "documentation/cards/docs-cards" "cardsPerRow" 2 "cards" $cards) }}
@@ -0,0 +1,80 @@
{{ $current := .RelPermalink }}
{{ $isGuide := eq .Params.learning_kind "guides" }}
{{ $goals := slice }}
{{ range site.Data.examples }}{{ $goals = $goals | append .goal }}{{ end }}
{{ $exampleLinks := slice }}
{{ range sort (uniq $goals) }}
{{ $exampleLinks = $exampleLinks | append (dict "title" . "url" (printf "/learn/examples/?%s" (querify "goal" .)) "goal" . "active" false) }}
{{ end }}
{{ $guideLinks := slice }}
{{ range (site.GetPage "/documentation").Sections.ByWeight }}
{{ if eq .Params.learning_kind "guides" }}
{{ $pages := slice }}
{{ range .RegularPages.ByWeight }}
{{ if not .Params.hideInSidebar }}
{{ $pages = $pages | append (dict "title" .Title "url" .RelPermalink "series" .Params.guide_series) }}
{{ end }}
{{ end }}
{{ $active := or (eq .RelPermalink $current) (and $isGuide (eq $.Parent .)) }}
{{ $guideLinks = $guideLinks | append (dict "title" .Title "url" .RelPermalink "active" $active "children" $pages "seriesTitle" .Params.guide_series_title) }}
{{ end }}
{{ end }}
{{ $articleLinks := slice }}
{{ range (site.GetPage "/articles").Sections.ByWeight }}
{{ if and .Params.isCategoryPage (not .Params.hideFromList) }}
{{ $active := or (eq .RelPermalink $current) (and (eq $.Section "articles") (eq .Params.category (partial "documentation/articles/topic" $))) }}
{{ $articleLinks = $articleLinks | append (dict "title" .Title "url" .RelPermalink "active" $active) }}
{{ end }}
{{ end }}
{{ $items := slice
(dict "title" "Guides" "url" "/documentation/guides/" "active" $isGuide "children" $guideLinks)
(dict "title" "Tutorials & Examples" "url" "/learn/examples/" "active" (eq $current "/learn/examples/") "children" $exampleLinks)
(dict "title" "Courses" "url" "/course/" "active" (eq .Section "course") "children" (slice
(dict "title" "Qdrant Essentials" "url" "/course/essentials/")
(dict "title" "Multivector Search" "url" "/course/multi-vector-search/")))
(dict "title" "Articles" "url" "/articles/" "active" (eq .Section "articles") "children" $articleLinks)
}}
<nav class="docs-menu__links" aria-label="Learning Resources">
<h3 class="docs-menu__links-title">Learn</h3>
<div class="docs-menu__links-group{{ if eq $current "/learn/" }} active{{ end }}">
<div class="docs-menu__links-group-heading"><a href="/learn/" {{ if eq $current "/learn/" }}aria-current="page"{{ end }}>Overview</a></div>
</div>
{{ range $items }}
<details class="docs-menu__links-group link-group{{ if .active }} active{{ end }}" {{ if .active }}open{{ end }}>
<summary class="docs-menu__links-group-heading">
<a href="{{ .url }}" {{ if eq .url $current }}aria-current="page"{{ end }}><span>{{ .title }}</span></a>
</summary>
<nav aria-label="{{ .title }}">
<ul class="docs-menu__links-submenu">
<li class="docs-menu__links-submenu-item{{ if eq .url $current }} active{{ end }}"><a href="{{ .url }}" {{ if eq .url $current }}aria-current="page"{{ end }}>Explore {{ .title }}</a></li>
{{ range .children }}
{{ if .children }}
<li class="docs-menu__links-submenu-item docs-menu__links-submenu-item--section">
<details class="guide-topic{{ if .active }} active{{ end }}" {{ if .active }}open{{ end }}>
<summary{{ if eq .url $current }} class="active"{{ end }}><a href="{{ .url }}"><span>{{ .title }}</span></a></summary>
<ul class="docs-menu__links-sub-submenu">
{{ $seriesTitle := .seriesTitle }}
{{ $seriesStarted := false }}
{{ range .children }}
{{ if and .series (not $seriesStarted) }}
<li class="guide-series-label">{{ $seriesTitle }}</li>
{{ $seriesStarted = true }}
{{ end }}
<li class="docs-menu__links-sub-submenu-item{{ if eq .url $current }} active{{ end }}">
<a href="{{ .url }}" data-guide-link {{ if eq .url $current }}aria-current="page"{{ end }}>{{ .title }}</a>
</li>
{{ end }}
</ul>
</details>
</li>
{{ else }}
<li class="docs-menu__links-submenu-item{{ if .active }} active{{ end }}" {{ with .goal }}data-example-nav-goal="{{ . }}"{{ end }}>
<a href="{{ .url }}" {{ if eq .url $current }}aria-current="page"{{ end }}>{{ .title }}</a>
</li>
{{ end }}
{{ end }}
</ul>
</nav>
</details>
{{ end }}
</nav>
@@ -3,24 +3,8 @@
{{ $currentNode := .context }} {{ $currentNode := .context }}
<div class="docs-menu"> <div class="docs-menu">
<div id="sidebar" class="docs-menu__content"> <div id="sidebar" class="docs-menu__content">
{{ if eq $currentNode.Section "articles" }} {{ if eq $partition "learn" }}
<h3 class="docs-menu__links-title">Articles</h3> {{ partial "documentation/learn-menu" $currentNode }}
<nav>
<div class="docs-menu__links-group">
{{ $subject := site.GetPage "articles" }}
{{ range $subject.Pages }}
{{ if .Params.isCategoryPage }}
<div
class="docs-menu__articles-link docs-menu__links-group-heading
{{ if eq .File.UniqueID $currentNode.File.UniqueID }}active{{ end }}"
>
<a href="{{ .Params.url }}">{{ .Params.title }}</a>
</div>
{{ end }}
{{ end }}
</div>
</nav>
{{ else }} {{ else }}
{{ $pathSegments := split (trim $currentNode.RelPermalink "/") "/" }} {{ $pathSegments := split (trim $currentNode.RelPermalink "/") "/" }}
@@ -0,0 +1,15 @@
<div class="row g-3 guide-read-more">
{{ range split (trim .Inner "\n ") "\n" }}
{{ $line := trim . " " }}
{{ if $line }}
{{ $pattern := `^- \[([^\]]+)\]\(([^)]+)\)\s*(.*)$` }}
{{ if not (findRE $pattern $line) }}{{ errorf "Invalid Read More card: %s" $line }}{{ end }}
{{ $title := replaceRE $pattern "${1}" $line }}
{{ $url := replaceRE $pattern "${2}" $line }}
{{ $description := replaceRE $pattern "${3}" $line | replaceRE `^:\s*` "" }}
{{ if $description }}{{ $description = printf "%s%s" (upper (substr $description 0 1)) (substr $description 1) }}{{ end }}
{{ $description = $.Page.RenderString $description }}
{{ partial "documentation/cards/docs-cards" (dict "cardsPerRow" 2 "card" (dict "title" $title "description" $description "link" (dict "text" "Read More" "url" $url))) }}
{{ end }}
{{ end }}
</div>