Full-text indexing

FalkorDB leverages the indexing capabilities of RediSearch to provide full-text indices through procedure calls.

Creating a full-text index for a node label

To construct a full-text index on the title property of all nodes with label Movie, use the syntax:


graph.query("CALL db.idx.fulltext.createNodeIndex('Movie', 'title')")

await graph.query("CALL db.idx.fulltext.createNodeIndex('Movie', 'title')");

graph.query("CALL db.idx.fulltext.createNodeIndex('Movie', 'title')").execute().await?;

graph.query("CALL db.idx.fulltext.createNodeIndex('Movie', 'title')");

GRAPH.QUERY DEMO_GRAPH "CALL db.idx.fulltext.createNodeIndex('Movie', 'title')"

More properties can be added to this index by adding their names to the above set of arguments, or using this syntax again with the additional names.


graph.query("CALL db.idx.fulltext.createNodeIndex('Person', 'firstName', 'lastName')")

await graph.query("CALL db.idx.fulltext.createNodeIndex('Person', 'firstName', 'lastName')");

graph.query("CALL db.idx.fulltext.createNodeIndex('Person', 'firstName', 'lastName')").execute().await?;

graph.query("CALL db.idx.fulltext.createNodeIndex('Person', 'firstName', 'lastName')");

GRAPH.QUERY DEMO_GRAPH "CALL db.idx.fulltext.createNodeIndex('Person', 'firstName', 'lastName')"

Index configuration options:

  1. Language - Define which language to use for stemming text, which is adding the base form of a word to the index. This allows the query for “going” to also return results for “go” and “gone”, for example.
  2. Stopwords - These are words that are usually so common that they do not add much information to search, but take up a lot of space and CPU time in the index.

To construct a full-text index on the title property using German language and using custom stopwords of all nodes with label Movie, use the syntax:


graph.query("CALL db.idx.fulltext.createNodeIndex({ label: 'Movie', language: 'German', stopwords: ['a', 'ab'] }, 'title')")

await graph.query("CALL db.idx.fulltext.createNodeIndex({ label: 'Movie', language: 'German', stopwords: ['a', 'ab'] }, 'title')");

graph.query("CALL db.idx.fulltext.createNodeIndex({ label: 'Movie', language: 'German', stopwords: ['a', 'ab'] }, 'title')").execute().await?;

graph.query("CALL db.idx.fulltext.createNodeIndex({ label: 'Movie', language: 'German', stopwords: ['a', 'ab'] }, 'title')");

GRAPH.QUERY DEMO_GRAPH "CALL db.idx.fulltext.createNodeIndex({ label: 'Movie', language: 'German', stopwords: ['a', 'ab'] }, 'title')"

Additional field configuration options:

  1. Weight - The importance of the text in the field
  2. Nostem - Skip stemming when indexing text
  3. Phonetic - Enable phonetic search on the text

To construct a full-text index on the title property with phonetic search of all nodes with label Movie, use the syntax:


graph.query("CALL db.idx.fulltext.createNodeIndex('Movie', {field: 'title', phonetic: 'dm:en'})")

await graph.query("CALL db.idx.fulltext.createNodeIndex('Movie', {field: 'title', phonetic: 'dm:en'})");

graph.query("CALL db.idx.fulltext.createNodeIndex('Movie', {field: 'title', phonetic: 'dm:en'})").execute().await?;

graph.query("CALL db.idx.fulltext.createNodeIndex('Movie', {field: 'title', phonetic: 'dm:en'})");

GRAPH.QUERY DEMO_GRAPH "CALL db.idx.fulltext.createNodeIndex('Movie', {field: 'title', phonetic: 'dm:en'})"

Query Syntax and Features

FalkorDB uses RediSearch query syntax which provides powerful search capabilities including fuzzy matching, prefix matching, and tokenization.

Tokenization

When text is indexed, it is automatically tokenized (split into words). By default, text is split on whitespace and punctuation. This allows you to search for individual words within larger text fields.

For example, if you index a title property containing “The Lord of the Rings”, you can search for any of the individual words like “Lord” or “Rings”.

Prefix Matching

Prefix matching allows you to search for words that start with a specific prefix using the * wildcard. This is useful for autocomplete functionality or when you want to match word variations.


# Find all movies with titles containing words starting with "Jun"
result = graph.query("CALL db.idx.fulltext.queryNodes('Movie', 'Jun*') YIELD node RETURN node.title")
for record in result:
    print(record["node.title"])
# This would match "Jungle", "June", "Junior", etc.

// Find all movies with titles containing words starting with "Jun"
const result = await graph.query("CALL db.idx.fulltext.queryNodes('Movie', 'Jun*') YIELD node RETURN node.title");
for (const record of result.data) {
    console.log(record["node.title"]);
}
// This would match "Jungle", "June", "Junior", etc.

// Find all movies with titles containing words starting with "Jun"
let result = graph.query("CALL db.idx.fulltext.queryNodes('Movie', 'Jun*') YIELD node RETURN node.title").execute().await?;
for record in result.data() {
    println!("{}", record["node.title"]);
}
// This would match "Jungle", "June", "Junior", etc.

// Find all movies with titles containing words starting with "Jun"
ResultSet result = graph.query("CALL db.idx.fulltext.queryNodes('Movie', 'Jun*') YIELD node RETURN node.title");
for (Record record : result) {
    System.out.println(record.get("node.title"));
}
// This would match "Jungle", "June", "Junior", etc.

# Find all movies with titles containing words starting with "Jun"
GRAPH.QUERY DEMO_GRAPH "CALL db.idx.fulltext.queryNodes('Movie', 'Jun*') YIELD node RETURN node.title"
# This would match "Jungle", "June", "Junior", etc.

Note: Prefix matching only works at the end of a word (e.g., Jun*). The wildcard must appear at the end of the search term.

Fuzzy Matching

Fuzzy matching allows you to find words that are similar to your search term, accounting for typos and spelling variations. Use the % symbol followed by the Levenshtein distance (number of character changes allowed).


# Find movies with titles containing words similar to "Jangle" (allowing 1 character difference)
result = graph.query("CALL db.idx.fulltext.queryNodes('Movie', '%Jangle%1') YIELD node RETURN node.title")
for record in result:
    print(record["node.title"])
# This would match "Jungle" (1 character different)

# Allow up to 2 character differences
result = graph.query("CALL db.idx.fulltext.queryNodes('Movie', '%Jngle%2') YIELD node RETURN node.title")
# This would also match "Jungle" (1 character missing)

// Find movies with titles containing words similar to "Jangle" (allowing 1 character difference)
const result = await graph.query("CALL db.idx.fulltext.queryNodes('Movie', '%Jangle%1') YIELD node RETURN node.title");
for (const record of result.data) {
    console.log(record["node.title"]);
}
// This would match "Jungle" (1 character different)

// Allow up to 2 character differences
const result2 = await graph.query("CALL db.idx.fulltext.queryNodes('Movie', '%Jngle%2') YIELD node RETURN node.title");
// This would also match "Jungle" (1 character missing)

// Find movies with titles containing words similar to "Jangle" (allowing 1 character difference)
let result = graph.query("CALL db.idx.fulltext.queryNodes('Movie', '%Jangle%1') YIELD node RETURN node.title").execute().await?;
for record in result.data() {
    println!("{}", record["node.title"]);
}
// This would match "Jungle" (1 character different)

// Allow up to 2 character differences
let result2 = graph.query("CALL db.idx.fulltext.queryNodes('Movie', '%Jngle%2') YIELD node RETURN node.title").execute().await?;
// This would also match "Jungle" (1 character missing)

// Find movies with titles containing words similar to "Jangle" (allowing 1 character difference)
ResultSet result = graph.query("CALL db.idx.fulltext.queryNodes('Movie', '%Jangle%1') YIELD node RETURN node.title");
for (Record record : result) {
    System.out.println(record.get("node.title"));
}
// This would match "Jungle" (1 character different)

// Allow up to 2 character differences
ResultSet result2 = graph.query("CALL db.idx.fulltext.queryNodes('Movie', '%Jngle%2') YIELD node RETURN node.title");
// This would also match "Jungle" (1 character missing)

# Find movies with titles containing words similar to "Jangle" (allowing 1 character difference)
GRAPH.QUERY DEMO_GRAPH "CALL db.idx.fulltext.queryNodes('Movie', '%Jangle%1') YIELD node RETURN node.title"
# This would match "Jungle" (1 character different)

# Allow up to 2 character differences
GRAPH.QUERY DEMO_GRAPH "CALL db.idx.fulltext.queryNodes('Movie', '%Jngle%2') YIELD node RETURN node.title"
# This would also match "Jungle" (1 character missing)

Fuzzy matching syntax: %term%distance where:

  • term is the word to match
  • distance is the maximum Levenshtein distance (1-3, default is 1 if not specified)

Note: Fuzzy matching is computationally more expensive than exact or prefix matching, so use it judiciously on large datasets.

Combining Query Features

