> ## Documentation Index
> Fetch the complete documentation index at: https://docs.stackra.figentra.com/llms.txt
> Use this file to discover all available pages before exploring further.

# 0404 — Search: facets under record visibility, one Arabic normaliser, and the embedder token from the secret store

> Proposed · 2026-10-26

> [!NOTE]
>
> Proposed (STA-787). It amends
> [0221](https://docs.stackra.figentra.com/adr/0221-search-postgres-hybrid-elasticsuite-parity-arabic-and-operations.md) (decision 3 and
> its 2026-10-24 note) and applies
> [0333](https://docs.stackra.figentra.com/adr/0333-record-visibility-rules-who-can-see-which-records.md) and
> [0173](https://docs.stackra.figentra.com/adr/0173-secrets-infisical-is-the-source-of-production-secrets.md) to `@stackra/search`.

## Context

After wave 10, `@stackra/search` ranked under the IAM `VisibilityFilter` but had no facet counts, so a screen that
counted by status would have counted outside the viewer's records. It kept its own Arabic folding, which differed from
`@stackra/i18n`'s `normalizeArabic` (the article and the Quranic marks). The embedder read its bearer token from an
environment variable named in config. "Global indexes" in the owner questionnaire (item 23) had two possible readings.

## Decision

1. **Facets.** `SearchDriver.facets(table, request)` returns, per requested filter path, how many matching rows hold
   each value, most frequent first. `SearchIndex.facets(phrase, { paths, filters, limit })` hands the driver the same
   words, filters and `visibleKeysQuery` subquery a ranked read uses, so record visibility bounds the rows before they
   are counted and a count never includes a row the viewer cannot see. The Postgres driver counts over the rows either
   lane matches (every indexed row for an empty phrase), each row once per value; the memory driver refuses a request
   with visibility, as it does for a search.
2. **One Arabic normaliser.** `searchWordsOf` folds Arabic through `normalizeArabic` and adds only the Latin accents,
   the case and the split. The SQL mirror is `search.fold(text)` (migration `search.0003_create_fold_function`), which
   the trigram lane calls. `search.unaccent_immutable` is not replaced, so no existing expression index holds values
   its function no longer computes.
3. **Reindex plan.** Per searchable model, in order: (a) deploy the package (migration 0003 creates `search.fold`);
   (b) the model's next migration creates `gin (search.fold(search_text) gin_trgm_ops)` concurrently (expand); (c) run
   `search.in(Model).reindex()` as a job in its own context, which rewrites `search_text` and the vectors with the
   article stripped; (d) a later migration drops the `search.unaccent_immutable` index (contract). Between (a) and
   (b) the trigram lane scans without an index; between (a) and (c) an article-prefixed word in an old document is
   found by its prefix only. When no index names `search.unaccent_immutable`, a search migration drops it.
4. **Embedder token.** `search.embedder.tokenSecret` names a runtime secret; `HttpEmbedderDriver` takes the
   `SecretStore` the runtime binds and reads the token at call time. The environment is never read; the value is never
   logged, kept or put in an error. Which provider and region is region data in runway.
5. **"Global indexes" means `Search.across`.** There is no cross-model index table. Each model keeps its own index in
   its own table and schema (so isolation and visibility stay per model), and a global search (the Cmd+K palette)
   reads the listed models' indexes in turn and fuses them by reciprocal rank.

## Consequences

* Product screens can show filter counts that match what the viewer may open.
* Arabic matching is the same in search, conflict-name checks and anywhere else that compares names.
* Deployments rename `tokenEnv` to `tokenSecret` in runway config and bind the secret.
* A global search costs one query per model; a shared index stays a later engine's choice behind the same port.

## Enforcement

planned — the package tests fix facets, folding, the migration and the secret read
(`packages/data/search/src/**/__tests__/`); the SQL runs in the integration tier.

## Supersedes and sources

Supersedes: none. Amends 0221 decision 3. Sources: owner questionnaire item 23;
`notes/handover/stackra-package-needs.md`; `notes/handover/build-wave10-2026-10-25.md`; STA-595; STA-787.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.