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:

  1. Cache structural embeddings — Store kg.get_node_embedding() outputs in semantica/vector_store/utils/metadata_store.py
  2. Reduce candidate set — Aggressive metadata_filter before graph similarity calculation
  3. Parallel execution — Vector and graph retrieval are independent until Stage 4
  4. Approximate graph embeddings — Use faster graph algorithms when exact structural similarity is non‑critical

Summary

  • HybridSearch in semantica/vector_store/hybrid_search.py orchestrates 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() in semantica/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:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →