Hybrid Search Architecture
Why Hybrid?
- Vector Similarity
- BM25 (Keyword)
- Hybrid
Strengths:Weaknesses:
- Finds conceptually similar content
- Works with synonyms and paraphrasing
- Understands context and intent
- May miss exact technical terms
- Can retrieve overly broad matches
Query Flow
Let’s trace a search query through OpenGround’s system.1. Query Input
From the CLI or MCP server:2. Query Embedding
Fromquery.py:94, the query is converted to a vector:
Query embedding uses the same model as document embedding, ensuring they’re in the same vector space.
3. Hybrid Search Execution
Fromquery.py:96-104, LanceDB performs the hybrid search:
1
Query Type: Hybrid
2
BM25 Component
content field.3
Vector Component
4
Metadata Filtering
5
Limit Results
4. Result Ranking
LanceDB internally combines scores from both search types:LanceDB’s hybrid search uses sophisticated score fusion techniques. The exact algorithm is internal to LanceDB.
5. Result Formatting
Fromquery.py:110-134, results are formatted for the user:
BM25 Full-Text Search
BM25 (Best Matching 25) is a probabilistic ranking function for keyword search.BM25 Index Creation
Fromingest.py:223-226, the full-text index is created after ingestion:
The
content field is indexed for full-text search. This enables BM25 scoring on chunk text.How BM25 Works
BM25 ranks documents based on:- Term Frequency (TF)
- Inverse Document Frequency (IDF)
- Document Length
- Combined Score
How often a query term appears in the document.Saturation: BM25 uses diminishing returns - 5 mentions isn’t 5x better than 1.
BM25 Example
Vector Similarity Search
Vector search finds chunks with embeddings close to the query embedding.Cosine Similarity
From mathematical perspective:Cosine similarity measures the angle between vectors, not their magnitude. Values range from -1 (opposite) to 1 (identical).
Normalized Embeddings
Fromembeddings.py:154, embeddings are normalized:
- Embeddings have unit length (magnitude = 1)
- Cosine similarity simplifies to dot product
- Faster computation:
similarity = dot(a, b)instead ofdot(a, b) / (norm(a) * norm(b))
Approximate Nearest Neighbor
LanceDB uses ANN (Approximate Nearest Neighbor) indexes for fast vector search:Metadata Filtering
Fromquery.py:98-103, filters are applied before ranking:
Why Filter First?
- Performance
- Accuracy
SQL Injection Prevention
Fromquery.py:46-65, user input is escaped:
Retrieving Full Content
Search results contain chunk content (800 chars). To get the full page, useget_full_content (from query.py:211-251):
1
Query All Chunks
Find all chunks belonging to the same URL and version.
2
Sort by Chunk Index
Ensure chunks are in original order (chunk_index: 0, 1, 2, …).
3
Concatenate Content
Join chunk content with double newlines to preserve formatting.
4
Format as Markdown
Return complete page with title, source, and full content.
Query Caching
Fromquery.py:12-15, database connections are cached:
Caching avoids reconnecting to the database for every query. Especially important for MCP server which handles many sequential requests.
Search Configuration
Fromconfig.py:69, default top K:
Performance Characteristics
- Query Latency
- Scalability
- Storage
- Embedding model speed (GPU vs CPU)
- Number of chunks in database
- Complexity of filters
Search Quality Tips
Write Clear Queries
Write Clear Queries
Good queries:
- “how to configure embeddings”
- “FastAPI dependency injection”
- “error handling best practices”
- “stuff” (too vague)
- “asdfasdf” (gibberish)
- Single words without context
Use Specific Technical Terms
Use Specific Technical Terms
BM25 rewards exact matches:
Filter by Version
Filter by Version
Always specify version for accurate results:
Use get_full_content for Context
Use get_full_content for Context
Search results are chunks (800 chars). For complete context:
Next Steps
Architecture
See how search fits into OpenGround’s architecture
Embeddings
Deep dive into the vector embeddings powering semantic search
Sources
Learn what documentation can be searched
CLI Reference
Complete reference for the query command