AgensGraph
/
  • v2.18 (latest)
  • v2.17
  • v2.16
  • v2.15
  • v2.18.6.1 (latest)
  • v2.18.6.0
PDF
EN한국어
  • Get Started
    • What AgensGraph is
    • Install AgensGraph
    • The example dataset
    • Your first query
  • Concepts
    • The property graph model
    • How a graph is stored
    • Label inheritance
    • Cypher, GQL and SQL
  • Querying
    • Reading a graph
    • Writing a graph
    • Composing a query
    • Paths
    • Indexes and constraints
    • Promoting a property to a column
    • Combining SQL and Cypher
    • Tuning a query
  • Reference
    • Syntax
    • Data types
    • Clauses
      • MATCH
      • OPTIONAL MATCH
      • WHERE
      • LOAD FROM
      • RETURN
      • WITH
      • LET
      • ORDER BY
      • SKIP and LIMIT
      • CREATE
      • INSERT
      • MERGE
      • SET
      • REMOVE
      • DELETE
      • UNWIND
      • FOR
      • FILTER
      • NEXT
      • FINISH
      • UNION
      • CALL
      • CALL … YIELD
      • CALL { subquery }
      • OPTIONAL CALL
      • Subquery value expressions
    • Data Definition
      • CREATE GRAPH
      • DROP GRAPH
      • CREATE VLABEL
      • CREATE ELABEL
      • ALTER VLABEL and ALTER ELABEL
      • DROP VLABEL and DROP ELABEL
      • CREATE PROPERTY INDEX
      • DROP PROPERTY INDEX
      • REINDEX VLABEL and REINDEX ELABEL
      • CREATE CONSTRAINT
      • DROP CONSTRAINT
    • Functions
      • All functions
      • Aggregate functions
      • Predicate functions
      • Scalar functions
      • List functions
      • String functions
      • Numeric functions
      • Path functions
      • Type conversion functions
      • jsonb operators and functions
    • Server settings
    • The agens client
  • Guides
    • Loading data
    • Vector search
      • Overview
      • Installing pgvector
      • Example data
      • Similarity search
      • Indexing with HNSW
      • Indexing a promoted column
      • GraphRAG
    • Full-text search
      • Overview
      • Example data
      • Searching a text property
      • Ranking results
      • Exact-term matching
      • Indexing with GIN
      • Indexing a promoted column
      • GraphRAG
    • Hybrid search
      • Overview
      • Reciprocal Rank Fusion
      • Example data
      • The hybrid query
      • Indexing both arms
      • Tuning
      • GraphRAG
    • AI and LLM integrations
      • Overview
      • LangChain
      • LlamaIndex
      • LightRAG
      • cognee
      • MCP servers
      • Without a framework
  • Drivers and client libraries
    • The Python driver
    • Other languages
    • Connection and parameters
    • AI and LLM integrations
      • Overview
      • LangChain
      • LlamaIndex
      • LightRAG
      • cognee
      • MCP servers
      • Without a framework
  • Operations
    • Deployment
    • Configuration
    • Security
    • Backup and recovery
    • Server-side tools
  • System Catalogs
    • Graph catalogs
    • Graph views
    • Graph metadata
  • Release Notes
    • AgensGraph 2.18.6.0
    • Upgrading to 2.18
    • Upgrade walkthrough
    • AgensGraph 2.17.0
  • Appendixes
    • GQL conformance
    • Cypher compatibility
    • Keywords
    • Error codes
    • Limits
    • Glossary

On this page

  • Overview
  • Example data
  • Searching a text property
  • Ranking results
  • Exact-term matching
  • Indexing with GIN
  • Indexing a promoted column
  • GraphRAG
  1. AgensGraph /
  2. Guides /
  3. Full-text search
View as Markdown Open in ChatGPT Open in Claude

Full-text search#

Overview#

AgensGraph is built on PostgreSQL and inherits its full-text search engine, so you can run linguistic keyword search directly over text stored in graph properties. Full-text search normalizes documents and queries into lexemes (stemming, stop-word removal, case folding). That makes it well suited to lexical retrieval: matching the words a user typed.

