Search: core lexical/graph and the semantic plugin
okfctl has two layers of search: a core, always-available lexical/graph layer
built into okfctl itself, and an optional semantic layer shipped as the
okfctl-search plugin.
Core lexical and graph search
Section titled “Core lexical and graph search”okfctl search is stdlib-only — no model, no index, always available.
okfctl search "query" [dir]matches concept nodes case-insensitively by title, tag, type, or body substring; restrict the surface with--field title|tag|type|body(defaultany). Reserved files (index.md/log.md) are never results. A zero-result query is not an error. Add--jsonfor a deterministic, CI-diffable array.okfctl search --neighbors <node-path> [dir]is a graph-structural query: the concept nodes within--depthhops (default 1) of a node in the link graph. Edges are treated as undirected, so a reader’s traversal is symmetric.
Run okfctl search --help for the full flag set.
okfctl-search — offline semantic search (plugin)
Section titled “okfctl-search — offline semantic search (plugin)”okfctl-search is a bundled plugin (a separate static binary; invoke as okfctl search … via plugin dispatch, or directly). It adds semantic search over a
bundle’s concept nodes, fully offline, with zero runtime dependencies — no
Python, no ONNX, no sqlite-vec, no model server. It shares the exact embedding
protocol with cwest/knowledge-base so vectors are cross-verifiable.
okfctl-search index build [bundle-dir]— embed every concept node into.okfctl/index.db, recording the embedder model + dimension. Content-hash keyed: an unchanged node is not re-embedded; deterministic for a fixed embedder.okfctl-search --semantic "query" [bundle-dir]— rank nodes by cosine similarity to the query (top---k, default 5). Refuses an index built under a different model (rebuild withindex build).okfctl-search related <node-path> [bundle-dir]— a node’s nearest neighbors (self excluded); the neighbor setlint --semanticconsumes for its similarity-driven checks (§8.6).--embedder hash(default) is the offline, dependency-free embedder. It is deterministic and needs no model, but it is lexical — it matches tokens, not meaning.--lexical-gate(off by default) gates the semantic results by a term-wise lexical match and preserves lexical recall. It runs the semantic query wide, keeps the results whose node also contains a query term (stopwords dropped, plurals/inflections stemmed sohashandhashesmatch the same nodes), in semantic order, then appends the lexical hits the semantic band missed so a correct exact match outside the embedding’s top band is never discarded. It is useful for exact-identifier-shaped queries where the embedding blurs a rare token. It degrades to pure semantic (a no-op, byte-identical to gate-off) when the query has no content terms (an all-stopword question like"how should the") or a term matches more than 60% of the bundle (a term that broad carries no discriminating signal — e.g.agentmatches 73% of the reference corpus). Composes with--path/--type/--tag(which constrain both the semantic band and the appended lexical tail) and with--half-life.
Real semantic search with --embedder model2vec
Section titled “Real semantic search with --embedder model2vec”--embedder model2vec runs a genuine static embedding model (for example
minishlab/potion-base-8M) in
pure Go — the BERT WordPiece tokenizer and the Model2Vec inference math are
both ported into internal/search, so there is still no CGO, no Python, and no
ONNX runtime. Vectors match the upstream model2vec library’s own output to
within 1e-5, which is verified against the real model in the test suite.
okfctl never downloads a model at runtime. Point it at a directory you already
have on disk:
# once — persisted in okfctl's JSON configokfctl config set model_path ~/models/potion-base-8Mokfctl-search --embedder model2vec index build ./my-bundleokfctl-search --embedder model2vec --semantic "tannin structure" ./my-bundle
# or per-invocation, overriding the configokfctl-search --embedder model2vec --model-path ~/models/potion-base-8M --semantic "…" ./my-bundleThe directory needs the standard model2vec layout: config.json,
model.safetensors, and tokenizer.json (or vocab.txt). If no path is
configured, model2vec fails with an actionable error rather than silently
falling back to hash — a query answered by the wrong embedder is worse than one
that refuses to run. An index records the model it was built with, so switching
embedders requires a rebuild.