ASDRP · Agentic RAG Track · Module 03

Similarity & Vector Search

Embed a handful of sentences, store them in Qdrant, and retrieve the nearest neighbours — no API key required.

cos θ = a · b / (‖a‖ × ‖b‖)
sentence-transformers qdrant-client keyless
Built by mui-group @ ASDRP· Advanced Student Directed Research Program

What we'll cover

Contents

1

The Math

  • Cosine similarity as an angle
  • Why direction beats magnitude
  • The hero formula
2

The Store

  • What Qdrant does
  • Collections & VectorParams
  • Upsert points
3

The Search

  • Query by vector
  • Top-k results
  • Cautions & limits

Track 1 · The Math

Cosine similarity measures the angle, not the distance

Two sentences as arrows in 384-D space
The arrow's direction encodes meaning; its length doesn't matter.
cos θ = a · b / (‖a‖ × ‖b‖)
  • cos θ = 1 → same direction, identical meaning
  • cos θ = 0 → orthogonal, unrelated
  • cos θ = −1 → opposite directions
x y a b θ cos θ = similarity(a, b)

Track 1 · The Math

Why direction beats magnitude

"Gold trades by the troy ounce."
Short sentence → short vector
"Gold is priced in troy ounces per unit."
Longer restatement → longer vector
Both arrows point in almost the same direction. Cosine similarity divides out the length, so both score ≈ 0.96 with each other — yet would score only ≈ 0.2 against a sentence about semiconductors.
⚠ Caution — Dot-product similarity (no normalisation) rewards longer documents. Cosine similarity removes that bias, making it the standard for semantic search. Qdrant's Distance.COSINE normalises internally.

Track 1 · The Math

Computing cosine by hand — then letting Qdrant do it

import numpy as np
from sentence_transformers import SentenceTransformer

model = SentenceTransformer("all-MiniLM-L6-v2")   # 384-dim, keyless

a = model.encode("What is a troy ounce?")
b = model.encode("Gold is measured in troy ounces.")
c = model.encode("Machine learning optimises loss functions.")

def cosine(x, y):
    x, y = np.asarray(x, float), np.asarray(y, float)
    return float(x @ y / (np.linalg.norm(x) * np.linalg.norm(y)))

print(f"sim(a, b) = {cosine(a, b):.4f}")   # high  (~0.85)
print(f"sim(a, c) = {cosine(a, c):.4f}")   # low   (~0.15)
In practice you never call cosine() manually — Qdrant runs it over millions of vectors in milliseconds. The manual version builds the intuition.

Track 2 · The Store

What Qdrant does

Vector database in one sentence: store vectors, search by cosine, return the closest ones fast — even at millions of points.
  • Collection — a named table of vectors (all same dimension)
  • Point — one vector + an integer id + optional payload (metadata)
  • :memory: — in-process, no disk, perfect for tutorials
HNSW index — Qdrant's default index approximates nearest-neighbour search in O(log n) instead of brute-force O(n).
sentence encode() [0.12, −0.45, …] upsert Qdrant :memory: collection query → top-k

Track 2 · The Store

Create a collection and upsert points

from qdrant_client import QdrantClient
from qdrant_client.models import VectorParams, Distance, PointStruct

client = QdrantClient(":memory:")   # in-process, no server needed

client.create_collection(
    collection_name="demo",
    vectors_config=VectorParams(size=384, distance=Distance.COSINE),
)

# encode a small toy corpus
sentences = [
    "Gold is priced in troy ounces.",
    "Silver has significant industrial demand.",
    "A troy ounce equals 31.1 grams.",
    "Platinum is rarer than gold.",
    "Machine learning optimises loss functions.",
]
vectors = model.encode(sentences)

client.upsert(
    collection_name="demo",
    points=[
        PointStruct(id=i, vector=vec.tolist(), payload={"text": s})
        for i, (vec, s) in enumerate(zip(vectors, sentences))
    ],
)

Track 3 · The Search

Query: "What is a troy ounce?" → top-3 results

query = "What is a troy ounce?"
q_vec = model.encode(query).tolist()

hits = client.query_points(
    collection_name="demo",
    query=q_vec,
    limit=3,
).points

for rank, hit in enumerate(hits, 1):
    print(f"#{rank}  score={hit.score:.4f}  →  {hit.payload['text']}")
Expected output (approximate):
#1 score=0.8831 → A troy ounce equals 31.1 grams.
#2 score=0.7245 → Gold is priced in troy ounces.
#3 score=0.5102 → Silver has significant industrial demand.

Track 3 · The Search

Where cosine similarity misleads you

⚠ Caution 1 — High similarity ≠ correct passage
Two sentences can share vocabulary and direction without one being the correct answer to a question. "Gold is priced in troy ounces" is topically close to "What is a troy ounce?" but doesn't actually define it. Retrieval scores tell you about relevance, not factual accuracy — that distinction matters enormously in Module 09 (Faithfulness).
⚠ Caution 2 — Embedding space ≠ semantic space
all-MiniLM-L6-v2 was trained on general text. Domain-specific jargon (e.g. "contango", "LBMA fix") may land in unexpected regions of the space. Module 05 migrates to a cloud embedder better tuned to finance.
⚠ Caution 3 — Top-k is a hard cut
Returning k=3 drops rank 4, even if scores 3 and 4 are nearly identical. Module 10 introduces reranking to handle this gracefully.

Track 3 · The Search

Cosine vs other distance functions

MetricMeasuresSensitive to vector length?Use case
CosineAngle between vectorsNo (normalised)Semantic text search ✓
Euclidean (L2)Straight-line distanceYesImage embeddings, clustering
Dot productProjection magnitudeYesWhen vectors are pre-normalised
Qdrant supports all three via Distance.COSINE, Distance.EUCLID, Distance.DOT. For text search with sentence-transformers, always use Distance.COSINE.

Summary

What you covered

1

The Math

  • Cosine similarity = angle between embedding vectors
  • Direction encodes meaning; length is normalised away
  • Score range: −1 (opposite) to 1 (identical)
2

The Store

  • Qdrant :memory: — zero-config, in-process
  • Create collection → upsert PointStructs
  • HNSW index: O(log n) approximate search
3

The Search

  • query_points(query_vec, limit=k) → ranked hits
  • High score ≠ factually correct passage
  • Hard top-k cut improved later by reranking (M10)

Run the notebook: 03_similarity_search.ipynb