It is the natural complement to vector search, which matches by meaning. Hybrid Search covers combining the two. All three pages use the same movies, so you can compare the results directly.

A graph property is stored as jsonb, so a text property is cast to text (with ::text) before being converted to a tsvector.

Example data#

The examples use the example dataset: 18 films, each with a plot and a four-dimensional embedding of that plot. Load it before going further.

Real embeddings come from a model, such as text-embedding-3-small or a local sentence-transformers model, and they have hundreds of dimensions. These are reduced to four so the geometry stays readable on the page.

Searching a text property#

Convert the property to a tsvector with to_tsvector, parse the user’s input with one of the *_to_tsquery functions, and match them with the @@ operator. websearch_to_tsquery accepts a forgiving, web-style syntax: quoted phrases, OR, and - to exclude a term. That makes it a good default for user input.

MATCH (m:movie)
WHERE to_tsvector('english', m.plot::text) @@ websearch_to_tsquery('english', 'machines')
RETURN m.title AS title;
           title
----------------------------
 "The Matrix"
 "The Matrix Reloaded"
 "The Matrix Revolutions"
 "The Matrix Resurrections"
(4 rows)

The query is matched against lexemes, not raw text. machines is reduced to the stem machin, so the singular machine would match exactly the same rows.

Look also at what is not returned. Total Recall and The Terminator are thematically about machine-dominated futures, but their plots never use the word, so keyword search cannot find them. That recall gap is exactly where vector search helps.

Other query parsers are available depending on the input you expect:

Function

Input style

websearch_to_tsquery

Web-style: machines "virtual reality" -documentary

plainto_tsquery

Plain text, all terms AND-ed together

to_tsquery

Explicit operators: machines & reality

Ranking results#

ts_rank scores how well each document matches the query, so results can be ordered by relevance. A plot that matches more of the query terms ranks higher:

MATCH (m:movie)
WHERE to_tsvector('english', m.plot::text) @@ websearch_to_tsquery('english', 'machines OR reality')
RETURN m.title AS title,
       ts_rank(to_tsvector('english', m.plot::text),
               websearch_to_tsquery('english', 'machines OR reality')) AS rank
ORDER BY rank DESC;
           title            |    rank
----------------------------+-------------
 "The Matrix"               |  0.06079271
 "The Matrix Reloaded"      | 0.030396355
 "The Matrix Revolutions"   | 0.030396355
 "The Matrix Resurrections" | 0.030396355
(4 rows)

The Matrix ranks above its sequels because its plot contains both machines and reality, while the sequels match only machines.

Exact-term matching#

Full-text search is the right tool when the query is a specific term that must match exactly: a name, a code, a rare keyword. Asked for the film about dinosaurs, it returns precisely one:

MATCH (m:movie)
WHERE to_tsvector('english', m.plot::text) @@ websearch_to_tsquery('english', 'dinosaurs')
RETURN m.title AS title;
      title
-----------------
 "Jurassic Park"
(1 row)

Vector search has no notion of the literal word dinosaurs. Asked for the films most similar to Jurassic Park, it returns The Avengers as the nearest neighbor. Both are large-scale spectacle blockbusters, a reasonable guess by meaning, but not what someone typing “dinosaurs” wants:

MATCH (m:movie), (q:movie {title: 'Jurassic Park'})
RETURN m.title AS title
ORDER BY m.embedding::vector(4) <=> q.embedding::vector(4)
LIMIT 3;
      title
-----------------
 "Jurassic Park"
 "The Avengers"
 "Interstellar"
(3 rows)

So the two methods have opposite blind spots. Full-text search is precise but misses synonyms and intent, while vector search captures meaning but drifts on exact terms. Hybrid Search runs both and fuses the results to get the strengths of each.

Indexing with GIN#

Recomputing to_tsvector for every row on each search does not scale. Create a GIN index on the same to_tsvector expression so matches are found through the index. The indexed value is an expression over a property, so use a property index and wrap the expression in parentheses:

CREATE PROPERTY INDEX ON movie
    USING gin ((to_tsvector('english', plot::text)));

The search then uses the index instead of scanning every row:

