QueryingText search

Text search

Text search allows you to look up nodes and edges whose properties contain specific text. To make a node or edge searchable, you must first create a text index for it.

Text indices and search are powered by the Tantivy full-text search engine.

Text search is commonly used as a retrieval technique in RAG systems to find entities based on exact and fuzzy matches.

Text search is no longer an experimental feature as of Memgraph version 3.6. You can use text search without any special configuration flags.

Create text index

Before you can use text search, you need to create a text index. Text indices are created using the CREATE TEXT INDEX command. To create the text index, you need to:

  1. Provide a name for the index.
  2. Specify the label or edge type the index applies to.
  3. (Optional) Define which properties should be indexed.

Create a text index on nodes

CREATE TEXT INDEX text_index_name ON :Label;

Create a text index on edges

CREATE TEXT EDGE INDEX text_index_name ON :EDGE_TYPE;

Index all properties

This statement creates a text index named complianceDocuments for nodes with the Report label, indexing all text-indexable properties:

CREATE TEXT INDEX complianceDocuments ON :Report;

Index specific properties

You can also create a text index on a subset of properties by specifying them explicitly:

CREATE TEXT INDEX index_name ON :Label(prop1, prop2, prop3);

For example, to create an index only on the title and content properties of Report nodes:

CREATE TEXT INDEX complianceDocuments ON :Report(title, content);

Edge text indices

Text indices can also be created on edges. To create a text index on edges:

CREATE TEXT EDGE INDEX edge_index_name ON :EDGE_TYPE;

You can also specify specific properties for edge indices:

CREATE TEXT EDGE INDEX edge_index_name ON :EDGE_TYPE(prop1, prop2);

If you attempt to create an index with an existing name, the statement will fail.

What is indexed

For any given node or edge, if a text index applies to it:

  • When no specific properties are listed, all properties with text-indexable types (String, Integer, Float, or Boolean) are stored.
  • When specific properties are listed, only those properties (if they have text-indexable types) are stored.
⚠️

Changes made within the same transaction are not visible to the index. To see your changes in text search results, you need to commit the transaction first.

To run text search, you need to call text_search query module procedures.

Unlike other index types, the query planner currently does not utilize text indices.

How text search matching works

Text search does not match raw substrings the way LIKE or a regular property comparison would. When a property is indexed, Tantivy breaks its value into tokens using its default tokenizer, which:

  1. splits the text on whitespace and punctuation (any non-alphanumeric character),
  2. drops very long tokens (more than 40 characters),
  3. lowercases every token.

There is no stemming, so running and run remain different tokens. A search matches when your query terms match these stored tokens. Three consequences trip people up most often:

  • Matching is case-insensitive. data.title:rules2024 matches a stored value of Rules2024.
  • A term matches a whole token, not a substring. data.title:Rules does not match Rules2024, because that value is the single token rules2024. To match partial terms, use fuzzy or prefix matching or regex search.
  • Multi-word values are split into separate tokens. Annual Compliance Report is indexed as annual, compliance and report, so data.body:compliance matches it.
⚠️

In text_search.search, text_search.fuzzy_phrase_search and text_search.aggregate, property names in the query must be prefixed with data., write data.title:Rules2024, not title:Rules2024. Memgraph stores each entity’s properties under a JSON object named data, and Tantivy addresses them by that path. Omitting the prefix raises an error (Field does not exist: 'title', or No default field declared... for a bare term). The search_all and regex_search procedures search every property, so they take a plain term with no property name and no data. prefix.

Which procedure should I use?

All matching is done through text_search procedures. Pick one based on what you want to match:

What you want to doProcedureWhat it searchesProperty name in the query
Match on specific properties, with boolean operatorstext_search.searchthe properties you nameyes, prefixed with data.
Match a term anywhere across all indexed propertiestext_search.search_allevery indexed propertyno — just the term
Match all properties against a regular expressiontext_search.regex_searchevery indexed propertyno — just the regex
Typo-tolerant, multi-word “search-as-you-type” on one propertytext_search.fuzzy_phrase_searchone named propertyyes, prefixed with data.
Summaries and counts (count, min, max, …) over matchestext_search.aggregatethe properties you nameyes, prefixed with data.

