How this site works
The documentation you are reading is stored in TessariDB, and the search box is the engine's own index.
Most documentation sites generate a pile of static HTML and bolt a hosted search service onto the side, because a static site cannot search itself. This one stores its content in TessariDB and answers search from the database's own full-text index.
That makes the site both the documentation for the engine and a working demonstration of it. A search box that returns the right passage over content the same store holds as records is a more honest claim than a benchmark table.
What is in the store
| Table | Holds |
|---|---|
section | a node of the left-hand tree |
page | one page, with its Markdown |
fragment | one heading-delimited passage — the unit of search |
holds | an EDGE table: the tree itself |
The tree is a graph
The left-hand navigation is not a file listing what should appear. It is edges, and a subtree is a traversal:
SELECT * FROM section:'query-language'->holds->page ORDER BY order;
A fourth level of nesting is therefore content and not a migration. A parent
column would say the same thing and then need a recursive read to walk.
The ->holds-> in that read is a graph traversal, and holds is an EDGE
table — both are Graphs, which you have not met yet if
you are reading in order. This page is a demonstration rather than a lesson: it
borrows from further in, and every construct on it is explained somewhere later.
Search is over passages, not pages
A reader searching for ANALYZER wants the paragraph, not the top of a
four-thousand-word reference. So each page is split at its headings into
fragments, and a fragment carries its own anchor — which is why a result on this
site lands you on the passage.
The searchable text of a fragment is composed at ingest from the page's title, the heading, and the body, and that one composed field carries the analyzer and the search index:
DEFINE ANALYZER english FILTERS lowercase, ascii, stemmer;
DEFINE FIELD text ON fragment TYPE string ANALYZER english;
DEFINE INDEX by_text ON fragment FIELDS text SEARCH;
Those three statements are Definitions, and what the index does with the words is Full-text search.
The stemmer is why searching backups finds the page about backup. It was
missing from this site's own chain until 2026-09-02, and the symptom is worth
recognising because it is not an error: backup returned twenty results and
backups returned none. Adding it meant a new analyzer name in a new namespace,
for the reason on the Full-text search page —
an analyzer name is unique across the store, and postings already written were
tokenised by the chain in force when they were written.
One field, not three. That is a correctness requirement rather than an economy:
search::score is refused over a field with no search index, and two scores
drawn from two different indexes measure against two different collections — so
ordering a title hit against a body hit is only honest when they are the same
field of the same collection.
The search route asks up to three questions and stops as soon as the page is full. The first is the one that matters, and the engine ranks it:
SELECT page, heading, anchor, body, search::score(text, $q) AS relevance
FROM fragment
WHERE text MATCHES $q
ORDER BY search::score(text, $q) DESC
LIMIT 20;
The server does no scoring of its own.
The other two questions exist because of what MATCHES is. It asks whether the
analyzed text holds every word of the query, and a word is a whole word — so
lovel is not lovelace, and a reader half-way through typing one is told the
site holds nothing. That is the correct answer to the question the statement
asked, and the wrong answer to the question the reader asked.
So when the first pass has not filled the page, the last word is taken as one still being typed and looked for as characters instead:
SELECT page, heading, anchor, body, text
FROM fragment
WHERE text ILIKE $p
LIMIT 20;
ILIKE is a pattern over the whole value, so %lovel% finds the fragments
holding those characters. That is a scan and not an index read.
It is a scan of the whole collection only when the query is a single half-typed
word. As soon as a finished word precedes it the statement carries both —
WHERE text MATCHES $q AND text ILIKE $p — and the pattern runs over what the
index already narrowed rather than over everything. The unnarrowed case is
affordable here because this site is a few hundred fragments, and would not be on
a collection that grows; below three characters it is not attempted at all, a
prefix that short being a prefix of nearly everything.
The third question is the useful one. The fragments that scan returned are text,
and the words in them say which word was being typed — lovelace, from lovel
— so that word goes back through the first statement and comes back ranked. It
is what puts the page a word is about above a page that merely mentions it in
passing: a scan can find the passage but cannot order it, because a scan has no
score.
Versions are namespaces
Each released version of the documentation is a namespace of its own:
USE NAMESPACE v0_0_3_alpha;
USE DATABASE docs;
Switching version is switching namespace, so an old version is never a filter somebody forgets to apply.
The query is a bound value
The term you type reaches the store as a parameter and never as syntax:
SELECT page FROM fragment WHERE text MATCHES $q;
That is what makes it safe to take a search term from a query string. A supplied value cannot become part of the statement, and being remote does not give that back.
The examples are executed
Every TessariQL block on this site is run against the engine before the page ships. Not linted and not read over — run, in order, against a scratch store.
A block the prose calls refused is held to a stricter standard than one that succeeds: the refusal has to be the one the page names. Establishing that took a second route, because the obvious one does not exist. A refusal's name is internal — what a caller receives is the message and nothing else, on every surface the store has (the catalogue says so in its opening) — so a checker that runs the engine and reads what it prints can see that a statement was refused and never which refusal it was.
The name is in the source rather than the output. Each variant carries the template its message is built from, so the message the engine printed can be matched back against all of them and the variant recovered. Every named refusal on this site goes through that, and each one names the error the engine actually raised. Where two templates can produce the same text the check reports both rather than picking, since a check that resolves its own doubt in favour of the page would agree with a wrong page too — and the number of those is pinned, so a new one has to be looked at rather than absorbed.
| the block | what is checked |
|---|---|
| a statement | it runs, and the engine accepts it |
| a named refusal | it is refused, and that the refusal is the one named |
| an answer printed under a statement | it is compared against what the engine actually printed |
| marked illustrative | skipped — the marker is in the source, so the skip is a decision rather than a gap |
Everything in that table runs before a page ships.
The third row is the newest and it has a reach worth stating: sixty-six of the
site's hundred and seven printed blocks sit under a statement and are compared.
The other forty-one are command-line sessions and quoted statements rather
than answers, so they are outside that comparison by their nature: its question
is what a statement returns, and a session's answer comes from the binary.
Thirty-nine of them are sessions, and they divide as follows.
Seven print something the binary can be asked to reproduce — a version,
a health check, an unknown flag, a missing password, a parameter carrying a
statement — and what they print is compared against what it prints. Twenty-three
print nothing at all, because they start a server or open a store, and three are
the prompt itself. Six are left: three curl requests to a running node, a
shell's own echo $?, a backup file, and a script the session does not write.
Those are shown rather than checked, and the largest group here is the one
nothing could ever check rather than the one waiting on a harness.
The table is about blocks. The prose around them makes claims no checker can run, and those were read a second way: grouped by the term each claim names, so that two pages explaining one thing land beside each other. Eighty-seven terms are explained on more than one page, and eighty-six of those groups agreed — the one that did not was a page contradicting itself two sentences apart about what a drop removes. That is a weaker guarantee than the table's and an honest one: it is a reading rather than a gate, it holds for the sentences that name something, and roughly three sentences in four name nothing in particular and have no index at all.
This is not a tidiness rule. The first run over the nine pages that existed at the time reported twenty-five defects: reads against tables no example had defined, an index rebuilt under a name that had already been renamed, a refusal demonstrated against a record the page never created. Every one of them would have read perfectly to a human reviewer, because each was locally correct and wrong only in the context of the page it sat on.
Two of the defects it turned up were not in the documentation. They were in the engine — a script ending in a comment, and a semicolon inside a comment — and both produced a plausible wrong answer rather than an error, which is the kind of defect a compiler and a passing test suite are both blind to. Writing the documentation is what found them.
The checker needs the content and a built engine in the same place, so it runs where those meet rather than in this site's own build.