Skip to content

M3-14 Search coordinator and prefix search

No Date Ruling Reason
D-M3-14-1 2026-10-23 The stateless library of 14 10 is internal/kernel/searchkit in kernel.core (standard library and kernel/ids only): query, page, hit and title types, the keyset position, the configured limits with the ts_headline options, the random markers and Split. search, messaging, chat and files share the types without depending on each other; a kernel core package is the place every module may use.
D-M3-14-2 2026-10-23 The search module builds the tsquery once per request inside the unit (search_tsquery over search.languages, the prefix statement of 14 4.9) and passes its text to the read operations, which cast it with ::tsquery. One construction for every type, and no read operation needs the language configuration (07 7.4 example).
D-M3-14-3 2026-10-23 Filters on thread facts (thread_state, tag_id, agent_id) for messages and files are resolved by the PostgreSQL index through chat.ThreadsMatching into a thread id list in the same unit, intersected with thread_id; an empty list runs no further query. For threads chat applies them itself. messaging and files may not read threads or thread_tags (03 principle 12, 07 7.4 rule 6).
D-M3-14-4 2026-10-23 The page check runs in filter mode in a fresh request scope: the coordinator drops the request memo and installs a new one (WithRequestScope(WithoutRequestScope(ctx))), because WithRequestScope alone keeps the memo of the request. A hit whose channel is missing from the fresh Scope drops silently; one authorize refuses although its channel is visible drops, logs once without IDs and counts filter_mismatch. The next cursor follows the last row the statement returned. The brief asks to read the rights version again; inside a request WithRequestScope returns the context unchanged, so only a fresh memo sees a revocation committed meanwhile.
D-M3-14-5 2026-10-23 The cursor carries the position and the reference time of the first page (the request time of kernel/clock, the clock that writes created_at of messages, threads and files); later pages measure the age of the score from it and keep only candidates created at or before it. A message created between two pages neither shifts the scores nor pushes a candidate out.
D-M3-14-6 2026-10-23 "Same cursor" of SR9 means the same decoded position and reference time; the cursor strings differ because the binding starts with the caller's principal. The M1-18 codec binds the principal, so a cursor of a person never works for an agent and the other way round.
D-M3-14-7 2026-10-23 The candidate limit search.max_candidates applies to both sorts. 14 4.5 item 1 and R-SU-04: the cost stays independent of the number of hits for every order.
D-M3-14-8 2026-10-23 A filter outside its types (14 4.4 column "Types"; thread_id applies to every type) answers 400 validation_failed with pointer #query/<name> and code not_applicable; several tag_id values match a thread with at least one of them; has_file=false keeps messages without attachments. A filter that cannot narrow the type would be ignored silently; the client learns of its mistake.
D-M3-14-9 2026-10-23 The length of q counts runes of the raw value, an invalid byte as one; invalid UTF-8 and control characters become spaces before the query construction. They cannot be part of a term, and PostgreSQL refuses a NUL byte in a text parameter; counting the raw value keeps the limit independent of the cleaning.
D-M3-14-10 2026-10-23 searchThreadTitles uses search.query_max_chars for q and search.page_size_default and search.page_size_max for its limit; it has no cursor. The operation shares the input rules of search; the quick switcher shows one short list.
D-M3-14-11 2026-10-23 Hit objects are the list representation of a message (messaging.HitMessages), Thread (chat.HitThreads) and the attached File (files.HitFiles), rendered with the REST renderers of the owning modules that internal/wire passes to the search handlers (MessageREST, the newly exported chat ThreadREST, FileREST); thread_title is absent for main threads, channel_name and space_name for channels without a visible space. A hit shows exactly what the owning module shows, and search/adapters may not import the adapters of other modules.
D-M3-14-12 2026-10-23 The unit of work option txn.StatementTimeout sets statement_timeout of the outermost unit instead of database.statement_timeout; pg.IsStatementTimeout reports SQLSTATE 57014 only while the context is live. Every search runs in one read unit with search.statement_timeout; a timeout answers 503 unavailable with reason search_timeout and is not retried. A cancelled request sends a cancel that gives the same SQLSTATE and is the caller giving up, not the deadline.
D-M3-14-13 2026-10-23 bothy.search.duration has bothy.outcome ok, client_error (any 4xx), timeout (search_timeout) and server_error; it measures search only, from the start of the use case to its end; the prefix search is measured by http.server.request.duration. A closed set keeps the cardinality bounded; the prefix search is a short list that the HTTP metric already covers.
D-M3-14-14 2026-10-23 Start checks search_page_size (search.page_size_default not above search.page_size_max) and search_headline_words (search.headline_min_words below search.headline_max_words, which ts_headline requires). Otherwise every search would fail at run time; the start names both keys and no value.
D-M3-14-15 2026-10-23 The handler type of the search module is httpapi.Finder, not Search. An embedded field named Search in wire.Handlers would hide the strict-server method Search.
D-M3-14-16 2026-10-23 The search coordinator starts in the start step files, after the files module (order 1410), because it reads through messaging, chat and files. The chat step builds messaging before files exists.
D-M3-14-17 2026-10-23 Authorizer.Scope serves file.read: its requirement goes by the purpose of the file, and the attachment branch is read on the path, so the scope is the list of channels whose attachments the principal reads, the same sets as message.read, cut by view_as and acting_for; an app gets them only with files:read (authz/app listAction, domain.Expr.IsAttachmentRead). The search of type files needs the visible channels of file.read (brief design note 2 step 5 and note 3); the merged Scope accepted only requirements of plain read and answered 500 for file.read.
D-M3-14-18 2026-10-24 search.max_candidates, search.page_size_max and search.headline_input_max_chars have the maximum 1048575, search.prefix_min_chars and the three search.headline_* word and fragment keys 2147483647, and search.statement_timeout 24d. The statements take the row counts as int32 and read one row more than the limit, the messaging adapter refuses a limit at or above 1<<20, PostgreSQL reads the prefix length and the ts_headline options as int4, and SET LOCAL statement_timeout takes at most 2147483647 ms. A larger value would fail every search of a type, so the start refuses it, as for the database deadlines.