UltipaDocs
Products
Solutions
Resources
Company
Start Free Trial
UltipaDocs
Start Free Trial
  • Introduction
  • GQL vs Other Languages
    • Overview
    • Node and Edge Patterns
    • Path Patterns
    • Quantified Paths
    • Questioned Paths
    • Shortest Paths
    • Cheapest Paths
    • K-Hop Traversal
    • Graph Patterns
    • Overview
    • Open Graphs
    • Closed Graphs
    • Graph Types
    • Constraints
    • Projections
    • Storage Maintenance
    • Node and Edge IDs
    • INSERT
    • INSERT OVERWRITE
    • UPSERT
    • MERGE
    • SET
    • REMOVE
    • DELETE
    • FOREACH
    • LOAD CSV
    • Query Composition
    • Result Table and Visualization
    • MATCH
    • OPTIONAL MATCH
    • FILTER
    • LET
    • FOR
    • ORDER BY
    • LIMIT
    • SKIP
    • CALL
    • RETURN
    • Composite Query
    • NEXT
    • All Functions
    • Element Functions
    • Path Functions
    • Aggregate Functions
    • Mathematical Functions
    • Trigonometric Functions
    • String Functions
    • List Functions
    • Datetime Functions
    • Spatial Functions
    • Null Functions
    • Utility Functions
    • Type Conversion Functions
    • Table Functions
  • Operators
  • Predicates
    • Overview
    • CASE
    • LET Value Expression
    • Value Query Expression
    • Count Query Expression
    • List Expressions
    • Current Values
    • Index
    • Full-text Index
    • Vector Index
  • Transactions
  • Triggers
  • Query Management
  • Execution Plan
    • Variables
    • Values and Types
    • Comments
    • Reserved Words
    • Naming Conventions
    • Syntactic Notation
  • GQL Conformance
  1. Docs
  2. /
  3. ISO GQL
  4. /
  5. Indexes

Full-text Index

Overview

A full-text index is a type of index specialized for efficient searching for textual properties, especially in large text fields like descriptions, comments, or articles.

Full-text indexes work by breaking down the text into smaller segments called tokens. When a query is performed, the search engine matches specified keywords against these tokens instead of the original full text, allowing for faster retrieval of relevant results. Full-text indexes support both precise and fuzzy matches.

Analyzers

An analyzer determines how text is broken into tokens, normalized (lower-casing, stemming, stop-word removal), and segmented (especially for CJK - Chinese, Japanese, Korean - text, which is written without whitespace between words). The analyzer is fixed at index-creation time; the same analyzer is applied to both indexed text and query terms, so they tokenize consistently.

AnalyzerTokenizationBest for
mixedDefault. Auto-detects language per segment: simple tokenization + Porter stemming + stop-word removal for English, GSE word-segmentation for CJK.Mixed-language content (English + Chinese in the same field). The safe default. pipeline is a synonym.
simpleWhitespace + punctuation split, lower-cased. No stemming, no stop-word removal.Identifiers, codes, or exact-form English where stemming would cause false matches (e.g., a search for 'run' shouldn't hit a SKU literally named 'running'; under the default analyzer the Porter stemmer collapses both to the same token).
cjkBigram tokenization for CJK characters; falls back to simple for non-CJK.CJK-only content where the GSE dictionary is undesirable.
gseGSE word-segmentation. Honors gseMode for segmentation aggressiveness.Chinese-heavy content where dictionary-based word segmentation is required.

The gseMode option (mixed and gse analyzers only) controls how aggressively GSE segments Chinese text:

gseModeBehavior
preciseDefault. Single best segmentation. 北京大学 → ["北京大学"]. Fewer, more specific tokens.
searchSearch-oriented overlapping segments. 北京大学 → ["北京", "大学", "北京大学"]. Recall-friendly.
fullEvery possible segmentation. Maximum recall, larger index.

To preview how a given analyzer tokenizes a string, use the ft.analyze procedure.

Relevance Scoring (BM25)

Once a query's tokens are matched against the index, BM25 (Best Matching 25) ranks the hits, where higher score means more relevant. It blends three signals per query term:

  • Term frequency: how often the term appears in the document (with saturation, so the 50th occurrence adds less than the 2nd).
  • Inverse document frequency: how rare the term is across all indexed documents. Rare terms score higher.
  • Length normalization: long documents are penalized so they don't win by size alone.

When an index covers multiple properties, each property contributes its own BM25 score and the weight_<property> option is a multiplier on that contribution. For example, OPTIONS { weight_p1: 3.0, weight_p2: 1.0 }, then weighted BM25 score is computed by 3.0 * BM25(p1, query) + 1.0 * BM25(p2, query). So it makes a match in p1 count for 3× a match in p2. Weights default to 1.0 (all properties treated equally).

Showing Full-text Index

Retrieve full-text indexes in the current graph:

GQL
SHOW FULLTEXT

-- Filtered by entity type
SHOW NODE FULLTEXT
SHOW EDGE FULLTEXT

The result includes the following fields:

FieldDescription
index_nameFull-text index name.
entity_typeNODE or EDGE.
schema_nameThe label of the full-text index.
propertiesThe indexed properties.
analyzerThe text analyzer used.
statusIndex status — see Index States below: ready, loading, building, pending, unloaded, degraded, or error.
doc_countNumber of documents indexed.
progressBuild or loading progress; for a degraded index the failure count and the last failure, and for an error index the reason the build failed.
memoryIn-memory footprint of the index's posting structure, in bytes. 0 when the index is unloaded.

Index States

StatusMeaningReads of the index
readySearchable.Answered
buildingBeing built by CREATE FULLTEXT, or after a restore.5020, retryable
loadingBeing streamed back into memory after a restart or ft.unload.5020, retryable
pendingBeing rebuilt once after an upgrade (see below).5020, retryable
unloadedIts in-memory structure was released; the next search or ft.load reloads it.Answered, after a reload
degradedSearchable, but it is missing documents.Answered, with a warning
errorIts last build failed.5017, naming the index and the statements that rebuild it

5020 is retryable; 5017 is not. Every read of an index that is not searchable yet — ~prop CONTAINS, ~index_name CONTAINS, CALL ft.search, CALL ft.suggest — fails with 5020 and a message saying what the index is doing and how far along it is:

[5020] fulltext search error: fulltext index loading: index "idx_doc" cannot be searched yet
because it is being built (1200 of about 5000 documents indexed); it becomes searchable when
the build finishes

The code is the same however the read is sent — on its own, in a multi-statement script, inside a transaction, as a stream, or under PROFILE / EXPLAIN ANALYZE. Match on the code rather than the text; the text fulltext index loading: is there for clients that can only see the message. No driver retries this for you: retry after a short wait, or wait for the index with ft.load('<index>', <timeoutMs>), which returns at once with the reason for an index whose build failed. A build can take minutes to hours on a large label, so bound the retries by time.

5017 means something a retry will not fix: a missing index ("no matching index found"), or one whose build failed.

A degraded Index Is Missing Documents

A write to an index can fail after the data itself was written — a disk error on the index's own store, an edge whose internal id could not be resolved. The statement succeeds, because the data is safe, but the index goes on without that document. Such an index is marked degraded and stays so across restarts:

  • SHOW FULLTEXT reports degraded, with the failure count and the last failure in progress.
  • Every query reading it returns its results with a warning that the index may be missing documents (the warnings field through a driver).
  • db.validate_graph() reports it as a problem.

Rebuild it when you see it — DROP FULLTEXT <name> then CREATE FULLTEXT <name> …. Nothing rebuilds it for you, because the missing documents cannot be known without a full scan.

An index in the error state is likewise not rebuilt automatically on the next open: a build is expensive and a failing one usually fails again, so the decision is left to you.

One-Time Rebuild of Multi-Property Indexes

An index over two or more properties — CREATE FULLTEXT idx_doc ON NODE Document(title, body) — is cleared and rebuilt once the first time a graph is opened on this version. Such an index could previously store a document's properties in each other's slots, which ranked those documents wrongly. Single-property indexes are not affected.

While it rebuilds, the index reports pending with the documents done so far in progress, and reads of it return the retryable 5020. The rebuild costs the time and memory of a CREATE FULLTEXT over the same data, so allow for it on the first start after upgrading. Nothing else is needed.

NOTE

Shut the server down cleanly before downgrading. An index rebuilt since the last start keeps its new data in a separate directory until a clean shutdown moves it where an earlier version reads it. After a crash, start this version again and shut it down cleanly first — an earlier version opened straight after such a crash reports the index as ready and returns no results from it. An earlier version also does not know the degraded mark and clears it whenever it rewrites the index's configuration, so note down any degraded index before downgrading.

Creating Full-text Index

You can create a full-text index using the CREATE FULLTEXT statement. The index is built asynchronously, use SHOW FULLTEXT to check build progress.

Syntax
<create full-text index statement> ::=
  "CREATE FULLTEXT" <index name> "ON" < "NODE" | "EDGE" > <label name>
  "(" <property> [ { "," <property> }... ] ")"
  [ "OPTIONS" "{" <option> [ { "," <option> }... ] "}" ]

<option> ::= 
    "analyzer:" <analyzer name>
  | "gseMode:" <gse mode>
  | "weight_" <property> ":" <positive number>

Details

  • The <index name> must be unique among nodes and among edges, but a node full-text index and an edge full-text index may share the same name.
  • The optional OPTIONS clause tunes tokenization and ranking:
OptionDefaultDescription
analyzermixedOne of mixed, simple, cjk, gse. (pipeline is accepted as an alias for mixed.) See Analyzers.
gseModepreciseOne of precise, search, full. Applies to the gse and mixed analyzers. See Analyzers.
weight_<property>1.0Per-property BM25 weight for indexes covering multiple properties. Use to bias scoring.
GQL
-- Full-text index prodDesc on product nodes' description
CREATE FULLTEXT prodDesc ON NODE product (description)

-- Full-text index reviewText on review edges' content and excerpt
CREATE FULLTEXT reviewText ON EDGE review (content, excerpt)

-- English-only index, no stemming
CREATE FULLTEXT skuCode ON NODE product (sku) OPTIONS { analyzer: 'simple' }

-- Chinese-only, recall-oriented segmentation
CREATE FULLTEXT zhArticle ON NODE article (body) OPTIONS { analyzer: 'gse', gseMode: 'search' }

-- Multi-property index, title weighted 3× over body in BM25 scoring
CREATE FULLTEXT articleText ON NODE article (title, body)
  OPTIONS { weight_title: 3.0, weight_body: 1.0 }

Dropping Full-text Index

Dropping a full-text index does not affect the actual property values. Full-text index names are unique within a graph, so the NODE / EDGE qualifier is optional on DROP:

GQL
-- Unqualified form (recommended)
DROP FULLTEXT prodDesc

-- Qualified forms
DROP NODE FULLTEXT prodDesc
DROP EDGE FULLTEXT reviewText

Use IF EXISTS to avoid errors when the index doesn't exist:

GQL
DROP FULLTEXT IF EXISTS prodDesc

Using Full-text Index

Use a full-text index in search conditions with the syntax WHERE ~<index name> CONTAINS "<search>":

  • The ~ symbol marks the full-text index.
  • The operator CONTAINS checks if the segmented tokens in the full-text index match the query.
  • Results are ranked by BM25 relevance score (highest relevance first).
  • If a double quotation mark appears in a keyword, prefix it with a backslash (\) to escape.

Search Syntax

By default, multiple keywords separated by spaces are combined with AND (all must match). Additional operators are supported within the <search> string:

OperatorSyntaxDescription
AND"graph database"Default. Entries whose tokens include both graph and database.
OR"graph OR database"Entries whose tokens include graph or database (or both).
NOT"-graph"Entries whose tokens do not include graph.
Phrase"\"graph database\""Entries whose tokens include graph followed immediately by database.
Proximity"\"graph database\"~5"Entries whose tokens include both graph and database within 5 token positions of each other.
Wildcard"graph*"Entries whose tokens start with graph (e.g., graph, graphics, graphdb).
Wildcard"grap?"Entries whose tokens match with ? as any single character (e.g., graph, grape).
Fuzzy"graph~2"Entries whose tokens are within N character edits of the term (N is 0–2, default 2). Catches typos: "graph~2" matches grph, garph, graphs. The exact term always matches and ranks highest, with closer edits ranked above farther ones. "graph~0" requires an exact match.
Grouped"(graph OR network) AND database"Entries matching the combined sub-expressions; parentheses control precedence.

Notes on fuzzy matching (term~N):

  • A letter swap counts as one edit. Transposing two adjacent letters (the most common typo) is a single edit, so "Einstien~1" matches Einstein and "teh~1" matches the.
  • The first letter is anchored. Fuzzy matching only varies characters from the second onward, so a first-letter typo (graph → hraph) is out of scope. This keeps fuzzy search fast on large term dictionaries; when the first character may vary, use a prefix search (graph*) instead.
  • Edits are measured against the stored token. Fuzziness compares against the analyzed form held in the index, so under a stemming analyzer database is stored as its stem databas and edits count against that. Use the simple analyzer (no stemming) to fuzzy-match the surface word.

Retrieving Nodes or Edges

Find nodes using the full-text index prodDesc where their tokens include graph and database:

GQL
MATCH (n WHERE ~prodDesc CONTAINS "graph database")
RETURN n

Find nodes using the full-text index prodDesc where their tokens include graph or database:

GQL
MATCH (n WHERE ~prodDesc CONTAINS "graph OR database")
RETURN n

Find edges using the full-text index reviewText where their tokens include graph and those start with ult:

GQL
MATCH ()-[e WHERE ~reviewText CONTAINS "graph ult*"]-()
RETURN e

Retrieving Paths

Note: In path-returning queries, putting the inline form on the first node is the only supported placement.

GQL
MATCH p = (WHERE ~prodDesc CONTAINS "graph")-[]-(n)
RETURN p

To filter on any other element, run the full-text match in its own MATCH first, then build the path from the bound variable:

GQL
-- ✗ not supported; uses full-text index on the last node
MATCH p = ()-[]-(WHERE ~prodDesc CONTAINS "graph") 
RETURN p

-- ✓ run the full-text match in its own MATCH first
MATCH (n WHERE ~prodDesc CONTAINS "graph")
MATCH p = ()-[]-(n)
RETURN p

-- ✗ not supported; uses full-text index on the edge
MATCH p = ()-[WHERE ~reviewText CONTAINS "ult*"]-()
RETURN p

-- ✓ run the full-text match in its own MATCH first
MATCH ()-[e WHERE ~reviewText CONTAINS "ult*"]-()
MATCH p = ()-[e]-()
RETURN p

Procedures

ft.search

The WHERE ~<index name> CONTAINS "<search>" form above is convenient as an inline filter inside MATCH, but for top-N ranked retrieval the ft.search procedure is the preferred entry point. It runs a BM25-ranked search over a named full-text index, and yields each matching node with its relevance score.

Syntax
CALL ft.search(<index name>, <search> [ , <options> ]) 
YIELD <column1>, <column2>, ...

Parameters:

ParameterTypeDefaultDescription
<index name>STRING—Name of a full-text index.
<search>STRING—The search string. Same syntax as the inline CONTAINS form, see Search Syntax.
<options>
MAP
limitINTEGER10Maximum number of ranked results to return.
offsetINTEGER0Number of top-ranked results to skip (pagination).
minScoreFLOAT0Drop results whose BM25 score is below this floor.
operatorSTRINGANDDefault combinator for the space-separated query clauses: AND (all clauses must match) or OR (any). Also applies to fuzzy (term~N) and wildcard clauses. An invalid value raises an error.
minShouldMatchINTEGER0Require at least this many of the query clauses to match; overrides operator. 0 uses operator.
highlightMAP(off) Returns an additional highlight column containing a snippet of the matching text with the matched tokens wrapped in tags. Use it for search-result UIs, snippet previews, or to confirm visually which tokens the engine matched.

Nested keys:
  • field: required; the indexed property to extract the snippet from. Highlighting is off unless this is set.
  • preTag: default <em>; inserted immediately before each matched token.
  • postTag: default </em>; inserted immediately after each matched token.
  • fragmentSize: approximate snippet length in characters; controls how much surrounding context is shown.

Return columns:

ColumnTypeDescription
nodeNODEThe matching node, bindable downstream.
scoreFLOATBM25 relevance score; higher is more relevant.
idSTRINGThe matching node's _id.
highlightSTRINGHighlighted fragment from the field named in highlight.field. Present only when highlighting is enabled.
GQL
-- Top 10 ranked results
CALL ft.search('prodDesc', 'graph database') YIELD node, score
RETURN node, score ORDER BY score DESC

-- Paginated with relevance floor
CALL ft.search('prodDesc', 'graph database', {limit: 20, offset: 20, minScore: 0.5})
YIELD node, score
RETURN node.name, score

-- Phrase and exclusion
CALL ft.search('prodDesc', '"graph database" -deprecated', {limit: 10})
YIELD node, score
RETURN node, score

-- Match ANY of the terms (operator: OR)
CALL ft.search('prodDesc', 'graph database nosql', {operator: 'OR', limit: 10})
YIELD node, score
RETURN node, score ORDER BY score DESC

-- Require at least 2 of the 3 terms (minShouldMatch overrides operator)
CALL ft.search('prodDesc', 'graph database nosql', {minShouldMatch: 2})
YIELD node, score
RETURN node, score ORDER BY score DESC

-- Search then traverse
CALL ft.search('prodDesc', 'graph database', {limit: 5}) YIELD node, score
MATCH (node)-[:WROTE]-(a:Author)
RETURN node.title, score, collect(a.name) AS authors
ORDER BY score DESC

-- With highlighted snippet
CALL ft.search('prodDesc', 'graph database', {limit: 5, highlight: {field: 'description', preTag: '<mark>', postTag: '</mark>', fragmentSize: 150}})
YIELD node, score, highlight
RETURN node._id, score, highlight

ft.analyze

Tokenizes a string with a named analyzer and returns the resulting tokens with their positions. Use this to debug why a query term does or doesn't match indexed text. Common causes are stemming ('Running' → 'run'), stop-word removal ('the' dropped), or CJK segmentation that differs from what you expected.

Syntax
CALL ft.analyze(<analyzer>, <text>)
YIELD <column1>, <column2>, ...

Parameters:

ParameterTypeDescription
<analyzer>STRINGOne of mixed, simple, cjk, gse. (pipeline is accepted as an alias for mixed.)
<text>STRINGText to tokenize. Empty string yields zero rows.

Return columns:

ColumnTypeDescription
tokenSTRINGA normalized token (after lower-casing, stemming, CJK segmentation).
positionINTEGER0-based token position in the text.
GQL
-- English stemming under the default analyzer
CALL ft.analyze('mixed', 'Running graphs') YIELD token, position
RETURN token, position

-- Same input, no stemming
CALL ft.analyze('simple', 'Running graphs') YIELD token
RETURN token

-- CJK segmentation
CALL ft.analyze('mixed', '北京大学') YIELD token, position
RETURN token, position

ft.suggest

Returns indexed terms that start with a given prefix, most frequent first. Suggestions come from the index's term dictionary, so they reflect the analyzed (lower-cased, stemmed, segmented) forms actually stored. A typical search box uses ft.suggest for the autocomplete dropdown to complete the word a user is typing.

Syntax
CALL ft.suggest(<index name>, <prefix> [ , <options> ])
YIELD <column1>, <column2>, ...

Parameters:

ParameterTypeDescription
<index name>STRINGName of a full-text index.
<prefix>STRINGThe partial term typed so far. It is lower-cased to match indexed terms; an empty string returns the most frequent terms.
<options>RECORDOptional. {limit: <n>} caps the number of suggestions (default 10, minimum 1).

Return columns:

ColumnTypeDescription
suggestionSTRINGA matching indexed term.
docFreqINTEGERNumber of documents containing the term (popularity).
GQL
-- Autocomplete: terms starting with 'grap', most popular first
CALL ft.suggest('prodDesc', 'grap', {limit: 5}) YIELD suggestion, docFreq
RETURN suggestion ORDER BY docFreq DESC

-- Works script-agnostically: a CJK prefix returns the segmented terms that start with it
CALL ft.suggest('zhDesc', '社', {limit: 5}) YIELD suggestion, docFreq
RETURN suggestion ORDER BY docFreq DESC

ft.unload

Releases a full-text index's in-memory structure (its posting lists, usually the dominant part of the index's memory) back to the operating system. The index stays registered and its on-disk data intact, so it remains searchable; the next search, or a ft.load call, streams it back into memory. SHOW FULLTEXT then reports the index with status = unloaded. This is useful for deployments with many large but rarely searched indexes.

Syntax
CALL ft.unload(<index name>)
YIELD <column>

Parameters:

ParameterTypeDescription
<index name>STRINGName of a full-text index (see SHOW FULLTEXT).

Return columns:

ColumnTypeDescription
resultSTRINGOutcome message.
GQL
-- Release a rarely-used index's memory
CALL ft.unload('prodDesc') YIELD result
RETURN result

ft.load

Loads (prewarms) a full-text index's in-memory structure and waits until it is ready to search. Use it to pay the load cost at a chosen moment rather than on the first user query, for example to restore the fast in-memory path after a ft.unload, or to warm an index before a query burst.

Syntax
CALL ft.load(<index name> [ , <timeoutMs> ])
YIELD <column>

Parameters:

ParameterTypeDescription
<index name>STRINGName of a full-text index (see SHOW FULLTEXT).
<timeoutMs>INTEGEROptional. Maximum time to wait for the load, in milliseconds (default 60000, minimum 1).

Return columns:

ColumnTypeDescription
resultSTRINGOutcome message.
GQL
-- Prewarm before a query burst
CALL ft.load('prodDesc') YIELD result
RETURN result