o'ailly

Chapter 6 — Search Is Recall

Draft status: author draft, gate-checked; human verification pending. Outputs are real transcripts; the journal entries indexed below are drawn from this book’s own production history.

Memory that cannot be recalled is storage

The estate so far remembers perfectly and recalls narrowly. Every query in chapters 4 and 5 addressed rows by their structure — by key, by status, by date — which serves the questions an operator knows it will ask. But a working memory accumulates a second kind of holding: prose. Findings written in sentences, incident notes, decisions with their reasoning, excerpts from documents that mattered once and might again. Structure cannot address these, because their content is their address: the future question will be “what do I know about power capping?”, asked in words, answerable only by matching words against words. A memory that cannot answer that question does not really hold its prose; it stores it, the way the midden stored everything — present, and unreachable.

The register’s operators feel this gap acutely because their native recall is so poor. A human admin half-remembers last month’s incident and greps her shell history; a session-bound operator has no half-memories to steer by. For it, recall is search — whatever the estate’s search can surface is, functionally, everything the operator has ever known. That makes the quality of the estate’s text search a first-order property of the operator itself, and it makes the right tool worth learning properly. SQLite ships that tool: FTS5, a full-text index that lives in the same file, transacts with the same transactions, and needs nothing installed. This chapter builds the operator’s journal on it, then draws the tool’s honest boundaries — because “full-text search” sits near enough to fashionable retrieval technology that confusing them costs estates real design mistakes in both directions.

The searchable journal

The pattern chapter 4 might have called the sixth shape: a journal of prose entries, indexed for content. FTS5 tables are declared virtual — the engine maintains the index structures behind an interface that reads like a table:

import sqlite3
db = sqlite3.connect("estate.db")
db.execute("""CREATE VIRTUAL TABLE journal USING fts5(entry, written_at UNINDEXED)""")
notes = [
    ("gpu-power-cap.service fails at boot; exit status 2; journal unreadable from this seat", "2026-08-24"),
    ("rotated backup credentials; restore drill passed; old key revoked", "2026-08-25"),
    ("nginx upgraded; config drop-in preserved; is-active confirmed after restart", "2026-08-26"),
    ("disk pressure on /mnt/train at 98 percent; quarantined stale checkpoints", "2026-08-27"),
    ("power cap applied to RTX PRO 6000 after PSU transient trip; verified with vendor tool", "2026-08-28"),
]
with db:
    db.executemany("INSERT INTO journal VALUES (?, ?)", notes)
print("query: power NEAR cap")
for row in db.execute("""SELECT written_at, highlight(journal, 0, '[', ']')
                         FROM journal WHERE journal MATCH 'NEAR(power cap)'
                         ORDER BY rank"""):
    print(" ", row[0], "|", row[1])
print("query: credential*")
for row in db.execute("SELECT written_at, entry FROM journal WHERE journal MATCH 'credential*'"):
    print(" ", row[0], "|", row[1][:60])
query: power NEAR cap
  2026-08-24 | gpu-[power]-[cap].service fails at boot; exit status 2; journal unreadable from this seat
  2026-08-28 | [power] [cap] applied to RTX PRO 6000 after PSU transient trip; verified with vendor tool
query: credential*
  2026-08-25 | rotated backup credentials; restore drill passed; old key re

The five entries are this book’s own production history — the failed unit from the previous volume’s postmortem, the disk pressure this machine really carried — and the queries against them exercise the toolkit an operator’s recall actually needs. MATCH takes a query language, not a substring: NEAR(power cap) found the incident whether the words appeared hyphenated in a unit name or spaced in prose, and would have found them sentences apart. credential* is prefix search — the recall question rarely knows the exact inflection it stored. ORDER BY rank sorts by relevance (BM25, the standard lexical ranking, built in), which begins to matter the day the journal holds five thousand entries instead of five. And highlight() returns the entry with the matches marked — for this book’s reader the killer feature, because an operator budgeting transcript volume (register rule, chapter 1 of the previous volume) wants evidence of why a result matched without re-reading it whole; the companion snippet() function goes further and excerpts just the matching neighborhood. The one schema note: written_at UNINDEXED stores the date alongside without polluting the text index — metadata rides along, content gets searched.

Tokens, not meanings

What the index actually holds is tokens — words, as a tokenizer defines words — and the operator who knows this predicts every search behavior from first principles. The definition is configurable, and one configuration choice illustrates the whole layer:

import sqlite3
db = sqlite3.connect("estate.db")
db.execute("CREATE VIRTUAL TABLE plain USING fts5(entry)")
db.execute("CREATE VIRTUAL TABLE stemmed USING fts5(entry, tokenize = 'porter unicode61')")
for t in ("plain", "stemmed"):
    db.execute(f"INSERT INTO {t} VALUES ('deployed the staging build and verified the endpoints')")
