Skip to content
docs-bundle okf-loom wiki
GraphIndex

Live studio unavailable. Showing a read-only view. Browse the index.

Reference 6 min read

Search modes

The six search modes behind one `--mode` flag, their backends, and the `SearchBackend` Protocol for plugging in custom backends.

Search modes

okf-loom's search command and the live studio's /__search endpoint dispatch to one of six modes behind a single --mode flag. All six modes are dependency-free out of the box: lexical (BM25), semantic (fuzzy-semantic SemanticLite), and hybrid (RRF fusion of the two) require no extra packages. See index.md for the rest of the reference quadrant.

Mode summary

ModeBackendWhat it ranks byActivate
lexical (default)LexicalBackendField-weighted BM25 (title 5× > headings 3× > description 2× > body 1× > tags 1×).Always.
tagLexicalBackendTag membership (case-insensitive); ties broken by title.Always.
semanticSemanticLiteBackendFuzzy-semantic similarity over a per-concept profile. α·cosine(tokens) + (1-α)·cosine(char-trigrams), α=0.6.Always.
hybridHybridBackendReciprocal Rank Fusion (k=60) of lexical ∪ semantic-lite.Activates okf.cap.search_hybrid on first successful match.
entity(in-module)Match against concept id, title, description, entity labels, and aliases.Activates okf.cap.entities / okf.cap.aliases when those keys are present.
relation(in-module)Filter typed-relation + markdown graph edges by --relation / --source / --target.Activates okf.cap.typed_relations when relations: is present.

All modes accept the cross-cutting filters --type TYPE and --tag TAG (applied as post-filters).

lexical — BM25

Pure-Python BM25 with field weighting. Default mode. Zero deps.

scripts/okf-loom search path/to/bundle "customer order"
scripts/okf-loom search path/to/bundle "user" --type "API Endpoint"

Scoring

For query terms t in field f of document d:

idf(t, f)     = ln( (N - df(t, f) + 0.5) / (df(t, f) + 0.5) + 1 )
bm25(t, d, f) = idf(t, f) * ( tf(t, d, f) * (k1 + 1) )
                                / ( tf(t, d, f) + k1 * (1 - b + b * dl(d, f) / avgdl(f)) )
bm25(q, d, f) = Σ_t bm25(t, d, f)
score(q, d)   = Σ_f weight(f) * bm25(q, d, f)
SymbolDefaultMeaning
k11.5Term-frequency saturation.
b0.75Length normalisation.
Ncorpus sizeTotal number of concepts.
weight(f)title 5.0, headings 3.0, description 2.0, body 1.0, tags 1.0Field weight.

The +1 inside the idf log keeps idf strictly positive (the standard variant).

Determinism

Documents are scored in sorted concept-id order and the final ranking tie-breaks by concept id. The same (bundle, query) always yields the same ordering.

Tokenisation

Lowercase, split on Unicode word boundaries, drop a small English stopword set, split CJK runs into single-character tokens (so 比特币 indexes as ["比", "特", "币"]).

tag — tag membership

scripts/okf-loom search path/to/bundle "#sales" --mode tag
scripts/okf-loom search path/to/bundle "sales" --mode tag

Matches Concept.tags case-insensitively. Sorted by title. The # prefix is optional. Every hit carries a constant score=1.0 plus matched_tags listing the subset of the concept's own tags that matched.

semantic — SemanticLite

scripts/okf-loom search path/to/bundle "customer order" --mode semantic

Not embeddings. Combines token-level TF cosine with character-trigram TF cosine over a weighted per-concept profile.

ComponentDetail
Token cosineShares the lexical tokenizer; rewards shared domain terms.
Trigram cosineRobust to typos and morphology (ordersorder).
Profile stringtitle ×3 + description ×2 + type + headings + tags + entity labels + aliases (the last two only if okf.cap.entities / okf.cap.aliases are active).
Scoreα * cosine(tokens) + (1-α) * cosine(trigrams), α=0.6.

SemanticLite is the cheap fuzzy-semantic layer that helps an agent find candidate concepts. The agent itself (an LLM in opencode / Claude Code / Codex) is the best true-semantic engine, so a local embedding backend would be redundant.