Every procedure has an _edges counterpart (search_edges, search_all_edges, regex_search_edges, fuzzy_phrase_search_edges, aggregate_edges) that does the same over an edge text index. Typo tolerance (fuzzy_distance) is available on search, search_all and fuzzy_phrase_search through the search configuration.

Show text indices

To list all text indices in Memgraph, use the SHOW INDEX INFO statement.

Search configuration

The text_search.search, text_search.search_edges, text_search.search_all, text_search.search_all_edges, text_search.regex_search, text_search.regex_search_edges, text_search.fuzzy_phrase_search and text_search.fuzzy_phrase_search_edges procedures accept an optional config map as their last argument. All keys are optional:

KeyTypeDefaultDescription
limitinteger1000The maximum number of results to return.
fuzzy_distanceinteger0The maximum number of single-character edits (Levenshtein distance) a term may differ from the query and still match. Allowed values are 0, 1 and 2 (0 is exact matching); a higher value is rejected.
fuzzy_prefixbooleanfalseWhen true and fuzzy_distance is greater than 0, each query term is matched as a prefix (matching indexed tokens that start with it, within fuzzy_distance edits). Terms are matched independently and in any order, not as a phrase, see Fuzzy search. Has no effect when fuzzy_distance is 0.
fuzzy_transpositionsbooleantrueWhen true, swapping two adjacent characters counts as a single edit; when false, it counts as two.

Query text index

⚠️

Tantivy takes a snapshot of the index each time a search is performed, meaning each search has its own snapshot. This behavior may differ from the database isolation level you are using (for example, READ_UNCOMMITTED or READ_COMMITTED), as each search sees the index state at the time it was executed.

Use the text_search.search() and text_search.search_edges() procedures to search for text within a text index. These procedures allow you to find nodes or edges that match your search query based on their indexed properties.

Input:

  • index_name: string ➡ The text index to search.
  • search_query: string ➡ The query to search for in the index.
  • config: Map (optional) ➡ The search configuration, including limit and the fuzzy-matching keys. Defaults to an empty map.

Output:

When the index is defined on nodes:

  • node: Node ➡ A node in the text index matching the given query.
  • score: double ➡ The relevance score of the match. Higher scores indicate more relevant results.

When the index is defined on edges:

  • edge: Relationship ➡ An edge in the text index matching the given query.
  • score: double ➡ The relevance score of the match. Higher scores indicate more relevant results.

Usage:

The syntax for the search_query parameter is available here. If the query contains property names, attach the data. prefix to them.

CALL text_search.search("index_name", "data.title:Rules2024") YIELD node, score RETURN *;

To query an index on edges, use:

CALL text_search.search_edges("index_name", "data.title:Rules2024") YIELD edge, score RETURN *;

Example

CREATE TEXT INDEX complianceDocuments ON :Document;
CREATE (:Document {title: 'Rules2024', version: 1});
CREATE (:Document {title: 'Rules2024', version: 2});
CREATE (:Document {title: 'Other', version: 2});
 
// Search for documents with title containing 'Rules2024'
CALL text_search.search('complianceDocuments', 'data.title:Rules2024') 
YIELD node
RETURN node.title AS title, node.version AS version
ORDER BY version ASC;

Result:

+-------------+-------------+
| title       | version     |
+-------------+-------------+
| "Rules2024" | 1           |
| "Rules2024" | 2           |
+-------------+-------------+

Fuzzy matching lets a search tolerate typos and partial terms. Using the config map, you can search with fuzzy matching:

// exact match (default behavior)
CALL text_search.search('complianceDocuments', 'data.title:memgraph') YIELD node RETURN node;
 