for t in ("plain", "stemmed"):
    hits = db.execute(f"SELECT count(*) FROM {t} WHERE {t} MATCH 'deploy'").fetchone()[0]
    print(f"{t:8s} matches for 'deploy':", hits)
plain    matches for 'deploy': 0
stemmed  matches for 'deploy': 1

The default tokenizer stores deployed as deployed, and the query deploy misses it — surprising until the token model is explicit, obvious after. The porter option stems English words to their roots at both index and query time, so deploy, deployed, and deploying converge; the cost is the occasional false collision and a mild English-centrism, which for operator journals is usually the right trade. The general lesson outranks the specific knob: FTS matches token identity, nothing else. It does not know that “rotated credentials” and “changed the password” are the same event; no tokenizer bridges vocabulary. The operator’s countermeasure is a writing discipline, not a search feature — journal entries name their subjects in stable terms (unit names, paths, error strings verbatim: the register’s exact-transcript rule paying a second dividend), because the entry is written once and queried by a stranger who can only guess words. Entries written for the searcher are the estate’s equivalent of the previous volume’s labels-on-state.

This is also where the fashionable comparison belongs, stated plainly per this book’s boundaries. Embedding-based retrieval — vectors, semantic similarity, the machinery behind modern RAG — solves the vocabulary problem FTS cannot: it would land “changed the password” for the credentials query. It pays in machinery (a model, its runtime, an index, all versioned and maintained), in opacity (a match has no highlight()why this result?), and in exactness (the query exit status 2 should match exit status 2, not things shaped like it). Operator estates skew hard toward the exact: unit names, error strings, hosts, keys. The honest architecture, when both needs are real, is FTS as the estate’s native recall with semantic search added beside it, as its own indexed artifact — not a replacement, and never a reason to skip the lexical index that costs nothing and explains itself. A LIKE '%pattern%' scan, finally, keeps its small place: for a rare query over a small table it is fine, and chapter 3’s EXPLAIN QUERY PLAN will say SCAN and remind you what it costs at scale.

Writing for the searcher

The index amplifies whatever discipline the entries carry, which makes journal writing half of recall quality, and the half entirely under the operator’s control. The disciplines are few and specific. Entries are written at outcome time, not planning time — the journal records what happened and what it meant, so a session’s entry lands beside chapter 4’s ledger completion, one truth in two registers (the row for machines that query structure, the sentence for searchers that query words). Entries name their subjects in the terms a stranger will guess: unit names verbatim, paths absolute, error strings pasted exactly — the previous volume’s exact-transcript rule, now justified a third way, since every paraphrase is a query that will someday miss. Entries state outcomes and reasons, not just events — “quarantined stale checkpoints because /mnt/train hit 98 percent” is findable from either end of the causation, and the because is what the future searcher is usually really hunting. And entries are atomic per subject: one entry about the disk pressure, another about the nginx upgrade, because the searcher retrieving one should not pay transcript volume for the other — the register’s bounding economics, applied to memory retrieval.

The anti-patterns mirror them. The diary entry (“busy session, lots of firefighting, mostly done”) indexes nothing a query will ever ask. The dump entry — three hundred lines of pasted transcript — makes its keywords findable and its retrieval cost absurd; the right decomposition stores the transcript as a chapter-4 artifact and journals the three-sentence account with the artifact’s path, so search finds the summary and the summary points at the evidence. And the secret-bearing entry violates chapter 4’s exclusion in the one table designed to be read broadly; the reference-not-value rule binds hardest exactly here.

Query craft: words and structure together

Recall questions in practice are rarely pure text — they are text within bounds: what do I know about cert renewals, from June? The estate answers hybrid questions in one statement, because the FTS table’s indexed text and its UNINDEXED metadata live in the same rows:

import sqlite3
db = sqlite3.connect("estate.db")
db.execute("CREATE VIRTUAL TABLE journal USING fts5(entry, written_at UNINDEXED)")
rows = [("cert renewal failed: acme challenge timeout", "2026-06-02"),
        ("cert renewal succeeded after dns fix", "2026-06-03"),
        ("cert renewal succeeded", "2026-08-01")]
with db: db.executemany("INSERT INTO journal VALUES (?,?)", rows)
q = """SELECT written_at, snippet(journal, 0, '[', ']', '…', 8)
       FROM journal WHERE journal MATCH 'cert AND renewal'
         AND written_at >= '2026-06-01' AND written_at < '2026-07-01'
       ORDER BY written_at"""
for r in db.execute(q): print(r[0], "|", r[1])
2026-06-02 | [cert] [renewal] failed: acme challenge timeout
2026-06-03 | [cert] [renewal] succeeded after dns fix

