Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Full-Text Search

patinaDB supports user-defined full-text indexes with BM25 ranking, using Neo4j-compatible syntax. You create an index over chosen string properties of a label (nodes) or relationship type (edges), then query it through the full-text procedures, getting back ranked entities and relevance scores.

Creating an index

-- Over node properties
CREATE FULLTEXT INDEX docs FOR (n:Doc) ON EACH [n.title, n.body]

-- Over relationship properties
CREATE FULLTEXT INDEX mentions FOR ()-[r:MENTIONS]-() ON EACH [r.context]

-- With an analyzer
CREATE FULLTEXT INDEX articles FOR (n:Article) ON EACH [n.body]
OPTIONS { indexConfig: { `fulltext.analyzer`: "english" } }
  • Only string properties are indexed (matching Neo4j). Non-string values on the listed properties are ignored.
  • The index is maintained synchronously: it is updated as part of every committed write, so it is never stale on read (no eventual consistency).

Analyzers

The analyzer controls tokenization and stemming:

AnalyzerBehaviour
standardLowercase, tokenize on non-alphanumerics. (Default.)
keywordTreat the whole value as a single token (exact-match indexing).
englishStandard + English Snowball stemming + English stop words.
germanStandard + German Snowball stemming + German stop words.

Managing indexes

SHOW FULLTEXT INDEXES
DROP INDEX docs

DROP FULLTEXT INDEX <name> and DROP INDEX <name> drop a full-text index. An unknown name is an error; add IF EXISTS for an explicit no-op when absent. Malformed trailing text is rejected without deleting the index. Registered ordinary indexes take precedence for the shorthand DROP INDEX <name>; DROP INDEX ON :Label(prop) remains the ordinary-index form.

Querying

CALL db.index.fulltext.queryNodes('docs', 'graph database')
YIELD node, score
RETURN node, score ORDER BY score DESC

CALL db.index.fulltext.queryRelationships('mentions', '"exact phrase"')
YIELD relationship, score
RETURN relationship, score

The alias patinadb.fulltext.queryNodes / …queryRelationships is equivalent. Results are ranked by BM25 (k1 = 1.2, b = 0.75).

Query syntax (Lucene subset)

FormExampleMeaning
TermgraphDocuments containing the term.
Implicit ORgraph databaseAdjacent terms are OR’d.
AND / OR / NOTgraph AND NOT sqlBoolean operators; AND binds tighter than OR.
Phrase"graph database"Terms in that exact order (consecutive positions).
Prefixdata*Terms starting with data.
Fuzzydatabse~, db~2Edit-distance match (default / explicit distance).
Boostgraph^3 databaseMultiply a term’s contribution to the score.
Fieldtitle:graphRestrict a term to one indexed property.
Grouping(graph OR sql) AND dbParenthesised sub-expressions.

Prefix and fuzzy queries expand against the index dictionary and are capped at 256 expansions per term to bound cost.

In the server

CREATE FULLTEXT INDEX / DROP INDEX are replicated as Raft control commands (like CREATE DATABASE): the definition propagates to every node and each node builds the index from its own copy of the graph. Index definitions are carried in Raft snapshots and rebuilt on snapshot install, so a node that joins or restarts ends up with the same indexes. Query procedures read the local node’s index.

This works end-to-end over Bolt, including the Neo4j Browser: create an index and run queryNodes, and matching nodes come back as graph nodes with scores.

Limitations

Measured Jira-summary workload (0.17.0)

On 334,770 real Apache Jira summaries, standard analyzer, one indexed summary property, local Ryzen 7 3800X/NVMe, the 2026-09-10 measurement under #1261 found:

  • Seventeen tested term/boolean/phrase/prefix/fuzzy/project-filtered forms had warm Bolt Top-10/20 p90 between 2.173 and 63.868 ms (20 samples per cell).
  • A short 2 reads/s plus 0.1 summary updates/s run had read p90 66.816 ms and p99 122.826 ms; updates, batch changes, graceful reopen and deletion passed search-visibility checks. Exact hit sets and single-term BM25 scores were independently checked.
  • Server high-water RSS reached 378.957 MiB through the batch phase; the standalone build peaked at 464.113 MiB. These exclude the Python oracle.
  • Initial construction took 7.950 seconds, versus 1727.563 seconds in the retained 0.16.0 baseline: 217.31 times faster on this fixture (#1261). Allocated database-directory size after building fell from 1375772672 to 477433856 bytes; this is the whole directory, not posting-only storage.

These are summary-only, single-node measurements, not 10M-ticket, multi-region, long-soak, ticket-body/comment or other-analyzer guarantees. Full methodology and raw artifact locations are recorded in the repository’s docs/fulltext-benchmark-1261.md (#1261).

Construction and resource use

Initial construction and reconstruction stage postings into sorted runs and write each term’s final list once. The existing format and synchronous DDL boundary remain unchanged; updates after construction still use per-document maintenance. Existing indexes remain compatible without rebuilding.

The builder uses the sort-run byte setting and private temporary files under TMPDIR. Budget additional memory for merge readers, the largest posting list, serialization and database caches; the run buffer cap is not a total-RAM ceiling. Scratch files are removed on ordinary completion/error/cancellation, but a hard process kill can leave temporary files.

Feature boundaries

  • No phrase slop / proximity ("a b"~3) — phrases must be exactly consecutive.
  • No highlighting / snippet extraction.
  • No numeric or range queries inside the full-text string (full-text indexes cover string properties only).
  • Postings are updated read-modify-write per document with no segment merging, so very high write throughput on a large indexed corpus is not the design target. See Limitations.