Search and retrieval paths
All surfaces restrict candidates to the selected system, require entry, and apply
document visibility inside their database query. Admins bypass membership/ACLs,
never explicit request/token systems. Name/code completion, counts, snippets,
scores and pagination cannot return foreign-system rows. Public-demo exceptions
apply only to the unnamed original demo archive. See Permissions.
Whole canonical addresses such as
S01.13.147 in Search/Omnibox use
/api/jd/resolve; stale locations fail, not alias to a new category or fall back
to broad text search. jd:13 remains a category filter within the current system.
The final address component is an existing installation-global document ID.
FTS5 index
SQLite owns an external-content FTS5 table with two columns:titleis document metadata.contentis extracted text from PDF, OCR, email, office-document, barcode, and other ingestion paths.unicode61provides Unicode tokenization.remove_diacritics 2makes diacritic-insensitive matching consistent for the supported scripts where SQLite can fold it.portersupplies stemming for applicable terms.
documents. The index stores searchable text, not original file bytes.
Suchi currently has no vector index or embedding queue. “Semantic” behavior in
the product comes from explicit metadata, approved extracted facts, lexical
similarity, and bounded model synthesis after retrieval—not from hidden vector
search.
Ranked Search
GET /api/search/?q=... compiles the rich query language
into:
- one positive FTS5
MATCHexpression for text clauses; - fixed SQL predicates for negation and metadata filters;
- the caller’s visibility predicate; and
- live-document exclusion unless
is:trashcontrols trash state.
<mark> around the matching span.
The snippet window is 20 FTS tokens. Filter-only queries do not join FTS; they
return an empty snippet and use newest-first ordering.
Default recency-ranked searches first materialize only the requested page of
document IDs and scores. A row-ID-restricted FTS pass then builds one snippet
for each page result, so broad matches do not build display text for the whole
candidate set. Raw recency=off searches stay in one FTS scan because a second
bounded MATCH costs more than it saves for plain BM25. Both paths use score and
document ID as a stable order. See SQLite WITH-clause materialization.
BM25 weighting
When text clauses exist, ranking begins with SQLite FTS5 BM25:title, the second to content. A title match is
therefore three times as strong as the corresponding body-text contribution.
SQLite’s BM25 rank is ordered ascending: a more negative value is a stronger
match.
Recency decay
By default Suchi subtracts a bounded hyperbolic recency term:2_592_000 seconds is 30 days:
The term breaks ties and nudges close matches; lexical relevance remains the
main signal. Hyperbolic decay avoids depending on optional SQLite math
extensions. Use
recency=off for raw BM25 ordering:
Pagination and wire score
Search defaults to 25 results and caps pages at 100. Each hit includesid,
title, marked snippet, raw/blended rank, created_at, and mime_type.
The response uses the standard {count,next,previous,results} envelope.
Documents
GET /api/documents/?q=... uses the same parser, resolver, and compiled
predicates as ranked Search, so both endpoints select the same documents for the
same rich query and authorization context.
Documents does not apply BM25 or recency ordering. It supports list orders such
as newest, oldest, updated, and title because the screen is also a bulk-action
workspace. Legacy URL filters remain additive while older links and imported
Views migrate to normalized queries.
When a rich query contains positive text, Documents starts with the FTS match
and joins the resulting row IDs to document metadata. Calendar’s source-document
filter uses the same shape. This avoids scanning every document and running a
correlated FTS probe for each row. Filter-only document lists stay on ordinary
tables; the default live newest-first page uses a partial
(created_at DESC, id DESC) index. Every supported list order includes ID as a
deterministic tie-breaker, so equal timestamps or titles cannot move between
pages.
Query completion and autocomplete
GET /api/autocomplete/?q=... first examines the active final token. If it is a
recognized query qualifier, the endpoint returns full replacement queries for:
- filter names;
- Johnny.Decimal categories;
- tags;
- correspondents;
- document types;
- sensitivity values;
- Calendar date roles; and
is:states.
Similar documents
GET /api/documents/{id}/similar uses FTS5 as a local “more like this” reader:
- Read the source title plus at most the first 4,096 content bytes.
- Extract alphanumeric tokens of length four or more.
- Remove a small deterministic stopword set.
- Select at most ten tokens by frequency, then alphabetically for ties.
- OR-join each token as a quoted FTS term.
- Exclude the source document and trash.
- Apply the caller’s visibility predicate.
- Rank with title
3.0, content1.0, and no recency term.
score is -bm25, so a higher positive value is more similar. The UI
endpoint drops scores below 0.001; this removes the noise band produced by a
single calendar-year overlap. Default limit is 10, maximum 50. A source with no
content falls back to title tokens and sets matched_on_title_only so the UI can
warn that results are noisier.
Archive matching imports this same implementation. It changes thresholds and
what happens after a match, not tokenization, SQL, ACLs, or ranking.
Saved Views and Calendar
A live View stores a normalized rich query. A snapshot View stores exactdocument_ids. New View visual controls compile into query clauses before
persistence.
Older/imported Views may use flat fields. Calendar sends view_id, never a
client-reconstructed subset. The extracted-fact API loads the View only when
owned or shared, applies its complete filter through the shared document-scope
predicates, and then reapplies document ACLs. Calendar adds its approved-date
range and role after that document scope.
See Query language: Saved Views for serialization
and Archive research snapshot behavior.
Archive research retrieval
Archive research is RAG evidence retrieval, not the rich query parser applied to the natural-language question. For each question Suchi:- Normalizes up to 64 unique Unicode terms.
- Drops common question/document words when useful terms exist.
- Keeps at most 12 useful terms.
- Converts them to FTS prefixes and OR-joins them for recall.
- Ranks title
3.0, content1.0, without recency. - Selects at most six document IDs, keeping cited follow-up sources first and current BM25 results after them.
- Reloads up to three cited context sources directly in requested order, with fresh ACL, trash, sensitivity, and scope checks, then checks those authorized IDs for a current full-question match in one restricted FTS query. That match cannot be displaced by globally higher-ranked documents and applies in Focused mode too.
- Keeps the first full-question FTS passage for each matching document. For Balanced and Detailed, it runs each normalized term across all selected long document IDs in one batch and adds at most one passage per term.
- Removes whitespace-normalized duplicate or contained passages without changing primary-passage or source order, then appends a distinct document ending when the source-text ceiling has room.
- Adds approved extracted facts attached to those sources when permitted.
snippet() function returns at most 64 tokens. Suchi also caps
each returned passage at 1,200 Unicode characters before assembly. Temporary
markers locate the first match so the character cap is applied around it, then
all markers are removed before the passage reaches the browser, logs, or model.
Each already-ranked or row-ID-restricted result materializes its marked passage
once before clipping, avoiding duplicate snippet() evaluation.
Merely increasing one contiguous snippet would still inspect only the
neighborhood chosen for that single FTS window, so it would not reliably recover
far-apart clauses or repeated totals in a long document. Term-specific windows
improve that coverage while the Focused, Balanced, and Detailed source ceilings
remain hard bounds. See SQLite FTS5 snippets
and Archive research: Research context presets.
Short documents that fit the active ceiling are sent whole. A cited long
document with no current-term match uses a bounded beginning-and-ending
fallback. Source numbering remains stable, and the assembled source text
returned by the API is exactly what the provider receives.
Prefix-OR raises recall for the small evidence set. The model and citation
validator then decide whether that evidence is sufficient. A research scope
can still be a rich query, category, current document, exact document set, or
the complete visible Documents/Search filter set. The shared document-scope
predicate builder ANDs every field with the retrieval candidates.
This distinction is why Archive research saves exact source snapshots instead
of manufacturing a live rich query from question words. See
Archive research for the model and citation contract.
MCP
The MCP adapter’ssearch_documents tool calls /api/search/; it does not have
a second parser or ranking implementation. Query errors, pagination, scoring,
and ACLs are therefore identical to HTTP and the web app.
get_document retrieves one authorized result after selection. MCP never
bypasses object authorization.
Performance and safety bounds
- FTS content stays in SQLite; no network search service is required.
- Queries are limited to 1 KiB, 64 tokens, and 256 bytes per value.
- Search pages cap at 100; similar results at 50; Archive research sources at 6.
- Archive research term-expansion reads cap at 12, passage windows at 64 FTS5 tokens and 1,200 Unicode characters, and per-source text at 1,600–4,800 characters.
- SQL fragments are fixed; all user values use bound parameters.
- Metadata filter names resolve to IDs before execution.
- Snippets are returned only after visibility predicates match.
- Archive evidence includes at most three extracted facts per source and twelve total, while fact summaries remain complete.
- Fast route, search, and completion changes discard stale browser responses.