August’s entry matched the words and fell to the date bound; June’s two arrived excerpted by snippet() with the matches marked. The MATCH clause meanwhile carries its own small language, worth the ten minutes its documentation costs: implicit AND between terms, explicit OR and NOT, quoted phrases for exact sequences ("exit status 2" — invaluable for error strings), NEAR() from the first listing for proximity without adjacency, ^term for entries beginning with a term, and column filters (title:deploy) once journals grow structured fields. Two habits complete the craft. Chapter 3’s date conventions are what make the hybrid WHERE clause work at all — ISO text comparing correctly is the payoff arriving — and bounded output applies to recall exactly as to every read the register ever taught: ORDER BY rank LIMIT 10, because a memory that answers with everything it has is the unbounded journalctl of the previous volume, reborn indoors.

Identifiers deserve a tokenizer note of their own, because operator prose is full of them and word-shaped tokenization serves them poorly: gpu-power-cap.service splits on its punctuation into pieces a searcher must guess, and a path or commit hash is one opaque “word” findable only whole. FTS5’s trigram tokenizer answers the identifier case by indexing overlapping three-character windows, buying indexed substring search — cap.serv finds the unit, half a hash finds the commit — at the price of a fatter index and no notion of words at all. The estate’s arrangement, when identifier recall matters enough: the journal keeps its word index for prose, and the identifier-dense columns (ledger actions, artifact paths) get a small trigram-indexed shadow — each index shaped to what it searches, unioned at query time by the front-door view below. The general tokenizer lesson closes where the section opened: tokenization is a declaration about the text’s nature, chosen per column at index time, and the estate that declares it deliberately — words here, stems there, trigrams for the machine-named — searches the way its content deserves rather than the way the default guesses.

And recall’s audience, like the estate’s, includes the supervisor. The journal a session keeps for its successors is, unchanged, the narrative an incident review reads months later — ranked, dated, cause-carrying sentences, each written when the knowledge was fresh, retrievable by the reviewer’s own words. The previous volume taught operators to report plainly because reports are load-bearing; the journal extends the principle across time: it is the report that never stopped being queryable. Institutions pay technical writers for worse.

The recall budget

Retrieval has the same economics as every read the register prices: results pulled into a session’s working context cost attention, and recall that floods is recall that gets skipped next time. The budget discipline has three dials. LIMIT, always — the ritual search opens with the top five by rank, and widens only on a miss, the previous volume’s cheap-aggregate-then- drill rhythm applied to memory. snippet() over full entries at the survey stage — excerpts with matches marked, at a tenth the volume, with the full entry fetched only for the one or two results that survive triage. And ranking tuned to the estate’s shape where it earns it: BM25 accepts per-column weights, so a journal that grows a title column can weight it above the body (bm25(journal, 10.0, 1.0)), making the operator’s own one-line summaries the strongest signal — which quietly rewards exactly the entry discipline the writing section asked for. The composed form of all three dials is worth keeping as the estate’s canonical recall query: top five by weighted rank, snippets only, hybrid-filtered by any structural bounds the task supplies. One query, bounded, explained — recall as a disciplined shot rather than a rummage, which is what distinguishes an operator consulting its memory from an operator lost in it.

Searching everything at once

Estates accumulate more than one searchable surface — the journal here, the ledger’s action strings, the facts table’s prose — and the recall ritual should not require remembering which drawer holds what. The estate’s answer is a union view: each searchable table gets its FTS index, and one view stitches their results into a single ranked stream tagged by origin (SELECT 'journal' AS kind, rank, snippet(...) FROM journal WHERE journal MATCH :q UNION ALL SELECT 'ledger', ...), so the operator’s one query sweeps the whole estate and the results say where each hit lives. The pattern’s discipline is to keep it shallow: the view unions indexes, it does not try to merge scores across them into false precision (BM25 ranks are comparable within an index, not between), so the composed query interleaves by kind deliberately — top three journal hits, top three ledger hits — rather than pretending one global ranking exists. Chapter 4’s standing-questions rule then applies to recall itself: the union view is written down with the schema, tested at adoption, and becomes the estate’s front door for every “what do I know about X” a successor will ever ask.

Keeping the index honest

One integration decision remains: the journal above is an FTS table — the text lives in the index’s own storage. That is the simplest correct arrangement and the right default for a journal. When the text already lives in a regular table (chapter 4’s ledger actions, the facts table), FTS5’s external-content mode indexes it in place without duplicating storage — at the price of a covenant: the index only learns what it is told, so the base table and index must change together, conventionally via three small triggers (insert, update, delete) the FTS5 documentation supplies verbatim. The estate treats those triggers like schema (chapter 3: born in a migration, explained by comments), and treats the covenant with chapter-7 suspicion: a verification query — count of base rows vs count of indexed rows, plus a spot-check MATCH for a recently inserted row — belongs in the estate’s trust suite, because an index that silently stopped syncing is recall quietly going blind, which for this book’s reader means memory quietly going blind. The rebuild command (INSERT INTO idx(idx) VALUES ('rebuild')) is the recovery ladder’s one rung, cheap and total.