You can combine multiple search terms using boolean operators:

  • AND (or space): All terms must match
  • OR (|): At least one term must match
  • NOT (-): Term must not be present

# Find movies with "Jungle" AND "Book" in the title
result = graph.query("CALL db.idx.fulltext.queryNodes('Movie', 'Jungle Book') YIELD node RETURN node.title")

# Find movies with "Jungle" OR "Forest" in the title
result = graph.query("CALL db.idx.fulltext.queryNodes('Movie', 'Jungle|Forest') YIELD node RETURN node.title")

# Find movies with "Book" but NOT "Jungle"
result = graph.query("CALL db.idx.fulltext.queryNodes('Movie', 'Book -Jungle') YIELD node RETURN node.title")

# Combine prefix and fuzzy matching
result = graph.query("CALL db.idx.fulltext.queryNodes('Movie', 'Jun*|%Forst%1') YIELD node RETURN node.title")

// Find movies with "Jungle" AND "Book" in the title
const result = await graph.query("CALL db.idx.fulltext.queryNodes('Movie', 'Jungle Book') YIELD node RETURN node.title");

// Find movies with "Jungle" OR "Forest" in the title
const result2 = await graph.query("CALL db.idx.fulltext.queryNodes('Movie', 'Jungle|Forest') YIELD node RETURN node.title");

// Find movies with "Book" but NOT "Jungle"
const result3 = await graph.query("CALL db.idx.fulltext.queryNodes('Movie', 'Book -Jungle') YIELD node RETURN node.title");

// Combine prefix and fuzzy matching
const result4 = await graph.query("CALL db.idx.fulltext.queryNodes('Movie', 'Jun*|%Forst%1') YIELD node RETURN node.title");

// Find movies with "Jungle" AND "Book" in the title
let result = graph.query("CALL db.idx.fulltext.queryNodes('Movie', 'Jungle Book') YIELD node RETURN node.title").execute().await?;

// Find movies with "Jungle" OR "Forest" in the title
let result2 = graph.query("CALL db.idx.fulltext.queryNodes('Movie', 'Jungle|Forest') YIELD node RETURN node.title").execute().await?;

// Find movies with "Book" but NOT "Jungle"
let result3 = graph.query("CALL db.idx.fulltext.queryNodes('Movie', 'Book -Jungle') YIELD node RETURN node.title").execute().await?;

// Combine prefix and fuzzy matching
let result4 = graph.query("CALL db.idx.fulltext.queryNodes('Movie', 'Jun*|%Forst%1') YIELD node RETURN node.title").execute().await?;

// Find movies with "Jungle" AND "Book" in the title
ResultSet result = graph.query("CALL db.idx.fulltext.queryNodes('Movie', 'Jungle Book') YIELD node RETURN node.title");

// Find movies with "Jungle" OR "Forest" in the title
ResultSet result2 = graph.query("CALL db.idx.fulltext.queryNodes('Movie', 'Jungle|Forest') YIELD node RETURN node.title");

// Find movies with "Book" but NOT "Jungle"
ResultSet result3 = graph.query("CALL db.idx.fulltext.queryNodes('Movie', 'Book -Jungle') YIELD node RETURN node.title");

// Combine prefix and fuzzy matching
ResultSet result4 = graph.query("CALL db.idx.fulltext.queryNodes('Movie', 'Jun*|%Forst%1') YIELD node RETURN node.title");

# Find movies with "Jungle" AND "Book" in the title
GRAPH.QUERY DEMO_GRAPH "CALL db.idx.fulltext.queryNodes('Movie', 'Jungle Book') YIELD node RETURN node.title"

# Find movies with "Jungle" OR "Forest" in the title
GRAPH.QUERY DEMO_GRAPH "CALL db.idx.fulltext.queryNodes('Movie', 'Jungle|Forest') YIELD node RETURN node.title"

# Find movies with "Book" but NOT "Jungle"
GRAPH.QUERY DEMO_GRAPH "CALL db.idx.fulltext.queryNodes('Movie', 'Book -Jungle') YIELD node RETURN node.title"

# Combine prefix and fuzzy matching: Find "Jun*" OR words similar to "Forst"
GRAPH.QUERY DEMO_GRAPH "CALL db.idx.fulltext.queryNodes('Movie', 'Jun*|%Forst%1') YIELD node RETURN node.title"

For more advanced query syntax features, see the RediSearch query syntax documentation.

Utilizing a full-text index for a node label

An index can be invoked to match any whole words contained within:


