Merge pull request #1960 from qdrant/essentials-course-cleanup

Essentials course cleanup
This commit is contained in:
kanungle
2025-10-23 09:13:19 -07:00
committed by GitHub
14 changed files with 195 additions and 149 deletions
@@ -58,7 +58,7 @@ Build the vector search skills that matter: hybrid retrieval, multivector rerank
- Qdrant data modeling: points, payloads, and schemas
- Embeddings, chunking, and similarity metrics
- Indexing and retrieval tuning ([HNSW](https://qdrant.tech/articles/filtrable-hnsw/), filters, recall/latency)
- Indexing and retrieval tuning (HNSW, filters, recall/latency)
- Hybrid search with sparse + dense vectors and re-ranking
- Performance optimization, compression, and quantization
- Scaling, sharding/replication, and security
@@ -67,9 +67,9 @@ Build the vector search skills that matter: hybrid retrieval, multivector rerank
### The Path
**Days 0–2**: Foundations. Connect to Qdrant Cloud, work with points and payloads, compute semantic similarity, chunk text, and tune HNSW for speed and recall.
**Days 0-2**: Foundations. Connect to Qdrant Cloud, work with points and payloads, compute semantic similarity, chunk text, and tune HNSW for speed and recall.
**Days 3–5**: Advanced retrieval. Combine dense and sparse signals, do hybrid search with server-side fusion, use multivectors (ColBERT) with the Universal Query API, and build recommendations.
**Days 3-5**: Advanced retrieval. Combine dense and sparse signals, do hybrid search with server-side fusion, use multivectors (ColBERT) with the Universal Query API, and build recommendations.
**Day 6**: Ship. Wire ingestion, hybrid retrieval, multivector re-ranking, and evaluation (Recall@10, MRR, latency P50/P95).
@@ -185,11 +185,11 @@ ML, backend, data, and search engineers building RAG, semantic search, or recomm
## Time commitment
- Duration: 7 days at 1–2 hours/day + optional bonus day
- Duration: 6 days at 1-2 hours/day + 1 optional bonus day
- Video learning: ~3 hours
- Hands-on learning: 4-5 hours
- Final project: 2–4 hours
- Total: 9–12 hours
- Final project: 2-4 hours
- Total: 9-12 hours
{{< course-card
@@ -5,4 +5,4 @@ weight: 100
# Qdrant Essentials Certification
Coming soon!
Coming soon! [Click here](https://forms.gle/QPSfdMjs3QpUCtGT9) to be notified when certifications become available.
@@ -40,7 +40,7 @@ Before creating data, decide what each of the four dimensions in your vectors wi
**Example Ideas:**
- **Product categories**: Create vectors where each dimension represents a feature (affordability, quality, popularity, innovation). Electronics might be `[0.8, 0.7, 0.9, 0.6]`, while books could be `[0.3, 0.9, 0.4, 0.8]`.
- **Color palettes**: Each dimension represents color intensity (red, green, blue, brightness). Bright red: `[0.9, 0.1, 0.1, 0.8]`, forest green: `[0.1, 0.8, 0.2, 0.5]`.
- **Color palettes**: Each dimension represents color (red, green, blue). Bright red: `[0.9, 0.1, 0.1]`, forest green: `[0.1, 0.8, 0.2]`.
- **Data types**: Dimensions for structure, size, complexity, frequency. Spreadsheets: `[0.9, 0.6, 0.3, 0.7]`, images: `[0.2, 0.8, 0.5, 0.4]`.
- **Movie genres**: Action, drama, comedy, sci-fi intensities. Action thriller: `[0.9, 0.3, 0.1, 0.7]`, romantic comedy: `[0.1, 0.6, 0.9, 0.2]`.
@@ -148,7 +148,7 @@ For a new concept (not the Product Categories concept) run the code above and do
### Step 2: Post Your Results
Show what you built and compare notes with others. **Post your results in** <a href="https://discord.com/invite/qdrant" target="_blank" rel="noopener noreferrer" aria-label="Qdrant Discord">
Show what you built and compare notes with others. **Post your results in** <a href="https://discord.com/channels/907569970500743200/1429673887590776832" target="_blank" rel="noopener noreferrer" aria-label="Qdrant Discord">
<img src="https://img.shields.io/badge/Qdrant%20Discord-5865F2?style=flat&logo=discord&logoColor=white&labelColor=5865F2&color=5865F2"
alt="Post your results in Discord"
style="display:inline; margin:0; vertical-align:middle; border-radius:9999px;" />
@@ -291,7 +291,7 @@ Now it's time to analyze your results and share what you've learned. Follow thes
### Step 2: Post Your Results
**Post your results in** <a href="https://discord.com/invite/qdrant" target="_blank" rel="noopener noreferrer" aria-label="Qdrant Discord">
**Post your results in** <a href="https://discord.com/channels/907569970500743200/1429673887590776832" target="_blank" rel="noopener noreferrer" aria-label="Qdrant Discord">
<img src="https://img.shields.io/badge/Qdrant%20Discord-5865F2?style=flat&logo=discord&logoColor=white&labelColor=5865F2&color=5865F2"
alt="Post your results in Discord"
style="display:inline; margin:0; vertical-align:middle; border-radius:9999px;" />
@@ -117,10 +117,10 @@ print(f"Dataset size: {len(ds['train'])} articles")
# Explore the dataset structure
print("\nDataset structure:")
print("Available columns:", ds['train'].column_names)
print("Available columns:", ds["train"].column_names)
# Look at a sample entry
sample = ds['train'][0]
sample = ds["train"][0]
print(f"\nSample article:")
print(f"Title: {sample['title']}")
print(f"Text preview: {sample['text'][:200]}...")
@@ -132,7 +132,7 @@ print(f"Embedding dimensions: {len(sample['text-embedding-3-large-1536-embedding
- **Content**: Pre-computed Wikipedia article embeddings
- **Size**: 100,000 articles
- **Embeddings**: with OpenAI's `text-embedding-3-large` truncated to 1536 dims
- **Metadata**: `_id`, `titles` and `text`
- **Metadata**: `_id`, `title` and `text`
## Step 4: Strategic Collection Creation
@@ -156,18 +156,20 @@ print(f"Creating collection: {collection_name}")
client.create_collection(
collection_name=collection_name,
vectors_config=models.VectorParams(
size=1536, # Matches dataset dims
distance=models.Distance.COSINE # Good for normalized embeddings
size=1536, # Matches dataset dims
distance=models.Distance.COSINE, # Good for normalized embeddings
),
hnsw_config=models.HnswConfigDiff(
m=0, # Skip links during upload for speed
ef_construct=100, # Used after we set m>0
full_scan_threshold=10000
m=0, # Bulk load fast: m=0 (build links after ingest).
ef_construct=100, # Build quality: used after we set m>0
full_scan_threshold=10, # force HNSW instead of full scan
),
optimizers_config=models.OptimizersConfigDiff(
indexing_threshold=10
), # Force indexing even on small sets for demo
strict_mode_config=models.StrictModeConfig(
enabled=False, # More flexible while testing
unindexed_filtering_retrieve=True # Allow filters without payload indexes
)
enabled=False,
), # More flexible while testing
)
print(f"Collection '{collection_name}' created successfully!")
@@ -182,11 +184,11 @@ print(f"HNSW m: {collection_info.config.hnsw_config.m}")
**Configuration details:**
- **`size=1536`**: To match the dimensions parameter we set for the OpenAI `text-embedding-3-large`
- **`distance=COSINE`**: Standart for normalized embeddings and semantic similarity
- **`distance=COSINE`**: Standard for normalized embeddings and semantic similarity
- **`full_scan_threshold=10000`**: Uses exact search for smaller result sets
- **`strict_mode_config`**: Managed Cloud runs in strict mode by default. We set `enabled=False` to let you experiment with unindexed payload keys during the demo.
**Side note:** `text-embedding-3-large` outputs 3072 dims. Trunkating that to only 1536 dimensions cuts compute and memory, with some accuracy loss.
**Side note:** `text-embedding-3-large` outputs 3072 dims. Truncating that to only 1536 dimensions cuts compute and memory, with some accuracy loss.
## Step 5: Bulk Upload with Rich Payloads
@@ -218,7 +220,7 @@ def upload_batch(start_idx, end_idx):
return 0
batch_size = 10000
batch_size = 64 * 10
total_points = len(ds["train"])
print(f"Uploading {total_points} points in batches of {batch_size}")
@@ -239,8 +241,8 @@ Now switch from `m=0` to `m=16` to build HNSW connections and improve search tim
client.update_collection(
collection_name=collection_name,
hnsw_config=models.HnswConfigDiff(
m=16 # Each node connects to 16 neighbors
)
m=16 # Build HNSW now: m=16 after the bulk load.
),
)
print("HNSW indexing enabled with m=16")
@@ -312,17 +314,14 @@ Let's measure search performance on the HNSW‑enabled collection.
print("Running baseline performance test...")
# Warm up the RAM index/vectors cache with a test query
print("Warming up caches...")
client.query_points(collection_name=collection_name, query=query_embedding, limit=1)
# Measure vector search performance
search_times = []
for _ in range(3): # Multiple runs for a stable average
for _ in range(25): # Multiple runs for a stable average
start_time = time.time()
response = client.query_points(
collection_name=collection_name,
query=query_embedding,
limit=10
collection_name=collection_name, query=query_embedding, limit=10
)
search_time = (time.time() - start_time) * 1000
search_times.append(search_time)
@@ -332,14 +331,16 @@ baseline_time = sum(search_times) / len(search_times)
print(f"Average search time: {baseline_time:.2f}ms")
print(f"Search times: {[f'{t:.2f}ms' for t in search_times]}")
print(f"Found {len(response.points)} results")
print(f"Top result: '{response.points[0].payload['title']}' (score: {response.points[0].score:.4f})")
print(
f"Top result: '{response.points[0].payload['title']}' (score: {response.points[0].score:.4f})"
)
# Show a few more results for context
print(f"\nTop 3 results:")
for i, point in enumerate(response.points[:3], 1):
title = point.payload['title']
title = point.payload["title"]
score = point.score
text_preview = point.payload['text'][:100] + "..."
text_preview = point.payload["text"][:100] + "..."
print(f" {i}. {title} (score: {score:.4f})")
print(f" {text_preview}")
```
@@ -347,7 +348,7 @@ for i, point in enumerate(response.points[:3], 1):
**Performance factors:**
- **Cache warming**: First query loads relevant index parts/vectors into memory, subsequent queries are faster
- **HNSW with m=16**: Graph-based search is much faster than full scan
- **MRepeated runs**: Average of several queries gives more reliable timing results
- **Repeated runs**: Average of several queries gives more reliable timing results
## Step 9: Filtering Without Payload Indexes
@@ -356,26 +357,31 @@ Now, let's test filtering performance without indexes. This forces Qdrant to sca
```python
print("Testing filtering without payload indexes")
# Warning: We enable unindexed_filtering_retrieve only for demonstration purposes. In production, don’t use it.
# Demo only: allow filtering without an index by scanning. Turn this off later.
client.update_collection(
collection_name=collection_name,
strict_mode_config=models.StrictModeConfig(unindexed_filtering_retrieve=True),
)
# Create a text-based filter
text_filter = models.Filter(
must=[
models.FieldCondition(
key="text",
match=models.MatchText(text="data")
)
]
must=[models.FieldCondition(key="text", match=models.MatchText(text="data"))]
)
# Warmup
client.query_points(collection_name=collection_name, query=query_embedding, limit=1)
# Run multiple times for more reliable measurement
unindexed_times = []
for i in range(3):
for i in range(25):
start_time = time.time()
response = client.query_points(
collection_name=collection_name,
query=query_embedding,
limit=10,
search_params=models.SearchParams(hnsw_ef=100),
query_filter=text_filter
query_filter=text_filter,
)
unindexed_times.append((time.time() - start_time) * 1000)
@@ -386,7 +392,9 @@ print(f"Individual times: {[f'{t:.2f}ms' for t in unindexed_times]}")
print(f"Overhead vs baseline: {unindexed_filter_time - baseline_time:.2f}ms")
print(f"Found {len(response.points)} matching results")
if response.points:
print(f"Top result: '{response.points[0].payload['text']}'\nScore: {response.points[0].score:.4f}")
print(
f"Top result: '{response.points[0].payload['text']}'\nScore: {response.points[0].score:.4f}"
)
else:
print("No results found - try a different filter term")
```
@@ -396,25 +404,25 @@ else:
Create a [full‑text index](/documentation/concepts/indexing/#full-text-index) for faster filtering.
```python
# Create a payload index for 'text' so filters use an index, not a scan.
client.create_payload_index(
collection_name=collection_name,
field_name="text",
wait=True,
field_schema=models.TextIndexParams(
type="text",
tokenizer="word",
phrase_matching=False
)
)
type="text", tokenizer="word", phrase_matching=False
),
)
client.update_collection(
collection_name=collection_name,
hnsw_config=models.HnswConfigDiff(
ef_construct=101
), # Added payload index after HNSW; bump ef_construct (+1) to rebuild with filter data.
strict_mode_config=models.StrictModeConfig(unindexed_filtering_retrieve=False),
)
print("Payload index created for 'text' field")
# If you want filter‑aware HNSW and you built the graph before creating payload indexes,
# rebuild the graph to attach filter data structures.
# Note: Reindexing takes up a lot of resources, and it is advised to set payload
# indexes only once, before building HNSW.
# client.update_collection(collection_name=collection_name, hnsw_config=models.HnswConfigDiff(m=0))
# client.update_collection(collection_name=collection_name, hnsw_config=models.HnswConfigDiff(m=16))
```
## Step 11: Filtering With Payload Indexes
@@ -424,16 +432,20 @@ Run the same query with the index in place.
```python
print("Testing filtering WITH payload indexes...")
# Warmup
client.query_points(collection_name=collection_name, query=query_embedding, limit=1)
# Run multiple times for more reliable measurement
indexed_times = []
for i in range(3):
for i in range(25):
start_time = time.time()
response = client.query_points(
collection_name=collection_name,
query=query_embedding,
limit=10,
search_params=models.SearchParams(hnsw_ef=100),
query_filter=text_filter
query_filter=text_filter,
)
indexed_times.append((time.time() - start_time) * 1000)
@@ -444,7 +456,9 @@ print(f"Individual times: {[f'{t:.2f}ms' for t in indexed_times]}")
print(f"Overhead vs baseline: {indexed_filter_time - baseline_time:.2f}ms")
print(f"Found {len(response.points)} matching results")
if response.points:
print(f"Top result: '{response.points[0].payload['text']}'\nScore: {response.points[0].score:.4f}")
print(
f"Top result: '{response.points[0].payload['text']}'\nScore: {response.points[0].score:.4f}"
)
else:
print("No results found - try a different filter term")
```
@@ -454,9 +468,9 @@ else:
Compare your results and see the effect of each optimization:
```python
print("\n" + "="*60)
print("\n" + "=" * 60)
print("FINAL PERFORMANCE SUMMARY")
print("="*60)
print("=" * 60)
# Key metrics
if unindexed_filter_time > 0 and indexed_filter_time > 0:
@@ -481,7 +495,7 @@ print(f"Key insights:")
print(f" • HNSW (m=16) enables fast vector search")
print(f" • Payload indexes dramatically improve filtering")
print(f" • Upload strategy (m=0→m=16) optimizes ingestion")
print("="*60)
print("=" * 60)
```
@@ -72,10 +72,10 @@ Test different HNSW configurations to find what works best:
```python
# Test configurations
configs = [
{"name": "fast_initial_upload", "m": 0, "ef_construct": 100},
{"name": "memory_optimized", "m": 8, "ef_construct": 100},
{"name": "balanced", "m": 16, "ef_construct": 200},
{"name": "high_quality", "m": 32, "ef_construct": 400},
{"name": "fast_initial_upload", "m": 0, "ef_construct": 100}, # m=0 = ingest-only
{"name": "memory_optimized", "m": 8, "ef_construct": 100}, # m=8 = lower RAM
{"name": "balanced", "m": 16, "ef_construct": 200}, # m=16 = balanced
{"name": "high_quality", "m": 32, "ef_construct": 400}, # m=32 = higher recall, slower build
]
for config in configs:
@@ -87,12 +87,13 @@ for config in configs:
collection_name=collection_name,
vectors_config=models.VectorParams(size=384, distance=models.Distance.COSINE),
hnsw_config=models.HnswConfigDiff(
m=config["m"], ef_construct=config["ef_construct"], full_scan_threshold=10
),
optimizers_config=models.OptimizersConfigDiff(indexing_threshold=0),
strict_mode_config=models.StrictModeConfig(
unindexed_filtering_retrieve=True, unindexed_filtering_update=True
m=config["m"],
ef_construct=config["ef_construct"],
full_scan_threshold=10, # force HNSW instead of full scan
),
optimizers_config=models.OptimizersConfigDiff(
indexing_threshold=10
), # Force indexing even on small sets for demo
)
print(f"Created collection: {collection_name}")
```
@@ -103,7 +104,8 @@ Measure upload performance for each configuration:
```python
def upload_with_timing(collection_name, data, config_name):
embeddings = [encoder.encode(dat["description"]).tolist() for dat in data]
embeddings = encoder.encode([d["description"] for d in data], show_progress_bar=True).tolist()
points = []
for i, item in enumerate(data):
embedding = embeddings[i]
@@ -117,13 +119,15 @@ def upload_with_timing(collection_name, data, config_name):
"length": len(item["description"]),
"word_count": len(item["description"].split()),
"has_keywords": any(
keyword in item["description"].lower()
for keyword in ["important", "key", "main"]
keyword in item["description"].lower() for keyword in ["important", "key", "main"]
),
},
)
)
# Warmup
client.query_points(collection_name=collection_name, query=points[0].vector, limit=1)
start_time = time.time()
client.upload_points(collection_name=collection_name, points=points)
upload_time = time.time() - start_time
@@ -132,35 +136,43 @@ def upload_with_timing(collection_name, data, config_name):
return upload_time
# Load your dataset here
# Load your dataset here. The larger the dataset, the more accurate the benchmark will be.
# your_dataset = [{"description": "This is a description of a product"}, ...]
# Upload to each collection
upload_times = {}
for config in configs:
collection_name = f"my_domain_{config['name']}"
upload_times[config["name"]] = upload_with_timing(
collection_name, your_dataset, config["name"]
)
upload_times[config["name"]] = upload_with_timing(collection_name, your_dataset, config["name"])
# Wait for index to be built
def wait_for_index_built(collection_name, vectors_per_point=1):
info = client.get_collection(collection_name=collection_name)
count = 0
while info.points_count * vectors_per_point - info.indexed_vectors_count != 0 and count < 10:
time.sleep(1)
def wait_for_indexing(collection_name, timeout=60, poll_interval=1):
print(f"Waiting for collection '{collection_name}' to be indexed...")
start_time = time.time()
while time.time() - start_time < timeout:
info = client.get_collection(collection_name=collection_name)
count += 1
if count == 10:
raise Exception(
f"Indexed vectors count ({info.indexed_vectors_count}) is not equal to points count ({info.points_count}). Upload enough points to trigger index rebuild."
)
if info.indexed_vectors_count > 0 and info.status == models.CollectionStatus.GREEN:
print(f"Success! Collection '{collection_name}' is indexed and ready.")
print(f" - Status: {info.status.value}")
print(f" - Indexed vectors: {info.indexed_vectors_count}")
return
print(f" - Status: {info.status.value}, Indexed vectors: {info.indexed_vectors_count}. Waiting...")
time.sleep(poll_interval)
info = client.get_collection(collection_name=collection_name)
raise Exception(
f"Timeout reached after {timeout} seconds. Collection '{collection_name}' is not ready. "
f"Final status: {info.status.value}, Indexed vectors: {info.indexed_vectors_count}"
)
for config in configs:
collection_name = f"my_domain_{config['name']}"
wait_for_index_built(collection_name)
if config["m"] > 0: # m=0 has no HNSW to wait for
collection_name = f"my_domain_{config['name']}"
wait_for_indexing(collection_name)
```
### Step 4: Benchmark Search Performance
@@ -170,19 +182,15 @@ Test search speed with different `hnsw_ef` values:
```python
def benchmark_search(collection_name, query_embedding, ef_values=[64, 128, 256]):
# Warmup
_ = client.query_points(
collection_name=collection_name,
query=query_embedding,
limit=10,
search_params=models.SearchParams(hnsw_ef=ef_values[0]),
)
client.query_points(collection_name=collection_name, query=query_embedding, limit=1)
# hnsw_ef: higher = better recall, but slower. Tune per your latency goal.
results = {}
for hnsw_ef in ef_values:
times = []
# Run multiple queries for more reliable timing
for _ in range(5):
for _ in range(25):
start_time = time.time()
_ = client.query_points(
@@ -190,6 +198,7 @@ def benchmark_search(collection_name, query_embedding, ef_values=[64, 128, 256])
query=query_embedding,
limit=10,
search_params=models.SearchParams(hnsw_ef=hnsw_ef),
with_payload=False,
)
times.append((time.time() - start_time) * 1000)
@@ -225,57 +234,73 @@ def test_filtering_performance(collection_name):
# Test filter without index
filter_condition = models.Filter(
must=[models.FieldCondition(key="length", range=models.Range(gte=100, lte=500))]
must=[models.FieldCondition(key="length", range=models.Range(gte=10, lte=200))]
)
# Timing without payload index
start_time = time.time()
_ = client.query_points(
# Demo only: unindexed_filtering_retrieve=True forces a scan; turn it off right after measuring.
client.update_collection(
collection_name=collection_name,
query=query_embedding,
query_filter=filter_condition,
limit=10,
strict_mode_config=models.StrictModeConfig(unindexed_filtering_retrieve=True),
)
time_without_index = (time.time() - start_time) * 1000
# Warmup
client.query_points(collection_name=collection_name, query=query_embedding, limit=1)
# Timing without payload index
times = []
for _ in range(25):
start_time = time.time()
_ = client.query_points(
collection_name=collection_name,
query=query_embedding,
query_filter=filter_condition,
limit=10,
with_payload=False,
)
times.append((time.time() - start_time) * 1000)
time_without_index = np.mean(times)
# Create payload index
client.create_payload_index(
collection_name=collection_name, field_name="length", field_schema="integer"
collection_name=collection_name,
field_name="length",
field_schema=models.PayloadSchemaType.INTEGER,
wait=True,
)
# Rebuild HNSW to attach filter data structures.
# Note: This is not advised for production. Better create payload index before uploading any data to avoid rebuild.
suffix = collection_name.replace("my_domain_", "")
config = next((c for c in configs if c["name"] == suffix), None)
client.update_collection(
collection_name=collection_name, hnsw_config=models.HnswConfigDiff(m=0)
)
# HNSW was already built; adding the payload index doesn’t rebuild it.
# Bump ef_construct (+1) once to trigger a safe rebuild.
base_ef = client.get_collection(
collection_name=collection_name
).config.hnsw_config.ef_construct
new_ef_construct = base_ef + 1
client.update_collection(
collection_name=collection_name,
hnsw_config=models.HnswConfigDiff(
m=16,
ef_construct=config["ef_construct"],
full_scan_threshold=10,
payload_m=None,
max_indexing_threads=1,
),
optimizers_config=models.OptimizersConfigDiff(vacuum_min_vector_number=0),
hnsw_config=models.HnswConfigDiff(ef_construct=new_ef_construct),
strict_mode_config=models.StrictModeConfig(
unindexed_filtering_retrieve=False
), # Turn off scanning and use payload index instead.
)
# Wait for index to be built
wait_for_index_built(collection_name)
wait_for_indexing(collection_name)
# Warmup
client.query_points(collection_name=collection_name, query=query_embedding, limit=1)
# Timing with index
start_time = time.time()
_ = client.query_points(
collection_name=collection_name,
query=query_embedding,
query_filter=filter_condition,
limit=10,
)
time_with_index = (time.time() - start_time) * 1000
times = []
for _ in range(25):
start_time = time.time()
_ = client.query_points(
collection_name=collection_name,
query=query_embedding,
query_filter=filter_condition,
limit=10,
with_payload=False,
)
times.append((time.time() - start_time) * 1000)
time_with_index = np.mean(times)
return {
"without_index": time_without_index,
@@ -334,7 +359,7 @@ You'll know you've succeeded when:
### Step 2: Post Your Results
**Post your results in** <a href="https://discord.com/invite/qdrant" target="_blank" rel="noopener noreferrer" aria-label="Qdrant Discord"> <img src="https://img.shields.io/badge/Qdrant%20Discord-5865F2?style=flat&logo=discord&logoColor=white&labelColor=5865F2&color=5865F2"
**Post your results in** <a href="https://discord.com/channels/907569970500743200/1429673887590776832" target="_blank" rel="noopener noreferrer" aria-label="Qdrant Discord"> <img src="https://img.shields.io/badge/Qdrant%20Discord-5865F2?style=flat&logo=discord&logoColor=white&labelColor=5865F2&color=5865F2"
alt="Post your results in Discord"
style="display:inline; margin:0; vertical-align:middle; border-radius:9999px;" /> </a> **using this:**
@@ -274,7 +274,7 @@ performance = benchmark_search_performance(collection_name, test_queries, ef_val
Use [`get_collection`](/api-reference/collections/get-collection) to inspect your collection. It returns Current statistics and configuration of the collection like `points_count`, `indexed_vectors_count` or `hnsw_config`. It also lists `payload_schema` for payload indexes you created.
To see whether your data is actually indexed check vector and point counts: if `indexed_vectors_count` is far below `points_count * vectors_per_point`, a large part of your data is not in HNSW yet.
To see whether your data is actually indexed, you need to check two things: the number of indexed vectors and the collection's status. If `indexed_vectors_count` is low, indexing may not have completed. More importantly, you should check the collection `status`. A `YELLOW` status means optimization (indexing) is still in progress, while a `GREEN` status confirms it is complete and ready for optimal performance.
If queries feel slow check:
- whether filter fields have [payload indexes](/documentation/concepts/indexing/#payload-index).
@@ -290,9 +290,9 @@ info = client.get_collection(collection_name)
vectors_per_point = 1 # set per your vectors_config
vectors_count = info.points_count * vectors_per_point
print(f"Total vectors: {vectors_count}")
print(f"Collection status: {info.status}")
print(f"Total points: {info.points_count}")
print(f"Indexed vectors: {info.indexed_vectors_count}")
print(f"HNSW config: {info.config.hnsw_config}")
if vectors_count:
proportion_unindexed = 1 - (info.indexed_vectors_count / vectors_count)
@@ -300,6 +300,13 @@ else:
proportion_unindexed = 0
print(f"Proportion unindexed: {proportion_unindexed:.2%}")
if info.status == models.CollectionStatus.GREEN:
print("\n✅ Collection is indexed and ready!")
elif info.status == models.CollectionStatus.YELLOW:
print("\n⚠️ Collection is still being indexed (optimizing).")
else:
print(f"\n❌ Collection status is {info.status}.")
```
## When Not to Use HNSW
@@ -77,7 +77,7 @@ client.recreate_collection(
max_segment_size=5_000_000, # Create larger segments for faster search
),
hnsw_config=models.HnswConfigDiff(
m=6, # Lower M to reduce memory usage
m=6, # Lower m to reduce memory usage
on_disk=False # Keep the HNSW index graph in RAM
),
)
@@ -525,7 +525,7 @@ You'll know you've succeeded when:
### Step 2: Post Your Results
**Post your results in** <a href="https://discord.com/invite/qdrant" target="_blank" rel="noopener noreferrer" aria-label="Qdrant Discord"> <img src="https://img.shields.io/badge/Qdrant%20Discord-5865F2?style=flat&logo=discord&logoColor=white&labelColor=5865F2&color=5865F2"
**Post your results in** <a href="https://discord.com/channels/907569970500743200/1429673887590776832" target="_blank" rel="noopener noreferrer" aria-label="Qdrant Discord"> <img src="https://img.shields.io/badge/Qdrant%20Discord-5865F2?style=flat&logo=discord&logoColor=white&labelColor=5865F2&color=5865F2"
alt="Post your results in Discord"
style="display:inline; margin:0; vertical-align:middle; border-radius:9999px;" /> </a> **using this:**
@@ -15,7 +15,7 @@ You've built and shipped a complete vector search application and gained the exp
You've progressed from vector search fundamentals to production-ready expertise:
**Foundation Building** (Days 0-2): You mastered the core concepts of vector search, learned how similarity metrics work, and understood how [HNSW](https://qdrant.tech/articles/filtrable-hnsw/) indexing enables fast retrieval at scale.
**Foundation Building** (Days 0-2): You mastered the core concepts of vector search, learned how similarity metrics work, and understood how HNSW indexing enables fast retrieval at scale.
**Advanced Retrieval** (Days 3-5): You implemented hybrid search combining semantic and keyword signals, explored quantization for performance optimization, and mastered the Universal Query API with multivector reranking.
@@ -40,13 +40,13 @@ Your final project demonstrates several production-critical capabilities:
{{< course-card
title="Earn your Qdrant Essentials Certificate"
image="/icons/outline/training-white.svg"
link="/course/certification/" >}}
link="/course/essentials/certification/" >}}
Get recognized for completing Day 0–6 and the final project. Add it to your LinkedIn and portfolio.
{{< /course-card >}}
## What's Next?
**Explore Advanced Integrations**: Check out [Day 9 Partner Integrations](../../day-9/) to see how Qdrant works with leading AI frameworks and data platforms.
**Explore Advanced Integrations**: Check out [Day 7 Partner Integrations](../../day-7/) to see how Qdrant works with leading AI frameworks and data platforms.
**Join the Community**: Share your final project results and connect with other practitioners building vector search systems. The Qdrant community is always excited to see what people build.
@@ -144,7 +144,7 @@ Transform raw results into user-friendly output: page title, section title, URLs
### Step 7: Analyze Your Results
Build a small eval set and measure quality and latency. Use results to guide tuning (fusion strategy, candidate sizes, search-time `ef`, etc.).
Build a small eval set and measure quality and latency. Use results to guide tuning (fusion strategy, candidate sizes, search-time `hnsw_ef`, etc.).
**Ground Truth**:
- Create 20–30 realistic queries with expected section URLs/anchors.
@@ -199,7 +199,7 @@ As you test your search engine, consider:
* **Rerank or not:** Are multivectors worth it, or is fusion alone enough?
* **Performance tuning:** Which search and HNSW settings hit your accuracy/latency goals?
* Search time: raise `ef` from 64 → 128 → 256 until gains flatten.
* Search time: raise `hnsw_ef` from 64 → 128 → 256 until gains flatten.
* Index time (if rebuilding): try higher `m` (16, 32) and `ef_construct` (200, 400).
### Step 2: Post Your Results
@@ -228,7 +228,7 @@ Show your run and learn from others. **Post your results in** <a href="https://d
- **Payload fields:** <page_title, section_title, section_url, breadcrumbs, tags, prev/next>
- **Fusion:** <RRF/DBSF>, k_dense=<100>, k_sparse=<100>
- **Reranker:** ColBERT (MaxSim), top-k=<N>
- **Index/Search params:** ef=<...>, m=<...>, ef_construct=<...> # if tuned
- **Index/Search params:** hnsw_ef=<...>, m=<...>, ef_construct=<...> # if tuned
**Queries (examples)**
1) "<user query>"
@@ -23,9 +23,9 @@ Learn about the Qdrant ecosystem and integration strategies.
## Choose Your Integration
{{< cards-list >}}
- icon: /courses/course-integrations/haystack.svg
- icon: /courses/course-integrations/haystack.png
title: Haystack
content: Build end-to-end NLP pipelines with Qdrant
content: Build end-to-end agentic pipelines with Qdrant
link: haystack/
- icon: /courses/course-integrations/tensorlake.svg
@@ -7,7 +7,7 @@ weight: 2
# Integrating with Haystack
Build end-to-end NLP pipelines with Haystack and Qdrant.
Build end-to-end agentic pipelines with Qdrant.
{{< youtube "lMinhPZufTc" >}}
Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB