You have a repository of technical documents, a product catalog, or a knowledge base. Exact keyword search falls short: a user queries “quick oven repair” but your content says “fast oven maintenance”. Zero results. Frustration. Abandonment.
The machine doesn’t understand meaning; it compares strings. Are the words the same? No. That’s why you need a different approach: embeddings. They transform text into numbers that represent intent, not characters. Now “quick repair” and “fast maintenance” become neighbors in a vector space. That’s semantic search.
Here at Meteora Web, we’ve integrated this technique in real projects: from internal search engines to customer-support chatbots. In this guide we’ll explore how embeddings work, which models to use, and how to build a semantic search system in Python. No abstract theory — working code, concrete examples, reasoned choices.
What are embeddings and why are they essential for semantic search?
An embedding is a dense vector representation of text: a list of numbers (typically 384 to 4096 dimensions) that captures meaning. Words or phrases with similar meanings have close vectors in a multidimensional space. Distance is measured using cosine similarity or dot product.
Why does it work? Embedding models (like Sentence-BERT, OpenAI ada-002, Cohere embed) are trained on massive text corpora to learn semantic relationships. It doesn’t matter if words differ; what matters is context and sense.
Practical example: suppose you need to search a database of manuals. The query “emergency printer shutdown” must find documents talking about “immediate device halt” even though words don’t match. With embeddings, the two texts will have a cosine > 0.8, while a classical lexical search would return zero.
Sponsored Protocol
Business benefit: reduced search time, higher user satisfaction, natural language queries. If your site or intranet has hundreds or thousands of documents, semantic search is a game changer.
How does the text-to-vector conversion work?
Take a sentence. Feed it into a pre‑trained model that returns an array of floats. It sounds like magic, but it’s math: the model converts each word into a base vector, then combines them (averaging, pooling, transformers) to produce a fixed vector for the whole sentence.
Let’s see a concrete example using sentence-transformers (open-source library based on BERT/GPT). The code below loads an Italian model, generates embeddings for two sentences, and computes similarity.
from sentence_transformers import SentenceTransformer, util
# Italian model: distiluse-base-multilingual-cased-v2
model = SentenceTransformer('distiluse-base-multilingual-cased-v2')
sentence1 = "quick oven repair"
sentence2 = "fast oven maintenance"
emb1 = model.encode(sentence1)
emb2 = model.encode(sentence2)
similarity = util.cos_sim(emb1, emb2)
print(f"Similarity: {similarity.item():.4f}") # Typical output: 0.85-0.95
With just a few lines we have a semantic similarity measure. This is the core of search: each document in your database is indexed as a vector, the user’s query is converted the same way, and you retrieve the nearest vectors.
Sponsored Protocol
Important: embeddings aren’t magic. If the model wasn’t trained on your domain (e.g., medical or legal language), results may be less accurate. Specialised models exist (BioBERT, Legal-BERT). For most business use cases, a multilingual or English‑specific model works well.
Which embedding models to choose for English (and multilingual) semantic search?
Not all models are equal. Here’s an overview of options for semantic search in English (most also support other languages):
- Sentence-Transformers (open source): pre‑trained on hundreds of languages. Our go‑to:
all-MiniLM-L6-v2(384 dim, fast, good quality) orall-mpnet-base-v2(768 dim, higher accuracy). - OpenAI Embeddings (paid API):
text-embedding-ada-002(1536 dim). Powerful but requires API calls (cost per token) and data leaves your server. Good for prototyping, less for high‑volume production. - Cohere Embed (paid API): multilingual support, good for classification and search. Pay per call.
- Google Vertex AI Embeddings (Google Cloud API): supports English and many other languages, integrated with Google infrastructure.
We always start with open‑source models: zero API costs, data privacy, and optional fine‑tuning. For most SME projects, all-MiniLM-L6-v2 is more than enough. It only requires CPU time, and once vectors are indexed, latency is low (a few milliseconds per query).
Selection criteria:
Sponsored Protocol
- Document volume: millions of docs? Choose a smaller model (384 dim) and approximate vector indices (FAISS, Annoy).
- Required precision: highly technical queries may need larger models (768–1024 dim).
- Latency: real‑time (chatbot) requires fast encoding models.
- Privacy: don’t send sensitive data to external APIs; use local models.
How to build a semantic search system with embeddings and Python?
Here are the steps for an end‑to‑end solution. Assume we have a list of documents (article texts, product descriptions, FAQs). We’ll create a vector index and a query function.
1. Index the documents
from sentence_transformers import SentenceTransformer
import numpy as np
model = SentenceTransformer('all-MiniLM-L6-v2')
documents = [
"How to perform routine oven maintenance",
"Emergency shutdown procedure for laser printer",
"Quick repair of gas oven",
"Configuring printer on Wi-Fi network"
]
embeddings = model.encode(documents, show_progress_bar=True)
# Save to file or in memory
np.save('document_embeddings.npy', embeddings)
with open('documents.txt', 'w') as f:
for doc in documents:
f.write(doc + '\n')
2. Query the index
import numpy as np
from sentence_transformers import SentenceTransformer, util
model = SentenceTransformer('all-MiniLM-L6-v2')
embeddings = np.load('document_embeddings.npy')
with open('documents.txt', 'r') as f:
documents = [line.strip() for line in f]
query = "emergency printer stop"
query_embedding = model.encode(query)
scores = util.cos_sim(query_embedding, embeddings)[0]
top_indices = np.argsort(scores)[::-1][:3]
for i, idx in enumerate(top_indices):
print(f"{i+1}. {documents[idx]} (score: {scores[idx]:.4f})")
Typical output:
Sponsored Protocol
1. Emergency shutdown procedure for laser printer (score: 0.9123)
2. Configuring printer on Wi-Fi network (score: 0.4210)
3. How to perform routine oven maintenance (score: 0.3872)
The top result is semantically relevant even though “stop” doesn’t appear in the text.
3. Scale with vector indices
For >10,000 documents, brute‑force cosine becomes slow. Use FAISS (Facebook AI Similarity Search) for approximate search:
import faiss
import numpy as np
dimension = embeddings.shape[1]
index = faiss.IndexFlatIP(dimension) # Inner Product ≈ cosine after L2 normalization
faiss.normalize_L2(embeddings)
index.add(embeddings)
query_vec_norm = query_embedding / np.linalg.norm(query_embedding)
scores, indices = index.search(np.array([query_vec_norm]), k=3)
With FAISS you can handle millions of vectors in milliseconds. See official FAISS documentation.
How to evaluate embedding quality for semantic search?
Don’t just run the code — validate that the system returns useful results. Two practical metrics:
- Recall@k: how often the correct result appears in the top‑k results.
- Precision@k: how many of the top‑k results are truly relevant.
Create a small test set with queries and manually labelled relevant documents. Then compute metrics and compare different models.
Sponsored Protocol
Common mistake: using the pre‑trained model without adapting to your domain. If your documents contain specialised jargon, consider fine‑tuning with similar/dissimilar sentence pairs from your field. Sentence-Transformers makes this possible with just a few lines of code.
We, at Meteora Web, have seen a client boost precision from 40% to 78% simply by switching from a generic model to one fine‑tuned on machinery technical manuals. The training cost was negligible compared to the efficiency gain.
What to do next
- Identify your use case: site search, internal virtual assistant, product recommendation?
- Choose a model: start with
all-MiniLM-L6-v2orparaphrase-multilingual-MiniLM-L12-v2– open source, free, no data leaves your perimeter. - Index your content: extract clean text, generate embeddings, save to disk or a vector database (Chroma, Pinecone, Qdrant – but numpy is fine to start).
- Test with real queries: take 10–20 questions your users often ask and verify the results are relevant.
- Integrate into your frontend: a simple Flask or Express API that receives a query and returns the top‑N documents.
If you have questions about which model to adopt or want us to evaluate your dataset, get in touch. Semantic search is not a luxury — it’s what separates a frustrating user experience from one that converts.
Deepen your knowledge of the broader context with the Pillar Guide on LangChain and LLMs, where embeddings are one of the fundamental building blocks.