result = graph.query("CALL db.idx.fulltext.queryNodes('Movie', 'Book') YIELD node RETURN node.title")
for record in result:
    print(record["node.title"])
# Output:
# The Jungle Book
# The Book of Life

const result = await graph.query("CALL db.idx.fulltext.queryNodes('Movie', 'Book') YIELD node RETURN node.title");
for (const record of result.data) {
    console.log(record["node.title"]);
}
// Output:
// The Jungle Book
// The Book of Life

let result = graph.query("CALL db.idx.fulltext.queryNodes('Movie', 'Book') YIELD node RETURN node.title").execute().await?;
for record in result.data() {
    println!("{}", record["node.title"]);
}
// Output:
// The Jungle Book
// The Book of Life

ResultSet result = graph.query("CALL db.idx.fulltext.queryNodes('Movie', 'Book') YIELD node RETURN node.title");
for (Record record : result) {
    System.out.println(record.get("node.title"));
}
// Output:
// The Jungle Book
// The Book of Life

GRAPH.QUERY DEMO_GRAPH
"CALL db.idx.fulltext.queryNodes('Movie', 'Book') YIELD node RETURN node.title"
1) 1) "node.title"
2) 1) 1) "The Jungle Book"
   2) 1) "The Book of Life"
3) 1) "Query internal execution time: 0.927409 milliseconds"

This CALL clause can be interleaved with other Cypher clauses to perform more elaborate manipulations:

GRAPH.QUERY DEMO_GRAPH
"CALL db.idx.fulltext.queryNodes('Movie', 'Book') YIELD node AS m
WHERE m.genre = 'Adventure'
RETURN m ORDER BY m.rating"
1) 1) "m"
2) 1) 1) 1) 1) "id"
            2) (integer) 1168
         2) 1) "labels"
            2) 1) "Movie"
         3) 1) "properties"
            2) 1) 1) "genre"
                  2) "Adventure"
               2) 1) "rating"
                  2) "7.6"
               3) 1) "votes"
                  2) (integer) 151342
               4) 1) "year"
                  2) (integer) 2016
               5) 1) "title"
                  2) "The Jungle Book"
3) 1) "Query internal execution time: 0.226914 milliseconds"

In addition to yielding matching nodes, full-text index scans will return the score of each node. This is the TF-IDF score of the node, which is informed by how many times the search terms appear in the node and how closely grouped they are. This can be observed in the example:

GRAPH.QUERY DEMO_GRAPH
"CALL db.idx.fulltext.queryNodes('Node', 'hello world') YIELD node, score RETURN score, node.val"
1) 1) "score"
   2) "node.val"
2) 1) 1) "2"
      2) "hello world"
   2) 1) "1"
      2) "hello to a different world"
3) 1) "Cached execution: 1"
   2) "Query internal execution time: 0.335401 milliseconds"

Deleting a full-text index for a node label

For a node label, the full-text index deletion syntax is:


graph.query("CALL db.idx.fulltext.drop('Movie')")

await graph.query("CALL db.idx.fulltext.drop('Movie')");

graph.query("CALL db.idx.fulltext.drop('Movie')").execute().await?;

graph.query("CALL db.idx.fulltext.drop('Movie')");

GRAPH.QUERY DEMO_GRAPH "CALL db.idx.fulltext.drop('Movie')"

Creating Full-Text indexing for Relation Labels

To create a full-text index on the name property of all relations with the label Manager and enable phonetic search, use the following syntax:


graph.query("CREATE FULLTEXT INDEX FOR ()-[m:Manager]-() on (m.name)")

await graph.query("CREATE FULLTEXT INDEX FOR ()-[m:Manager]-() on (m.name)");

graph.query("CREATE FULLTEXT INDEX FOR ()-[m:Manager]-() on (m.name)").execute().await?;

graph.query("CREATE FULLTEXT INDEX FOR ()-[m:Manager]-() on (m.name)");

GRAPH.QUERY DEMO_GRAPH "CREATE FULLTEXT INDEX FOR ()-[m:Manager]-() on (m.name)"

Querying with a Full-Text Index

To search for specific words within the indexed relations, use:


result = graph.query("CALL db.idx.fulltext.queryRelationships('Manager', 'Charlie Munger') YIELD relationship RETURN relationship.name")

const result = await graph.query("CALL db.idx.fulltext.queryRelationships('Manager', 'Charlie Munger') YIELD relationship RETURN relationship.name");

let result = graph.query("CALL db.idx.fulltext.queryRelationships('Manager', 'Charlie Munger') YIELD relationship RETURN relationship.name").execute().await?;

