Chapter 21 · Full-Text Search, Text Analysis, Relevance, and Hybrid Retrieval with Graph Context
Evaluate Search Quality with Queries/Judgments Instead of Tuning Only by Anecdotal Examples
Evaluate search quality with a versioned judged query set, precision/recall/MRR/nDCG-style metrics, failure slices, freshness tests, and repeatable regression gates instead of tuning to a handful of attractive demos.
Search teams often tune on the last embarrassing query: add a synonym for “camra,” boost a field for one product, change the analyzer, celebrate the demo, then unknowingly damage dozens of other intents. AtlasMart needs a search acceptance suite: versioned queries, relevance judgments, reproducible retrieval settings, ranking metrics, failure slices and explicit freshness/latency SLOs. The goal is not one perfect number; it is evidence that a change improves the intended population without unacceptable regressions elsewhere.
A judged query set is to search what a test suite is to application logic. Every analyzer/schema/query/rank change is an experiment. Freeze inputs and environment, capture ranked IDs, compute metrics, inspect failures, then decide whether to ship.
Learning outcomes
Create versioned binary/graded relevance judgments for representative AtlasMart search intents and edge cases.
Compute precision@k, recall@k, reciprocal rank and nDCG@k from ranked IDs without using full-text score as the ground truth.
Compare lexical-only and graph-aware variants with the same source data, sourceK/finalK and freshness state.
Build regression slices for misspellings, phrases, language, stock/inactive filtering, freshness, empty/long queries and security-sensitive cases.
Define a production search release gate that includes quality, tail latency, freshness, observability, rollback and bridge-to-vector/hybrid readiness.
Current Neo4j Database is 2026.07.1; current 5.26
LTS patch is 5.26.30. Version-sensitive examples
use explicit CYPHER 25. The mandatory lab uses
self-managed Neo4j Community 2026.07.1,
database neo4j, user neo4j, disposable
password atlasmart-course-2026, loopback Bolt
7687 and HTTP 7474. Neo4j 2026.x
supports Java 21/25. Optional application examples pin the
official Python driver to neo4j==6.3.0. No APOC,
GDS, embeddings, paid AI service, Aura subscription, or
Enterprise license is required for the chapter.
Full-text indexes and the
db.index.fulltext.* procedures used in the
mandatory lab are part of the normal Neo4j database feature set
and are reproducible in Community. Enterprise adds fine-grained
authorization and built-in metrics surfaces that can change what
full-text queries are allowed to return or what operational
counters are available. Aura also supports database full-text
functionality, but server configuration/filesystem/metrics
controls are platform-managed and must not be presented as
identical to self-managed Neo4j.
Lab contract and exact assumptions
| Dimension | Chapter 21 assumption |
|---|---|
| server | Neo4j Community 2026.07.1, single disposable local database |
| Cypher | Explicit CYPHER 25 for version-sensitive examples; current packaged configs default new databases to Cypher 25 from 2026.02 |
| Java | Java 21 or 25 for the 2026.07 server line |
| database/auth | neo4j / neo4j / atlasmart-course-2026 |
| transport | bolt://localhost:7687 and http://localhost:7474 only for disposable loopback lab; remote/production uses verified TLS |
| plugins | none required; no APOC/GDS/GenAI plugin |
| graph | 8 Product nodes, 4 Category nodes, 1 Store, 2 Customers, 8 STOCKED_AT edges, 2 REVIEWED edges |
| full-text | catalog node index uses english analyzer and synchronous updates; reviews relationship index uses english analyzer |
| score policy | never hard-code expected score numbers; verify candidate IDs/order and collect actual scores because Lucene/corpus changes can alter values |
| measurement | learner captures real query latency, ranking, freshness and index state; generated lesson does not claim server execution |
| Term | Mechanism-first meaning |
|---|---|
| lexical retrieval | Retrieval based on analyzed text terms and Lucene query semantics. It is different from graph traversal and different from embedding/vector similarity. |
| full-text index |
Lucene-backed Neo4j semantic index over one or more STRING
or LIST |
| schema | The labels or relationship types plus indexed properties associated with one full-text index. An entity qualifies when it has at least one indexed label/type and at least one indexed property. |
| tokenization | Breaking a character stream into searchable terms. The analyzer decides token boundaries, normalization, stemming and stop-word behavior. |
| analyzer | Index/query text-processing pipeline. The default is standard-no-stop-words; language analyzers can stem/filter language-specific terms. |
| query analyzer | Optional analyzer selected in queryNodes/queryRelationships options. It analyzes the query string only; it does not rebuild or reinterpret already-indexed tokens. |
| Lucene query string | The second argument to a full-text query procedure. It can contain Boolean, phrase, field and fuzzy query syntax; parameterizing it protects Cypher syntax but does not make Lucene operators literal. |
| score | Lucene relevance score returned with each hit. It orders results for that query/index; it is not a probability, confidence percentage or portable score scale. |
| candidate set | Top lexical hits retained before graph/business filtering or downstream rank fusion. Candidate limit is a retrieval-recall decision, not merely a UI page size. |
| eventually consistent index | Full-text mode that removes Lucene update work from the commit path and applies queued updates in the background, introducing a freshness window. |
| freshness SLO | Application requirement for how soon committed text must become searchable. It determines whether eventual consistency is acceptable and how staleness is measured. |
| judged query set | Versioned collection of user queries and relevance labels used to evaluate ranking changes reproducibly. |
| precision@k | Fraction of the first k returned results judged relevant. |
| recall@k | Fraction of all judged relevant items retrieved in the first k results. |
| MRR | Mean Reciprocal Rank: rewards returning the first relevant result near the top. |
| nDCG | Normalized Discounted Cumulative Gain: graded relevance metric that rewards putting highly relevant items earlier. |
| post-ranking | Reordering or filtering a retrieved candidate set using graph context, business rules, permissions or a separate ranking model. |
| rank fusion | Combining multiple retrieval lists by their rank positions instead of directly comparing source-specific raw scores. |
Direct-entry setup
Run the fixture and wait for indexes to be ONLINE. If you test eventual mode, force a refresh barrier before comparing relevance variants so freshness is not a confounder unless freshness is the thing being evaluated.
CYPHER 25
// Safe to rerun in the disposable Chapter 21 lab.
CREATE CONSTRAINT ch21_product_id IF NOT EXISTS
FOR (p:Product) REQUIRE p.productId IS UNIQUE;
CREATE CONSTRAINT ch21_category_id IF NOT EXISTS
FOR (c:Category) REQUIRE c.categoryId IS UNIQUE;
CREATE CONSTRAINT ch21_store_id IF NOT EXISTS
FOR (s:Store) REQUIRE s.storeId IS UNIQUE;
CREATE CONSTRAINT ch21_customer_id IF NOT EXISTS
FOR (c:Customer) REQUIRE c.customerId IS UNIQUE;
MERGE (cam:Category {categoryId:'CAT-21-CAM'}) SET cam.name='Cameras', cam.labTag='ch21'
MERGE (out:Category {categoryId:'CAT-21-OUT'}) SET out.name='Outdoor', out.labTag='ch21'
MERGE (sec:Category {categoryId:'CAT-21-SEC'}) SET sec.name='Security', sec.labTag='ch21'
MERGE (acc:Category {categoryId:'CAT-21-ACC'}) SET acc.name='Accessories', acc.labTag='ch21'
MERGE (st:Store {storeId:'ST-21-CENTRAL'}) SET st.name='AtlasMart Central', st.labTag='ch21'
MERGE (c1:Customer {customerId:'C-2101'}) SET c1.name='Mina Rahimi', c1.labTag='ch21'
MERGE (c2:Customer {customerId:'C-2102'}) SET c2.name='Omid Karimi', c2.labTag='ch21';
UNWIND [
{id:'P-2101',name:'Trail Camera Pro',description:'Weatherproof wildlife trail camera with infrared night vision and long battery life',tags:['wildlife','trail','infrared','outdoor'],active:true,category:'CAT-21-CAM',qty:5,featured:true},
{id:'P-2102',name:'Trail Camera Mini',description:'Compact wildlife camera for trails, gardens, and backyard monitoring',tags:['wildlife','trail','compact'],active:true,category:'CAT-21-CAM',qty:0,featured:false},
{id:'P-2103',name:'Indoor Security Camera',description:'Wi-Fi home security camera with motion alerts and night vision',tags:['security','indoor','night vision'],active:true,category:'CAT-21-SEC',qty:7,featured:false},
{id:'P-2104',name:'Trail Running Hydration Vest',description:'Lightweight hydration vest for long trail runs and mountain races',tags:['running','trail','hydration'],active:true,category:'CAT-21-OUT',qty:11,featured:false},
{id:'P-2105',name:'Action Camera 4K',description:'Water-resistant action sports camera for cycling, hiking, and travel',tags:['action','sports','camera'],active:true,category:'CAT-21-CAM',qty:3,featured:true},
{id:'P-2106',name:'Wildlife Field Guide',description:'Illustrated guide to birds and mammals for outdoor observation',tags:['wildlife','book','outdoor'],active:true,category:'CAT-21-OUT',qty:6,featured:false},
{id:'P-2107',name:'Solar Trail Charger',description:'Solar charger for outdoor cameras, sensors, and trail equipment',tags:['solar','trail','charger'],active:true,category:'CAT-21-ACC',qty:0,featured:false},
{id:'P-2108',name:'Refurbished Trail Camera',description:'Older trail camera unit retained for support reference only',tags:['trail','camera','refurbished'],active:false,category:'CAT-21-CAM',qty:2,featured:false}
] AS row
MERGE (p:Product {productId:row.id})
SET p.name=row.name, p.description=row.description, p.tags=row.tags,
p.active=row.active, p.featured=row.featured, p.labTag='ch21'
WITH row,p
MATCH (cat:Category {categoryId:row.category}), (st:Store {storeId:'ST-21-CENTRAL'})
MERGE (p)-[:IN_CATEGORY]->(cat)
MERGE (p)-[stock:STOCKED_AT]->(st)
SET stock.quantity=row.qty, stock.labTag='ch21';
MATCH (c1:Customer {customerId:'C-2101'}), (p1:Product {productId:'P-2101'})
MERGE (c1)-[r1:REVIEWED]->(p1)
SET r1.reviewId='REV-2101', r1.rating=5,
r1.message='Excellent night vision for wildlife at the cabin', r1.labTag='ch21';
MATCH (c2:Customer {customerId:'C-2102'}), (p4:Product {productId:'P-2104'})
MERGE (c2)-[r2:REVIEWED]->(p4)
SET r2.reviewId='REV-2102', r2.rating=4,
r2.message='Comfortable on long trail runs', r2.labTag='ch21';
CREATE FULLTEXT INDEX ch21_catalog_ft IF NOT EXISTS
FOR (p:Product) ON EACH [p.name, p.description, p.tags]
OPTIONS {indexConfig: {
`fulltext.analyzer`: 'english',
`fulltext.eventually_consistent`: false
}};
CREATE FULLTEXT INDEX ch21_reviews_ft IF NOT EXISTS
FOR ()-[r:REVIEWED]-() ON EACH [r.message]
OPTIONS {indexConfig: {`fulltext.analyzer`: 'english'}};
CALL db.awaitIndexes(300);
CYPHER 25
SHOW FULLTEXT INDEXES
YIELD name, state, populationPercent, entityType, labelsOrTypes,
properties, indexProvider, options, lastRead, readCount
WHERE name STARTS WITH 'ch21_'
RETURN * ORDER BY name;
MATCH (p:Product {labTag:'ch21'})
RETURN count(p) AS products,
count { (p)-[:STOCKED_AT]->(:Store {storeId:'ST-21-CENTRAL'}) } AS stockEdges;
// Deterministic graph invariant: products = 8 and stockEdges = 8.
// Full-text state invariant before search: both indexes are ONLINE and 100% populated.
1. Judgments describe user intent, not current ranking
Assign relevance labels before looking at the proposed ranking change whenever possible. Grade 3 can mean “ideal,” 2 “useful,” 1 “weakly relevant,” and 0/unlisted “not relevant.” Keep judgments versioned with intent notes. The fixture deliberately includes ambiguity: P-2106 contains “wildlife” but is a field guide, while P-2108 is a strong lexical trail-camera match that is inactive.
| Query ID | Intent | Graded relevant IDs |
|---|---|---|
| q1 | wildlife camera | P-2101=3, P-2102=2, P-2106=1 |
| q2 | trail camera | P-2101=3, P-2102=3, P-2108=1 |
| q3 | night vision camera | P-2101=3, P-2103=2 |
| q4 | camera outdoor | P-2101=3, P-2105=2, P-2102=1 |
For a “buy now at ST-21-CENTRAL” result set, out-of-stock P-2102 and inactive P-2108 may be judged non-eligible even if lexically relevant. Keep lexical-relevance judgments and final-business judgments as separate datasets when both questions matter.
2. Capture ranked IDs; never make Lucene score the label
Run each query with identical index/analyzer/sourceK settings and store product IDs in returned order. Keep actual scores as diagnostics, but the evaluator consumes ranks and human/business judgments. That prevents the system from “proving itself correct” using its own score.
// Capture ranked IDs for the four judged queries. Run each query separately and
// put the returned productId order into results.json; do not copy score values into the judgments.
CYPHER 25
CALL db.index.fulltext.queryNodes('ch21_catalog_ft', $q, {limit: 10})
YIELD node, score
RETURN node.productId AS productId, node.name AS name, score
ORDER BY score DESC;
{
"q1": ["P-2101", "P-2106", "P-2102"],
"q2": ["P-2101", "P-2102", "P-2108"],
"q3": ["P-2103", "P-2101"],
"q4": ["P-2105", "P-2101", "P-2102"]
}
The JSON above only demonstrates file format. Replace it with IDs captured from your Neo4j 2026.07.1 lab before interpreting metrics.
3. Evaluate with multiple metrics because they answer different questions
Precision@k asks how many shown items are relevant. Recall@k asks how much known relevant inventory was recovered. MRR emphasizes the first relevant hit. nDCG@k uses graded relevance and position. A retail autosuggest may prioritize early precision/MRR; a support discovery workflow may care more about recall.
#!/usr/bin/env python3
"""Evaluate AtlasMart Chapter 21 ranked product IDs captured from Neo4j.
Usage: python evaluate_search.py results.json
results.json example: {"q1":["P-2101","P-2102","P-2106"], ...}
"""
import json, math, sys
JUDGMENTS = {
"q1": {"query":"wildlife camera", "grades":{"P-2101":3,"P-2102":2,"P-2106":1}},
"q2": {"query":"trail camera", "grades":{"P-2101":3,"P-2102":3,"P-2108":1}},
"q3": {"query":"night vision camera", "grades":{"P-2101":3,"P-2103":2}},
"q4": {"query":"camera outdoor", "grades":{"P-2101":3,"P-2105":2,"P-2102":1}},
}
K = 3
def dcg(ids, grades, k):
total = 0.0
for rank, pid in enumerate(ids[:k], start=1):
gain = grades.get(pid, 0)
total += (2**gain - 1) / math.log2(rank + 1)
return total
def metrics(ids, grades, k):
rel = {pid for pid,g in grades.items() if g > 0}
top = ids[:k]
hits = [pid for pid in top if pid in rel]
precision = len(hits) / k
recall = len(hits) / len(rel) if rel else 0.0
rr = 0.0
for rank,pid in enumerate(ids, start=1):
if pid in rel:
rr = 1.0 / rank; break
ideal = [pid for pid,_ in sorted(grades.items(), key=lambda x:x[1], reverse=True)]
idcg = dcg(ideal, grades, k)
ndcg = dcg(ids, grades, k) / idcg if idcg else 0.0
return precision, recall, rr, ndcg
results = json.load(open(sys.argv[1], encoding='utf-8'))
rows=[]
for qid,spec in JUDGMENTS.items():
ids = results.get(qid, [])
p,r,rr,n = metrics(ids, spec['grades'], K)
rows.append((qid,p,r,rr,n))
print(f"{qid} P@{K}={p:.3f} R@{K}={r:.3f} RR={rr:.3f} nDCG@{K}={n:.3f}")
if rows:
print('macro', ' '.join([
f"P@{K}={sum(x[1] for x in rows)/len(rows):.3f}",
f"R@{K}={sum(x[2] for x in rows)/len(rows):.3f}",
f"MRR={sum(x[3] for x in rows)/len(rows):.3f}",
f"nDCG@{K}={sum(x[4] for x in rows)/len(rows):.3f}",
]))
python evaluate_search.py results.json
# The script prints measured metrics from your captured ranks.
# Do not paste fixed expected values into the course: ranking can change with corpus/analyzer/release.
4. Compare variants with controlled experiments
| Variant | Change exactly one factor | Hold constant |
|---|---|---|
| A | english analyzer catalog index | fixture, query set, sourceK, graph rules |
| B | different analyzer/schema | fixture, query set, sourceK, graph rules |
| C | lexical only | same index, same sourceK, finalK |
| D | lexical + graph eligibility/rank | same index/query set/sourceK |
| E | eventual consistency | same quality model but measure visibility lag separately |
Do not compare a cold rebuilt index, different corpus, different candidate count and new ranking rule in one experiment. If four things change, a better metric cannot tell you which mechanism helped.
5. Build failure slices, not only macro averages
| Slice | Why it can regress independently |
|---|---|
| exact product names / IDs | tokenization or stop-word changes can harm precision |
| misspellings / fuzzy | query parser and fuzzy expansion can add noise/cost |
| phrases | analyzer/token positions affect phrase behavior |
| multilingual | stemming/stop words/tokenization differ by language |
| inactive/out-of-stock | graph/business eligibility can reduce lexical recall intentionally |
| long/empty/operator-heavy queries | query grammar and candidate explosion risk |
| freshly updated products | eventual indexing changes visibility timing |
| security-sensitive roles | semantic-index conservative filtering can reduce available candidates |
6. Quality is only one release gate
A ranking variant that raises nDCG but doubles p99 latency or violates freshness is not automatically better. Search release evidence should join relevance, latency, resource, correctness and operations.
| Gate | Example evidence |
|---|---|
| quality | no unacceptable regression in P@k/R@k/MRR/nDCG overall and critical slices |
| latency | retrieval + graph expansion p50/p95/p99 under representative concurrency |
| freshness | commit-to-search visibility distribution meets chosen SLO |
| capacity | index/store growth, CPU/disk, result bytes and candidate expansion headroom |
| security | exact application role and tenant filters tested |
| operations | ONLINE/rebuild/recovery checks, dashboards/log correlation, rollback plan |
| versioning | server/Cypher/index provider/analyzer/config and judged-set versions recorded |
7. Wrong approach → failure → repair
| Wrong approach | Failure | Repair |
|---|---|---|
| tune from one anecdotal query | local fix harms unseen intents | versioned representative judged set |
| use full-text score as “truth” | self-referential evaluation | human/business judgments + rank metrics |
| optimize macro average only | critical language/security/typo slice regresses | slice metrics and minimum gates |
| change corpus/analyzer/rules together | causality disappears | one controlled change per experiment |
| ship relevance gain despite p99/freshness collapse | search quality improves while product SLO fails | multi-dimensional release gate |
8. Production runbook and bridge to Chapter 22
- Freeze fixture/corpus snapshot, server/Cypher/index/analyzer configuration and judged-set version.
- Wait for indexes ONLINE; if testing quality rather than freshness, synchronize eventual indexes.
- Warm the workload consistently and execute the complete query suite.
- Capture candidate IDs/ranks/scores, filter reasons, p50/p95/p99, zero-result rate and resource evidence.
- Compute quality metrics and slices.
- Review security/freshness/capacity regressions.
- Approve or reject the single change; retain rollback commands/index definition.
- When adding vector retrieval in Chapter 22, keep the lexical baseline unchanged and fuse ranked lists rather than raw score magnitudes.
9. Cleanup / reset
Run only against the disposable Chapter 21 lab. This removes the chapter indexes, synthetic graph entities and chapter constraints.
CYPHER 25
DROP INDEX ch21_catalog_ft IF EXISTS;
DROP INDEX ch21_reviews_ft IF EXISTS;
DROP INDEX ch21_freshness_ft IF EXISTS;
MATCH (n) WHERE n.labTag='ch21' DETACH DELETE n;
DROP CONSTRAINT ch21_product_id IF EXISTS;
DROP CONSTRAINT ch21_category_id IF EXISTS;
DROP CONSTRAINT ch21_store_id IF EXISTS;
DROP CONSTRAINT ch21_customer_id IF EXISTS;
10. Final production judgment
| Production decision | Evidence to require before changing the system |
|---|---|
| graph/workload fit | Search logs and judged queries show a lexical need; traversal-only or exact/text-index predicates are not sufficient. |
| correctness/non-guarantees | Document analyzer, query syntax contract, candidate limit, freshness mode and the fact that relevance score is not a probability. |
| model/cardinality/degree | Measure candidate counts and graph expansion fan-out; cap/bound traversal after retrieval. |
| latency | Track p50/p95/p99 for retrieval plus graph expansion separately; do not optimize only the Lucene call. |
| transactions/freshness | Choose synchronous vs eventual full-text updates from freshness SLO and write-path cost, not folklore. |
| memory/storage | Observe index size, page cache/store pressure, heap impact of eventual-consistency queues and result materialization. |
| CPU/disk/network | Correlate query rate, index update rate, store I/O and response bytes; large candidate sets can shift cost to application/network. |
| indexes/constraints | Keep business-key constraints separate from full-text access paths; wait for ONLINE before querying or benchmarking. |
| driver/pool/timeouts | Use bounded result limits, parameterized Cypher and explicit timeout/retry policy; avoid keeping sessions open while users inspect results. |
| security/tenant risk | Verify graph privileges plus semantic-index conservative filtering; do not assume index membership equals authorization. |
| backup/recovery | Include full-text index recreation/check behavior in recovery drills and verify search after restore rather than assuming index health. |
| observability | Community: SHOW FULLTEXT INDEXES + application logs/OS evidence. Enterprise: add supported metrics such as fulltext queried/populated counters. |
| testing/failure injection | Regression-test analyzer changes, misspellings, empty queries, high-result queries, stale-index windows, denied-data cases and graph-filter effects. |
| version/tier | Record Neo4j/Cypher/analyzer/index provider/platform versions; Aura/self-managed controls and metrics are not identical. |
| cost/migration | Account for reindex time, storage, write amplification, evaluation maintenance and eventual move to vector/hybrid retrieval. |
Check your understanding
- Why should judgments not be copied from current full-text scores?
- What does nDCG add beyond precision@k?
- Why keep failure slices?
- What must be held constant in a search experiment?
- What changes when Chapter 22 adds vector search?
Review the answers
1. The score is the system output being evaluated; using it as truth makes evaluation circular.
2. It uses graded relevance and discounts lower ranks, rewarding highly relevant items near the top.
3. Macro averages can hide regressions in languages, typo queries, security roles, freshness or exact-name intents.
4. Corpus/fixture, query/judgment set, sourceK/finalK, freshness state and all settings except the intended factor as far as practical.
5. A second retrieval source and ranking signal are introduced; full-text and vector lists must be ranked independently and fused deliberately, not compared by raw score.
Summary and next step
Chapter 21 now treats lexical search as an engineered retrieval system: schema + analyzer + query contract + freshness + bounded candidates + graph/business context + judged evaluation. Chapter 22 can add embeddings/vector search without destroying that discipline: semantic/vector scores remain source-specific, and hybrid retrieval will fuse independently ranked lexical and vector lists before graph-aware context assembly.
Authoritative references
- Current Neo4j versions — Current database release 2026.07.1 and current 5.26 LTS patch 5.26.30.
- Full-text indexes — Cypher 25 — Current schema, analyzer, query, score, eventual-consistency, SHOW FULLTEXT INDEXES and procedure semantics.
- Semantic indexes — Why full-text and vector indexes are explicit semantic retrieval systems and why raw cross-source scores should not be compared.
- Built-in full-text procedures — Current signatures for queryNodes/queryRelationships/listAvailableAnalyzers/awaitEventuallyConsistentIndexRefresh and limit/skip/query-analyzer options.
- Index configuration — Default analyzer, eventual-consistency queue model, background update settings and operational implications.
- Configuration settings — Current db.index.fulltext.* defaults, including standard-no-stop-words and eventual-consistency settings.
- Index syntax — Current CREATE/SHOW/DROP and semantic-index query syntax; SHOW FULLTEXT INDEXES is the supported filtered SHOW form.
- Security limitations for semantic indexes — Lucene-backed full-text/vector security filtering can conservatively return partial or zero results under fine-grained restrictions.
- Hybrid search developer guide — Current rank-fusion guidance for lexical, vector and structural sources; fuse ranks, not incomparable raw scores.
- Hybrid search engineering article — 2026 worked explanation of combining words, meaning and graph topology with rank-based fusion.
- Metrics — Enterprise-only built-in metrics surface and monitoring responsibilities.
- Metrics reference — Current full-text queried/populated counters; edition boundary is Enterprise.
- System requirements — Neo4j 2026.07 supported Java 21/25 and current OS/runtime boundaries.
- Python driver 6.3 — Official driver API used by the optional evaluation harness; current 6.3 supports Neo4j 2026.x.
- Vector SEARCH clause — Bridge to Chapter 22 and explicit current warning to rank vector/full-text sources independently.