Skip to main content
VectorCypher is Khora’s retrieval engine: a hybrid engine that combines vector similarity search (pgvector) with Cypher graph traversal (Neo4j). Inspired by Graph RAG 2026 and HippoRAG 2, it excels at complex multi-hop queries while maintaining efficient simple lookups through intelligent query routing.

What it’s good at

VectorCypher shines when:
  • Multi-hop queries matter: “Who works on deals with companies that Alex mentioned?”
  • Graph traversal is essential: Navigate organizational hierarchies, deal chains, team structures
  • Relationship discovery is key: Find implicit connections across data sources
  • Mixed query complexity: Automatic routing adapts retrieval per query, so simple lookups stay fast while complex questions get full graph expansion
Because it traverses a knowledge graph with Cypher, VectorCypher needs a graph store: Neo4j on the production stack, or the embedded sqlite_lance graph store for local development. See Storage backends. For comprehensive extraction over 100% of chunks, pass engine_kwargs={"vectorcypher_config": VectorCypherConfig(skeleton_core_ratio=1.0)}. VectorCypher’s KET-RAG selectivity defaults to the top 50% of chunks but accepts a 1.0 override that extracts from every chunk.

Architecture Overview

Core Design Principles

  1. Dual-Node Architecture: Inspired by HippoRAG 2, maintains both Chunk and Entity nodes in Neo4j, linked via MENTIONED_IN relationships
  2. Query Routing: Intelligent classification routes queries to optimal search paths (vector-only for simple queries, full VectorCypher for complex)
  3. Skeleton-Based Extraction: Only core chunks (identified via PageRank, default 70%) get full LLM entity extraction, balancing cost and quality
  4. RRF Fusion: Reciprocal Rank Fusion combines vector and graph results with configurable weights
  5. Bi-Temporal Support: Tracks occurred_at (when an event happened) vs ingested_at (when Khora learned about it)

Key Components

VectorCypherEngine

The main engine class (src/khora/engines/vectorcypher/engine.py) implementing MemoryEngineProtocol:
Key Methods:

RecallResult Context

recall() returns RecallResult objects whose typed projections expose:
  • chunks: Matching text passages as RecallChunk entries (chunk.content)
  • entities: Entities mentioned in matching chunks
  • relationships: Connections between entities in the result set
  • documents: Full DocumentProjection rows for every document referenced by a chunk, entity, or relationship (always populated; see Source Document Population)
Callers that need a flat context string for an LLM render one with the public khora.context_text(result, max_chunks=...) helper:

Source Document Population

recall() always returns a RecallResult whose documents list holds full DocumentProjection rows for every document referenced by a chunk, entity, or relationship in the result. This is a producer-enforced invariant. Khora batch-fetches DocumentSource metadata after the engine returns (chunked at 1,000 IDs) and replaces the engine’s lightweight stubs in place. The engine itself uses the namespace-scoped coordinator facade for that lookup, so cross-namespace ids never leak through.
Entity-read methods (get_entity(), list_entities(), find_related_entities(), search_entities()) accept include_sources: bool = False to opt-in to per-entity source_documents population. All four require namespace_id= (kwarg-only) on every call. The IDOR close-out enforces this at the Protocol level on every storage backend.

VectorCypherRetriever

Code in src/khora/engines/vectorcypher/retriever.py implements the hybrid retrieval pipeline:
Retrieval Pipeline:
  1. Route Query: Classify as SIMPLE, MODERATE, or COMPLEX
  2. Embed Query: Generate query embedding via LiteLLM
  3. Vector Search: Find entry entities via pgvector similarity (with hnsw.ef_search = 100)
  4. Cypher Expand: Traverse graph to find related entities (if complex)
  5. Fetch Chunks: Get chunks via MENTIONED_IN relationships, with optional temporal sort
  6. RRF Fusion: Combine vector and graph results
  7. Recency Boost: Apply recency decay so newer chunks rank higher (configurable, tune down for evergreen corpora)

QueryComplexityRouter

Routes queries to optimal search paths (src/khora/engines/vectorcypher/router.py):
Routing Heuristics:

DualNodeManager

Code in src/khora/engines/vectorcypher/dual_nodes.py. Manages HippoRAG 2 dual-node structure in Neo4j:
Key Operations:
The temporal_sort parameter controls Cypher ordering: Neo4j already has an index on Chunk.occurred_at, so the temporal sort adds negligible overhead.

RRF Fusion

Code in src/khora/engines/vectorcypher/fusion.py. Combines vector and graph results using Reciprocal Rank Fusion:
RRF Formula:

