Range Index
Range indexes in FalkorDB allow the query planner to efficiently locate nodes and relationships by scanning only the entries that satisfy a filter, rather than performing a full label or type scan. They support equality, range, and geospatial predicates on string, numeric, geospatial, and scalar-array properties for both node labels and relationship types.
Supported Data Types
Range indexes support the following data types:
- String: Text values for exact matching and range queries
- Numeric: Integer and floating-point numbers for range comparisons
- Geospatial: Point data types for location-based queries
- Arrays: Single-property arrays containing scalar values (integers, floats, strings)
Note: Complex types like nested arrays, maps, or vectors are not supported for range indexing.
Creating an index for a node label
For a node label, the index creation syntax is:
graph.query("CREATE INDEX FOR (p:Person) ON (p.age)")
await graph.query("CREATE INDEX FOR (p:Person) ON (p.age)");
graph.query("CREATE INDEX FOR (p:Person) ON (p.age)").execute().await?;
graph.query("CREATE INDEX FOR (p:Person) ON (p.age)");
GRAPH.QUERY DEMO_GRAPH "CREATE INDEX FOR (p:Person) ON (p.age)"
An old syntax is also supported:
graph.query("CREATE INDEX ON :Person(age)")
await graph.query("CREATE INDEX ON :Person(age)");
graph.query("CREATE INDEX ON :Person(age)").execute().await?;
graph.query("CREATE INDEX ON :Person(age)");
GRAPH.QUERY DEMO_GRAPH "CREATE INDEX ON :Person(age)"
After an index is explicitly created, it will automatically be used by queries that reference that label and any indexed property in a filter.
result = graph.explain("MATCH (p:Person) WHERE p.age > 80 RETURN p")
print(result)
# Output:
# Results
# Project
# Index Scan | (p:Person)
const result = await graph.explain("MATCH (p:Person) WHERE p.age > 80 RETURN p");
console.log(result);
// Output:
// Results
// Project
// Index Scan | (p:Person)
let result = graph.explain("MATCH (p:Person) WHERE p.age > 80 RETURN p").execute().await?;
println!("{}", result);
// Output:
// Results
// Project
// Index Scan | (p:Person)
String result = graph.explain("MATCH (p:Person) WHERE p.age > 80 RETURN p");
System.out.println(result);
// Output:
// Results
// Project
// Index Scan | (p:Person)
GRAPH.EXPLAIN DEMO_GRAPH "MATCH (p:Person) WHERE p.age > 80 RETURN p"
1) "Results"
2) " Project"
3) " Index Scan | (p:Person)"
This can significantly improve the runtime of queries with very specific filters. An index on :employer(name), for example, will dramatically benefit the query:
result = graph.query("MATCH (:Employer {name: 'Dunder Mifflin'})-[:EMPLOYS]->(p:Person) RETURN p")
const result = await graph.query("MATCH (:Employer {name: 'Dunder Mifflin'})-[:EMPLOYS]->(p:Person) RETURN p");
let result = graph.query("MATCH (:Employer {name: 'Dunder Mifflin'})-[:EMPLOYS]->(p:Person) RETURN p").execute().await?;
ResultSet result = graph.query("MATCH (:Employer {name: 'Dunder Mifflin'})-[:EMPLOYS]->(p:Person) RETURN p");
GRAPH.QUERY DEMO_GRAPH
"MATCH (:Employer {name: 'Dunder Mifflin'})-[:EMPLOYS]->(p:Person) RETURN p"
An example of utilizing a geospatial index to find Employer nodes within 5 kilometers of Scranton are:
result = graph.query("WITH point({latitude:41.4045886, longitude:-75.6969532}) AS scranton MATCH (e:Employer) WHERE distance(e.location, scranton) < 5000 RETURN e")
const result = await graph.query("WITH point({latitude:41.4045886, longitude:-75.6969532}) AS scranton MATCH (e:Employer) WHERE distance(e.location, scranton) < 5000 RETURN e");
let result = graph.query("WITH point({latitude:41.4045886, longitude:-75.6969532}) AS scranton MATCH (e:Employer) WHERE distance(e.location, scranton) < 5000 RETURN e").execute().await?;
ResultSet result = graph.query("WITH point({latitude:41.4045886, longitude:-75.6969532}) AS scranton MATCH (e:Employer) WHERE distance(e.location, scranton) < 5000 RETURN e");
GRAPH.QUERY DEMO_GRAPH
"WITH point({latitude:41.4045886, longitude:-75.6969532}) AS scranton MATCH (e:Employer) WHERE distance(e.location, scranton) < 5000 RETURN e"
Geospatial indexes can currently only be leveraged with < and <= filters; matching nodes outside the given radius are matched using conventional traversal.
Creating an index for a relationship type
For a relationship type, the index creation syntax is:
graph.query("CREATE INDEX FOR ()-[f:FOLLOW]-() ON (f.created_at)")
await graph.query("CREATE INDEX FOR ()-[f:FOLLOW]-() ON (f.created_at)");
graph.query("CREATE INDEX FOR ()-[f:FOLLOW]-() ON (f.created_at)").execute().await?;
graph.query("CREATE INDEX FOR ()-[f:FOLLOW]-() ON (f.created_at)");
GRAPH.QUERY DEMO_GRAPH "CREATE INDEX FOR ()-[f:FOLLOW]-() ON (f.created_at)"
Then the execution plan for using the index:
result = graph.explain("MATCH (p:Person {id: 0})-[f:FOLLOW]->(fp) WHERE 0 < f.created_at AND f.created_at < 1000 RETURN fp")
print(result)
# Output:
# Results
# Project
# Edge By Index Scan | [f:FOLLOW]
# Node By Index Scan | (p:Person)
const result = await graph.explain("MATCH (p:Person {id: 0})-[f:FOLLOW]->(fp) WHERE 0 < f.created_at AND f.created_at < 1000 RETURN fp");
console.log(result);
// Output:
// Results
// Project
// Edge By Index Scan | [f:FOLLOW]
// Node By Index Scan | (p:Person)
let result = graph.explain("MATCH (p:Person {id: 0})-[f:FOLLOW]->(fp) WHERE 0 < f.created_at AND f.created_at < 1000 RETURN fp").execute().await?;
println!("{}", result);
// Output:
// Results
// Project
// Edge By Index Scan | [f:FOLLOW]
// Node By Index Scan | (p:Person)
String result = graph.explain("MATCH (p:Person {id: 0})-[f:FOLLOW]->(fp) WHERE 0 < f.created_at AND f.created_at < 1000 RETURN fp");
System.out.println(result);
// Output:
// Results
// Project
// Edge By Index Scan | [f:FOLLOW]
// Node By Index Scan | (p:Person)
GRAPH.EXPLAIN DEMO_GRAPH "MATCH (p:Person {id: 0})-[f:FOLLOW]->(fp) WHERE 0 < f.created_at AND f.created_at < 1000 RETURN fp"
1) "Results"
2) " Project"
3) " Edge By Index Scan | [f:FOLLOW]"
4) " Node By Index Scan | (p:Person)"
This can significantly improve the runtime of queries that traverse super nodes or when we want to start traverse from relationships.
Deleting an index for a node label
For a node label, the index deletion syntax is:
graph.query("DROP INDEX ON :Person(age)")
await graph.query("DROP INDEX ON :Person(age)");
graph.query("DROP INDEX ON :Person(age)").execute().await?;
graph.query("DROP INDEX ON :Person(age)");
GRAPH.QUERY DEMO_GRAPH "DROP INDEX ON :Person(age)"
Deleting an index for a relationship type
For a relationship type, the index deletion syntax is:
graph.query("DROP INDEX ON :FOLLOW(created_at)")
await graph.query("DROP INDEX ON :FOLLOW(created_at)");
graph.query("DROP INDEX ON :FOLLOW(created_at)").execute().await?;
graph.query("DROP INDEX ON :FOLLOW(created_at)");
GRAPH.QUERY DEMO_GRAPH "DROP INDEX ON :FOLLOW(created_at)"
Array Indices
FalkorDB supports indexing on array properties containing scalar values (e.g., integers, floats, strings), enabling efficient lookups for elements within such arrays.
Note: Complex types like nested arrays, maps, or vectors are not supported for indexing.
The following example demonstrates how to index and search an array property:
# Create a node with an array property
graph.query("CREATE (:Person {samples: [-21, 30.5, 0, 90, 3.14]})")
# Create an index on the array property
graph.query("CREATE INDEX FOR (p:Person) ON (p.samples)")
# Use the index to search for nodes containing a specific value in the array
result = graph.query("MATCH (p:Person) WHERE 90 IN p.samples RETURN p")
// Create a node with an array property
await graph.query("CREATE (:Person {samples: [-21, 30.5, 0, 90, 3.14]})");
// Create an index on the array property
await graph.query("CREATE INDEX FOR (p:Person) ON (p.samples)");
// Use the index to search for nodes containing a specific value in the array
const result = await graph.query("MATCH (p:Person) WHERE 90 IN p.samples RETURN p");
// Create a node with an array property
graph.query("CREATE (:Person {samples: [-21, 30.5, 0, 90, 3.14]})").execute().await?;
// Create an index on the array property
graph.query("CREATE INDEX FOR (p:Person) ON (p.samples)").execute().await?;
// Use the index to search for nodes containing a specific value in the array
let result = graph.query("MATCH (p:Person) WHERE 90 IN p.samples RETURN p").execute().await?;
// Create a node with an array property
graph.query("CREATE (:Person {samples: [-21, 30.5, 0, 90, 3.14]})");
// Create an index on the array property
graph.query("CREATE INDEX FOR (p:Person) ON (p.samples)");
// Use the index to search for nodes containing a specific value in the array
ResultSet result = graph.query("MATCH (p:Person) WHERE 90 IN p.samples RETURN p");
# Create a node with an array property
GRAPH.QUERY DEMO_GRAPH "CREATE (:Person {samples: [-21, 30.5, 0, 90, 3.14]})"
# Create an index on the array property
GRAPH.QUERY DEMO_GRAPH "CREATE INDEX FOR (p:Person) ON (p.samples)"
# Use the index to search for nodes containing a specific value in the array
GRAPH.QUERY DEMO_GRAPH "MATCH (p:Person) WHERE 90 IN p.samples RETURN p"
Verifying Index Usage
To verify that an index is being used by your query, use GRAPH.EXPLAIN before and after creating the index:
# Before creating the index
result = graph.explain("MATCH (p:Person) WHERE p.age > 30 RETURN p")
print(result) # Shows: Label Scan | (p:Person)
# Create the index
graph.query("CREATE INDEX FOR (p:Person) ON (p.age)")
# After creating the index
result = graph.explain("MATCH (p:Person) WHERE p.age > 30 RETURN p")
print(result) # Now shows: Index Scan | (p:Person)
// Before creating the index
let result = await graph.explain("MATCH (p:Person) WHERE p.age > 30 RETURN p");
console.log(result); // Shows: Label Scan | (p:Person)
// Create the index
await graph.query("CREATE INDEX FOR (p:Person) ON (p.age)");
// After creating the index
result = await graph.explain("MATCH (p:Person) WHERE p.age > 30 RETURN p");
console.log(result); // Now shows: Index Scan | (p:Person)
// Before creating the index
let result = graph.explain("MATCH (p:Person) WHERE p.age > 30 RETURN p").execute().await?;
println!("{}", result); // Shows: Label Scan | (p:Person)
// Create the index
graph.query("CREATE INDEX FOR (p:Person) ON (p.age)").execute().await?;
// After creating the index
let result = graph.explain("MATCH (p:Person) WHERE p.age > 30 RETURN p").execute().await?;
println!("{}", result); // Now shows: Index Scan | (p:Person)
// Before creating the index
String result = graph.explain("MATCH (p:Person) WHERE p.age > 30 RETURN p");
System.out.println(result); // Shows: Label Scan | (p:Person)
// Create the index
graph.query("CREATE INDEX FOR (p:Person) ON (p.age)");
// After creating the index
result = graph.explain("MATCH (p:Person) WHERE p.age > 30 RETURN p");
System.out.println(result); // Now shows: Index Scan | (p:Person)
# Before creating the index
GRAPH.EXPLAIN DEMO_GRAPH "MATCH (p:Person) WHERE p.age > 30 RETURN p"
# Output shows: Label Scan | (p:Person)
# Create the index
GRAPH.QUERY DEMO_GRAPH "CREATE INDEX FOR (p:Person) ON (p.age)"
# After creating the index
GRAPH.EXPLAIN DEMO_GRAPH "MATCH (p:Person) WHERE p.age > 30 RETURN p"
# Output now shows: Index Scan | (p:Person)
Index Management
Listing Existing Indexes
To view all indexes in your graph, use the db.indexes() procedure:
CALL db.indexes()
This returns information about all indexes including their type (RANGE), entity type (node/relationship), labels, and properties.
Performance Tradeoffs and Best Practices
When to Use Range Indexes
Range indexes are ideal for:
- Filtering by specific values: Queries with equality filters (e.g.,
WHERE p.name = 'Alice') - Range queries: Numeric or string comparisons (e.g.,
WHERE p.age > 30,WHERE p.name >= 'A' AND p.name < 'B') - Geospatial queries: Finding entities within a certain distance
- Array membership: Checking if a value exists in an array property
Performance Considerations
Benefits:
- Dramatically improves query performance for filtered searches
- Reduces the number of nodes/relationships that need to be scanned
- Enables efficient range scans and point lookups
Costs:
- Write overhead: Every insert or update to an indexed property requires updating the index
- Storage: Indexes consume additional memory and disk space
- Maintenance: Index structures need to be maintained during graph modifications
Recommendations:
- Index properties that are frequently used in
WHEREclauses - Avoid indexing properties that are rarely queried or have high write frequency
- For properties with very few distinct values (low cardinality), indexes may not provide significant benefits
- Monitor query performance with
GRAPH.PROFILEto validate index effectiveness
Example: Profiling Index Performance
// Profile query to see actual execution metrics
GRAPH.PROFILE DEMO_GRAPH "MATCH (p:Person) WHERE p.age > 30 RETURN p"
This shows detailed timing information and confirms whether the index was used.
Frequently Asked Questions 5
What data types can be range-indexed?
Range indexes support string, numeric, and geospatial (point) data types. They handle equality checks, comparisons (<, >, <=, >=), and string prefix operations.
Are range indexes used automatically?
Yes. When a WHERE clause filters on an indexed label-property pair, FalkorDB automatically introduces an index scan. Use GRAPH.EXPLAIN or GRAPH.PROFILE to verify index usage.
Can I have multiple range indexes on the same label?
Yes. You can create separate range indexes on different properties of the same label. Each index covers a single property.
Do range indexes support not-equal (< >) filters?
No. The current range index implementation does not optimize <> (not-equal) filters. These queries will still perform a full label scan.
What is the performance tradeoff of range indexes?
Range indexes speed up read queries with filters but add overhead on writes (inserts and updates to indexed properties) and consume additional memory. Index properties that are frequently filtered in WHERE clauses.