Skip to content
Forward Deployed

Forward Deployed
Lessons and contents
Knowledge Graphs with Neo4jLesson 5 of 8
8 lessons
Learn

Vector indexes on graph nodes

Source
From the RAG & Agents Engineering course
By
Sourav Banerjee
Last updated
9 Nov 2026

Report an issue

When the question has no exact name

What you'll learn

  • Explain graph plus vector retrieval as two stages: similar candidates, then exact checks.
  • Trace the candidate budget in the lab, and why recall jumps at k=3 and k=5.
  • Diagnose the similar-but-ineligible candidate and per-row embedding calls.
  • Choose vector-first or graph-first retrieval from what the question pins down.

Lesson 3 of T13.2 assumed the question names its entities. A Larkspur customer writes I need a plan that lets me pull my data out from the Frankfurt side. No label, no plan id, no "export" and no "EU". A Cypher pattern has nothing to bind.

Vector search can find a starting point for wording like this. The failure is that it finds text that sounds right. A refund guide linked to the EU region can rank above every plan that actually supports export there.

Graph + vector hybrid

Graph plus vector retrieval uses a vector index to rank nodes whose text resembles the question, then uses graph relationships to keep only the candidates that meet the question's exact constraints. The vector stage handles vague wording; the graph stage handles facts such as "supports export" and "available in the EU". Keep the two stages visible: a candidate dropped at either stage then has a recorded reason.

Diagram: Question vector; Rank candidates; Check graph predicates; Deduplicate evidence; Answer with source textDiagram: Question vector; Rank candidates; Check graph predicates; Deduplicate evidence; Answer with source text

Figure 1: Vector-first retrieval. Ranking proposes; the graph predicates decide.

How the notebook builds the index

The notebook adds a vector index to the healthcare graph from lesson 2 of T13.2. The pieces, in the order it creates them:

Piece Notebook setting What it decides
Index health_providers_embeddings on HealthcareProvider.comprehensiveEmbedding which nodes and which property are searchable
Dimensions 1536 must equal the embedding model's output size
Similarity cosine how closeness between vectors is scored
Stored vectors each provider's bio, embedded inside the database and written to the node what text the ranking sees
Query the question embedded the same way, top 3 nodes the candidate budget k

Table 1: The notebook's vector index, piece by piece.

The 1536 dimensions belong to the notebook's embedding model, not to every model. Query vectors and stored vectors must come from the same model with the same dimensionality, or the scores mean nothing. After creating the index, list the vector indexes and wait until this one reports ONLINE before searching.

The notebook searches with the db.index.vector.queryNodes procedure. Current Neo4j vector-index documentation prefers a SEARCH clause on recent releases and marks the procedure deprecated from 2026.04. Check the deployed database version before copying either form.

Planted failure: one embedding call per row

The embedding step starts by matching every provider that TREATS a patient, then embeds the provider's bio. A MATCH returns one row per matching path, not one per provider. On the graph that lesson 2 of T13.2's import builds there are 368 TREATS edges, which means 368 embedding calls for 5 distinct bios (replayed from the CSV; no database or embedding call was run). We call this row multiplication. Reduce to distinct providers before calling anything that costs money.

Text and edges can disagree

The notebook's question asks for providers "in the area of dermatology". Only one bio mentions dermatology, Dr. Jessica Lee's. The graph links four providers to the Dermatology node, because the import flattened its rows. The vector stage and the graph stage would give different answers, and neither is the truth: one reads a bio, the other reads edges that lost their row context. Hybrid retrieval inherits the quality of both sources, which is why the next section measures them separately.

Candidate budget and constraints

The interactive lab runs Figure 1 on Larkspur's teaching catalogue: eight records, hand-placed two-dimensional vectors and a question vector of [1, 0]. It ranks every record by exact cosine similarity, keeps the top k, and checks each kept record for an outgoing SUPPORTS edge to export and an AVAILABLE_IN edge to the EU. These are illustrative vectors and invented records, not model embeddings and not a benchmark of Neo4j's approximate index.

Interactive lab: Candidates and graph constraints. Open full screen (opens in a new tab)

At k = 8 the lab's ledger shows every record with its score and its graph check:

