# Multi-hop questions break vector search. Here is a graph layer for Qdrant that fixes them.

> Source: <https://dev.to/demigoddsk/multi-hop-questions-break-vector-search-here-is-a-graph-layer-for-qdrant-that-fixes-them-35cl>
> Published: 2026-08-05 01:45:45+00:00

Qdrant answers "which vectors are near this query?" in milliseconds at

billion scale. But there's a class of questions where nearness is the

wrong criterion entirely:

"Where did the founder of the company that acquired Slack study?"

The passage that answers this talks about Marc Benioff and USC. It

never mentions Slack. Cosine similarity — any similarity — ranks it

low, because the answer doesn't *look like* the question. It's

*connected to* the question, three entity-hops away: Slack → acquired

by Salesforce → founded by Benioff → studied at USC. That's a topology

problem, and no amount of ANN speed solves a topology problem.

[hubmesh](https://github.com/DemigodDSK/hubmesh) is a small MIT

library that adds the topology layer on top of your existing Qdrant

collection. Qdrant keeps doing what it's best at (first-pass ANN);

hubmesh builds an entity–document graph at index time and, at query

time, diffuses Personalized PageRank from the question's entities over

that graph, fusing graph reachability with your cosine scores. No LLM

is involved at query time — retrieval is one sparse matrix iteration,

deterministic, roughly 100ms on a 30K-document corpus.

```
pip install "hubmesh[qdrant,kg]"
python -m spacy download en_core_web_sm
python
from hubmesh import Planner
from hubmesh.adapters import QdrantStore
from hubmesh.kg import build_entity_kg
import spacy

embed = ...  # your embedding callable: text -> np.ndarray

# any of: in-memory, on-disk, or your running Qdrant server
store = QdrantStore.from_documents(docs, url="http://localhost:6333")

# entity-document graph via spaCy NER — zero LLM tokens to build
nlp = spacy.load("en_core_web_sm")
kg = build_entity_kg(store.get_many(store.all_ids()), nlp=nlp)

planner = Planner(store=store, kg=kg, nlp=nlp, embed=embed)
result = planner.retrieve(
    "Where did the founder of the company that acquired Slack study?",
    top_k=10,
)
for path in result.reasoning:
    print(f"{path.score:.3f}  " + " -> ".join(path.node_ids))
# 0.031  ent:slack -> doc:acquisition -> ent:salesforce -> doc:benioff_bio
```

That `reasoning`

field is not a post-hoc explanation — it's the actual

graph route that surfaced each document, which means your RAG pipeline

can show *why* a passage was retrieved.

Each candidate document gets a composite of three signals, normalized

and combined (the formula descends from a network-topology paper —

NNSI, ICOMP'25 — where the same lesson appeared: no single centrality

metric identifies important nodes, but a weighted composite does):

| Benchmark | recall@10 vs naive cosine, same embeddings |
|---|---|
| HotpotQA full dev (7,405 q) | 75.2% vs 69.3% (+5.9 pts) |
| MuSiQue 2/3/4-hop | +6.0 / +3.2 / +5.0 pts |
| vs HippoRAG-style PPR-only, same graph | +29.8 pts |

Disclosed trade-off: the convergence term buys depth recall with

top-rank precision — recall@2 is 0.75 pts below naive on full dev. If

you retrieve with `top_k=2`

, disable it (`use_convergence=False`

).

Everything above reproduces with the scripts in `benchmarks/`

.

`retrieve`

accepts `seed_entities`

and `exclude_docs`

, so an agent can

iterate: retrieve, read, then aim hop two at the entity it just

discovered. There's an MCP server included (`hubmesh-mcp`

, listed in

the official MCP Registry) — the repo contains a field report of

Perplexity driving a 3-hop chain through it, tool call by tool call.

Single-hop corpora where similarity already wins; corpus-wide summary

questions ("what are the main themes?") — that's community-summary

territory (Microsoft GraphRAG's use case), a different query class.

hubmesh is for multi-hop factual retrieval, and it deliberately keeps

Qdrant as the geometry engine underneath.

*Repo: github.com/DemigodDSK/hubmesh · PyPI: pip install hubmesh · MIT*
