Search

Add full-text search to your site with no search service to run.

Content2 min read

static or Postgres

Every compile also builds a full-text search index of your content. You do not run a search service, sync anything, or write indexing code. Call searchContent and you get the best matches first, each with a snippet showing where the words matched.

Search a collection

const hits = await graft.searchContent("docs", "preview branch");

for (const hit of hits) {
  hit.slug;       // which document
  hit.data.title; // typed, like any read
  hit.snippet;    // the matching passage, with matched words in <b>
}

Agents search the same way with the search_content MCP tool. Each of their results also includes sourcePath, the file to edit.

On a preview branch, search covers the branch's changes plus everything it inherits from main, the same as any other read.

What readers can type

The query syntax is the one people already use in web search engines:

TypeFinds
preview branchDocuments with both words
"preview branch"That exact phrase
preview or draftEither word
branch -neon"branch", but not "neon"

An empty query fails with INPUT_VALIDATION_FAILED. On Postgres, a query made only of common words such as "the" returns no results rather than an error, because Postgres drops those words. The static engine keeps them, so the same query matches the documents that contain them.

Show the snippets safely

Snippets wrap the matched words in <b> tags, on both engines. To show the highlighting, render the snippet as HTML. Snippets come from your own content, but if writers you do not fully trust can edit it, strip every tag except <b> first. To show plain text instead, remove the tags.

How results are ranked

Matches in the slug count most, then matches in frontmatter text fields, then matches in the body, on both storage engines. The query syntax is the same too. The scoring formula is not, so the same query finds the same kind of matches on both but can list them in a different order:

Static enginePostgres engine
Search technologySQLite FTS5Postgres full-text search (tsvector, GIN index)
Rankingbm25, weighted by fieldts_rank, weighted by field
Extra setupNoneNone beyond the database

Search your users' data

To search operational data, such as form submissions, use searchRecords inside a typed function. It takes the same query syntax. Like every read of user data, it only runs inside a function, so access rules apply. It needs the Postgres engine.

Language

Search uses English word rules on both engines: it matches "branches" when someone types "branch". Other languages are not configurable yet.

Next steps