Recall in the loop

A recall instrument earns its keep only if consulted, and session-bound operators need the consultation scheduled, because the reflex humans call “this feels familiar” is exactly what they lack. The discipline is a ritual search at task start: before acting on any named subject — a unit, a host, a procedure — the operator queries the journal for it, bounded and ranked, and reads what its predecessors knew. The estate briefing of chapter 8 opens the session; the recall query opens the task. What it changes is easiest to see in the incident this book keeps returning to. The previous volume’s operator diagnosed a failed GPU power-cap unit from scratch, spending turns establishing that the unit fails at boot, that exit 2 was the vendor tool’s “no device”, that the journal was unreadable from its seat. Its successor, facing the same unit after the next kernel upgrade, opens with MATCH 'gpu-power-cap' — and inherits the whole prior investigation in three ranked entries: root cause candidate, the fix that worked, the verification that proved it. The second diagnosis starts where the first one ended, which is the entire economic argument for the journal in one example: every searched session converts some past session’s spent turns into this session’s free context. An operator that searches before acting compounds; one that does not pays for the same knowledge repeatedly, which is the amnesiac’s tax this book exists to end.

The ritual has a write-side twin: promotion. Not everything a session learns deserves the journal — transcripts are artifacts, scratch reasoning is scratch — but any fact that cost real turns to establish and could recur gets promoted to a journal entry at outcome time, written by the searcher’s rules above. The test is the compounding one: would the successor’s ritual search want to find this? Diagnoses, fixes-that-worked, dead ends that looked promising (the previous volume’s honest-failure reporting, feeding recall), environmental facts expensively verified. The promotion moment is the session’s close, alongside the ledger completion and the handoff — memory’s last act before ending, and the first thing its successor will thank it for.

One long-horizon honesty note completes the craft: vocabulary drifts. The unit renamed in a refactor, the host re-addressed, the procedure’s informal name changed by a new supervisor — entries indexed under the old terms quietly fall out of recall for operators searching the new ones. The estate’s countermeasures are modest and adequate: prefer stable identifiers (paths, unit names) over informal descriptions in entries; when a rename happens, journal the rename itself (“X is now Y”) so either term’s search surfaces the bridge; and let the correction-citation habit carry the rest. Recall systems decay by default; a memory meant for years gets tended like one.

What recall cannot do

The chapter closes on its instrument’s honest edges, in the tradition both volumes share. Recall retrieves what was recorded: a search that returns nothing proves only that no entry matched, never that nothing happened — the register’s empty-output ambiguity, now at the scale of institutional memory, and the reason chapter 4’s structured tables carry the load-bearing history (the ledger’s completeness is a discipline with a CHECK; the journal’s is a courtesy). The searcher therefore treats journal silence as a prompt to consult structure — the ledger by subject, the registry by date — before concluding novelty; “the journal has nothing on X” and “X never happened” are different sentences, and only middens confuse them. Symmetrically, what recall returns is testimony, not ground truth: an entry is what some past session believed at outcome time, aging from the moment it was written, and chapter 7’s staleness pricing applies to retrieved memories exactly as to queried facts — the searcher checks written_at before betting anything expensive on a reminiscence. None of this diminishes the instrument; it locates it. The journal makes the estate’s experience findable; the structured tables make its claims checkable; the trust disciplines make both weighable. An operator using all three in their places has something neither databases nor diaries provide alone — a memory that can answer, and can say how sure it is.

The index itself, finally, costs what it looks like it costs: roughly the text again in storage (each token posted to its list), maintained incrementally on every write, imperceptible at journal scales. Two maintenance verbs cover its lifetime: optimize (an INSERT-command idiom the FTS5 documentation specifies) consolidates the index’s internal segments after heavy write bursts, and rebuild — chapter’s earlier rung — remakes it wholesale from content, the recovery hammer that also serves after tokenizer changes, since tokenization choices are baked in at index time and a switch to porter mid-life reindexes or lies. Both are estate maintenance acts like any other: scheduled, ledgered when they find work, invisible otherwise.

Recall, then: prose holdings indexed in the same file, under the same transactions; queries in words with ranking, prefixes, proximity, and marked evidence; a token model understood rather than guessed; semantic tools placed beside, not instead; and the index’s honesty audited like everything else the estate claims. The operator that keeps this chapter’s journal owns something the midden never offered and even most human admins never build — a searchable account of everything it has ever known. What it does not yet own is confidence in the file that holds all of this. Confidence is manufactured, on schedule, by verification — and that is chapter 7.

1 / 1