Build Semantic Search with Python and Sentence Transformers
Build a practical semantic search system with Python and Sentence Transformers. Learn how text becomes embeddings, compare queries with documents, rank results by similarity, and turn the prototype into a reusable search component.

Tools used: Python, VS Code, Jupyter Notebook, Sentence Transformers, Hugging Face, Git, GitHub
Prerequisites: Basic Python, lists/dictionaries, functions, installing Python packages, and basic understanding of machine-learning concepts.
Build Semantic Search with Python and Sentence Transformers
Traditional search often asks:
Does this document contain the same words as my query?
Semantic search asks a more useful question:
Does this document mean something similar to what the user is asking?
That difference is important.
Consider a search query:
How can I learn Python programming?A keyword-based system might prioritize documents containing:
learn
Python
programmingBut a semantic search system can also recognize that:
Beginner resources for becoming a Python developeris closely related even though the wording is different.
In this tutorial, we'll build a small semantic search engine with Python and Sentence Transformers.
You'll learn how to:
- represent text as embeddings
- encode documents
- encode search queries
- calculate similarity
- rank results
- handle short queries against longer documents
- turn the prototype into a reusable search function
- understand where this approach works well
- recognize when you need a vector database or reranker
This tutorial uses a small in-memory dataset so you can understand the retrieval process before introducing databases, APIs, or production infrastructure.
What We Are Building
Our final program will take a query such as:
How do I start learning Python?and search a small collection of documents such as:
Python is a beginner-friendly programming language widely used for web development, automation, data analysis, and AI.
Docker packages applications and their dependencies into portable containers.
React is a JavaScript library for building user interfaces.
AWS provides cloud infrastructure and managed services for applications.The system should rank the Python document near the top even though the query and document don't use exactly the same wording.
The basic architecture is:
The important idea is simple:
Text
↓
Embedding
↓
Vector
↓
Similarity
↓
RankingWhy Keyword Search Isn't Always Enough
Imagine your knowledge base contains:
Python is commonly used for machine learning and data analysis.A user searches:
What language should I learn for AI?There may be no exact match for:
language
learn
AIYet the document is clearly relevant.
Semantic search attempts to capture the meaning represented by the text rather than relying only on exact word overlap.
This makes it useful for:
- documentation search
- FAQ systems
- knowledge bases
- support systems
- recommendation systems
- job search
- article discovery
- RAG pipelines
- internal company search
First Understand Embeddings
An embedding converts text into a numerical representation.
For example, conceptually:
"Python is useful for AI"
↓
[0.12, -0.48, 0.71, 0.09, ...]The actual embedding contains many dimensions.
You should not think of those numbers as individual words.
Instead, think of the complete vector as a representation that can be compared with other vectors.
For example:
Document A
↓
[ ... ]
Document B
↓
[ ... ]
Query
↓
[ ... ]Then we can calculate how similar they are.
The overall idea becomes:
Similar meaning
↓
Similar vector representation
↓
Higher similarity scoreInstall Sentence Transformers
Create a project:
mkdir semantic-search
cd semantic-searchCreate a virtual environment.
On Windows:
python -m venv .venv
.venv\Scripts\activateOn Linux or macOS:
python3 -m venv .venv
source .venv/bin/activateInstall Sentence Transformers:
pip install -U sentence-transformersYou can verify the installation:
python -c "import sentence_transformers; print(sentence_transformers.__version__)"The exact version will depend on the environment at installation time.
Create the Document Collection
Create:
semantic_search.pyStart with a small corpus:
documents = [
{
"title": "Python for Beginners",
"text": "Python is a beginner-friendly programming language used for web development, automation, data analysis, and artificial intelligence."
},
{
"title": "Docker Fundamentals",
"text": "Docker packages applications and their dependencies into portable containers that can run consistently across environments."
},
{
"title": "React Fundamentals",
"text": "React is a JavaScript library for building user interfaces using reusable components."
},
{
"title": "AWS Cloud Basics",
"text": "AWS provides cloud infrastructure and managed services for building, deploying, and scaling applications."
},
{
"title": "Git Version Control",
"text": "Git is a distributed version control system used to track changes in source code and collaborate with other developers."
}
]This is intentionally small.
The purpose is to understand the retrieval pipeline before worrying about millions of documents.
Load an Embedding Model
Import Sentence Transformers:
from sentence_transformers import SentenceTransformerLoad a pretrained model:
model = SentenceTransformer("sentence-transformers/all-MiniLM-L6-v2")The model converts text into embeddings.
Now extract the document text:
texts = [document["text"] for document in documents]Encode the documents:
document_embeddings = model.encode(texts)Conceptually:
5 documents
↓
Embedding model
↓
5 vector representationsThe exact vector dimensions depend on the model you choose.
Encode a Search Query
Now create a query:
query = "How can I start learning Python?"Encode it:
query_embedding = model.encode(query)Now you have:
Documents
↓
Document embeddings
Query
↓
Query embeddingThe next question is:
How similar is the query to each document?
Calculate Similarity
Sentence Transformers provides utilities for comparing embeddings.
Import:
from sentence_transformers import utilCalculate similarities:
scores = util.cos_sim(query_embedding, document_embeddings)[0]Now scores contains a similarity value for each document.
Conceptually:
Python document → 0.78
Docker document → 0.21
React document → 0.19
AWS document → 0.25
Git document → 0.18The exact scores will vary.
What matters is the relative ranking.
Turn Scores Into Search Results
We need to associate every score with its document.
results = []
for index, score in enumerate(scores):
results.append({
"title": documents[index]["title"],
"score": float(score)
})Now sort the results:
results.sort(
key=lambda item: item["score"],
reverse=True
)Display them:
for result in results:
print(
f"{result['score']:.4f} - "
f"{result['title']}"
)A result could look conceptually like:
0.78 - Python for Beginners
0.25 - AWS Cloud Basics
0.21 - Docker Fundamentals
0.19 - React Fundamentals
0.18 - Git Version ControlThe important part is that the Python document rises to the top because its meaning is relevant to the query.
The Complete First Version
Put the pieces together:
from sentence_transformers import SentenceTransformer, util
documents = [
{
"title": "Python for Beginners",
"text": "Python is a beginner-friendly programming language used for web development, automation, data analysis, and artificial intelligence."
},
{
"title": "Docker Fundamentals",
"text": "Docker packages applications and their dependencies into portable containers that can run consistently across environments."
},
{
"title": "React Fundamentals",
"text": "React is a JavaScript library for building user interfaces using reusable components."
},
{
"title": "AWS Cloud Basics",
"text": "AWS provides cloud infrastructure and managed services for building, deploying, and scaling applications."
},
{
"title": "Git Version Control",
"text": "Git is a distributed version control system used to track changes in source code and collaborate with other developers."
}
]
model = SentenceTransformer(
"sentence-transformers/all-MiniLM-L6-v2"
)
texts = [document["text"] for document in documents]
document_embeddings = model.encode(texts)
query = "How can I start learning Python?"
query_embedding = model.encode(query)
scores = util.cos_sim(
query_embedding,
document_embeddings
)[0]
results = []
for index, score in enumerate(scores):
results.append({
"title": documents[index]["title"],
"score": float(score)
})
results.sort(
key=lambda item: item["score"],
reverse=True
)
for result in results:
print(
f"{result['score']:.4f} - "
f"{result['title']}"
)Run:
python semantic_search.pyYou now have a working semantic search prototype.
Why the Search Works
The important part isn't the loop.
It's this:
document_embeddings = model.encode(texts)and:
query_embedding = model.encode(query)The model maps both documents and the query into a comparable vector space.
Then:
util.cos_sim(
query_embedding,
document_embeddings
)compares them.
The search process becomes:
Documents
↓
Embeddings
↓
Stored in memory
User query
↓
Embedding
↓
Compare against document embeddings
↓
Similarity scores
↓
Sort
↓
Top resultsThat is the basic semantic retrieval pipeline.
Don't Re-Encode Documents on Every Search
Our first example works, but there is an important performance problem.
Imagine you have:
100,000 documentsYou should not regenerate all document embeddings every time someone searches.
Instead:
Documents
↓
Encode once
↓
Store embeddingsThen:
User query
↓
Encode query
↓
Compare with stored embeddingsConceptually:
This is one of the most important architectural ideas in semantic search.
Query and Document Encoding
For information-retrieval tasks, modern Sentence Transformers documentation provides specialized methods:
model.encode_query(...)and:
model.encode_document(...)For example:
query_embedding = model.encode_query(
"How can I learn Python?"
)
document_embeddings = model.encode_document(
texts
)These methods are designed for retrieval workloads and can use model-specific query/document prompts when available.
For models without specialized prompts, their behavior can be equivalent to the general encoding approach.
For a retrieval application, prefer the query/document-specific encoding methods when the selected model supports them.
A Cleaner Search Function
Once the basic version works, turn it into a reusable function.
def semantic_search(
query,
documents,
model,
document_embeddings,
top_k=3
):
query_embedding = model.encode_query(query)
scores = util.cos_sim(
query_embedding,
document_embeddings
)[0]
ranked = []
for index, score in enumerate(scores):
ranked.append({
"title": documents[index]["title"],
"text": documents[index]["text"],
"score": float(score)
})
ranked.sort(
key=lambda item: item["score"],
reverse=True
)
return ranked[:top_k]Now the application can simply call:
results = semantic_search(
"What should I learn for AI?",
documents,
model,
document_embeddings,
top_k=3
)And display the results.
Add a Search Interface
You don't need a frontend framework to make this useful.
Start with a command-line interface:
while True:
query = input("\nSearch: ").strip()
if query.lower() in {"exit", "quit"}:
break
results = semantic_search(
query,
documents,
model,
document_embeddings,
top_k=3
)
for result in results:
print(
f"\n{result['score']:.4f} "
f"{result['title']}"
)
print(result["text"])Now you can try queries such as:
Search: How do I learn AI?or:
Search: containerize my applicationor:
Search: frontend JavaScript frameworkThe system is no longer tied to exact wording.
Semantic Search vs Keyword Search
The difference becomes clearer with an example.
Suppose the document says:
Docker packages applications into portable containers.User query:
How do I package my application so it runs consistently?A keyword system may have limited overlap.
Semantic search can recognize the relationship between:
package applicationand:
portable containersThat is where embeddings become useful.
| Search approach | Main strength |
|---|---|
| Keyword search | Exact terms and lexical matching |
| Semantic search | Meaning and related concepts |
| Hybrid search | Combines lexical and semantic signals |
Semantic search isn't automatically better for every query.
Exact identifiers, names, error codes, and product numbers can benefit greatly from lexical matching.
Why Hybrid Search Can Be Better
Imagine someone searches:
ERR_CONNECTION_RESET nginx 502Exact tokens matter.
A semantic model might understand the overall meaning, but a keyword-based system can preserve important exact matches.
This is why production search systems often combine approaches.
Conceptually:
Keyword Search
+
Semantic Search
↓
Combined Ranking
↓
Better RetrievalFor Karyvio, this could become particularly useful for searching:
- developer resources
- job descriptions
- blog posts
- learning paths
- technical documentation
Choosing the Right Model
Not every embedding model is equally good for every task.
A general-purpose sentence embedding model may be enough for a small prototype.
For retrieval systems, use a model intended for information retrieval when appropriate.
Sentence Transformers provides models specifically trained for semantic search and distinguishes between symmetric and asymmetric search tasks.
Symmetric Search
The query and candidate text are roughly similar in length.
Example:
Query:
How do I learn Python?
Candidate:
How to learn Python online?Asymmetric Search
The query is short and the documents are longer.
Example:
Query:
What is Python?
Document:
Python is a general-purpose programming language...This distinction matters when selecting an embedding model.
Search Quality Depends on Your Data
A powerful model cannot fix badly prepared documents.
Suppose you have:
Document 1:
Python
Document 2:
Python is used for data science, automation, backend development, and AI.The second document contains far more useful semantic information.
For a knowledge-base system, you should think about:
- document quality
- document length
- chunking
- metadata
- duplicate content
- titles
- headings
- source information
The retrieval model is only one part of the system.
Chunking Matters for Larger Documents
Suppose you have a 5,000-word article.
You probably don't want one enormous embedding representing the entire article.
Instead, split it into meaningful chunks.
For example:
Article
↓
Introduction
↓
Section 1
↓
Section 2
↓
Section 3
↓
ConclusionOr use fixed-size chunks with sensible overlap.
Then:
Query
↓
Search chunks
↓
Retrieve relevant sectionsThis becomes especially important when building RAG systems.
Semantic Search and RAG
Semantic search is closely related to retrieval-augmented generation.
A basic RAG pipeline might look like:
The semantic search component is responsible for finding useful information.
The language model then uses that retrieved information to generate an answer.
So:
Semantic Search
=
Retrieval
RAG
=
Retrieval + GenerationUnderstanding semantic search first makes RAG much easier to understand.
When You Need a Vector Database
Our tutorial compares vectors directly in Python.
That's perfectly reasonable for a small dataset.
But eventually you may have:
100,000
1,000,000
10,000,000+documents or chunks.
At that point, you should investigate specialized retrieval infrastructure.
A typical architecture becomes:
Documents
↓
Embedding Model
↓
Vector Database
↓
Similarity Search
↓
Top K ResultsExamples of technologies you may encounter include:
- Qdrant
- pgvector
- Elasticsearch
- OpenSearch
- Milvus
- other vector-capable search systems
Don't introduce one simply because a tutorial says you need one.
Start with the simplest architecture that meets the requirement.
Retrieval and Reranking
A more advanced system can use two stages.
User Query
↓
Fast Retrieval
↓
Top 20–100 Candidates
↓
Reranker
↓
Top 5 ResultsThe first model quickly finds candidates.
A second model performs more detailed relevance scoring.
This retrieve-and-rerank architecture is useful when search quality matters more than the simplicity of a basic prototype.
Measure Search Quality
A search engine isn't successful just because it returns results.
Ask:
Are the top results actually useful?
Create a small evaluation set:
| Query | Expected top result |
|---|---|
| How do I learn Python? | Python learning |
| How do I containerize an app? | Docker |
| How do I build interfaces? | React |
| What is cloud infrastructure? | AWS |
| How do I track source changes? | Git |
Run these queries.
Then inspect:
Top 1
Top 3
Top 5If the right result repeatedly appears near the top, your retrieval system is moving in the right direction.
Karyvio Use Case: Search Across Developer Content
This is where semantic search becomes directly useful for a platform like Karyvio.
Imagine Karyvio has:
Blog Posts
Developer Resources
Learning Paths
Jobs
Career ArticlesA user searches:
I want to learn AWS for a backend careerA semantic search layer could potentially surface:
AWS Lambda Tutorial
AWS S3 Guide
Cloud DevOps Learning Path
Backend Developer Resources
Cloud Engineer JobsThe user didn't need to type:
AWS Lambda S3 cloud backend developerThe search system can work from the meaning of the request.
A Better Karyvio Search Architecture
A future Karyvio search system could look like:
Each result could retain metadata such as:
title
url
content type
category
technology
difficulty
tagsThat metadata can then be used to improve ranking and filtering.
Don't Replace Normal SEO With Semantic Search
Semantic search improves your site's internal discovery.
It does not replace normal search-engine optimization.
Your public pages still need:
- descriptive titles
- useful content
- crawlable URLs
- internal links
- appropriate metadata
- accessible page structure
- canonical handling where needed
- useful images
- clear topical relevance
Think of the two systems separately:
Google Search
↓
Finds Karyvio pages
Karyvio Semantic Search
↓
Helps users find content inside KaryvioBoth serve different purposes.
Common Mistakes
Encoding Everything on Every Search
Bad:
User query
↓
Re-encode all documents
↓
SearchBetter:
Documents
↓
Encode once
↓
Store embeddings
Query
↓
Encode
↓
Search stored embeddingsUsing the Wrong Model
A model optimized for one task may not be ideal for another.
Consider whether your task is:
Similarityor:
Information retrievalbefore selecting the model.
Ignoring Chunk Size
Huge documents can produce poor retrieval granularity.
Tiny chunks can lose context.
Chunking should follow the content structure and retrieval goal.
Returning Too Many Results
If your system returns:
Top 50users may have difficulty finding the useful result.
Start with a small top_k and measure whether the right information appears near the top.
Assuming High Similarity Means Correct
A high similarity score does not guarantee that the result actually answers the user's question.
Always evaluate retrieval quality with real queries.
A Practical Learning Path
If you want to go deeper into semantic search, follow this progression:
Python
↓
Embeddings
↓
Sentence Transformers
↓
Cosine Similarity
↓
Semantic Search
↓
Chunking
↓
Vector Database
↓
Hybrid Search
↓
Reranking
↓
RAG
↓
EvaluationDon't start with a complicated vector database before understanding what an embedding actually represents.
The simple Python prototype teaches the core idea.
Karyvio Project Challenge
Build a semantic search engine for technology learning resources.
Create at least 30 records covering:
Java
Python
JavaScript
TypeScript
React
Next.js
AWS
Docker
Kubernetes
Cybersecurity
AI
Machine Learning
Git
SQLEach record should contain:
{
"title": "Docker Containers vs Virtual Machines",
"category": "Cloud & DevOps",
"tags": [
"Docker",
"Containers",
"Virtual Machines"
],
"description": "..."
}Generate embeddings from the descriptive content.
Then test queries such as:
How can I package applications consistently?What should I learn for a cloud engineering career?How do I build frontend applications with JavaScript?Where can I learn about container orchestration?Measure whether the expected Karyvio resources appear in the top results.
That project is much closer to a real product feature than a toy example.
The Core Code to Remember
The entire concept can be reduced to a few important operations:
from sentence_transformers import SentenceTransformer
model = SentenceTransformer(
"sentence-transformers/all-MiniLM-L6-v2"
)
document_embeddings = model.encode_document(
documents
)
query_embedding = model.encode_query(
"How do I learn Python?"
)Then compare:
similarities = model.similarity(
query_embedding,
document_embeddings
)Then rank the results.
Everything else is engineering around this core idea:
Represent
↓
Compare
↓
Rank
↓
RetrieveFinal Takeaway
Semantic search is not magic.
It is a retrieval system built around representations of meaning.
The basic pipeline is:
Documents
↓
Embeddings
User Query
↓
Query Embedding
Both
↓
Similarity
↓
Ranking
↓
Relevant ResultsOnce you understand that pipeline, technologies such as vector databases, RAG, hybrid search, and reranking become much easier to reason about.
Start with five documents.
Then try fifty.
Then try a thousand.
Only introduce more infrastructure when the problem actually requires it.
The most important lesson is this:
Good semantic search is not just about generating embeddings. It is about retrieving the right information for the user's actual intent.




Comments (0)
Be the first to share your thoughts.