Chapter 09 · Constraints and Indexes: Range, Text, Point, Token, Full-Text, and Schema Enforcement
Range, Text, Point, and Token Lookup Indexes: Supported Predicates and Workload Fit
Match each AtlasMart predicate to the index family that can actually serve it.
Learning outcomes
Once integrity is explicit, AtlasMart needs predictable starting points for reads: price filters, product-name substring search, nearby-store/location queries, and simple label/type scans. Search-performance indexes are access paths selected by the planner; choosing the wrong family can add write/storage cost without serving the predicate that motivated it.
Map range, text, point and token lookup indexes to the predicates they can solve.
Distinguish property indexes from the default label/type token lookup indexes.
Use EXPLAIN/PROFILE to verify planner access-path choice instead of assuming it.
Create node and relationship indexes only for evidenced query patterns.
Identify over-indexing and unsupported predicate/index combinations.
The mandatory lab continues Neo4j Community
2026.07.1, database neo4j, explicit
CYPHER 25 for version-sensitive examples,
authentication enabled, no mandatory APOC/GDS plugin, and the
AtlasMart identifiers/model established in Chapters 01–08.
Neo4j 5.26.30 remains the LTS comparison line.
Community currently supports node and relationship
property-uniqueness constraints. Property-existence,
property-type and key constraints—and Cypher 25 graph
types—are Enterprise-only; Enterprise examples in this chapter
are optional and are never presented as Community output.
This generation environment does not run Neo4j or Docker.
Commands were checked against current official documentation
but were not executed here. Expected plans, counts, index
states and scores are therefore described by invariant rather
than fabricated as captured output. All disposable data uses
labTag='ch09'; all disposable schema objects are
named ch09_*. Never drop an index/constraint
merely because its name looks similar to a lab object—verify
SHOW INDEXES/SHOW CONSTRAINTS first.
1. Four search-performance index families solve different access problems
| Index family | Primary workload | Node/relationship | Important boundary |
|---|---|---|---|
| Range | Equality, inequalities, many other property predicates, ordering | Both | Default search-performance index; not specialized substring search |
| Text | STRING predicates, especially CONTAINS / ENDS WITH | Both | Single property only; string-focused |
| Point | Spatial distance and bounding-box predicates on POINT | Both | Single point property; not a general numeric range index |
| Token lookup | Node labels or relationship types | Separate node + relationship indexes | No property predicates; two lookup indexes normally exist by default |
Search-performance indexes are considered automatically by the cost planner. An index definition is therefore only a candidate access path; the final plan still depends on the complete query, statistics and available alternatives.
2. Build a deterministic AtlasMart search fixture
CYPHER 25CREATE CONSTRAINT product_id IF NOT EXISTSFOR (p:Product) REQUIRE p.productId IS UNIQUE;CREATE CONSTRAINT category_id IF NOT EXISTSFOR (c:Category) REQUIRE c.categoryId IS UNIQUE;CREATE CONSTRAINT order_id IF NOT EXISTSFOR (o:Order) REQUIRE o.orderId IS UNIQUE;CREATE CONSTRAINT ch09_contains_line_id_unique IF NOT EXISTSFOR ()-[r:CONTAINS]-() REQUIRE r.lineId IS UNIQUE;CYPHER 25MERGE (c1:Category {categoryId:'CAT-9001'}) SET c1.name='Field Electronics',c1.labTag='ch09'MERGE (c2:Category {categoryId:'CAT-9002'}) SET c2.name='Power',c2.labTag='ch09'MERGE (p1:Product {productId:'P-9001'}) SET p1.name='Trail Camera Pro',p1.description='weatherproof wildlife camera with night vision',p1.categoryCode='CAM',p1.price=189.0,p1.location=point({latitude:40.7128,longitude:-74.0060}),p1.labTag='ch09'MERGE (p2:Product {productId:'P-9002'}) SET p2.name='Trail Camera Mini',p2.description='compact wildlife camera',p2.categoryCode='CAM',p2.price=99.0,p2.location=point({latitude:40.7138,longitude:-74.0040}),p2.labTag='ch09'MERGE (p3:Product {productId:'P-9003'}) SET p3.name='Field Battery',p3.description='high capacity outdoor battery pack',p3.categoryCode='PWR',p3.price=49.0,p3.location=point({latitude:40.7100,longitude:-74.0100}),p3.labTag='ch09'MERGE (p1)-[:IN_CATEGORY]->(c1)MERGE (p2)-[:IN_CATEGORY]->(c1)MERGE (p3)-[:IN_CATEGORY]->(c2)MERGE (o:Order {orderId:'O-9001'}) SET o.labTag='ch09',o.status='PAID'MERGE (o)-[r1:CONTAINS]->(p1) SET r1.lineId='L-9001',r1.quantity=1,r1.unitPrice=189.0,r1.labTag='ch09'MERGE (o)-[r2:CONTAINS]->(p3) SET r2.lineId='L-9002',r2.quantity=2,r2.unitPrice=49.0,r2.labTag='ch09';
CYPHER 25SHOW INDEXESYIELD name,type,state,entityType,labelsOrTypes,properties,owningConstraintRETURN name,type,state,entityType,labelsOrTypes,properties,owningConstraintORDER BY type,name;
3. Range and relationship range indexes
AtlasMart filters product prices and occasionally starts from an
order-line unitPrice. Both are scalar-property
access patterns, so range indexes are appropriate candidates.
The relationship example matters because indexable graph state
is not limited to nodes.
CYPHER 25CREATE RANGE INDEX ch09_product_price_range IF NOT EXISTSFOR (p:Product) ON (p.price);CREATE RANGE INDEX ch09_contains_unitprice_range IF NOT EXISTSFOR ()-[r:CONTAINS]-() ON (r.unitPrice);CALL db.awaitIndexes(300);
CYPHER 25EXPLAIN MATCH (p:Product) WHERE p.price >= 100 RETURN p.productId,p.price;EXPLAIN MATCH ()-[r:CONTAINS]-() WHERE r.unitPrice >= 100 RETURN r.lineId,r.unitPrice;
Do not hard-code an operator name as a permanent acceptance
criterion. Verify that the plan uses an appropriate index-backed
access path and compare actual rows/database hits with
PROFILE on the target release and data
distribution.
4. Text indexes serve substring-oriented string predicates
CYPHER 25CREATE TEXT INDEX ch09_product_name_text IF NOT EXISTSFOR (p:Product) ON (p.name);CALL db.awaitIndex('ch09_product_name_text',300);EXPLAIN MATCH (p:Product) WHERE p.name CONTAINS 'Camera' RETURN p.productId,p.name;EXPLAIN MATCH (p:Product) WHERE p.name ENDS WITH 'Mini' RETURN p.productId,p.name;
If both a range and text index exist on the same string
property, the planner normally prefers the text index for
CONTAINS/ENDS WITH and the range index
for other supported predicates. That is a workload distinction,
not a declaration that one index family is globally “faster.”
5. Point and token lookup indexes answer different dimensions
CYPHER 25CREATE POINT INDEX ch09_product_location_point IF NOT EXISTSFOR (p:Product) ON (p.location);CALL db.awaitIndex('ch09_product_location_point',300);EXPLAIN MATCH (p:Product)WHERE point.distance(p.location,point({latitude:40.7128,longitude:-74.0060})) < 1000RETURN p.productId,p.name;
CYPHER 25SHOW LOOKUP INDEXESYIELD name,state,entityType,labelsOrTypes,propertiesRETURN name,state,entityType,labelsOrTypes,properties;EXPLAIN MATCH (p:Product) RETURN count(p);EXPLAIN MATCH ()-[r:CONTAINS]->() RETURN count(r);
Token lookup indexes solve the label/type token lookup; they
cannot solve p.price, p.name or any
other property predicate. Do not drop the default lookup indexes
as a “cleanup” experiment in a shared database.
6. Deliberately wrong: create an index for every visible property
Over-indexing increases schema-build work, storage and mutation overhead. The repair is query-first: record a representative query, its predicate shape, cardinality/selectivity, current plan and latency distribution, then create the smallest access path that changes evidence in the desired direction.
Check your understanding
- Which index family is specialized for CONTAINS and ENDS WITH on strings?
- What do token lookup indexes index?
- Can relationships have range/text/point indexes?
- Why use EXPLAIN before PROFILE?
- Why is “index every property” unsafe advice?
Review the answers
1. A text index.
2. Node labels and relationship types, not properties.
3. Yes, current search-performance index syntax supports relationship properties.
4. EXPLAIN shows the planned access path without executing; PROFILE executes and collects runtime evidence.
5. Indexes consume storage/build resources and add write maintenance while many may never serve a representative query.
Summary and next step
Index type follows predicate type. Next, the chapter examines composite order, selectivity and the lifecycle state that determines whether an index is actually usable.
Authoritative references
- Current Neo4j versions — Release/LTS snapshot used for this chapter.
- Constraints — Current constraint types and edition boundaries.
- Create constraints — Current uniqueness, existence, type and key syntax and backing-index behavior.
- Search-performance indexes — Range, text, point and token lookup index semantics.
- Show indexes — Index lifecycle, state, population and usage evidence.
- Full-text indexes — Full-text schema, analyzers, query procedures and eventual-consistency behavior.
- Built-in index procedures — db.awaitIndex(es) and full-text refresh/analyzer procedures.
- Create indexes — Current node/relationship index syntax and supported predicates.
- Index impact on performance — Planner behavior and evidence-based index heuristics.