How to Implement Hybrid Search Combining Vector and Graph Similarity in Semantica
Semantica provides a composable pipeline that fuses dense vector similarity with knowledge‑graph structural similarity through weighted score combination and reciprocal rank fusion.
Implementing hybrid search combining vector and graph similarity requires orchestrating two distinct retrieval signals: semantic embeddings capture textual meaning, while graph embeddings encode relational structure. The Semantica open‑source framework exposes this capability through a modular architecture in semantica/vector_store/hybrid_search.py, allowing developers to plug in any vector store backend and graph embedding technique while maintaining consistent fusion logic. This article walks through the core components, their interactions, and production‑ready implementation patterns.
Core Hybrid Search Architecture
Semantica's hybrid search system separates concerns across five primary components. Understanding each role is essential for effective implementation.
Component Responsibilities
| Component | Location | Purpose |
|---|---|---|
HybridSearch |
semantica/vector_store/hybrid_search.py |
Orchestrates end‑to‑end flow: embedding generation, vector store queries, metadata filtering |
MetadataFilter |
semantica/vector_store/hybrid_search.py |
Builds boolean filter expressions for metadata fields |
SearchRanker |
semantica/vector_store/hybrid_search.py |
Implements fusion strategies (RRF, weighted average) |
HybridSimilarityCalculator |
semantica/vector_store/hybrid_similarity.py |
Computes weighted combination of semantic and structural similarity scores |
KnowledgeGraph |
semantica/kg/knowledge_graph.py |
Supplies structural embeddings and graph distance metrics |
This separation enables independent optimization of each retrieval channel—upgrade your vector store or swap graph embedding algorithms without touching fusion logic.
How Hybrid Search Executes
The pipeline progresses through six distinct stages, from query ingestion to final ranked results.
Stage 1: Dual Embedding Generation
HybridSearch handles semantic vector creation internally through its EmbeddingGenerator. For structural similarity, the knowledge graph provides parallel embeddings:
from semantica.kg.knowledge_graph import KnowledgeGraph
kg = KnowledgeGraph()
# Structural embedding for query concept
struct_query_vec = kg.get_node_embedding("refinance mortgage rates") # np.ndarray
The semantic embedding is dense and captures lexical/phrase meaning. The structural embedding encodes graph position—typically node2vec or GraphSAGE outputs representing relational context.
Stage 2: Vector Similarity Retrieval
HybridSearch.search() dispatches to the configured vector store (FAISS, Qdrant, SQLite, etc.):
from semantica.vector_store import HybridSearch, MetadataFilter
# Initialize with any VectorStore implementation
hybrid = HybridSearch(vector_store=faiss_store)
# Optional metadata pre‑filtering
filter_ = MetadataFilter().eq("category", "finance").gt("year", 2020)
vector_results = hybrid.search(
query="refinance mortgage rates",
k=20,
metadata_filter=filter_,
)
Results contain id, score, distance, and metadata fields. The distance is typically cosine distance—convert to similarity with 1.0 - distance.
Stage 3: Structural Embedding Attachment
Each candidate receives its graph embedding via KnowledgeGraph.get_node_embedding():
for res in vector_results:
node_id = res["id"]
res["struct_embedding"] = kg.get_node_embedding(node_id)
This stage is compute‑intensive for large candidate sets; consider caching graph embeddings in semantica/vector_store/metadata_store.py for repeated queries.
Stage 4: Hybrid Score Calculation
HybridSimilarityCalculator implements the core fusion logic in semantica/vector_store/hybrid_similarity.py. The default weighting is 0.7 semantic + 0.3 structural, configurable per query:
from semantica.vector_store.hybrid_similarity import HybridSimilarityCalculator
sim_calc = HybridSimilarityCalculator(
semantic_weight=0.7,
structural_weight=0.3,
)
# Manual calculation for illustration
for res in vector_results:
semantic_score = 1.0 - res["distance"]
structural_score = sim_calc._calculate_similarity(
struct_query_vec,
res["struct_embedding"],
metric="cosine",
)
res["hybrid_score"] = (0.7 * semantic_score) + (0.3 * structural_score)
The calculate_hybrid_similarity() method encapsulates this logic, accepting both embedding pairs and returning a unified score.
Stage 5: Result Fusion with SearchRanker
SearchRanker resolves conflicts when multiple scoring signals produce divergent rankings. It supports multiple strategies:
- Reciprocal Rank Fusion (RRF) — default; robust to different score scales
- Weighted Average — direct score combination
- Custom strategies — pluggable via registry pattern
from semantica.vector_store import SearchRanker
ranker = SearchRanker(strategy="reciprocal_rank_fusion")
final_results = ranker.rank([vector_results], k=10)
RRF handles the common case where semantic and structural rankings disagree—high semantic similarity but distant graph position, or vice versa.
Stage 6: Final Output
Returned results preserve original metadata with a unified score reflecting both signals:
for r in final_results[:5]:
print(f"{r['id']}: {r['score']:.4f} — {r['metadata']}")
Complete Implementation Pattern
The following pattern integrates all components for production deployment:
# -------------------------------------------------
# 1. Initialize all components
# -------------------------------------------------
from semantica.vector_store import HybridSearch, MetadataFilter, SearchRanker
from semantica.vector_store.hybrid_similarity import HybridSimilarityCalculator
from semantica.kg.knowledge_graph import KnowledgeGraph
vector_store = FaissStore(index_path="embeddings.faiss") # or QdrantStore, etc.
hybrid = HybridSearch(vector_store=vector_store)
kg = KnowledgeGraph(graph_path="knowledge_graph.db")
sim_calc = HybridSimilarityCalculator(semantic_weight=0.7, structural_weight=0.3)
# -------------------------------------------------
# 2. Build query with metadata constraints
# -------------------------------------------------
filter_ = (
MetadataFilter()
.eq("category", "finance")
.eq("region", "us")
.gt("confidence", 0.85)
)
# -------------------------------------------------
# 3. Execute hybrid retrieval
# -------------------------------------------------
query = "refinance mortgage rates"
# Vector search with filtering
candidates = hybrid.search(query=query, k=50, metadata_filter=filter_)
# Attach structural embeddings
struct_query = kg.get_node_embedding(query)
for c in candidates:
c["struct_embedding"] = kg.get_node_embedding(c["id"])
# Compute hybrid scores
scored = []
for c in candidates:
sem_score = 1.0 - c["distance"]
struct_score = sim_calc._calculate_similarity(
struct_query, c["struct_embedding"], metric="cosine"
)
c["hybrid_score"] = sim_calc.calculate_hybrid_similarity(
semantic_score=sem_score,
structural_score=struct_score,
)
scored.append(c)
# Fuse and rank
ranker = SearchRanker(strategy="reciprocal_rank_fusion")
results = ranker.rank([scored], k=10)
# -------------------------------------------------
# 4. Consume results
# -------------------------------------------------
for r in results:
print(f"{r['id']:20} score={r['score']:.4f} {r['metadata']}")
Configuration Options and Trade‑offs
Weight Tuning
The 0.7/0.3 default balances semantic dominance with structural influence. Adjust based on your domain:
- 0.9/0.1 — Pure semantic search with minor graph smoothing
- 0.5/0.5 — Equal contribution; useful when graph structure is highly reliable
- 0.3/0.7 — Structure‑dominant; for navigational queries in dense knowledge graphs
Pass weights to HybridSimilarityCalculator or override per‑query through the calculator's stateful methods.
Vector Store Selection
semantica/vector_store/registry.py registers implementations. Consider:
| Store | Best For | Hybrid Search Consideration |
|---|---|---|
| FAISS | High‑speed exact search | Excellent for prototyping; limited metadata filtering |
| Qdrant | Production filtering | Native metadata filtering reduces pre‑fetch volume |
| SQLite | Embedded deployments | Slower but zero external dependencies |
HybridSearch abstracts these differences—the same code works across backends.
Graph Embedding Strategies
KnowledgeGraph in semantica/kg/knowledge_graph.py supports multiple structural representations:
- Node embeddings — Pre‑computed node2vec/GraphSAGE outputs
- On‑demand encoding — Dynamic embedding generation for unseen nodes
- Raw graph distances — Shortest‑path or personalized PageRank when embeddings unavailable
For hybrid search, dense structural embeddings integrate most cleanly with HybridSimilarityCalculator.
Performance Considerations
Embedding two retrieval channels introduces computational overhead. Mitigation strategies:
- Cache structural embeddings — Store
kg.get_node_embedding()outputs insemantica/vector_store/utils/metadata_store.py - Reduce candidate set — Aggressive
metadata_filterbefore graph similarity calculation - Parallel execution — Vector and graph retrieval are independent until Stage 4
- Approximate graph embeddings — Use faster graph algorithms when exact structural similarity is non‑critical
Summary
- HybridSearch in
semantica/vector_store/hybrid_search.pyorchestrates vector retrieval, metadata filtering, and result ranking - HybridSimilarityCalculator computes weighted combinations of semantic and structural scores, defaulting to 0.7/0.3 weighting
- KnowledgeGraph provides structural embeddings through
get_node_embedding()insemantica/kg/knowledge_graph.py - SearchRanker applies Reciprocal Rank Fusion or weighted averaging to resolve multi‑signal rankings
- The architecture is backend‑agnostic—swap vector stores and graph algorithms without changing fusion logic
Frequently Asked Questions
How do I tune the semantic versus structural weighting?
Pass custom weights to HybridSimilarityCalculator(semantic_weight=0.6, structural_weight=0.4). For query‑specific tuning, instantiate multiple calculators or modify weights between calls. Higher semantic weights favor lexical matches; higher structural weights prioritize entities connected in the knowledge graph.
Can I use hybrid search without a pre‑built knowledge graph?
No—the structural component requires graph embeddings from KnowledgeGraph. However, semantica/kg/knowledge_graph.py supports incremental construction and on‑demand encoding. For pure vector search, use HybridSearch with structural weight set to 0.0, though this defeats the hybrid purpose.
Which fusion strategy should I choose for my use case?
Reciprocal Rank Fusion (RRF) is the safest default—it handles score scale mismatches between semantic and structural signals. Use Weighted Average only when both scores are calibrated to comparable ranges (e.g., both cosine similarities). Implement custom strategies by extending SearchRanker for domain‑specific ranking logic.
How does metadata filtering interact with the hybrid score?
MetadataFilter applies before structural similarity calculation, reducing the candidate pool that reaches HybridSimilarityCalculator. This improves latency but means filtered items receive no hybrid score. For post‑filtering by hybrid score, apply additional logic after ranker.rank() returns.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →