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: ready, loading, or building.
doc_countNumber of documents indexed.
progressBuild/loading progress.

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 (default N=2). Catches typos — "graph~2" matches grph, garph, graphs.
Grouped"(graph OR network) AND database"Entries matching the combined sub-expressions; parentheses control precedence.

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

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