Update module 6

This commit is contained in:
Dylan Couzon
2026-08-22 21:57:15 -04:00
parent fac93477cf
commit 7c4e8c5fd6
3 changed files with 115 additions and 155 deletions
@@ -1,211 +1,171 @@
---
title: "Module 6: Beyond Similarity (Bonus)"
short_description: "Bonus module of the Beginners course: the Qdrant features that fix ranking when vector similarity alone puts the wrong results first."
description: "Map search problems to Qdrant features beyond similarity: score boosting, MMR diversity, two-stage reranking, grouping, relevance feedback, and discovery."
short_description: "Bonus module of the Beginners course: the Qdrant features for better ranking, more variety, grouping, and search without a query."
description: "Match a search problem to the Qdrant feature that fixes it: score boosting, MMR diversity, reranking, grouping, relevance feedback, and discovery."
isLesson: true
weight: 70
---
{{< date >}} Module 6 {{< /date >}}
<!--
NOTE: this bonus module intentionally ships with no video and no knowledge
check. It is optional further reading, not a taught module. Module 4 currently
promises "the course's closing video" points readers to these topics, so
module-4/_index.md line 274 needs to name Module 6 instead.
-->
<!--
TODO (diagram): one diagram for The Map section, showing where each feature
runs inside a query: prefetch stage, rescore stage, selection over retrieved
candidates, and next-query traversal. Build from the Docs/Diagrams library
using Text-Boxes and Connectors, N98 fills with N30 text, P50 for the brand
accent and Teal 50 for the second stage, Mona Sans labels and Geist Mono for
parameter names.
-->
# Beyond Similarity
Every feature on this page fixes one class of problem: ranking by vector similarity alone either puts the wrong document first, or puts eight versions of the right document first. Modules 1 through 3 gave you the parts, and Module 4 turned them into a design. This is the map of what sits past that, and none of it is required to finish the course.
Modules 1 through 5 showed you how to design and build a complete retrieval pipeline. This bonus module covers the next layer: measuring and improving its results.
## How to Use This Module
#### Overview
Read by symptom from the table, then follow the link for code in each client language. The entries after it add only the parameter to reach for and the trap that costs people an afternoon, since the docs already hold the mechanics. MMR is the exception and gets a worked example, because its defaults make it look broken.
> You'll match common search problems to Qdrant features: score boosting and reranking for order, Maximal Marginal Relevance (MMR) for variety, grouping for one slot per document, and the Recommendation and Discovery APIs for searches from examples instead of text. You'll set up a way to measure relevance first, and pick up the trap that comes with each feature along the way.
## The Map
## Today's Path
Each row is a failure you can hit with what the course already taught, the feature that addresses it, and where in the query that feature runs.
1. Find Your Problem
2. Measure First
3. Ranking: Score Boosting and Reranking
4. Diversity: MMR
5. Grouping: One Slot per Document
6. Searching From Examples and Feedback
7. Inspecting a Collection
8. Knowledge Check
| Symptom | Feature | Runs |
|---------|---------|------|
| Right documents, wrong order | [Score boosting](/documentation/search/search-relevance/#score-boosting) with a Formula Query, v1.14 | Rescoring step over a prefetch |
| Ranking should account for recency or distance | [Decay functions](/documentation/search/search-relevance/#decay-functions) inside the formula | Same rescoring step |
| Ranking is good but the accurate model is too slow to run over everything | [Multi-stage query](/documentation/search/hybrid-queries/#multi-stage-queries) | Cheap prefetch, accurate rescore |
| Top results are near-identical | [MMR](/documentation/search/search-relevance/#maximal-marginal-relevance-mmr), v1.15 | Selection over retrieved candidates |
| One document fills the page with its own chunks | [Grouping](/documentation/search/search/#grouping-api) | Server-side grouping of hits |
| A better model, or user clicks, disagree with the retriever | [Relevance feedback](/documentation/search/search-relevance/#relevance-feedback), v1.17 | Vector space traversal on the next query |
| There is no query, only examples of good and bad | [Recommendation and Discovery](/documentation/search/explore/#recommendation-api) | Replaces the query vector |
| You cannot tell whether any of the above helped | [Golden set evaluation](/documentation/improve-search/retrieval-relevance/) | Offline, outside Qdrant |
## 1. Find Your Problem
Start with the last row, because every other row changes ranking and can make it worse. A golden set, meaning queries paired with the documents that should come back for them, turns that into a number: Precision@K counts how many of the top K results are relevant, Recall@K how many relevant documents reached the top K, and NDCG (Normalized Discounted Cumulative Gain) also rewards ordering.
Each row pairs a problem with the feature that addresses it, and names the stage of a query where it runs.
## Ranking: Score Boosting and Two-Stage Retrieval
| Problem | Feature | Stage |
|---------|---------|-------|
| The right documents come back in the wrong order | [Score boosting](/documentation/search/search-relevance/#score-boosting) with a formula query | Rescore |
| The order should account for recency or distance | [Decay functions](/documentation/search/search-relevance/#decay-functions) inside the formula | Rescore |
| The accurate model is too slow to run over the whole collection | [Multi-stage query](/documentation/search/hybrid-queries/#multi-stage-queries) | Rescore |
| The top results are near-identical | [Maximal Marginal Relevance](/documentation/search/search-relevance/#maximal-marginal-relevance-mmr) | Select |
| One document fills the page with its own chunks | [Grouping](/documentation/search/search/#grouping-api) | Select |
| A better model, or user clicks, disagree with retrieval | [Relevance feedback](/documentation/search/search-relevance/#relevance-feedback) | Next query |
| There is no query text, only examples of good and bad | [Recommendation and Discovery APIs](/documentation/search/explore/#recommendation-api) | Replaces the query |
Module 4 introduced formula queries and the optional cross-encoder rerank stage, so this entry covers what it left out. A formula runs only as a rescoring step, so it needs a prefetch under it. Results sort descending, so Euclidean scores have to be negated, and every `$score[i]` past the first needs an entry in `defaults` or a missing value errors. Module 4's multi-shard constraint still holds: one query cannot be both a fusion and a formula.
![One query drawn as three stages left to right. Retrieve, which is wide and cheap, holds prefetch, hybrid, and filters. Rescore, which runs over the candidates, holds score boosting, decay, and reranking. Select, which decides what fills the page, holds MMR and grouping.](/courses/beginners/module-6/pipeline.png)
Decay functions carry their own calibration trap. Linear, exponential, and Gaussian decay clamp an age or a distance into the range 0 to 1, while fused RRF scores are sums of `1/(k+rank)` terms and are much smaller than that. An unweighted decay term will dominate the fused score, so wrap it in a multiplication with a coefficient sized to your own scores.
## 2. Measure First
For two-stage retrieval, the cheap first stage can be quantized vectors, a truncated Matryoshka vector, or a plain dense vector, with full precision, the full-length vector, or a late-interaction model such as ColBERT doing the rescore. Each prefetch needs a `limit` of at least the outer query's `limit` plus `offset`, or results come back empty. A vector used only for rescoring can set `m=0` in its HNSW config, skipping a graph nothing will traverse.
Ranking changes are hard to judge by eye, because a worse results page still looks like a list of plausible documents. Measure what you have before you change anything.
- [Multi-Stage Queries](/documentation/search/hybrid-queries/#multi-stage-queries)
- [Reranking for Better Search](/documentation/search-precision/reranking-semantic-search/), including which reranker types fit which budget
- [Late Interaction Retrieval with Dense Token Embeddings](/articles/late-interaction-models/)
A golden set pairs queries with the documents that should come back for them. It turns a ranking change into a number.
## Diversity: MMR, Measured
Sample query and click pairs from your logs, or have someone who knows the domain write 20 or 30 queries with the answers they expect. [Measuring Retrieval Relevance](/documentation/improve-search/retrieval-relevance/) covers both and computes the metrics with the Python library [`ranx`](https://amenra.github.io/ranx/).
MMR picks results one at a time, each time taking the candidate with the best combination of similarity to the query and distance from what it already picked. In Qdrant it is a parameter on a nearest neighbors query, and both of its parameters have defaults that surprise people: `diversity` defaults to 0.5, and `candidates_limit` defaults to the query's `limit`.
Pick the metric that matches the labels you ended up with.
That second default is the one worth running. This fixture holds five near-duplicate headlines about one event and three unrelated shipping stories.
- `Recall@K`: the share of the relevant documents that reach the top K. Start here, since logs and a hand-written set both give you the binary labels it needs.
- `MRR` (Mean Reciprocal Rank): how high the first relevant document lands. Use it when the page shows a single answer, as in a chatbot.
- `NDCG@K` (Normalized Discounted Cumulative Gain): how closely the top K matches the best possible order. Use it once you have graded labels, such as 0, 1, and 2.
```bash
pip install "qdrant-client[fastembed]"
```
Measure a baseline at one K, change one thing, then measure the same metric at the same K.
```python
from qdrant_client import QdrantClient, models
## 3. Ranking: Score Boosting and Reranking
MODEL = "sentence-transformers/all-MiniLM-L6-v2"
client = QdrantClient(":memory:")
Two ways to change the order when the right documents are already coming back.
headlines = [
"Port congestion worsens at Singapore as container backlog grows",
"Container backlog grows at Singapore port amid worsening congestion",
"Singapore port congestion deepens as containers pile up on the docks",
"Vessel queues lengthen outside Singapore as port congestion continues",
"Congestion at Singapore port leaves containers waiting for berths",
"Dockworker strike in Rotterdam halts container handling",
"Bunker fuel prices climb across Asian shipping hubs",
"Carriers reroute Asia-Europe services away from the Red Sea",
]
### Score Boosting
client.create_collection(
"headlines",
vectors_config=models.VectorParams(size=384, distance=models.Distance.COSINE),
)
client.upsert(
"headlines",
points=[
models.PointStruct(
id=i,
vector=models.Document(text=text, model=MODEL),
payload={"headline": text},
)
for i, text in enumerate(headlines)
],
)
Similarity is not always the final ranking signal. A result may be relevant, but you may still want to prefer an exact title match, a nearby store, or a recent article. A [formula query](/documentation/search/search-relevance/#score-boosting) lets you rescore the candidates returned by retrieval, combining their similarity score with payload values and conditions you define.
query = models.Document(text="port congestion in Southeast Asia", model=MODEL)
A formula only runs as a rescoring step. With no prefetch under it to supply candidates, the request fails with `cannot apply Formula without prefetches`.
# candidates_limit=8 is the whole fixture here; in production set it well above limit
runs = [
("plain nearest neighbors", None),
("mmr, diversity=0.5, default candidates_limit", models.Mmr(diversity=0.5)),
("mmr, diversity=0.5, candidates_limit=8", models.Mmr(diversity=0.5, candidates_limit=8)),
("mmr, diversity=0.9, candidates_limit=8", models.Mmr(diversity=0.9, candidates_limit=8)),
]
[Decay functions](/documentation/search/search-relevance/#decay-functions) turn a value such as age or distance into a score from 0 to 1. Newer or closer items get a higher score; older or farther items get a lower one.
for label, mmr in runs:
q = query if mmr is None else models.NearestQuery(nearest=query, mmr=mmr)
results = client.query_points("headlines", query=q, limit=4).points
print(label)
for point in results:
print(f" {point.score:.3f} {point.payload['headline']}")
```
On a hybrid query, a decay score can overwhelm RRF: when two retrievers agree, the top result has an RRF score of 1.0. Print your fused scores, then scale the decay coefficient to match them.
Real output:
### Reranking
```text
plain nearest neighbors
0.710 Vessel queues lengthen outside Singapore as port congestion continues
0.694 Port congestion worsens at Singapore as container backlog grows
0.633 Singapore port congestion deepens as containers pile up on the docks
0.623 Congestion at Singapore port leaves containers waiting for berths
mmr, diversity=0.5, default candidates_limit
0.710 Vessel queues lengthen outside Singapore as port congestion continues
0.694 Port congestion worsens at Singapore as container backlog grows
0.623 Congestion at Singapore port leaves containers waiting for berths
0.633 Singapore port congestion deepens as containers pile up on the docks
mmr, diversity=0.5, candidates_limit=8
0.710 Vessel queues lengthen outside Singapore as port congestion continues
0.514 Carriers reroute Asia-Europe services away from the Red Sea
0.382 Bunker fuel prices climb across Asian shipping hubs
0.694 Port congestion worsens at Singapore as container backlog grows
mmr, diversity=0.9, candidates_limit=8
0.710 Vessel queues lengthen outside Singapore as port congestion continues
0.104 Dockworker strike in Rotterdam halts container handling
0.382 Bunker fuel prices climb across Asian shipping hubs
0.514 Carriers reroute Asia-Europe services away from the Red Sea
```
A reranker reorders a shortlist with a slower, more accurate model. It reads the query and one document together, rather than comparing two vectors computed separately, which is what makes it both more accurate and too expensive to run over a whole collection.
**What to look for:**
Reranking can only reorder what retrieval already returned. Check the right answers reach the shortlist before you add one. When they are missing, the fix belongs in retrieval.
- The default run returns the same four near-duplicates as plain search, reordered. With `candidates_limit` equal to `limit`, MMR chooses among exactly the four results it was asked for, so reordering is all it can do. Most reports of MMR having no effect are this.
- Raising `candidates_limit` to 8 changes the result set: two duplicates give way to the rerouting and fuel stories at 0.514 and 0.382, against the top result's 0.710. Whether that trade is worth it is a question for your golden set.
- At `diversity=0.9` a Rotterdam strike enters at 0.104, for a query about Southeast Asia. Diversity has no notion of topical relevance, so past some point it buys spread by giving up the query.
- No MMR run descends by score, and row four of the second run outscores row three. Points come back in selection order, and each score is similarity to the query rather than MMR's objective, so do not sort or threshold them as ranked scores.
The model scores text rather than vectors, so you run it yourself, on the shortlist Qdrant returned. FastEmbed provides `TextCrossEncoder` for it. When the more accurate scorer is another vector instead, Qdrant can do the rescoring itself with a [multi-stage query](/documentation/search/hybrid-queries/#multi-stage-queries).
Local mode searches all eight points here, so a server returns this same order. On a large collection the candidates come from approximate search, so `candidates_limit` sets both how much diversity is available and how much extra retrieval you pay for.
Shortlist size is the cost knob, because the model runs once per candidate: tens of candidates stay cheap, and hundreds do not. `Xenova/ms-marco-MiniLM-L-6-v2` is an 80 MB reranker available through FastEmbed, small enough to find out whether reranking helps you at all.
## Grouping
- [Multi-Stage Queries](/documentation/search/hybrid-queries/#multi-stage-queries): the prefetch and rescore syntax, in every client language.
- [Reranking with FastEmbed](/documentation/fastembed/fastembed-rerankers/): running a cross-encoder over the shortlist, with a worked example.
- [Hybrid Search with Qdrant's Query API](/articles/hybrid-search/): fusion and reranking as competing designs, with the reasoning behind the Query API.
Chunking creates the neighboring problem: one long document becomes many points, and a strong match on that document can fill the whole first page with its own chunks. `query_points_groups` groups hits by a payload field and returns a set number of groups, so one document takes one slot. If the parent records live in their own collection, `lookup_from` fetches them alongside the groups.
## 4. Diversity: MMR
The trap is the index. Strict mode is on by default in Qdrant Cloud, so grouping on a field with no payload index returns a 400, and `document_id` is the index people forget. Create it before ingestion, so the filterable HNSW graph is built with it rather than needing a rebuild.
Maximal Marginal Relevance (MMR) picks results one at a time, preferring candidates that match the query and differ from what it has already picked. In Qdrant it is a parameter on a nearest neighbors query, and `diversity` sets how much relevance it trades for variety.
- [Grouping API](/documentation/search/search/#grouping-api)
- [Multi-Representation Search](/documentation/tutorials-search-engineering/multi-representation-search/)
The trap is `candidates_limit`. It defaults to the query's `limit`, which leaves MMR nothing spare to choose from, so all it can do is reorder the results it was already given. This is the most common reason MMR looks like it did nothing.
## Feedback: Relevance Feedback, Recommendation, and Discovery
![Two rows over the same eight documents, drawn as five identical squares followed by a circle, a triangle, and a diamond. In the first row, candidates_limit equal to limit gives MMR a pool of only the first four squares, all four of which it selects, so the outcome is a reorder. In the second row, candidates_limit of 8 gives it the whole set, and it selects one square plus the circle, triangle, and diamond, leaving four squares unselected, so the outcome is a different set of documents.](/courses/beginners/module-6/mmr-pool.png)
Sometimes a stronger model or a click log knows more about relevance than the embedding model does. A `RelevanceFeedbackQuery` takes the original query as `target`, plus `feedback`: three to five results, each carrying a score from a relevance oracle, meaning any model that judges relevance. Qdrant turns the disagreement between oracle and retriever into a change in how it traverses the vector space on the next query, across the whole collection rather than only the shortlist.
- [Maximal Marginal Relevance](/documentation/search/search-relevance/#maximal-marginal-relevance-mmr): both parameters, and the scores an MMR query returns.
- [The Use of MMR, Diversity-Based Reranking for Reordering Documents and Producing Summaries](https://www.cs.cmu.edu/~jgc/publication/The_Use_MMR_Diversity_Based_LTMIR_1998.pdf): the 1998 paper the `diversity` parameter implements.
Two traps. The `naive` strategy's `a`, `b`, and `c` weights are specific to your combination of retriever, oracle, and collection, and the `qdrant-relevance-feedback` package fits them. A point passed by ID as `target` or `example` is excluded from the results, so pass its raw vector to keep it eligible.
## 5. Grouping: One Slot per Document
When there is no query text at all, the Recommendation API searches from positive and negative examples using `average_vector`, `best_score`, or `sum_scores`, and the Discovery API adds context pairs that steer results toward one region of the vector space and away from another.
Chunking creates the neighbor problem: one long document becomes many points, and a strong match on it can fill the whole first page with its own chunks. [`query_points_groups`](/documentation/search/search/#grouping-api) groups results by a payload field and returns a set number of groups, so one document takes one slot.
- [Relevance Feedback](/documentation/search/search-relevance/#relevance-feedback)
- [Relevance Feedback Retrieval in Qdrant](/documentation/tutorials-search-engineering/using-relevance-feedback/), the tutorial that fits the weights and evaluates the result
- [Recommendation API](/documentation/search/explore/#recommendation-api) and [Discovery API](/documentation/search/explore/#discovery-api)
`group_size` caps how many chunks come back inside each group, and `with_lookup` attaches a parent record from another collection.
## Smaller Things Worth Knowing
Deduplicating the results yourself after the search does not fill the page. If the top 10 hits are all chunks from three documents, you are left with three results and no way to get more without searching again. Asking the server for 10 groups returns 10 documents the first time.
These solve narrower problems, and each one is a page rather than a lesson.
Strict mode is on by default in Qdrant Cloud, and it rejects grouping on a field with no payload index, returning a 400. The field most often missing one is `document_id`.
| Feature | What It Does |
- [Grouping API](/documentation/search/search/#grouping-api): `group_by`, `group_size`, and `with_lookup`.
- [Multi-Representation Search](/documentation/tutorials-search-engineering/multi-representation-search/): storing one document as several points and grouping them back together.
## 6. Searching From Examples and Feedback
Some searches have no query text. A reader clicks "more like this", or an analyst has three documents that are right and two that are wrong and no words for what separates them.
The [Recommendation API](/documentation/search/explore/#recommendation-api) searches from positive and negative examples. The [Discovery API](/documentation/search/explore/#discovery-api) takes context pairs, where each pair names one region of the vector space to move toward and one to move away from.
When a better model or a click log disagrees with your ranking, [relevance feedback](/documentation/search/search-relevance/#relevance-feedback) folds that disagreement into the next query, across the whole collection. It needs a second model and three weights fitted to your own setup, so start with the tutorial below.
A point passed in by ID, whether as `example`, `positive`, `negative`, or a relevance feedback `target`, is left out of the results. Pass its raw vector instead to keep it eligible.
- [Recommendation API](/documentation/search/explore/#recommendation-api) and [Discovery API](/documentation/search/explore/#discovery-api): searching from examples and from context pairs.
- [Relevance Feedback](/documentation/search/search-relevance/#relevance-feedback): the query interface and what the `naive` strategy computes.
- [Relevance Feedback Retrieval in Qdrant](/documentation/tutorials-search-engineering/using-relevance-feedback/): fitting the weights and evaluating the result.
## 7. Inspecting a Collection
Two features for checking what you actually ingested.
| Feature | What it does |
|---------|--------------|
| [Facet counts](/documentation/manage-data/payload/#facet-counts) | Counts how many points hold each unique value of a payload field, for sidebar counts and for checking how selective a filter would be. |
| [Distance matrix](/documentation/search/explore/#distance-matrix) | Samples points and returns their pairwise distances as a sparse matrix, the input for clustering and visualization. |
| [Random sampling](/documentation/search/search/#random-sampling) | Returns a random subset of a collection, for spot-checking ingested data and drawing evaluation sets. |
| [Quantization](/documentation/manage-data/quantization/) | Shrinks stored vectors to cut memory and cost, with a rescoring step to recover the precision compression loses. |
| [miniCOIL and SPLADE++](/documentation/search/text-search/full-text-search/#minicoil) | Sparse retrievers that learn contextual term weights instead of using raw frequency, dropped into the same hybrid slot as BM25 and needing the same `IDF` modifier. |
| [Matryoshka models](/documentation/inference/matryoshka-models/) | Embeddings that stay usable when truncated, so a cheap first stage and an accurate second stage can share one model. |
| [Low-latency search](/documentation/search/low-latency-search/) | Replica scaling, delayed fan-outs, and `indexed_only` for holding tail latency flat while data is still being indexed. |
| [Facet counts](/documentation/manage-data/payload/#facet-counts) | Counts how many points hold each value of a payload field, which also shows how selective a filter would be. |
| [Random sampling](/documentation/search/search/#random-sampling) | Returns a random subset of a collection, for spot-checking ingested data. For a subset that repeats across queries, such as an evaluation set, use the [slice](/documentation/search/filtering/#slice) filter condition instead. |
## References & Further Reading
## 8. Knowledge Check
**Papers and deep dives:**
<details>
<summary>You turn MMR on and your Recall@10 drops. Is MMR broken?</summary>
- [The Use of MMR, Diversity-Based Reranking for Reordering Documents and Producing Summaries](https://www.cs.cmu.edu/~jgc/publication/The_Use_MMR_Diversity_Based_LTMIR_1998.pdf)
- The 1998 paper the `diversity` parameter implements.
- [Relevance Feedback in Information Retrieval](/articles/search-feedback-loop/)
- Feedback loops as a family of methods, before narrowing to Qdrant's implementation.
- [Relevance Feedback in Qdrant](/articles/relevance-feedback/)
- What the naive strategy does to vector space traversal, and how its weights behave.
- [Hybrid Search with Qdrant's Query API](/articles/hybrid-search/)
- Fusion and reranking as competing designs, with the reasoning behind the Query API.
No. MMR spends result slots on documents further from the query, so a measure that only counts relevance goes down while the page stops repeating itself. Check what those slots held before: if they were eight versions of one story, the drop bought something your metric cannot see. If they were eight distinct relevant documents, lower `diversity`.
</details>
<details>
<summary>Your formula boosts newer articles on top of a hybrid query, and recency now decides every ranking. Why?</summary>
A decay term reaching 1.0 is the same size as the entire RRF score it is being added to, so it decides every comparison on its own. That ceiling rises with each extra prefetch, so measure your own fused scores rather than assuming 1.0.
</details>
<details>
<summary>You group results by <code>document_id</code> on a Qdrant Cloud cluster and the request returns a 400. What is missing?</summary>
A keyword payload index on `document_id`. Strict mode rejects the request without it, so the fix belongs on the field rather than in the query.
</details>
<details>
<summary>A reader clicks "more like this" on an article. You pass its point ID to the Recommendation API as a <code>positive</code> example, and the article itself never comes back. Is something broken?</summary>
No. A point passed in by ID is left out of the results, and here that is what you want, since the reader is already looking at it. When you do need it back, look the point up and pass its raw vector.
</details>
## Where to Go After the Course
- [Qdrant Essentials](/course/essentials/) goes deeper on indexing, quantization, large-scale ingestion, and multitenancy.
- [Multi-Vector Search](/course/multi-vector-search/) covers ColBERT and ColPali properly, including MaxSim scoring, pooling, and MUVERA indexing.
- [Qdrant Cloud](https://cloud.qdrant.io/) gives you a free cluster, where payload indexes, strict mode, and approximate search behave the way this module describes and local mode cannot.
- [Qdrant Essentials](/course/essentials/) goes deeper on HNSW tuning, quantization and rescoring, high-throughput ingestion, and the full Query API.
- [Multi-Vector Search](/course/multi-vector-search/) covers ColBERT and ColPali, including MaxSim scoring, pooling, and MUVERA indexing.
- [Qdrant Cloud](https://cloud.qdrant.io/) has a free cluster to run any of this on.
Binary file not shown.

After

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 47 KiB