okf-gem · docs
CLI · read

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 index told you where to look: scope the search with --dir, --type, or --tag and it stays surgical.
  • Instead of grep. Grep returns line noise from a tree of files; search returns 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,body restricts which fields are searched (title, id, tags, type, description, body).
  • --type, --dir, --tag filter the candidate concepts first, the same filters every list view takes (the deprecated --area still works over the first path segment).
  • --fields / --except project 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
matchesraw text, literallywhole tokens and the tokens they prefix
ranks bysummed field weightBM25+
finds ustomer inside customeryesno
tolerates typosnowith --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 --fuzzy is there for the times you would rather the tool were. When terms miss, learn the bundle's vocabulary from okf tags and okf 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_id indexes as customer plus id and 7.2.0 as 7, 2, 0. An infix finds nothing (ustomer matches customer under the scan and not under the index), and a backtick is Unicode Sk rather 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 in 7, 2 and 0 can outrank the one that actually says 7.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 index indexes 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 index shows 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.
esc
navigate open