okf search
Which concept covers X? A few rows, not a body read.
When to use it
- The question is lexical: an exact symbol, an error code, a column name, a phrase. Structure will not surface those; text matching will.
- After
okf indextold you where to look: scope the search with--dir,--type, or--tagand it stays surgical. - Instead of grep. Grep returns line noise from a tree of files;
searchreturns ranked concepts with a snippet, and it knows what a title or a tag is. - When you do not know which bundle holds the answer.
okf search @all <term>asks every bundle in the registry at once.
How it works
okf search <dir> <term ...> matches every concept against all terms: each term must hit at least one searched field, though not necessarily the same one. Terms are case-insensitive substrings, or Ruby regular expressions with --regexp (short: -e).
It spans bundles. The identity slot also takes several leading @slugs, or @all for every bundle in the registry: rankings merge across bundles with every row labeled by the bundle that answered (a bundles list and a per-match slug key in the JSON). Asking for everything tolerates gaps, so @all steps over a bundle whose directory has vanished with a note, while naming one insists on it. One search across every bundle walks a multi-bundle query end to end.
okf search @all rate limit
Search — @handbook @wiki · rate limit (2 of 40 concepts)
@handbook runbooks/rate-limits Rate-limit runbook · Runbook · title
@wiki onboarding Onboarding · Note · body
…rate limits live in the billing docs…
Matches rank by where they hit. A title hit weighs 5, id 4, tags 3, type and description 2, body 1, summed over the fields that matched. Each row carries the concept's identity (id, title, type, dir, tags), the matched fields, the score, and one bounded context snippet from the strongest match that needs context.
Three knobs keep it narrow:
--in title,bodyrestricts which fields are searched (title, id, tags, type, description, body).--type,--dir,--tagfilter the candidate concepts first, the same filters every list view takes (the deprecated--areastill works over the first path segment).--fields/--exceptproject the JSON rows down to the properties you will actually read.
It is an advisory read: exit 0 even with zero matches. An unknown --in field or an invalid pattern is a usage error, exit 2.
Two engines, and why the default is the one it is
Since 1.9.0 the matching itself is pluggable, and two engines ship:
scan (default) | index | |
|---|---|---|
| matches | raw text, literally | whole tokens and the tokens they prefix |
| ranks by | summed field weight | BM25+ |
finds ustomer inside customer | yes | no |
| tolerates typos | no | with --fuzzy |
--engine index routes to minifts, the pure-Ruby port of the same MiniSearch build the graph page loads. It buys exactly three things: BM25+ relevance ranking, --fuzzy (typo tolerance at edit distance 0.2 x term length), and parity with the graph page, which runs the same engine, so the two rank identically.
The index is opt-in, and the benchmark is why. A one-shot CLI builds an index, asks one question, and exits: end to end that is 3.00 s against 0.24 s at 1,000 concepts, with the build accounting for around 95% of it. The per-query throughput that recommends an index (roughly 44 to 56 times the scan's) is the right measure for a long-lived index, a page or a server, and the wrong one for a process that exits.
--fuzzy implies --engine index, and that routing is silent: no note, no header change, no new JSON key. Naming an engine that cannot do what you also asked is a usage error that names one that can (--engine index -e answers try --engine scan), and an unknown name lists what is available. --help reads the engine registry, so an addon's engine shows up without the CLI knowing it exists.
Try it
okf search docs/ dedup key
Search — docs · dedup key (2 of 37 concepts)
decisions/dedup-key Invoice dedup key · Decision · title+id+tags+body
We chose the (account_id, external_id) pair as the dedup key, so a retried…
services/billing Billing service · Service · body
…retries reuse the dedup key, so a replayed invoice upserts instead of doub…
Patterns and machine output compose the same way as everywhere else in the CLI:
okf search docs/ 'err_[a-z]+_409' --regexp --json --fields id,snippet
{"bundle":"docs","query":["err_[a-z]+_409"],"count":1,"matches":[{"id":"runbooks/rate-limits","snippet":"…the gateway answers ERR_DEDUP_409 when a replay hits an open invoice…"}]}
When a term is a guess rather than a quotation, --fuzzy is the one that answers:
okf search @okf serch # 0 of 24 concepts
okf search @okf serch --fuzzy # 13 of 24 concepts
The default is not being unhelpful there. It is reporting, correctly, that nothing in the bundle contains those letters in that order.
Pitfalls
- The default is exact on purpose. No stemming, no approximation: the agent reading the results is the fuzzy layer, and
--fuzzyis there for the times you would rather the tool were. When terms miss, learn the bundle's vocabulary fromokf tagsandokf types, then search again in the bundle's own words. - Know what the index costs before you name it. Its tokenizer splits on punctuation, so
customer_idindexes ascustomerplusidand7.2.0as7,2,0. An infix finds nothing (ustomermatchescustomerunder the scan and not under the index), and a backtick is UnicodeSkrather than punctuation, so a word inside a code span indexes with its backticks attached. Ranking does not rescue this: BM25+ normalizes by field length, so a short concept dense in7,2and0can outrank the one that actually says7.2.0. The default has none of these problems, because raw-text matching has no tokenizer. - Cross-bundle scores mean different things per engine. BM25+ prices a term by how rare it is, so
--engine indexindexes the searched bundles as one corpus: a score is relative to the whole answer, and the same concept scores lower searched beside other bundles than alone. The default's scores are absolute and need no such treatment. - Map first on an unfamiliar bundle. Search cuts across structure, but only
okf indexshows what a directory claims to hold and what is missing from it. The skill's search playbook sequences the two. - Zero matches is an answer, not an error. The exit code stays
0; an empty result means the bundle does not carry those words, which is itself a curation signal worth writing back.