// tolerate a single typo, e.g. 'memgrahp' still matches 'memgraph'
CALL text_search.search('complianceDocuments', 'data.title:memgrahp', {fuzzy_distance: 1}) YIELD node RETURN node;
 
// prefix + fuzzy, capped at 5 results
CALL text_search.search('complianceDocuments', 'data.title:mem', {fuzzy_distance: 1, fuzzy_prefix: true, limit: 5}) YIELD node RETURN node;
 
// fuzzy matching over all indexed properties
CALL text_search.search_all('complianceDocuments', 'memgrahp', {fuzzy_distance: 1}) YIELD node RETURN node;

How `fuzzy_prefix` handles multiple terms

fuzzy_prefix turns each query term into an independent fuzzy prefix. The terms are combined as a boolean query, so a match only requires the terms to appear somewhere in the property, in any order and not necessarily next to each other. It does not treat the query as a phrase.

For example, with names lucky luke, lucky lucy, lucy lucky and unlucky lu, the query data.name:lucky + data.name:lu with {fuzzy_distance: 1, fuzzy_prefix: true} matches all of them, because every value contains a token that starts with lucky or lu. Order and adjacency are ignored.

If you want behavior where only the last term treated as a prefix (so lucky lu matches lucky luke and lucky lucy, but not lucy lucky or unlucky lu) — use text_search.fuzzy_phrase_search instead of fuzzy_prefix.

The text_search.fuzzy_phrase_search and text_search.fuzzy_phrase_search_edges procedures match an ordered, adjacent sequence of terms on a single property, where the last term is treated as a prefix. This is useful for typo-tolerant “search-as-you-type” over a multi-word phrase, for example matching big bad wolf while the user is still typing big bad wo.

They behave differently from the fuzzy_prefix option of text_search.search, which applies a separate fuzzy prefix to every term independently. Fuzzy phrase search instead requires the terms to appear next to each other, in the same order, and shares a single edit budget across the whole phrase.

Input:

  • index_name: string ➡ The text index to search.
  • search_query: string ➡ A single-property query of the form data.<property>:<terms>. Any other form (multiple properties, no property, boolean or regex syntax) is rejected.
  • config: Map (optional) ➡ The search configuration. Supports limit, fuzzy_distance and fuzzy_transpositions. The last term is always a prefix, so fuzzy_prefix is implicitly true and passing fuzzy_prefix: false is rejected.

Output:

When the index is defined on nodes:

  • node: Node ➡ A node in the text index matching the given phrase.
  • score: double ➡ The relevance score of the match. Higher scores indicate more relevant results.

When the index is defined on edges:

  • edge: Relationship ➡ An edge in the text index matching the given phrase.
  • score: double ➡ The relevance score of the match. Higher scores indicate more relevant results.

fuzzy_distance (02) is a single edit budget shared across the whole phrase, not a per-term allowance. For example, at fuzzy_distance: 1 the query big bad wo matches big bd wolf (one edit total) but not bg bd wolf (two edits total).

Example

CREATE TEXT INDEX docIndex ON :Doc;
CREATE (:Doc {title: 'big bad wolf', n: 1});
CREATE (:Doc {title: 'big bad world', n: 2});
CREATE (:Doc {title: 'the big bad wolf returns', n: 3});
CREATE (:Doc {title: 'bad big wolf', n: 4});
CREATE (:Doc {title: 'big wolf', n: 5});
 
// ordered, adjacent phrase with a prefix on the last term
CALL text_search.fuzzy_phrase_search('docIndex', 'data.title:big bad wo')
YIELD node
RETURN node.title AS title, node.n AS n
ORDER BY n ASC;

Result:

+----------------------------+-----+
| title                      | n   |
+----------------------------+-----+
| "big bad wolf"             | 1   |
| "big bad world"            | 2   |
| "the big bad wolf returns" | 3   |
+----------------------------+-----+

