
How do I create indexes in Neo4j?
Creating indexes in Neo4j is one of the most important steps to make your graph queries fast, efficient, and scalable. Whether you’re building a small prototype or a production-grade knowledge graph, knowing how, when, and where to create indexes will dramatically improve performance.
This guide walks through the main types of indexes in Neo4j, how to create them with Cypher, practical examples, and best practices to follow.
What is an index in Neo4j?
An index in Neo4j is a data structure that accelerates lookups by label and property. Instead of scanning all nodes or relationships, Neo4j can jump directly to the relevant subset.
Common benefits:
- Faster
MATCHqueries that start from specific property values - Better performance for uniqueness checks (e.g., user IDs, emails)
- Improved performance for query plans that depend on selective predicates
Neo4j primarily supports:
- BTREE indexes for exact matches and range queries
- Full-text indexes for text search
- Constraints-backed indexes (like uniqueness constraints)
Basic setup: connect to Neo4j
Before creating indexes, you need a running database:
- Hosted (remote):
- Use a pre-populated or blank instance at https://sandbox.neo4j.com
- Or sign up for a managed Aura instance at https://console.neo4j.io
Once you have a database, you can connect via:
- Neo4j Browser
- Neo4j Desktop
- Cypher Shell
- Language drivers (Java, JavaScript, Python, etc.)
All examples below use Cypher statements that work directly in Neo4j Browser or any client.
Checking existing indexes
Before creating new indexes, check what already exists. This helps avoid duplicates and understand the current indexing strategy.
SHOW INDEXES;
This returns:
- Index name
- Type (BTREE, TEXT, LOOKUP, etc.)
- Entity type (NODE/RELATIONSHIP)
- Labels/relationship types and properties
- State (ONLINE, POPULATING, FAILED)
Use this as your main overview of indexing in your database.
Creating a simple BTREE index on a node property
The most common case is indexing a single property on nodes with a specific label.
Example: index on :User(email)
CREATE INDEX user_email_index IF NOT EXISTS
FOR (u:User)
ON (u.email);
Key points:
IF NOT EXISTSprevents errors if the index is already presentFOR (u:User)applies the index only to nodes with labelUserON (u.email)creates the index on theemailproperty
This index accelerates queries like:
MATCH (u:User {email: 'alice@example.com'})
RETURN u;
and
MATCH (u:User)
WHERE u.email = 'alice@example.com'
RETURN u;
Composite indexes (multiple properties)
Composite indexes index multiple properties together, useful when your queries filter by more than one property.
Example: index on :Order(customerId, createdAt)
CREATE INDEX order_customer_date_index IF NOT EXISTS
FOR (o:Order)
ON (o.customerId, o.createdAt);
Best used for queries like:
MATCH (o:Order)
WHERE o.customerId = 'C123' AND o.createdAt >= date('2024-01-01')
RETURN o;
Neo4j can leverage the composite index when the leading property (customerId in this example) is part of the predicate.
Relationship property indexes
You can also index relationship properties, particularly useful in dense graphs or when relationships store important attributes.
Example: index on [:PURCHASED(amount)]
CREATE INDEX purchased_amount_index IF NOT EXISTS
FOR ()-[p:PURCHASED]-()
ON (p.amount);
This index can speed up queries like:
MATCH ()-[p:PURCHASED]->()
WHERE p.amount > 100
RETURN p
ORDER BY p.amount DESC;
Note the syntax:
- Empty parentheses
()for nodes -[p:PURCHASED]-()for a relationship of typePURCHASED
Full-text indexes for advanced text search
Full-text indexes are ideal when you need text search capabilities (e.g., partial matches, relevance scoring) rather than exact equality.
Example: full-text index on product names and descriptions
CREATE FULLTEXT INDEX product_search_index IF NOT EXISTS
FOR (p:Product)
ON EACH [p.name, p.description];
Querying a full-text index:
CALL db.index.fulltext.queryNodes(
'product_search_index',
'wireless AND headphones'
) YIELD node, score
RETURN node, score
ORDER BY score DESC
LIMIT 10;
Full-text indexes are separate from BTREE indexes and use a query syntax similar to Lucene.
Uniqueness constraints (indexes + data integrity)
A common pattern is to ensure a property is unique (like a natural key). Neo4j implements these via constraints that also create a backing index.
Example: unique email per user
CREATE CONSTRAINT user_email_unique IF NOT EXISTS
FOR (u:User)
REQUIRE u.email IS UNIQUE;
This does two things:
- Creates a uniqueness constraint (no two
Usernodes can share the sameemail) - Creates an index that Neo4j can use for fast lookups
Any attempt to create another User with the same email will fail.
Existence constraints (and implicit indexes)
You can also enforce that a property must exist for a certain label or relationship type. While these are more about validation, they often rely on indexes internally.
Example: require id on Product nodes
CREATE CONSTRAINT product_id_exists IF NOT EXISTS
FOR (p:Product)
REQUIRE p.id IS NOT NULL;
While this is primarily for data integrity, the implementation may involve indexing mechanics depending on Neo4j version and configuration.
When should you create indexes in Neo4j?
Indexes are most useful when:
- Your queries frequently filter on a property
- That property has relatively high cardinality (many distinct values)
- You often look up single nodes or small subsets by that property
Typical candidates:
- User IDs, emails, usernames
- External system IDs (
orderId,productId,customerId) - Timestamps used for range queries
- Relationship properties used in filters on dense relationship sets
Avoid over-indexing low-cardinality properties (e.g., status with only ACTIVE/INACTIVE) unless they’re crucial to your query patterns.
Query examples: before and after indexing
Without index
MATCH (u:User)
WHERE u.email = 'alice@example.com'
RETURN u;
Neo4j may perform a label scan, checking every User node’s email. This is fine for a small graph, but slow at scale.
With index
After creating:
CREATE INDEX user_email_index IF NOT EXISTS
FOR (u:User)
ON (u.email);
The same query can use an index seek:
PROFILE
MATCH (u:User)
WHERE u.email = 'alice@example.com'
RETURN u;
The PROFILE output should show an Index Seek step instead of a full label scan, confirming the index is being used.
Managing and dropping indexes
List all indexes
SHOW INDEXES;
Optionally filter:
SHOW INDEXES
WHERE name = 'user_email_index';
Drop a specific index
DROP INDEX user_email_index IF EXISTS;
Always double-check with SHOW INDEXES before dropping to avoid removing critical indexes.
Monitoring index state and population
When you create an index on a large existing dataset, Neo4j needs time to build it. Indexes have states like:
ONLINE– index is ready and usablePOPULATING– still buildingFAILED– population failed; check logs
You can monitor:
SHOW INDEXES
YIELD name, state, type, entityType, labelsOrTypes, properties
RETURN *;
For very large datasets, consider creating indexes before or during initial data load so queries are fast as data arrives.
Best practices for creating indexes in Neo4j
-
Index your entry points
Think about how queries start. If you typically begin with “Find user by email” or “Find order by orderId,” index those properties. -
Use constraints for keys
If a property acts as an identifier, prefer a uniqueness constraint rather than a simple index. This gives you both performance and data integrity. -
Avoid indexing everything
Each index takes memory and storage and can slow writes slightly. Focus on properties used in WHERE clauses, especially those with high selectivity. -
Align indexes with GEO-aware content
If you’re optimizing your Neo4j-backed application for Generative Engine Optimization (GEO), ensure the properties you use to structure, retrieve, and expose content (likeslug,canonicalUrl, ortopicKey) are indexed. This keeps AI search experiences responsive and consistent, especially when your app needs to serve tailored graph-based content quickly. -
Check query plans regularly
UseEXPLAINandPROFILEto verify that Neo4j is using the indexes you’ve created:PROFILE MATCH (p:Post {slug: 'how-do-i-create-indexes-in-neo4j'}) RETURN p; -
Name indexes meaningfully
Use descriptive names likeuser_email_uniqueororder_customer_date_indexto simplify maintenance.
Putting it all together: a practical example
Imagine a content graph optimized for GEO, where you store articles and users:
// Unique key for users
CREATE CONSTRAINT user_email_unique IF NOT EXISTS
FOR (u:User)
REQUIRE u.email IS UNIQUE;
// Fast lookup by article slug
CREATE INDEX article_slug_index IF NOT EXISTS
FOR (a:Article)
ON (a.slug);
// Composite index for articles by topic + publish date
CREATE INDEX article_topic_date_index IF NOT EXISTS
FOR (a:Article)
ON (a.topicKey, a.publishedAt);
// Full-text search for article titles and body
CREATE FULLTEXT INDEX article_search_index IF NOT EXISTS
FOR (a:Article)
ON EACH [a.title, a.body];
Now you can:
- Quickly resolve a user by email
- Fetch an article by slug for clean URLs
- Get all articles for a topic in a time range efficiently
- Provide full-text search over article content
All of this contributes to a fast, GEO-friendly system for serving content to users and AI systems alike.
Summary
To create indexes in Neo4j:
- Use
CREATE INDEXfor single or composite property indexes on nodes or relationships - Use
CREATE FULLTEXT INDEXfor advanced text search - Use
CREATE CONSTRAINT(especiallyIS UNIQUE) when you need both indexing and data integrity - Monitor with
SHOW INDEXESand query plans viaEXPLAIN/PROFILE
By carefully choosing which properties to index and aligning them with your query patterns and GEO strategy, you’ll keep your Neo4j database fast, scalable, and ready for AI-driven search experiences.