EXPLAIN
MATCH (m:movie)
WHERE to_tsvector('english', m.plot::text) @@ websearch_to_tsquery('english', 'machines')
RETURN m.title;
 Bitmap Heap Scan on movie m
   Recheck Cond: (to_tsvector('english'::regconfig, (properties.'plot'::text)::text) @@ '''machin'''::tsquery)
   ->  Bitmap Index Scan on movie_plot_idx
         Index Cond: (to_tsvector('english'::regconfig, (properties.'plot'::text)::text) @@ '''machin'''::tsquery)

The text search configuration used at query time ('english' above) must match the one in the index expression for the index to be used.

On this small demo the planner may still prefer a sequential scan because it is cheaper for so few rows; add SET enable_seqscan = off to observe the index plan. On a real corpus the planner chooses the index automatically.

GIN property indexes over a to_tsvector(...) expression require AgensGraph 2.17 or later.

Indexing a promoted column#

From 2.18 a property can be promoted to a real typed column, which removes the per-row jsonb read and the ::text cast. ALTER VLABEL movie ADD COLUMN plot text GENERATED mirrors the property named by the column, here plot; GENERATED (sourceKey) mirrors a differently named key. Queries keep naming the property, never the column — resolution is by property key.

The index on this page needs rebuilding when you promote, because it is an expression over the property. A property index over an expression is built against the jsonb bag, and a query over a promoted key reads the column, so the two would no longer meet.

Write this one as SQL over the column. \dGi still lists it, and the query uses it:

DROP PROPERTY INDEX movie_plot_idx;
ALTER VLABEL movie ADD COLUMN plot text GENERATED;
CREATE INDEX movie_plot_idx ON moviekb.movie USING gin (to_tsvector('english', plot));
EXPLAIN (COSTS OFF)
MATCH (m:movie)
WHERE to_tsvector('english', m.plot) @@ websearch_to_tsquery('english', 'machines')
RETURN m.title;
 Bitmap Heap Scan on movie m
   Recheck Cond: (to_tsvector('english'::regconfig, plot) @@ '''machin'''::tsquery)
   ->  Bitmap Index Scan on movie_plot_idx
         Index Cond: (to_tsvector('english'::regconfig, plot) @@ '''machin'''::tsquery)

m.plot needs no ::text now: the promoted column is already text. See An expression property index is built over the bag.

GraphRAG#

Full-text search can also seed a graph traversal. Find the movies whose text best matches the question, then expand along relationships to gather connected context for a language model.

This is the lexical counterpart to the GraphRAG example in Vector Search. It shines when the question contains specific terms (names, codes, exact phrases) that keyword search handles better than embeddings.

The example dataset already carries related_to edges between films: franchise sequels and thematically linked titles. So the traversal below has something to follow.

A single Cypher query ranks the movies by full-text relevance to the question (here the term resistance), keeps the best matches, and expands them to their neighbors:

MATCH (seed:movie)
WHERE to_tsvector('english', seed.plot::text) @@ websearch_to_tsquery('english', 'resistance')
WITH seed, ts_rank(to_tsvector('english', seed.plot::text),
                   websearch_to_tsquery('english', 'resistance')) AS rank
ORDER BY rank DESC, seed.title
LIMIT 2
MATCH (seed)-[:related_to]-(ctx:movie)
RETURN seed.title AS seed, collect(DISTINCT ctx.title) AS related_context;
             seed             |               related_context
------------------------------+----------------------------------------------
 "Terminator 2: Judgment Day" | ["The Terminator"]
 "The Terminator"             | ["Terminator 2: Judgment Day", "The Matrix"]
(2 rows)

The WHERE filter and WITH … ORDER BY rank LIMIT select the movies whose text best matches the query. Those are the seeds, here the two Terminator films. The second MATCH follows related_to edges to gather related titles.

The seeds plus their neighbors form the context passed to the model. Notice that the traversal reaches The Matrix. The keyword resistance never matched that film, but it is linked to the Terminator films by their shared war-against-machines theme. To combine this lexical retrieval with semantic (vector) retrieval before expanding, see Hybrid Search.

For the same retrieval through a framework rather than by hand, see AI and LLM integrations.

Previous
Vector search
Next
Hybrid search

2026, SKAI Worldwide Co., Ltd

Made with Shibuya theme.