hubmesh

Centrality-aware GraphRAG retrieval planner. Drop-in layer over any vector DB.
hubmesh is a Python library that improves multi-hop RAG quality on top of an existing
vector database. You don't replace your infrastructure β you add a smart planner between
your vector DB and your LLM.
What problem this solves
Naive vector retrieval ("embed query, get top-k by cosine similarity") fails on multi-hop
questions like "Where was the founder of the company that acquired Slack born?" The
correct answer requires retrieving entities along a reasoning path, not the single most
similar item.
GraphRAG and HippoRAG showed that running a small Personalized PageRank over a knowledge
graph at query time can substantially improve multi-hop retrieval. hubmesh extends
that line with two contributions:
- Entity-anchored seeding, multi-component ranking. In KG mode the PPR
seeds are the question's own entities resolved against the corpus graph
(alias index; falls back to the entities of the top cosine matches when
the question names none); in kNN mode they are the ANN top-k. The
multi-component score β cosine relevance, pooled PPR mass, and
multi-anchor convergence, min-max normalized and fused 3:1:1 β is
applied to the document ranking, not to seed choice.
- Budget-aware context packing. Once relevant entities are scored, pack them into
the LLM's context window with explicit coverage and redundancy control rather than
just truncating top-k.
The multi-component scoring pattern is adapted from the NNSI framework
(Naidu et al., CCIS 2934, Springer, 2026)
for SDN topology optimization, repurposed here for retrieval planning.
Quickstart
In-memory (testing, small corpora)
from hubmesh import Planner
from hubmesh.adapters import InMemoryStore
embed = ...
docs = [...]
store = InMemoryStore.from_documents(docs, embed=embed)
planner = Planner(store=store, embed=embed)
result = planner.retrieve(query="...", top_k=10, budget_tokens=4000)
Qdrant adapter (production)
from hubmesh import Planner
from hubmesh.adapters import QdrantStore
store = QdrantStore.from_documents(docs)
store = QdrantStore.from_documents(docs, path="./qdrant_data")
store = QdrantStore.from_documents(docs, url="http://localhost:6333")
planner = Planner(store=store, embed=embed)
result = planner.retrieve(query="...", top_k=10)
Chroma adapter
from hubmesh.adapters import ChromaStore
store = ChromaStore.from_documents(docs)
store = ChromaStore.from_documents(docs, persist_directory="./chroma_data")
store = ChromaStore.from_documents(docs, host="localhost", port=8000)
Multi-hop / KG mode
from hubmesh.kg import build_entity_kg
import spacy
nlp = spacy.load("en_core_web_sm")
kg = build_entity_kg(docs, nlp=nlp)
planner = Planner(store=store, kg=kg, nlp=nlp, embed=embed)
result = planner.retrieve(query="Where was the founder of the company that bought Slack born?",
top_k=10, budget_tokens=4000)
for path in result.reasoning:
print(f" score={path.score:.3f} {' β '.join(path.node_ids)}")
from hubmesh.kg_llm import build_entity_kg_llm
from hubmesh.entity_linker import EmbeddingLinker, make_st_embedder
def llm(prompt):
return your_llm_call(prompt)
kg = build_entity_kg_llm(docs, llm=llm, cache_path="kg_cache.json")
kg = build_entity_kg_llm(docs, llm=llm, cache_path="kg_cache.json",
linker=EmbeddingLinker(embed=make_st_embedder()),
llm_identity="gpt-5-mini")
planner = Planner(store=store, kg=kg, nlp=nlp, embed=embed)
Better entity linking
from hubmesh.kg import build_entity_kg
from hubmesh.entity_linker import EmbeddingLinker, make_st_embedder
linker = EmbeddingLinker(embed=make_st_embedder(), threshold=0.82)
kg = build_entity_kg(docs, linker=linker)
Iterative multi-hop: let your agent drive
r1 = planner.retrieve(query=question, top_k=5)
r2 = planner.retrieve(
query=question, top_k=5,
seed_entities=["Nimbus Analytics"],
exclude_docs=[s.doc.id for s in r1.sources],
)
Seed mentions resolve through the alias index, so free-text entity names
work. The query path stays deterministic and LLM-free β the planning
intelligence lives in the caller.
MCP server: plug hubmesh into any agent
pip install "hubmesh[mcp]"
python -m spacy download en_core_web_sm
{"mcpServers": {"hubmesh": {"command": "hubmesh-mcp"}}}
Exposes the planner as deterministic operator tools over stdio β
index_corpus, retrieve (seed-steerable, as above), resolve_entities,
entity_neighbors, path_between, get_document, graph_stats,
list_corpora. Your agent is the solver: it decomposes the question,
reads each hop, and aims the next one; the server answers in
milliseconds with zero LLM calls. Corpora persist as plain JSON/NPZ
under ~/.hubmesh/corpora.
The server warms up models and persisted corpora in the background at
launch (~5-10s on first run), so tool calls stay fast from the start β
relevant for strict-timeout connector clients (Perplexity, etc.).
For web-based connector clients, serve SSE natively β no gateway
process needed:
export HUBMESH_API_KEY="$(openssl rand -hex 24)"
hubmesh-mcp --transport sse --port 8000 --allow-tunnel
ngrok http 8000
Tunneled serving requires the API key (the server refuses to start
without one) and defaults to read-only β pass --allow-writes to
keep index_corpus enabled. Clients must send
Authorization: Bearer <key>. If your connector client cannot set
headers, the tunnel edge must authenticate callers itself (ngrok
OAuth / IP-restriction traffic policy, Cloudflare Access, β¦) before
it adds the upstream header β injecting the header for anonymous
traffic hands every caller full read access (read-only protects corpora
from replacement, not from disclosure; get_document returns full
text). A client that can neither send the header nor sit behind an
authenticating edge is unsupported for private corpora.
Tunnel field notes (from a live Perplexity integration): ngrok works
(free tier included); cloudflared quick tunnels buffer SSE bodies
and hang tool calls; supergateway is unnecessary here and crashes
on reconnect. --allow-tunnel accepts the tunnel's forwarded Host
header β without it, proxied requests get 421 Misdirected Request.
Full field report β setup, error decoder, a 9/9 test battery run
through Perplexity, and two findings about reasoning-model behaviour β
in docs/perplexity.md.
Chunking long documents
from hubmesh import chunk_by_sentences, chunk_documents
chunks = chunk_documents(
[{"id": "doc1", "text": long_text}, ...],
strategy="sentences", target_tokens=200,
)
Installation
pip install hubmesh
pip install "hubmesh[qdrant]"
pip install "hubmesh[chroma]"
pip install "hubmesh[kg]"
pip install "hubmesh[linker]"
pip install "hubmesh[all]"
python -m spacy download en_core_web_sm
Design
KG mode β the benchmarked, production path:
query ββΊ spaCy NER ββΊ alias index ββΊ entity seeds ββΊ Personalized PageRank over the corpus KG
β (fallback: entities of the top-3 cosine documents) β
βββββββββββββΊ cosine similarity against every document ββββββββββββββββββββββββββββββ€
βΌ
3Β·minmax(cosine) + 1Β·minmax(pooled PPR) + 1Β·minmax(per-anchor geomean) [weighted sum]
βΌ
budget-aware packing ββΊ context + sources + reasoning paths
kNN mode (no KG; prototyping): first-pass ANN β capped induced proximity
subgraph β PPR from the ANN seeds β the same scoring and packing.
Community anchoring exists for single-topic retrieval and is off by
default.
Each layer is independently testable and replaceable. Adapters wrap your
existing vector DB so you don't have to migrate β note that KG mode
scores every document (vectors are gathered once per store version and
cached) and uses the store's ANN index only for the seed fallback.
Benchmarks
Supporting-fact paragraph recall over pooled distractor corpora. Every
row is a separate experiment: document representation and embedding
model change the absolute numbers materially, so rows are never
compared across representations. Protocol, ablations and limits are in
BENCHMARKS.md.
Full HotpotQA dev (7,405 questions, 66,581 pooled paragraphs),
hubmesh vs naive cosine, v0.4 defaults:
| representation Β· embedding | naive @10 | hubmesh @10 | Ξ @10 | Ξ @5 | Ξ @2 |
|---|
| body only Β· MiniLM-L6 | 69.3% | 75.2% | +5.90 | +4.21 | β0.75 |
| title+body Β· MiniLM-L6 | 70.0% | 77.3% | +7.24 | +5.88 | +0.25 |
| title+body Β· bge-m3 | 83.5% | 84.8% | +1.38 | β1.41 | β9.09 |
Read both directions. With a small embedding the graph layer adds 5β7
points of depth recall; with a strong one the depth gain shrinks to
+1.4 and the defaults hurt the top ranks (β9.1 at recall@2). The
convergence term trades top-rank precision for depth: for top-2/top-5
workloads on strong embeddings use use_convergence=False or plain
cosine, and evaluate on your own workload before turning the graph
layer on everywhere.
Full MuSiQue-Ans dev (2,417 questions, MiniLM, body only), hubmesh vs
naive, recall@10 with paired 95% CIs: +3.67 [+3.00, +4.39] overall
(+2.2 at @2, +3.3 at @5); by hop count +2.9 / +4.1 / +5.3
(n = 1,252 / 760 / 405). The gain grows with hop count, and on MuSiQue
hubmesh beats naive at recall@2 as well.
What the scoring adds (HotpotQA N=500, body only, recall@10): on the
same graph, seeds, fallback and packer, cosine-fused scoring reaches
0.871 against 0.676 for the pure structural (PPR-only) signal β
+19.5 pts [+16.3, +22.7] (MuSiQue N=300: +16.3). Earlier versions
quoted +29.8 against a HippoRAG-style ranker; that comparison also
changed the pipeline and is no longer cited as scoring attribution.
Convergence term (default on): +0.9 pts @10 over convergence-off on
full MuSiQue dev and +1.1 on HotpotQA N=500. A single-solve log-pooled
signal in the same slot matches it in aggregate; the geomean keeps ~1 pt
at three and four hops. Multi-seed queries cost ~1.5β2Γ (still zero LLM
tokens, deterministic).
Latency: ~22 ms mean / 26 ms p95 per query on a 7K-node KG (after PPR
matrix caching). ~3 s/query was measured at the 66K-paragraph full-dev
scale with convergence on, before the per-query vector re-gather was
removed; that scale has not been re-measured since.
Reproduce (each run writes a JSON with per-query records and a manifest
carrying the commit, dirty flag and source/harness content hashes):
python benchmarks/run_hotpotqa.py --n 500 --kg --out hotpot.json
python benchmarks/run_musique.py --n 300 --kg --out musique.json
python benchmarks/run_ablation_coherence.py --dataset musique --n 2417
python benchmarks/profile_query.py
Status
Pre-alpha (v0.4.2). Core algorithms implemented and validated; adapters for
in-memory, Qdrant, and Chroma; entity-linked KG with both spaCy NER and
LLM-based extraction (both linker-aware); alias-indexed entity resolution;
NNSI-KG scoring (multi-source convergence default-on, hub-discounted PPR
opt-in); agent-driven iterative multi-hop via seed_entities /
exclude_docs; MCP operator server (hubmesh-mcp, native SSE) with
JSON/NPZ corpus persistence; document chunking; reasoning-path
explanation; PPR-cache latency optimisation. Pinecone / pgvector / Weaviate adapters
and additional multi-hop benchmarks are tracked as
good first issues.
Acknowledgements
The multi-component scoring pattern is adapted from the Network Node Significance
Index (NNSI) framework introduced in:
D. S. K. Naidu et al., "A Framework for Improving Network Topology
Based on Graph Theory in Software-Defined Networking," in Internet Computing,
Internet of Things, Artificial Intelligence, and Applications, Communications
in Computer and Information Science, vol. 2934, H. R. Arabnia, L. Deligiannidis,
K. Ferens, F. Ghareh Mohammadi, F. Shenavarmasouleh, and S. Amirian, Eds.
Cham: Springer, 2026, pp. 3β18.
doi: 10.1007/978-3-032-22190-2_1
@inproceedings{naidu2026nnsi,
author = {Naidu, Datta Sai Krishna and others},
title = {A Framework for Improving Network Topology Based on Graph Theory
in Software-Defined Networking},
booktitle = {Internet Computing, Internet of Things, Artificial Intelligence,
and Applications},
series = {Communications in Computer and Information Science},
volume = {2934},
editor = {Arabnia, Hamid R. and Deligiannidis, Leonidas and Ferens, Ken and
Ghareh Mohammadi, Farid and Shenavarmasouleh, Farzan and
Amirian, Soheyla},
pages = {3--18},
publisher = {Springer},
address = {Cham},
year = {2026},
doi = {10.1007/978-3-032-22190-2_1},
isbn = {978-3-032-22189-6}
}
NNSI is repurposed here from SDN topology optimization to retrieval planning; the
application to retrieval over an entity-linked KG is new to this work.
License
MIT