3.6 KiB
Search and indexing
Goals
Search must find mixed Dutch, English and technical content quickly without leaking inaccessible titles, snippets, facets or counts.
The first release uses PostgreSQL full-text search and pg_trgm. Embeddings and external
search services are out of scope.
Indexed sources
Page document
Weighted fields:
- A: title
- B: tags, document type, breadcrumb labels
- C: stripped Markdown headings and body
- D: selected metadata such as course/module and attachment descriptions
Project document
- name and summary;
- current state;
- status and technologies;
- project updates;
- linked decision summaries;
- linked document titles.
Attachment metadata
Only safe metadata is indexed:
- original filename;
- description;
- caption;
- detected media type.
Attachment body extraction and OCR are deferred.
Text normalization
- Store original content unchanged.
- Build a normalized search projection.
- Use PostgreSQL
simpleconfiguration initially to avoid damaging code, commands, product names and bilingual terms through aggressive stemming. - Normalize Unicode and case for fuzzy title matching.
- Strip Markdown syntax without concatenating unrelated tokens.
- Keep code identifiers searchable.
- Never index hidden secrets or operator-only source paths.
Query behavior
- Parse and bound the query.
- Apply workspace, visibility, trash and permission filters.
- Generate workspace- and permission-filtered full-text candidates.
- If exact candidates exist, rank those first and skip workspace-wide fuzzy work; otherwise use the indexed
pg_trgmtitle predicate at the measured 0.2 threshold for typo-only searches. - Rank with title/exact phrase/recency weights.
- Generate snippets only from permitted candidates.
- Return deterministic
(rank, updatedAt, id)cursor ordering.
Exact title and exact quoted phrase matches outrank fuzzy matches.
This exact-first fallback is intentional: it keeps common-term latency bounded while preserving typo tolerance. The threshold is applied with PostgreSQL SET LOCAL in the request transaction; see ADR-020 and the 2026-08-01 production audit.
Filters
- document type;
- document status;
- collection/root;
- tag;
- project;
- author/editor;
- changed date range;
- content kind;
- stale/review state.
Facets are calculated from the permitted candidate population.
Index consistency
Publishing a page updates its current search projection transactionally where practical. Background repair handles:
- parser/version changes;
- project context rollups;
- bulk import;
- failed projection updates;
- full reindex.
Every projection stores a source version/hash. Administrators can see lag and repair it.
Permission safety
Search authorization is part of SQL selection. It is not acceptable to:
- retrieve the top global results;
- filter inaccessible rows in application memory;
- return fewer results or leaked facets.
Tests must include private pages whose titles and rare terms would reveal their existence.
Scale verification
Synthetic data must include:
- 100,000 current pages;
- mixed Dutch/English prose;
- code and command blocks;
- 1,000,000 revisions not all included in current-content search;
- tags and project links;
- deliberately inaccessible content;
- typo and phrase queries.
Capture:
- p50/p95/p99 latency;
- query plans;
- index sizes;
- result correctness;
- no-leak assertions.
Search API safeguards
- query length bound;
- result limit cap;
- cursor expiry/versioning;
- rate limit;
- cancellation/timeout;
- safe highlight tags generated by server or client—not arbitrary HTML;
- generic not-found/forbidden behavior where existence is sensitive.