Search
Add full-text search to your site with no search service to run.
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:
| Type | Finds |
|---|---|
preview branch | Documents with both words |
"preview branch" | That exact phrase |
preview or draft | Either 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 engine | Postgres engine | |
|---|---|---|
| Search technology | SQLite FTS5 | Postgres full-text search (tsvector, GIN index) |
| Ranking | bm25, weighted by field | ts_rank, weighted by field |
| Extra setup | None | None 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
- Reading content covers the other two reads.
- Operational data covers
searchRecords.