# 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](https://github.com/quickwit-oss/tantivy) full-text search engine.

Text search is commonly used as a [retrieval
technique](https://memgraph.com/docs/ai-ecosystem/graph-rag/atomic-pipelines/local-graph-search#text-search) in RAG systems
to find entities based on exact and fuzzy matches.

> **Info**
>
> 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

```shell
CREATE TEXT INDEX text_index_name ON :Label;
```

### Create a text index on edges

```shell
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:

```cypher
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:

```cypher
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:

```cypher
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:

```cypher
CREATE TEXT EDGE INDEX edge_index_name ON :EDGE_TYPE;
```

You can also specify specific properties for edge indices:

```cypher
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.

> **Warning**
>
> 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.

## Run text search

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

> **Info**
>
> 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](https://github.com/quickwit-oss/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](#fuzzy-search) or
  [regex search](#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.

> **Warning**
>
> 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 do                                             | Procedure                                                      | What it searches           | Property name in the query |
| --------------------------------------------------------------- | -------------------------------------------------------------- | -------------------------- | -------------------------- |
| Match on specific properties, with boolean operators            | [`text_search.search`](#query-text-index)                      | the properties you name    | yes, prefixed with `data.` |
| Match a term anywhere across all indexed properties             | [`text_search.search_all`](#search-over-all-indexed-properties)| every indexed property     | no — just the term         |
| Match all properties against a regular expression               | [`text_search.regex_search`](#regex-search)                    | every indexed property     | no — just the regex        |
| Typo-tolerant, multi-word "search-as-you-type" on one property  | [`text_search.fuzzy_phrase_search`](#fuzzy-phrase-search)      | one named property         | yes, prefixed with `data.` |
| Summaries and counts (count, min, max, …) over matches          | [`text_search.aggregate`](#aggregations)                       | the properties you name    | yes, 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](#search-configuration).

### Show text indices

To list all text indices in Memgraph, use the `SHOW INDEX INFO`
[statement](https://memgraph.com/docs/fundamentals/indexes#show-created-indexes).

### 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:

| Key                    | Type    | Default | Description                                                                                                                                                                          |
| ---------------------- | ------- | ------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `limit`                | integer | `1000`  | The maximum number of results to return.                                                                                                                                           |
| `fuzzy_distance`       | integer | `0`     | The 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_prefix`         | boolean | `false` | When `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](#fuzzy-search). Has no effect when `fuzzy_distance` is `0`. |
| `fuzzy_transpositions` | boolean | `true`  | When `true`, swapping two adjacent characters counts as a single edit; when `false`, it counts as two.                                                                             |

### Query text index

> **Warning**
>
> 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](#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](https://docs.rs/tantivy/latest/tantivy/query/struct.QueryParser.html).
If the query contains property names, attach the `data.` prefix to them.

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

To query an index on edges, use:
```shell
CALL text_search.search_edges("index_name", "data.title:Rules2024") YIELD edge, score RETURN *;
```

#### Example

```cypher
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 search

Fuzzy matching lets a search tolerate typos and partial terms. Using the
[`config` map](#search-configuration), you can search with fuzzy matching:

```cypher
// 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.

> **Info**
>
> 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`](#fuzzy-phrase-search) instead of `fuzzy_prefix`.

### Fuzzy phrase search

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](#search-configuration) 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](#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.

> **Info**
>
> `fuzzy_distance` (`0`–`2`) 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

```cypher
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`):

```cypher
// '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

```cypher
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](#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`:

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

To search edges:
```cypher
CALL text_search.search_all_edges("complianceEdges", "Rules2024")
YIELD edge
RETURN edge;
```

#### Example

```cypher
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}) |
+----------------------------------------------------------------------+
```

### Regex search

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](#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":

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

To search edges:
```cypher
CALL text_search.regex_search_edges("complianceEdges", "wor.*s")
YIELD edge
RETURN edge;
```

#### Example

```cypher
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:

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

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

#### Example

```cypher
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}}" |
+-------------------------------+
```

## Access control <sup style={{ fontSize: '0.6em', color: '#888' }}>Enterprise</sup>

When [fine-grained access control](https://memgraph.com/docs/database-management/authentication-and-authorization/role-based-access-control#fine-grained-access-control)
is active for the user running the query, text search respects their read
permissions: a match is returned only if the user could read it with a normal
query. Content the user is not allowed to read is never revealed through the
search results or their relevance scores.

A node or relationship appears in the results only when **both** of these hold:

- **The node or relationship is readable.** The user has `READ` on its
  [labels](https://memgraph.com/docs/database-management/authentication-and-authorization/role-based-access-control#label-based-access-control)
  — or, for a relationship, on its
  [type](https://memgraph.com/docs/database-management/authentication-and-authorization/role-based-access-control#relationship-permissions)
  and on both of its endpoint nodes. Permissions are evaluated against the
  entity's *actual* labels, so a `DENY` on any of them removes it from the
  results.
- **The matched property is readable.** Which properties must be readable
  depends on the procedure:
  - `text_search.search` and `text_search.search_edges` search a property named
    in the query (for example `data.name:Alice`), so only that
    [property](https://memgraph.com/docs/database-management/authentication-and-authorization/role-based-access-control#property-based-access-control)
    must be readable. The match surfaces even if the entity has other properties
    the user cannot read.
  - `text_search.search_all`, `text_search.regex_search` and their `_edges`
    variants search across every indexed property, so **all** indexed text
    properties the entity has must be readable — if even one is denied, the
    entity is dropped, because the match could have come from it.

A hit whose match depends on a property the user cannot read is **silently
dropped**: the results are simply empty, just as a
[denied property](https://memgraph.com/docs/database-management/authentication-and-authorization/role-based-access-control#how-denied-properties-behave)
reads back as `Null` in a normal query. No error is raised.

For example, with a text index on `:Employee` and an analyst who can read
employees and their properties, except `ssn`:

```cypher
GRANT READ ON NODES CONTAINING LABELS :Employee TO analyst;
GRANT READ {*} ON NODES CONTAINING LABELS :Employee TO analyst;
DENY READ {ssn} ON NODES CONTAINING LABELS :Employee TO analyst;
```

- `CALL text_search.search('employees', 'data.name:Alice')` returns matching
  employees — `name` is readable.
- `CALL text_search.search('employees', 'data.ssn:123456789')` returns nothing —
  the queried property is denied.
- `CALL text_search.search_all('employees', 'Alice')` returns nothing for an
  employee whose indexed `ssn` is set, because the match could have come from the
  denied property.

> **Warning**
>
> `text_search.aggregate` and `text_search.aggregate_edges` run inside the search
> engine and cannot filter individual results, so they cannot apply per-row
> permissions. When the caller has a fine-grained restriction on the index's
> properties, the aggregation returns an error message instead of a result — use
> the search procedures above when you need permission-aware results.

> **Info**
>
> These checks apply only when fine-grained access control is active for the user.
> An unrestricted user (for example an administrator) and Memgraph Community
> edition see all matches.

## 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.

```shell
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:

| Feature                 | Support                                                 |
|-------------------------|---------------------------------------------------------|
| 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)            |
