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:
| Analyzer | Behaviour |
|---|---|
standard | Lowercase, tokenize on non-alphanumerics. (Default.) |
keyword | Treat the whole value as a single token (exact-match indexing). |
english | Standard + English Snowball stemming + English stop words. |
german | Standard + German Snowball stemming + German stop words. |
Managing indexes
SHOW FULLTEXT INDEXES
DROP INDEX docs
DROP INDEX <name>drops a full-text index by name. The Neo4jDROP INDEX ON :Label(prop)form (for ordinary indexes) is a different, unsupported statement and is left to the engine.
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)
| Form | Example | Meaning |
|---|---|---|
| Term | graph | Documents containing the term. |
| Implicit OR | graph database | Adjacent terms are OR’d. |
AND / OR / NOT | graph AND NOT sql | Boolean operators; AND binds tighter than OR. |
| Phrase | "graph database" | Terms in that exact order (consecutive positions). |
| Prefix | data* | Terms starting with data. |
| Fuzzy | databse~, db~2 | Edit-distance match (default / explicit distance). |
| Boost | graph^3 database | Multiply a term’s contribution to the score. |
| Field | title:graph | Restrict a term to one indexed property. |
| Grouping | (graph OR sql) AND db | Parenthesised 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
- 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.