bad big wolf (n=4) is excluded because the term order differs, and big wolf (n=5) is excluded because the terms are not adjacent.

To tolerate typos, pass fuzzy_distance (a swapped pair of adjacent characters counts as a single edit unless fuzzy_transpositions is false):

// 'bd' -> 'bad' is a single edit, so this still matches 'big bad wolf'
CALL text_search.fuzzy_phrase_search('docIndex', 'data.title:big bd wo', {fuzzy_distance: 1})
YIELD node
RETURN node.title AS title
ORDER BY title ASC;

Boolean expressions

You can use boolean logic in your search queries to create more complex search conditions. Boolean operators include AND, OR, and NOT, and you can use parentheses to group conditions.

Usage:

Boolean expressions allow you to combine multiple search conditions:

  • AND - both conditions must be true
  • OR - at least one condition must be true
  • NOT - condition must be false
  • Use parentheses () to group conditions and control precedence

Example

CREATE TEXT INDEX complianceDocuments ON :Document;
CREATE (:Document {title: 'Rules2023', fulltext: 'nothing'});
CREATE (:Document {title: 'Rules2024', fulltext: 'words', version: 2});
 
// Search with boolean logic: (title is Rules2023 OR Rules2024) AND fulltext contains 'words'
CALL text_search.search('complianceDocuments', '(data.title:Rules2023 OR data.title:Rules2024) AND data.fulltext:words') 
YIELD node
RETURN node.title AS title, node.version AS version
ORDER BY version ASC, title ASC;

Result:

+-------------+-------------+
| title       | version     |
+-------------+-------------+
| "Rules2024" | 2           |
+-------------+-------------+

Search over all indexed properties

The text_search.search_all and text_search.search_all_edges procedures look for text-indexed nodes or edges where at least one property value matches the given query.

Unlike text_search.search, these procedures search over all properties, and there is no need to specify property names in the query.

Input:

  • index_name: string ➡ The text index to be searched.
  • search_query: string ➡ The query applied to the text-indexed nodes or edges.
  • config: Map (optional) ➡ The search configuration, including limit and the fuzzy-matching keys. Defaults to an empty map.

Output:

When the index is defined on nodes:

  • node: Node ➡ A node in index_name matching the given search_query.
  • score: double ➡ The relevance score of the match. Higher scores indicate more relevant results.

When the index is defined on edges:

  • edge: Relationship ➡ An edge in index_name matching the given search_query.
  • score: double ➡ The relevance score of the match. Higher scores indicate more relevant results.

Usage:

The following query searches the complianceDocuments index for nodes where at least one property value contains Rules2024:

CALL text_search.search_all("complianceDocuments", "Rules2024")
YIELD node
RETURN node;

To search edges:

CALL text_search.search_all_edges("complianceEdges", "Rules2024")
YIELD edge
RETURN edge;

Example

CREATE TEXT INDEX complianceDocuments ON :Document;
CREATE (:Document {title: 'Rules2024', fulltext: 'text words', version: 1});
CREATE (:Document {title: 'Other', fulltext: 'Rules2024 here', version: 3});
 
// Search for 'Rules2024' across all properties
CALL text_search.search_all('complianceDocuments', 'Rules2024') 
YIELD node
RETURN node
ORDER BY node.version ASC;

Result:

+----------------------------------------------------------------------+
| node                                                                 |
+----------------------------------------------------------------------+
| (:Document {fulltext: "text words", title: "Rules2024", version: 1}) |
| (:Document {fulltext: "Rules2024 here", title: "Other", version: 3}) |
+----------------------------------------------------------------------+

The text_search.regex_search and text_search.regex_search_edges procedures look for text-indexed nodes or edges where at least one property value matches the given regular expression (regex).

Input:

  • index_name: string ➡ The text index to be searched.
  • search_query: string ➡ The regex applied to the text-indexed nodes or edges.
  • config: Map (optional) ➡ The search configuration. Only limit is supported here; the fuzzy-matching keys are not allowed for regex search. Defaults to an empty map.

