# How to Implement Hybrid Search Combining Vector and Graph Similarity in Semantica

> Learn to implement hybrid search combining vector and graph similarity with Semantica. Fuse dense vectors and knowledge graphs using weighted scores and reciprocal rank fusion.

- Repository: [Semantica /semantica](https://github.com/semantica-agi/semantica)
- Tags: how-to-guide
- Published: 2026-08-14

---

**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`](https://github.com/semantica-agi/semantica/blob/main/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`](https://github.com/semantica-agi/semantica/blob/main/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`](https://github.com/semantica-agi/semantica/blob/main/semantica/vector_store/hybrid_search.py) | Builds boolean filter expressions for metadata fields |
| **`SearchRanker`** | [`semantica/vector_store/hybrid_search.py`](https://github.com/semantica-agi/semantica/blob/main/semantica/vector_store/hybrid_search.py) | Implements fusion strategies (RRF, weighted average) |
| **`HybridSimilarityCalculator`** | [`semantica/vector_store/hybrid_similarity.py`](https://github.com/semantica-agi/semantica/blob/main/semantica/vector_store/hybrid_similarity.py) | Computes weighted combination of semantic and structural similarity scores |
| **`KnowledgeGraph`** | [`semantica/kg/knowledge_graph.py`](https://github.com/semantica-agi/semantica/blob/main/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:

```python
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.):

```python
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()`:

```python
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`](https://github.com/semantica-agi/semantica/blob/main/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`](https://github.com/semantica-agi/semantica/blob/main/semantica/vector_store/hybrid_similarity.py). The default weighting is **0.7 semantic + 0.3 structural**, configurable per query:

```python
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

```python
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:

```python
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:

```python

# -------------------------------------------------

# 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`](https://github.com/semantica-agi/semantica/blob/main/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`](https://github.com/semantica-agi/semantica/blob/main/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`](https://github.com/semantica-agi/semantica/blob/main/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`](https://github.com/semantica-agi/semantica/blob/main/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`](https://github.com/semantica-agi/semantica/blob/main/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`](https://github.com/semantica-agi/semantica/blob/main/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.