Search
Full-text search over your content, powered by SQLite FTS5 — no external search service, nothing to install. Point it at a folder and start querying.
Basil-only.
@SEARCHrequires the Basil server environment.
Quick Start
let {search, error} = @SEARCH({watch: @./docs})
let q = @params.q ?? ""
let results = search.query(q, {limit: 10})
<form method="get">
<input type="search" name="q" value={q} placeholder="Search…"/>
</form>
<ul>
for (result in results.items) {
<li>
<a href={`/{result.path}`}>result.title</a>
<p>result.snippet</p>
</li>
}
</ul>
On the first query, Basil scans the watched folder, reads titles, tags, and dates from YAML frontmatter, extracts headings for ranking, and builds the index. Every query after that checks the folder for changes, so new, edited, and deleted files show up on their own.
@SEARCH(options)
Returns {search, error} — the search instance, or an error message if setup failed.
| Key | Type | Description |
|---|---|---|
path |
string | SQLite index file (":memory:" for tests) |
watch |
path or array | Folder(s) to index automatically |
snippetLength |
int | Approximate snippet length in characters (default 200, max ~320) |
highlightTag |
string | HTML tag wrapped around matched terms (default "mark") |
Indexes Markdown and HTML out of the box, plus text-based PDF and DOCX files (50 MB per-file limit). Unrecognised option keys are an error, so typos don't fall back to silent defaults. See the search guide for extensions, weights, and tokenizer.
search.query(text, options?)
| Key | Type | Default | Description |
|---|---|---|---|
limit |
int | 10 | Maximum results |
offset |
int | 0 | Pagination offset |
raw |
bool | false |
Pass the query straight to FTS5 (advanced) |
filters |
dictionary | — | Narrow results by tag or date (see below) |
The query syntax works like a search engine: words are ANDed together, "quotes make phrases", and a -minus excludes a word. An empty query returns empty results, so it's safe to wire straight to a form.
Returns {items, total, limit, offset, query}, where total reflects the filtered match count. Each item has:
| Field | Description |
|---|---|
url |
Link to the result — use this for documents added with search.add(), which have no source path |
path |
Path object for the source file — build links with `/{result.path}` (omitted for manually added documents) |
title |
Document title (frontmatter, first heading, or filename) |
snippet |
Excerpt with matched terms wrapped in <mark>…</mark> (or your highlightTag) |
rank |
1-based position in the results |
score |
Relevance score, mostly useful for debugging |
date |
The document's date, when it has one |
Filters
let results = search.query(@params.q, {
filters: {
tags: ["tutorial", "guide"], // Match any of these tags
dateAfter: @2024-01-01, // On or after this date (inclusive)
dateBefore: @2024-12-31 // On or before this date (inclusive)
}
})
| Key | Type | Description |
|---|---|---|
tags |
array or string | Keep documents carrying any of the listed tags |
dateAfter |
date or string | Keep documents dated on or after this point |
dateBefore |
date or string | Keep documents dated on or before this point |
Dates accept Parsley datetime literals (@2024-01-01) or ISO strings ("2024-01-01"). Bounds are inclusive and compared in UTC. Documents without a date are excluded whenever a date filter is set.
Beyond Watched Folders
The instance has a few more methods, all covered in the guide:
add,update,remove— index things that aren't files: database rows, API responses, generated pagesstats()— document count, index size, last indexed timereindex()— drop everything and rebuild from the watched folders
When to Use It
Good for documentation sites, blogs, wikis, and apps up to roughly 100k documents — anything where "just works, zero config" beats search-cluster features. If you need typo tolerance, multi-language stemming, or millions of documents, reach for Meilisearch or Elasticsearch instead.
See Also
- The Search Guide — how indexing works file type by file type, ranking and tuning, query syntax, manual indexing, and troubleshooting
- Routing —
@paramsand friends