Traditional keyword search matches exact words. Semantic search understands meaning — a query for “affordable cars” finds results about “budget vehicles” even if those exact words don’t appear. The technology behind semantic search is vector embeddings and vector databases. In 2026, these are essential building blocks for RAG systems, recommendation engines, and document similarity search. This guide covers everything you need to know.
What Are Embeddings?
An embedding is a dense numerical vector that represents the meaning of text (or images, audio, etc.) in a high-dimensional space. Similar texts have similar vectors — measured by cosine similarity or dot product. The embedding model (like all-MiniLM-L6-v2) converts raw text into a 384-dimensional float vector. Similar sentences like “the cat sat on the mat” and “a feline rested on the rug” will have vectors very close together, even with no shared words.
Generating Embeddings with Sentence-Transformers
pip install sentence-transformers
from sentence_transformers import SentenceTransformer
import numpy as np
model = SentenceTransformer('all-MiniLM-L6-v2') # 384 dimensions, fast
sentences = [
"Machine learning is a subset of artificial intelligence.",
"ML algorithms learn patterns from data.",
"Python is great for cooking recipes.",
]
embeddings = model.encode(sentences, normalize_embeddings=True)
print(embeddings.shape) # (3, 384)
# Cosine similarity (high = similar)
from sklearn.metrics.pairwise import cosine_similarity
sim_matrix = cosine_similarity(embeddings)
print(f"Sentence 0 vs 1: {sim_matrix[0][1]:.3f}") # ~0.82 (similar)
print(f"Sentence 0 vs 2: {sim_matrix[0][2]:.3f}") # ~0.12 (different)
FAISS – Fast Local Vector Search
pip install faiss-cpu # or faiss-gpu for GPU
import faiss
import numpy as np
# Create index (Inner Product = cosine similarity for normalized vectors)
dim = 384
index = faiss.IndexFlatIP(dim) # exact search
# Add document embeddings
corpus_embeddings = model.encode(corpus_texts, normalize_embeddings=True)
index.add(corpus_embeddings.astype(np.float32))
# Search
query = "how does gradient descent work?"
query_emb = model.encode([query], normalize_embeddings=True)
distances, indices = index.search(query_emb.astype(np.float32), k=5)
for i, idx in enumerate(indices[0]):
print(f"[{distances[0][i]:.3f}] {corpus_texts[idx][:100]}")
# For large datasets, use approximate search (much faster)
index = faiss.IndexIVFFlat(faiss.IndexFlatIP(dim), dim, 100)
index.train(corpus_embeddings.astype(np.float32))
index.add(corpus_embeddings.astype(np.float32))
index.nprobe = 10 # number of clusters to search
ChromaDB – Easy Local Vector Store
pip install chromadb
import chromadb
from chromadb.utils import embedding_functions
client = chromadb.PersistentClient(path="./chroma_store")
# Use sentence-transformers as embedding function
ef = embedding_functions.SentenceTransformerEmbeddingFunction(
model_name="all-MiniLM-L6-v2")
collection = client.get_or_create_collection(
name="knowledge_base", embedding_function=ef)
# Add documents (embeddings computed automatically)
collection.add(
documents=["Python is a programming language.",
"Machine learning requires data.",
"Deep learning uses neural networks."],
metadatas=[{"source": "intro.pdf", "page": 1},
{"source": "ml_book.pdf", "page": 12},
{"source": "dl_guide.pdf", "page": 5}],
ids=["doc1", "doc2", "doc3"]
)
# Query
results = collection.query(
query_texts=["what programming language for AI?"],
n_results=2,
include=["documents", "metadatas", "distances"])
for doc, meta, dist in zip(results['documents'][0],
results['metadatas'][0],
results['distances'][0]):
print(f"[{1-dist:.3f}] {doc} | Source: {meta['source']}")
Pinecone – Managed Vector Database
pip install pinecone-client
from pinecone import Pinecone, ServerlessSpec
pc = Pinecone(api_key="YOUR_API_KEY")
index = pc.Index("my-knowledge-base")
# Upsert vectors with metadata
vectors = [
{"id": f"doc_{i}",
"values": emb.tolist(),
"metadata": {"text": text, "source": source}}
for i, (text, emb, source) in enumerate(zip(texts, embeddings, sources))
]
index.upsert(vectors=vectors)
# Query
query_emb = model.encode(["best practices for MLOps"])[0].tolist()
results = index.query(vector=query_emb, top_k=5,
include_metadata=True,
filter={"source": {"$eq": "mlops_guide.pdf"}})
for match in results['matches']:
print(f"Score: {match['score']:.3f} | {match['metadata']['text'][:100]}")
Semantic Search Application
from fastapi import FastAPI
from pydantic import BaseModel
app = FastAPI()
class SearchRequest(BaseModel):
query: str
top_k: int = 5
@app.post("/search")
def semantic_search(req: SearchRequest):
query_emb = model.encode([req.query], normalize_embeddings=True)
results = collection.query(
query_embeddings=query_emb.tolist(), n_results=req.top_k)
return {
"results": [
{"text": doc, "score": round(1 - dist, 3), **meta}
for doc, dist, meta in zip(
results["documents"][0],
results["distances"][0],
results["metadatas"][0])
]
}
Choosing a Vector Database
FAISS is best for local, in-memory search where you control the hardware and need maximum performance — it’s used by Meta internally. ChromaDB is easiest for local development and prototyping (zero infrastructure setup). Pinecone and Weaviate are managed services best for production where you don’t want to manage infrastructure. Qdrant is a strong self-hosted option if you want full control in production. For most projects: start with ChromaDB locally, then migrate to Pinecone or Qdrant for production.
Conclusion
Vector databases and embeddings are foundational infrastructure for modern AI applications. Every RAG system, semantic search engine, recommendation engine, and duplicate detection pipeline is built on these primitives. The good news: with sentence-transformers and ChromaDB, you can have a working semantic search system running locally in under 30 minutes. Master these building blocks and you’ll be equipped to build the AI-powered products that define the current era.