hybrid — RRF fusion

scripts/okf-loom search path/to/bundle "customer order" --mode hybrid

Reciprocal Rank Fusion of LexicalBackend and SemanticLiteBackend, both zero-dep.

rrf(d) = Σ_backend 1 / (k + rank_backend(d))    with k = 60

Concepts missing from a backend contribute 0 from that backend. Sort by (-rrf, concept_id). A successful hybrid query (at least one match) activates okf.cap.search_hybrid for the bundle.

entity — entity/alias match

scripts/okf-loom search path/to/bundle "order" --mode entity

Match against concept id, title, description, entity labels, and aliases. Honours okf.cap.entities (the entities: frontmatter key) and okf.cap.aliases (the top-level aliases: key). Useful for "what concept represents this business term?" lookups when synonyms and aliases matter.

relation — typed-relation / graph-edge filter

scripts/okf-loom search path/to/bundle "" --mode relation --relation depends_on
scripts/okf-loom search path/to/bundle "" --mode relation --source tables/orders
scripts/okf-loom search path/to/bundle "" --mode relation --relation writes_to --target tables/orders

Filter typed-relation declarations (relations: frontmatter key) plus markdown graph edges. The query string is optional when at least one of --relation, --source, or --target is supplied. Honours okf.cap.typed_relations.

FlagEffect
--relation TYPEKeep only edges of this relation type.
--source IDKeep only edges whose source concept id matches.
--target IDKeep only edges whose target concept id matches.

Cross-cutting filters

FlagApplies toBehaviour
--type TYPEEvery modeKeep only concepts whose type equals this string (exact match).
--tag TAGEvery modeKeep only concepts carrying this tag (case-insensitive).
--limit NEvery modeMaximum number of results (default 20).

Result shape

{
  "concept_id": "tables/orders",
  "title": "Orders",
  "score": 12.41,
  "snippets": ["…one row per completed customer order…"],
  "source_backend": "lexical",
  "matched_tags": [],
  "description": "One row per completed customer order."
}
FieldNotes
concept_idThe dotted path with .md stripped (e.g. tables/orders).
titleBack-filled from the content index if the backend emitted a placeholder.
scoreBackend-specific. Tag-mode results carry 1.0.
snippets0..N ~120-char excerpts centred on first matches, ellipsized.
source_backendlexical / semantic-lite / hybrid / tag.
matched_tagsPopulated in tag mode; empty otherwise.
descriptionBack-filled from the content index for richer result context.

SearchBackend Protocol

@runtime_checkable
class SearchBackend(Protocol):
    name: str
    def index(self, content_index: ContentIndex) -> None: ...
    def search(self, query: str, *, limit: int) -> list[SearchResult]: ...

A backend indexes the canonical ContentIndex once and answers ranked queries. name identifies the backend on SearchResult.

Backends consume the canonical ContentIndex (the Quartz "one model" pattern) so search, graph, and listings all read the same object.

Caching

Every backend (lexical, semantic-lite) is cached on the Bundle instance itself. The cache is released when the bundle is GC'd and is reset by Bundle.invalidate(). In a long-running server that reloads bundles, reload the bundle (or call bundle.invalidate()) to evict.

okf_loom.search.clear_search_cache() is retained as a deprecated no-op for backward compatibility.

HTTP entry point

The live studio exposes search at GET /__search?q=… (HTML) and GET /__search?q=…&format=json (JSON). The HTTP path always uses the lexical backend with a limit of 30; for other modes use the CLI. See http_routes.md.

Defaults

The bundle-root okf-loom.config.yaml search.default_mode key selects the mode used when no --mode is passed. Default is lexical; allowed values are the six mode names (fail-closed on unknown values).

When to use which mode

GoalUse
Default keyword searchlexical
Tag browsetag
Typo-tolerant / fuzzy matchsemantic
Best of keyword + fuzzyhybrid
Find the concept for a business term (with aliases)entity
Find concepts connected by a typed relationrelation

See also

All fields
timestamp2026-06-29T00:00:00Z