Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Tokenized text search

RiffDB tokenized search is a finite family of compiled named queries over a declared text_index. It is separate from exact-text starts_with, ends_with, and contains: tokenized search matches analyzed terms, while exact text compares the stored value’s bytes.

Declare the index

One entity declaration names the indexed fields, their positive integer weights, one analyzer, provider lifecycle bounds, and query budgets:

text_index search(
  (title weight 4, body weight 1),
  analyzer standard_v1,
  staleness_slo 60,
  replay_age_seconds 86400,
  replay_bytes 1073741824,
  replay_backlog 100000,
  result boolean_v1,
  max_terms 16,
  max_candidates 10000,
  max_results 1000
)

keyword_v1 preserves the complete value as one term. It is intended for tags, identifiers, and enum-like strings. standard_v1 applies Unicode 17 word segmentation and then the pinned Unicode fold. Neither analyzer stems words or uses a language-specific stop-word list. Analyzer identity and field weights are part of the contract and provider identity; changing either rebuilds the derived index.

The three query budgets are whole-operation ceilings. An empty analyzed query, too many terms, too many candidates, or too many results returns a typed refusal. RiffDB never truncates an oversized match set and never falls back to an entity scan.

Compile a named operation

RiffQL V10 has exactly four match shapes. The source chooses the index, shape, and—only for proximity—the positive distance:

query search_docs(
    $org: Document.organization_id,
    $query: Document.title,
    $limit: Limit<20>,
    $cursor: Cursor,
) {
    many docs from Document where docs.organization_id == $org
        matching(search, conjunction, $query, riff_bm25_v1)
        order by docs.doc_id asc
        take $limit after $cursor
    return { doc_id: docs.doc_id title: docs.title }
}

Replace conjunction with disjunction, phrase, or proximity, 5 to compile the other shapes. Conjunction requires every analyzed term. Disjunction requires at least one. Phrase requires consecutive positions in one indexed field. Proximity requires the terms in order in one indexed field, with each adjacent distance no greater than the compiled bound. Repeated intermediate terms are considered at every reachable position: an earlier occurrence cannot hide a later occurrence that completes the match.

Omitting riff_bm25_v1 gives Boolean execution and canonical entity-key order. Adding it selects the only ranked order. Applications cannot submit a field, analyzer, shape, distance, weight, score function, boost, tie-breaker, provider, budget, cost hint, scan, or fallback at runtime. Generated Rust, Go, TypeScript, Python, gRPC, CLI, and MCP operations expose only the query’s typed values and the ordinary opaque cursor/options carriage. Scores are not public result fields.

Deterministic ranking

Ranking never changes membership. It orders the exact Boolean match set over the caller’s complete authorized corpus at one admission-head-fenced snapshot. Unauthorized rows contribute to neither results nor document-frequency and field-length statistics.

riff_bm25_v1 uses checked unsigned integer arithmetic and scale S = 1,000,000. For authorized document count N, term document frequency df, term frequency tf, document field length dl, total authorized token length L for the field, and its contract weight:

idf_scaled = floor(S * (N - df + 1) / (df + 1))
denominator = tf*S*L + 1200*(250*L + 750*dl*N)
tf_scaled = floor(S * tf * 2200 * 1000 * L / denominator)
term_contribution = floor(weight * idf_scaled * tf_scaled / S)
score = checked_sum(term_contribution)

Every division rounds toward zero. Equal scores use canonical entity-key bytes ascending. Arithmetic that cannot fit the frozen checked representation is a typed whole-result refusal, not saturation.

Freshness, rebuilds, and cursors

The production request path uses only a ready, policy-aligned posting segment. A background worker builds and checkpoints it from one bounded authoritative snapshot. Initial build, rebuild, capacity exhaustion, stale state, corruption, and an unavailable epoch are typed provider lifecycle outcomes. There is no request-time authoritative scan or stale-generation fallback.

A ranked first page captures admission-head consistency and binds its causal floor, provider epoch and generation, authorized statistics identity, immutable plan, invariant parameters, current capability identity and revision, history incarnation, total order, and result ceiling to the opaque cursor. Continuations reuse that snapshot and never recapture statistics. The production POC keeps eight epochs in each registered query shape’s discovery ring. A live ranked cursor additionally retains its actual immutable epoch, even after that epoch leaves the ring. Its native state stays charged until the cursor and any captured requests release their references. Cursor expiry, replacement, and cancellation release only their own references. If the bound epoch becomes unavailable—or the cursor expires, authority changes, or history is restored— the continuation fails typed rather than rescoring against newer data.

Boolean results do not expose a tokenized continuation. They use the compiled bounded numeric offset and canonical entity-key order; separate requests do not freeze the corpus across writes. Ranked pages use the ordinary generated cursor option and may request a different valid page size without changing the frozen operation identity.

Deliberate exclusions

V1 has no wildcard, regular expression, fuzzy matching, stemming, language packs, highlighting, snippets, caller-selected relevance controls, score output, hybrid vector/text ranking, general joins, or ad hoc search endpoint. Applications export their changelog or authoritative data when a different search product is the better fit.

Ranked execution computes each distinct query term’s document frequency and each field’s corpus length once for the captured provider snapshot. Scores and statistics identity share those values. Repeated query terms still contribute repeatedly to the frozen fixed-point score; they do not alter corpus identity.