Full-Text Search¶
Prerequisites
This guide assumes familiarity with Database access and Pagination. Read those first if you haven't already.
Den has built-in full-text search support. Mark fields with the fts tag option, and Den handles the FTS index creation and querying internally — for both SQLite (FTS5) and PostgreSQL (tsvector).
Compared to LIKE '%term%' queries, full-text search offers:
| Feature | LIKE |
Full-Text Search |
|---|---|---|
| Word-based matching | No (substring only) | Yes |
| Relevance ranking | No | Yes |
| Match highlighting | No | Yes (backend-dependent) |
| Performance at scale | O(n) full scan | O(log n) inverted index |
Defining FTS Fields¶
Add the fts option to your den struct tags on fields that should be searchable:
type Note struct {
document.Base
UserID string `json:"user_id"`
Title string `json:"title" den:"index,fts"`
Content string `json:"content" den:"fts"`
}
Den automatically creates and maintains the full-text search index when the document type is registered. No manual migration files, virtual tables, or triggers needed.
Searching¶
Use den.NewQuery with .Search() to perform full-text queries:
// Search across all FTS-indexed fields
results, err := den.NewQuery[Note](db).Search(ctx, "hello world")
// Search with additional conditions
results, err := den.NewQuery[Note](db,
where.Field("user_id").Eq(userID),
).Search(ctx, "hello world")
Repository Integration¶
Add a Search method that uses Den's built-in search and Burrow's pagination:
// Search performs a full-text search across note titles and content.
func (r *Repository) Search(
ctx context.Context,
userID string,
query string,
pr burrow.PageRequest,
) ([]Note, burrow.PageResult, error) {
query = strings.TrimSpace(query)
if query == "" {
return nil, burrow.PageResult{}, nil
}
// Count total matches for pagination.
count, err := den.NewQuery[Note](r.db,
where.Field("user_id").Eq(userID),
).Count(ctx)
if err != nil {
return nil, burrow.PageResult{}, fmt.Errorf("count search results: %w", err)
}
// Fetch matching notes ranked by relevance.
results, err := den.NewQuery[Note](r.db,
where.Field("user_id").Eq(userID),
).Limit(pr.Limit).Skip(pr.Offset()).Search(query)
if err != nil {
return nil, burrow.PageResult{}, fmt.Errorf("search notes: %w", err)
}
return results, burrow.OffsetResult(pr, count), nil
}
Handling User Input¶
Full-text search supports query syntax that users can leverage for more precise searches:
| Query | Meaning |
|---|---|
hello world |
Rows containing both "hello" and "world" |
hello OR world |
Rows containing either word |
"hello world" |
Exact phrase match |
hello NOT world |
"hello" but not "world" |
hel* |
Prefix match — "hello", "help", etc. |
In most cases, you want to pass user input directly and let users benefit from this syntax. The main thing to handle is invalid queries — e.g., unmatched quotes or stray operators — which cause the search to return an error.
A straightforward approach: attempt the query and fall back to an empty result set on syntax errors.
results, pageResult, err := repo.Search(ctx, userID, query, pr)
if err != nil {
// Search syntax errors — return empty results instead of a 500 error.
if strings.Contains(err.Error(), "syntax error") {
results, pageResult = nil, burrow.PageResult{}
} else {
return err
}
}
Empty queries
Always check for empty/whitespace-only input before calling search. The Search method above already handles this by returning early when query is blank.
Using FTS in Admin Views¶
When building admin views with hand-written handlers, use the same den FTS query methods described above. For example, a search handler can use where.Field("title").Match(query) for FTS-indexed fields, falling back to where.Field("title").Contains(query) for non-FTS fields.
See contrib/auth/repository.go (SearchUsers) for a working example of admin search with FTS-compatible queries.
Working Example¶
The Notes example app implements full-text search end-to-end — document definitions with FTS tags, repository search method, and an HTMX-powered search form. Read its source to see all the pieces working together.