Semantic Search with Vector Databases: A Developer's Guide
Semantic Search with Vector Databases: A Developer’s Guide
Imagine searching for “warm, cozy places to read” and getting results for a fireplace cafe—not because the words match, but because the meaning aligns. That’s the power of semantic search. Unlike traditional keyword-based search, semantic search understands intent and context. In this guide, I’ll walk you through building a semantic search system using vector databases, from embeddings to production deployment.
Why Semantic Search Matters
Traditional search relies on exact keyword matches. If a user types “affordable laptops,” a keyword search might miss results that say “budget-friendly notebooks” or “cheap computers.” Semantic search solves this by representing text as dense vectors (embeddings) that capture meaning. When you query, the system finds vectors closest in semantic space, not just lexical matches.
Real-world use cases:
- E-commerce product discovery
- Enterprise document retrieval
- Customer support ticket routing
- Recommendation systems
The Vector Database Landscape
Vector databases are purpose-built for storing and querying high-dimensional vectors. Here are the top contenders:
| Database | Type | Strengths |
|---|---|---|
| Pinecone | Managed | Zero ops, serverless, low latency |
| Weaviate | Open-source | Hybrid search, GraphQL, modular |
| Qdrant | Open-source | Rust-based, fast, disk-based |
| Milvus | Open-source | Distributed, GPU acceleration |
For this guide, I’ll use Weaviate because it’s developer-friendly and supports hybrid search out of the box.
Step 1: Generating Embeddings
Embeddings are the heart of semantic search. You can choose from several models:
- OpenAI embeddings (
text-embedding-ada-002): 1536 dimensions, good for general use - Sentence Transformers (e.g.,
all-MiniLM-L6-v2): 384 dimensions, lightweight - Cohere embeddings: 1024 dimensions, domain-specific options
Let’s generate embeddings using Python and Hugging Face’s Sentence Transformers:
1 | from sentence_transformers import SentenceTransformer |
Pro tip: Normalize embeddings to unit length for better cosine similarity accuracy.
1 | embeddings = embeddings / np.linalg.norm(embeddings, axis=1, keepdims=True) |
Step 2: Setting Up Weaviate
Spin up Weaviate using Docker Compose:
1 | # docker-compose.yml |
Start the service:
1 | docker-compose up -d |
Step 3: Creating a Schema and Inserting Data
Define a schema for your data. Here’s how to create a class for products:
1 | import weaviate |
Now insert data with precomputed embeddings:
1 | products = [ |
Step 4: Performing Semantic Search
Query with a natural language phrase:
1 | def semantic_search(query_text, top_k=5): |
Output:
1 | Cozy Fireplace Cafe - $5.5: A warm cafe with comfortable armchairs by the fireplace |
Notice it didn’t match “relax” or “book” literally—it understood the semantics.
Step 5: Hybrid Search (Vector + Keyword)
Pure semantic search can miss exact matches. Hybrid search combines vector and keyword scoring for the best of both worlds. In Weaviate, enable hybrid search:
1 | def hybrid_search(query_text, alpha=0.5, top_k=5): |
Step 6: Filtering and Metadata
Vector databases support metadata filtering. Let’s add a category and filter by price:
1 | # Add category to schema |
Step 7: Production Considerations
Scaling Vector Databases
- Indexing: Use HNSW (Hierarchical Navigable Small World) for high recall. In Weaviate, configure index parameters:
1 | class_config = { |
- Batching: Insert data in batches of 100-1000 for performance.
- Caching: Cache frequent queries using Redis or in-memory cache.
- Monitoring: Track latency, recall, and indexing speed with Prometheus.
Embedding Model Selection
| Model | Dimensions | Speed | Quality | Use Case |
|---|---|---|---|---|
all-MiniLM-L6-v2 |
384 | Fast | Good | General purpose, low latency |
text-embedding-ada-002 |
1536 | Slow | Excellent | High accuracy, budget for API costs |
BAAI/bge-large-en-v1.5 |
1024 | Medium | Very good | Open-source, competitive with OpenAI |
Rule of thumb: Start with all-MiniLM-L6-v2 for prototyping, then benchmark with larger models.
Step 8: Building a REST API (Java Example)
Let’s expose our search as a REST API using Spring Boot:
1 | // SearchController.java |
Step 9: Evaluation and Tuning
Measure search quality using:
- Recall@k: Fraction of relevant results in top k
- Mean Reciprocal Rank (MRR): Reciprocal rank of first relevant result
- Normalized Discounted Cumulative Gain (NDCG): Accounts for graded relevance
Create a test set with labeled queries and expected results. Use Weaviate’s built-in evaluation or custom scripts:
1 | from sklearn.metrics import ndcg_score |
Key Takeaways
- Semantic search captures meaning, not just keywords, using dense vector embeddings.
- Vector databases like Weaviate, Pinecone, and Qdrant are optimized for similarity search at scale.
- Hybrid search (vector + keyword) often outperforms pure semantic search in production.
- Embedding model choice impacts quality and latency; benchmark multiple models.
- Metadata filtering is essential for practical applications (e.g., price ranges, categories).
- Productionize with proper indexing, batching, caching, and monitoring.
- Evaluate using metrics like Recall@k and NDCG to iterate on your system.
Semantic search isn’t just a buzzword—it’s a practical tool that dramatically improves user experience. Start small, iterate, and watch your search results become smarter.