Query Routing

VectorCypher uses intelligent query routing to balance performance and quality:

SIMPLE Queries (Vector-Only)

Characteristics:
  • Simple factual questions
  • Single entity mentions
  • Direct lookups
Path: Query → Embed → pgvector Search → Results Latency: Sub-200ms P95

MODERATE Queries (Shallow Graph)

Characteristics:
  • Single relationship exploration
  • Moderate entity complexity
  • One-hop connections
Path: Query → Embed → Entry Entities → Shallow Expand (depth=1) → Fusion Latency: Sub-400ms P95

COMPLEX Queries (Full VectorCypher)

Characteristics:
  • Multi-hop relationships
  • Comparisons across entities
  • Aggregations over graph structure
Path: Query → Embed → Entry Entities → Deep Expand (depth=2-3) → Fusion → Recency Latency: Sub-800ms P95

Recency scoring

After RRF fusion, VectorCypher applies a configurable recency boost so newer chunks rank higher. _calculate_recency_scores() uses max(occurred_at) from the result set as the reference point instead of datetime.now(UTC), so historical or benchmark data still produces meaningful recency discrimination regardless of when the query runs. A result “2 days before the newest result” always gets the same score, whether the data is from 2024 or 2026. Tune it with temporal_recency_weight and temporal_recency_decay_days (see Tuning), or set the weight to 0 for evergreen corpora.

Configuration

VectorCypherConfig

Via engine_kwargs (Khora Constructor)

The recommended way to pass VectorCypherConfig is through the engine_kwargs parameter on Khora:
The engine_kwargs dict is forwarded directly to the VectorCypherEngine constructor, which accepts vectorcypher_config as a keyword argument.

Via Environment Variables

Engine selection is constructor-only: pass engine="vectorcypher" to Khora(...) (it is also the default).

Via YAML

Requirements

Required:
  • PostgreSQL with pgvector extension
  • Neo4j (required, not optional)
Recommended:
  • Neo4j GDS library (for efficient entity vector search)
  • Neo4j 5.x+ for best performance

Performance Characteristics

Tuning Guide

core_ratio

Controls what percentage of chunks get full knowledge graph extraction:

graph_depth

Controls Cypher traversal depth for complex queries:

Fusion Weights

Controls blending of vector and graph results:

Adaptive Depth

When adaptive_depth_enabled=True (the default), the retriever dynamically adjusts graph traversal depth based on how many entry entities the vector search returns: The thresholds are configurable:
This prevents two failure modes: (1) candidate explosion when many entities each fan out at depth 2+, and (2) under-retrieval when very few entities match and a shallow traversal misses relevant connections.

Score Normalization

The fusion function weighted_rrf_normalized normalizes vector and graph scores to [0, 1] via min-max normalization before computing Reciprocal Rank Fusion. This matters when the two sources produce scores on very different scales (for example, cosine similarity scores in [0.3, 0.9] vs graph proximity scores in [0.01, 0.5]). Without normalization, the source with larger absolute scores dominates the fusion. Both the SIMPLE and COMPLEX retrieval paths normalize final scores to [0,1] using min-max normalization.

Search Index Improvements

Three PostgreSQL indexes improve query-time performance: The HNSW index rebuild with higher ef_construction improves recall at index-build time. More candidates are considered during graph construction, producing a higher-quality approximate nearest neighbor index. Query-time ef_search can be tuned separately via PostgreSQL’s SET hnsw.ef_search = N. Run the migration with:

Recent Improvements

Cross-encoder reranking. After the initial vector + Cypher retrieval, an optional cross-encoder model rescores the top candidates for precision. The model is cached across queries to avoid reload overhead, and inference runs in asyncio.to_thread to keep the event loop free. Enable/disable via KHORA_QUERY_ENABLE_RERANKING. Optional BM25 channel. VectorCypher can run BM25 full-text search as a separate retrieval channel alongside vector and Cypher graph traversal, fused via RRF to give keyword-exact matches a dedicated signal path. It is off by default: the standard recall() path fuses vector + graph only. Enable it with enable_bm25_channel=True (or KHORA_QUERY_ENABLE_BM25_CHANNEL=true). VectorCypher is the default engine when creating a Khora without an explicit engine= argument.
  • Tuning: Every VectorCypher knob, when to adjust it, and the tradeoff
  • Retrieval pipeline: How recall() flows through the engine
  • Storage backends: The PostgreSQL + pgvector + Neo4j stack and the embedded sqlite_lance alternative