Keyword search finds "disk full" only when the document says "disk full". Vector search can also find "no space left on device" and "volume at 100%", because it compares meaning. Embeddings are what make that possible.
What an embedding is
An embedding model turns a piece of text into a fixed-length list of numbers (a vector), for example 384, 768, 1,536 or 3,072 numbers depending on the model. The model is trained so that texts with similar meaning get vectors pointing in similar directions.
"disk is full" -> [0.012, -0.341, 0.087, ...]
"no space left on device" -> [0.015, -0.329, 0.091, ...] (close)
"reset my password" -> [-0.210, 0.044, -0.163, ...] (far)
You cannot read meaning from individual numbers. Only distances between vectors matter.
Measuring similarity
| Metric | Idea | Notes |
|---|---|---|
| Cosine similarity | Angle between vectors (1 = same direction) | Most common for text |
| Dot product | Cosine times lengths | Same as cosine when vectors are normalised |
| Euclidean (L2) | Straight-line distance | Lower = more similar |
Many embedding models return normalised vectors, so cosine and dot product give the same ranking. Use the metric the model's documentation recommends.
A tiny semantic search in Python
pip install sentence-transformers numpy
from sentence_transformers import SentenceTransformer
import numpy as np
model = SentenceTransformer("all-MiniLM-L6-v2") # small open model, 384 dimensions, runs on CPU
docs = [
"Linux server root filesystem is 100% full; clean logs and grow the volume.",
"User cannot sign in: account locked after too many attempts.",
"Kubernetes pods evicted due to node disk pressure.",
"SQL Server transaction log is full, error 9002.",
"VPN tunnel down between on-premises and the cloud.",
]
doc_vecs = model.encode(docs, normalize_embeddings=True)
def search(query, k=3):
q = model.encode([query], normalize_embeddings=True)[0]
scores = doc_vecs @ q # cosine similarity (vectors are normalised)
top = np.argsort(-scores)[:k]
return [(round(float(scores[i]), 3), docs[i]) for i in top]
for score, doc in search("no space left on device"):
print(score, doc)
The disk-full runbook and the disk-pressure note rank first, though neither contains the words "no space left on device".
With a hosted model (Azure OpenAI), only the embedding call changes:
resp = client.embeddings.create(model="text-embedding-3-small-prod", input=docs) # deployment name
doc_vecs = np.array([d.embedding for d in resp.data])
Why vector databases and ANN indexes exist
Comparing a query with every vector (brute force) is fine for thousands of documents. With millions, it is too slow. Approximate nearest neighbour (ANN) indexes such as HNSW or IVF find almost the best matches much faster.
| Term | Meaning |
|---|---|
| Recall | Share of the true top-k results the index returns (for example 0.95) |
| HNSW | Graph-based index; fast and accurate, uses memory |
| IVF | Clusters vectors and searches only nearby clusters |
| Quantization | Compresses vectors to save memory, with some accuracy loss |
You trade a little recall for a lot of speed, and tune settings to choose where you sit.
Rules that save beginners pain
Warning
Vectors from different embedding models (or different versions of one model) are not comparable. If you change the model, you must re-embed all documents. Store the model name and version with every vector.
- Dimensions must match the index; a 1,536-dimension vector cannot go into a 768-dimension index.
- Chunk long documents. Embedding a 40-page PDF as one vector blurs its meaning. Split into passages of a few hundred tokens.
- Keep metadata (source, date, owner, permissions) next to each vector so you can filter results.
- Vector search is not exact match. For IDs, error codes and product names, keyword search often wins. Hybrid search (keyword plus vector) is common in production.
Where embeddings are used
| Use | How |
|---|---|
| Semantic search | Query vector vs document vectors |
| RAG (retrieval augmented generation) | Retrieve relevant chunks, give them to an LLM to answer |
| Deduplication | Very similar vectors = near-duplicate tickets or docs |
| Clustering / routing | Group tickets by topic, route to the right team |
Choosing a store
| Scale / situation | Start with |
|---|---|
| Experiments, under ~100k vectors | In-memory NumPy, Chroma, or pgvector |
| Already on PostgreSQL | pgvector |
| Azure search features (hybrid, semantic ranker) | Azure AI Search |
| Large-scale, dedicated | Qdrant, Milvus, Pinecone, Weaviate |
Prevent it next time
- Record embedding model, version and dimensions in your index metadata.
- Test search quality with 20-50 real questions before and after any change.
- Start simple; move to a vector database when brute force is too slow, not before.