ResultSet result = graph.query("CALL db.idx.fulltext.queryRelationships('Manager', 'Charlie Munger') YIELD relationship RETURN relationship.name");

GRAPH.QUERY DEMO_GRAPH
"CALL db.idx.fulltext.queryRelationships('Manager', 'Charlie Munger') YIELD relationship RETURN relationship.name"

Deleting a Full-Text Index

To delete the full-text index for a specific relation label, use:


graph.query("DROP FULLTEXT INDEX FOR ()-[m:Manager]-() ON (m.name)")

await graph.query("DROP FULLTEXT INDEX FOR ()-[m:Manager]-() ON (m.name)");

graph.query("DROP FULLTEXT INDEX FOR ()-[m:Manager]-() ON (m.name)").execute().await?;

graph.query("DROP FULLTEXT INDEX FOR ()-[m:Manager]-() ON (m.name)");

GRAPH.QUERY DEMO_GRAPH "DROP FULLTEXT INDEX FOR ()-[m:Manager]-() ON (m.name)"

Index Management

Listing Full-text Indexes

To view all indexes (including full-text) in your graph, use:

CALL db.indexes()

This returns information about all indexes, with full-text indexes marked with type FULLTEXT.

Performance Tradeoffs and Best Practices

When to Use Full-text Indexes

Full-text indexes are ideal for:

  • Text-heavy search: Searching within large text fields like descriptions, articles, or comments
  • Partial word matching: When users might not know the exact text
  • Fuzzy search: Handling typos and spelling variations
  • Multi-word queries: Searching for multiple terms with boolean logic

When NOT to Use Full-text Indexes

Full-text indexes are not optimal for:

  • Exact numeric filtering: Use range indexes instead for numeric comparisons
  • Exact-match queries: Range indexes are more efficient for exact property matches
  • Small or structured data: For short, well-defined strings, range indexes may be sufficient

Performance Considerations

Benefits:

  • Enables sophisticated text search capabilities (fuzzy, prefix, phonetic)
  • Supports stemming and language-specific optimizations
  • Returns relevance scores (TF-IDF) for ranking results

Costs:

  • Write overhead: Text must be tokenized and indexed on write
  • Storage: Requires more space than range indexes due to tokenization and inverted indices
  • Configuration complexity: Language, stopwords, and stemming settings affect results
  • Query performance: Fuzzy matching is more expensive than exact matching

Recommendations:

  • Choose the correct language setting for proper stemming
  • Configure appropriate stopwords for your use case
  • Use prefix matching (*) for autocomplete rather than full fuzzy search when possible
  • Test query performance with realistic data volumes
  • Consider the tradeoff between index configurability and query performance

Configuration Best Practices

Language Selection:

  • Wrong language settings can produce poor stemming results
  • Example: Searching “running” with English stemming finds “run”, but German stemming won’t

Stopwords:

  • Default stopwords are optimized for general text
  • Customize stopwords for domain-specific applications (e.g., legal, medical, technical documents)
  • Too many stopwords can hurt precision; too few increase index size

Phonetic Search:

  • Useful for name searches and when spelling variations are common
  • Increases index size and query time
  • Double Metaphone (dm:en) is recommended for English

Verifying Full-text Index Usage

Use GRAPH.EXPLAIN to verify that full-text queries use the index:


# Check if full-text index is used
result = graph.explain("CALL db.idx.fulltext.queryNodes('Movie', 'Book') YIELD node RETURN node")
print(result)
# Output shows: ProcedureCall | db.idx.fulltext.queryNodes

// Check if full-text index is used
const result = await graph.explain("CALL db.idx.fulltext.queryNodes('Movie', 'Book') YIELD node RETURN node");
console.log(result);
// Output shows: ProcedureCall | db.idx.fulltext.queryNodes

// Check if full-text index is used
let result = graph.explain("CALL db.idx.fulltext.queryNodes('Movie', 'Book') YIELD node RETURN node").execute().await?;
println!("{}", result);
// Output shows: ProcedureCall | db.idx.fulltext.queryNodes

// Check if full-text index is used
String result = graph.explain("CALL db.idx.fulltext.queryNodes('Movie', 'Book') YIELD node RETURN node");
System.out.println(result);
// Output shows: ProcedureCall | db.idx.fulltext.queryNodes

# Check if full-text index is used
GRAPH.EXPLAIN DEMO_GRAPH "CALL db.idx.fulltext.queryNodes('Movie', 'Book') YIELD node RETURN node"
# Output shows: ProcedureCall | db.idx.fulltext.queryNodes