Output:

When the index is defined on nodes:

  • node: Node ➡ A node in index_name matching the given search_query.
  • score: double ➡ The relevance score of the match. Higher scores indicate more relevant results.

When the index is defined on edges:

  • edge: Relationship ➡ An edge in index_name matching the given search_query.
  • score: double ➡ The relevance score of the match. Higher scores indicate more relevant results.

Usage:

Regex searches apply to all properties; do not include property names in the search query.

The following query searches the complianceDocuments index for nodes where at least one property value satisfies the wor.*s regex, e.g. “works” and “words”:

CALL text_search.regex_search("complianceDocuments", "wor.*s")
YIELD node
RETURN node;

To search edges:

CALL text_search.regex_search_edges("complianceEdges", "wor.*s")
YIELD edge
RETURN edge;

Example

CREATE TEXT INDEX complianceDocuments ON :Document;
CREATE (:Document {fulltext: 'words and things'});
CREATE (:Document {fulltext: 'more words'});
 
// Search using regex pattern 'wor.*s'
CALL text_search.regex_search('complianceDocuments', 'wor.*s') 
YIELD node
RETURN node
ORDER BY node.fulltext ASC;

Result:

+--------------------------------------------+
| node                                       |
+--------------------------------------------+
| (:Document {fulltext: "more words"})       |
| (:Document {fulltext: "words and things"}) |
+--------------------------------------------+

Aggregations

Aggregations allow you to perform calculations on text search results. By using them, you can efficiently summarize the results, calculate averages or totals, identify min/max values, and count indexed nodes or edges that meet specific criteria.

The text_search.aggregate and text_search.aggregate_edges procedures let you define an aggregation and apply it to the results of a search query.

Input:

  • index_name: string ➡ The text index to be searched.
  • search_query: string ➡ The query applied to the text-indexed nodes or edges.
  • aggregation_query: string ➡ The aggregation (JSON-formatted) to be applied to the output of search_query.

Output:

  • aggregation: string ➡ JSON-formatted string with the output of aggregation.

Usage:

Aggregation queries and results are strings with Elasticsearch-compatible JSON format, where "field" corresponds to node or edge properties. If the search or aggregation queries contain property names, attach the data. prefix to them.

The following query counts all nodes in the complianceDocuments index:

CALL text_search.aggregate(
    "complianceDocuments",
    "data.title:Rules2024",
    '{"count": {"value_count": {"field": "data.version"}}}'
)
YIELD aggregation
RETURN aggregation;

To aggregate edges:

CALL text_search.aggregate_edges(
    "complianceEdges",
    "data.title:Rules2024",
    '{"count": {"value_count": {"field": "data.version"}}}'
)
YIELD aggregation
RETURN aggregation;

Example

CREATE TEXT INDEX complianceDocuments ON :Document;
CREATE (:Document {title: 'Rules2024', version: 1});
CREATE (:Document {title: 'Rules2024', version: 2});
 
// Count documents matching the search query
CALL text_search.aggregate(
    'complianceDocuments', 
    'data.title:Rules2024', 
    '{"count":{"value_count":{"field":"data.version"}}}'
) 
YIELD aggregation
RETURN aggregation;

Result:

+-------------------------------+
| aggregation                   |
+-------------------------------+
| "{\"count\":{\"value\":2.0}}" |
+-------------------------------+

Drop text index

Text indices are dropped with the DROP TEXT INDEX command. You need to give the name of the index to be deleted.

DROP TEXT INDEX text_index_name;

Compatibility

Text search supports most usage modalities that are available in Memgraph. Refer to the table below for an overview:

FeatureSupport
Multitenancy✅ Yes
Durability✅ Yes
Replication✅ Yes
Concurrent transactions⚠️ Yes, but search results may vary within transactions
Storage modes❌ No (doesn’t work in IN_MEMORY_ANALYTICAL)