Search you can swap: okf 1.9.0
🇧🇷 Ler em portuguêsUntil 1.9.0, okf searched your bundle exactly one way. Now you choose the engine, and the choice arrives with a result worth sitting with: the new full-text engine is 44 to 56 times faster per query, and it ships switched off.
That is not a hedge. A CLI invocation builds an index, asks one question, and exits, so the throughput it bought is throughput it never lives to spend. What you get out of the release is a choice you did not have before: exact matching that starts instantly, or ranked, typo-tolerant matching you can ask for by name when the question is worth the wait.
Two engines, and how you pick one
Swapping the engine does not change the shape of what comes back. Two engines can disagree completely about which concepts match and still hand you rows with the same keys in the same order, so a script reading --json never has to know which one ran. What changes is the matching and the ranking:
scan (default) | index | |
|---|---|---|
| matches | raw text, literally | whole tokens and the tokens they prefix |
| ranks by | summed field weight | BM25+ |
| capability | regexp | fuzzy, prefix |
finds ustomer inside customer | yes | no |
Selection is by capability when the query requires one. --fuzzy requires :fuzzy, so it routes to the index without you naming an engine, and that routing prints nothing: no note, no header change, no new JSON key. --engine NAME picks one outright, for the case a capability flag cannot express, since a matching model requires nothing and so no flag selects one.
Getting it wrong is a usage error that names the engine that can do the job:
$ okf search @okf rate --engine index -e
error: --engine index does not support --regexp (try --engine scan)
$ okf search @okf rate --engine bogus
error: unknown search engine: bogus (available: index, scan)
A third engine can now arrive as an installed gem rather than a fork. --help lists whatever is registered instead of a hardcoded pair, so an engine okf has never heard of shows up in the help text once you install it, and it has to pass the same contract the built-in two do before it is allowed to answer anything. Nothing like that ships today; the point is that a SQLite or FTS5 backend no longer has to be built by this project to exist.
Why the fast engine is off
The per-query numbers are not close. Measured with benchmark-ips on Ruby 4.0.5 against the real @okf bundle, replicated to larger corpus sizes, the index sustains 44 to 56 times the scan's query throughput, and the multiple stays roughly constant because both approaches scale linearly on this corpus.
That is the right measure for anything that indexes once and then answers for hours, which is what a graph page or a running server does.
It is the wrong measure for a process that exits. A CLI invocation builds an index, asks one question, and dies. End to end at 1,000 concepts that is 3.00 s against 0.24 s, with the build accounting for roughly 95% of it. The throughput advantage is real and the process never lives long enough to collect it.
So the default did not move, and the tradeoff is written into --help rather than left in a benchmark nobody runs. The deep-dive on minifts covers the port itself, what the tokenizer costs you, and the one architectural change that would make the fast path the default.
The graph page draws the index layer
Every OKF bundle carries an authored index.md per directory, the §6 map. The graph page could only show those inside file-tree mode, where a folder node stood in for the directory's map.
Show indexes makes it a layer instead: each map becomes a tile edged to the concepts it lists and to the maps below it, drawn over whatever layout is already running, so turning it on no longer costs you the arrangement you were reading. Authorship shows as form, solid where an author wrote a map and hollow where the bundle only implies one, so the toggle reads as curation as much as navigation.
The Files view changed shape in the same release: the tree nests by path segment, the separate Indexes tab dissolved into an "Indexes only" toggle over that same tree, and a first-time reader now gets a note saying where the index is. That half of the release has its own post, with screenshots.
@slug is a target, not a path
1.8.0 added the registry. 1.9.0 finishes teaching the rest of the system that a name is a way to say which bundle.
The bare not-a-directory error now carries the grammar:
$ okf lint ./nope
error: ./nope is not a directory or a registry ref
(@slug names a registered bundle, @ the default; okf registry list)
That message is shared by every verb, which is why @all stays out of it: @all belongs to search alone. The bundled agent skill made the matching change, replacing "Which directory?" with "Which target?", and its consume and search playbooks name @slug in their orientation steps.
The skill also stopped probing for the CLI. It used to spend a tool round on a command -v okf presence check before every task. That check is gone: run the verb, and treat a shell "command not found" as the only signal to install. A line beginning error: is the CLI answering with a bundle or usage result, never a missing toolchain, so the common case now pays no guard round at all.
Also in this release
- Cross-bundle search ranks one corpus under
--engine index. Ranking each bundle separately and interleaving would produce a list that looks sorted and compares nothing, so the searched bundles are ranked together and the top row is the best answer across all of them. The default's scores stay absolute. - The bundle names its own root.
(root)is what a filesystem calls it. The tree's root row, file-tree mode's root node, the index layer's root map and the inspector all now carry the name the page header shows,--titleincluded. Escclears the graph selection, because a dense graph leaves almost no empty canvas to click.- Clustering stopped leaving phantom boxes when a filter hid every concept in an area.
- A title-less concept wears one name in every view.
catalogand the §6 listing fell back to the full id while the graph node fell back to the basename, so one concept answered to two labels across two views of one bundle.
Upgrading
gem install okf # or: bundle add okf
Nothing in this release requires a change to an existing bundle, and the default search behaviour is byte-identical to 1.8.0: same fields, same weights, same literal matching. minifts arrives as a third runtime dependency, pure Ruby, no native extension, no dependency tree of its own, and the same Ruby 2.4 floor the other two hold.
The multi-arch image tracks the release as always at ghcr.io/serradura/okf:1.9.0, and everything above is on the live demo.
Which brings this back to where it started. Most of what you do with okf search should feel identical, because it is. What changed is that the two questions the old search could not answer, "find this even though I spelled it wrong" and "rank these properly across four bundles", now have a flag each, and a third engine no longer has to be written by this project to exist.