Rank Record Cosine Graph check
1 Refund guide 0.995 Missing export
2 US Export 0.976 Missing EU
3 EU Team 0.936 Eligible
4 Backup guide 0.868 Missing export
5 EU Enterprise 0.768 Eligible
6 Billing guide 0.640 Missing export
7 Audit guide 0.496 Missing export
8 Legacy plan 0.351 Missing EU

Table 2: The lab's candidate ledger at k = 8. The two most similar records both fail a graph check.

The top two records are the similar-but-ineligible failure. The refund guide is linked to the EU and ranks first, but has no export edge. US Export supports export but is not in the EU. Moving the budget shows what that costs:

k Checked Eligible found Recall
2 2 0 / 2 0%
3 3 1 / 2 50%
4 4 1 / 2 50%
5 5 2 / 2 100%
8 8 2 / 2 100%

Table 3: Recall against the candidate budget, as the lab prints it.

The lab's recall divides surviving eligible records by every eligible record in a complete toy catalogue. A production system has no complete list; its recall needs independently labelled questions and answers.

Each unit of k is one more candidate scored and one more set of graph checks. At k = 5 the lab checks 5 records to find 2; at k = 8 it checks 8 and finds the same 2, so the last 3 checks add work and no answer. When an eligible plan is missing, move the budget before changing the schema. Raise k until recall stops changing on labelled questions, then stop.

What a bigger budget cannot fix

The graph check is a post-filter, so it can return fewer than k results, including none. A larger k gives the graph more candidates. It cannot fix a missing AVAILABLE_IN edge, an entity that was never resolved to the right plan, or an eligible plan whose text ranks below every budget you can afford. Authorisation is a separate constraint again: apply it to every branch of retrieval, and never treat it as a relevance filter that a bigger budget might relax.

Choosing the order

Vector-first is one of two orders. When the question names its constraints exactly, the graph can go first:

Vector-first (Figure 1) Graph-first
Starts from vague wording known ids and relationships
Finds candidates similar to the question every node meeting the constraints
Main risk eligible answer outside the top k no match for a vague question
Cost driver k times the graph checks graph degree and indexes
Larkspur example "pull my data out from the Frankfurt side" "EU plans with export"

Table 4: The two orders for combining vectors and graph constraints.

For "EU plans with export" the exact two-arm Cypher from lesson 1 of T13.2 returns both eligible plans over the whole graph, with no budget to tune. Use the vector stage when there is no exact starting point, and compare its recall curve with that exact match on the questions where both apply. Lesson 5 of T13.2 turns to where the graph's relationships come from when there is no CSV, only text.

Run the notebooks

The code for this lesson is in these notebooks. Colab runs one in your browser: install the packages its first cell imports and set the API keys it reads. To run every notebook as written, clone the repository and use its uv environment.

Notebook GitHub Colab
Vector indexing and embedding Open on GitHub Open in Colab

Key Takeaways

  • Vector search ranks text that resembles the question; graph checks confirm explicit relationships. Keep both stages visible so each dropped candidate has a reason.
  • Query and stored vectors need the same model and dimensionality. Check the database version for the current search syntax, wait for the index to be ONLINE, and embed distinct nodes, not matched rows.
  • In the lab, recall is 0% at k = 2, 50% at k = 3 and 100% at k = 5. A bigger budget cannot fix a missing edge or an unresolved entity, and authorisation applies to every branch.

Check yourself

1. Why keep vector candidate selection and graph relationship checks as separate stages?

Vector search finds text that resembles the question, and graph matching checks explicit relationships. In the lab the refund guide ranks first and US Export second, yet neither supports export in the EU. Separate stages record why each candidate was dropped.

2. In the graph filter lab, how many eligible plans survive at k = 2, 3 and 5, and what does raising k past 5 buy?

None at k = 2, EU Team at k = 3, and both EU Team and EU Enterprise at k = 5. Past that, each extra candidate is scored and checked without adding an answer on this fixture. A bigger k also cannot fix a missing relationship or an unresolved entity.

3. The notebook's embedding step matches every provider that treats a patient, then embeds the bio. What does that cost on the imported graph, and how do you fix it?

The match returns one row per TREATS edge, so the embedding runs once per row: 368 times for 5 distinct bios on the replayed graph. Reduce the rows to distinct providers before the embedding call.

Rate your confidence and keep notes on the module page.