Files
biggus-dickus/docs/superpowers/specs/2026-06-08-search-row-date-design.md

7.7 KiB

Search-Result Date Meta + Estimated-Count Copy — Design

Date: 2026-06-08 Status: Approved (brainstorming) — ready for implementation planning. Issue: #61 (scoped: add a date to result rows + soften the estimated count; object type/facet is declined — no such field exists in the domain).

Context

Search result rows show name + object_number + visibility badge + snippet, but no date — power users disambiguate hits by period. The count copy reads "{{count}} results" though it's Meilisearch's estimated_total. There is no object type/category/classification field anywhere in the domain (CatalogueObject = object_number/object_name/number_of_objects/brief_description/current_location/ current_owner/recorder/recording_date/visibility + flexible fields + timestamps), so a "type" meta/facet is impossible without a new domain concept (out of scope). The only disambiguating date available is recording_date (YYYY-MM-DD) — but it is not indexed into the search document or returned in the hit, so surfacing it needs a backend change.

Facts: SearchDocument (crates/search/src/lib.rs:30) and SearchHit (:52) carry id/object_number/object_name/brief_description/[current_owner/recorder]/visibility/[snippet] — no date. build_document (:302) projects a CatalogueObjectSearchDocument (does not copy recording_date). search_objects (:185) maps Meili results → SearchHit. SearchHitView (crates/api/src/admin_search.rs:26) mirrors SearchHit. The frontend type is components["schemas"]["SearchHitView"]; schema.d.ts is generated by pnpm gen:api. AdminObjectView already serializes recording_date as Option<String> (YYYY-MM-DD, via format_date). On-write sync_object re-projects an object after each catalogue write; reindex_all is the full rebuild path.

Decisions (from brainstorming)

  1. Thread recording_date (YYYY-MM-DD) through SearchDocumentSearchHitSearchHitView; show it on the row when present.
  2. Soften the count to ~{{count}} (it's an estimate).
  3. Decline object type/facet (no domain field).

Backend (crates/search, crates/api)

SearchDocument + build_document (crates/search/src/lib.rs)

  • Add field: pub recording_date: Option<String>, to SearchDocument.
  • In build_document's returned struct: recording_date: object.recording_date.map(|d| d.to_string()), (domain::Date's Display is ISO YYYY-MM-DD, matching AdminObjectView). Index it as a plain string (Meili stores/returns it; it need not be filterable).

SearchHit + search_objects mapping (crates/search/src/lib.rs)

  • Add pub recording_date: Option<String>, to SearchHit.
  • In search_objects's hit map: recording_date: doc.recording_date, (the executed SearchDocument deserialization carries it; old index docs missing the field deserialize to None because it's Option — graceful).

SearchHitView (crates/api/src/admin_search.rs)

  • Add pub recording_date: Option<String>, to SearchHitView.
  • In the map closure (:100): recording_date: h.recording_date,.

Indexing / backfill

  • New & edited objects get recording_date automatically via the existing on-write sync_object.
  • Already-indexed objects return recording_date: None until a reindex_all (the existing rebuild path) runs — a graceful, opt-in backfill; a reindex CLI command is a follow-up, not in scope.

Tests (Rust — need the docker stack: Postgres :5442, Meilisearch :7700)

  • Update any direct SearchDocument/SearchHit struct literals in crates/search/tests/* / crates/api/tests/* to include recording_date: None (compile fix). (Object fixtures construct CatalogueObject, which already has recording_date — unaffected.)
  • Extend crates/search/tests/search.rs (the search_objects_returns_hits_… test): give one seeded object a recording_date and assert the returned hit's recording_date is the YYYY-MM-DD string (proves it flows index → hit).

Frontend (web)

web/src/api/schema.d.ts (generated)

After the backend change, regenerate via pnpm gen:api (stack + server up) or hand-add recording_date?: string | null; to the SearchHitView block (mirroring brief_description?: string | null / snippet?: string | null) — a later gen:api reproduces it identically. Either is acceptable; the manual edit avoids running the server purely for codegen.

search-result-row.tsx

Add recording_date to the meta line (the flex items-center gap-2 text-xs text-muted-foreground row), after object_number, when present:

<span>{hit.object_number}</span>
{hit.recording_date && <span>· {hit.recording_date}</span>}
<VisibilityBadge visibility={hit.visibility} />

(Render the YYYY-MM-DD string as-is — it's a plain recording date, not a UTC timestamp, so no timezone formatting. A separator dot before it keeps the row scannable.)

search-panel.tsx + i18n — soften the count

The count uses t("search.resultCount", { count: total }) with resultCount_one/resultCount_other. Change the i18n values to flag the estimate (en + sv, parity preserved):

  • en: "resultCount_one": "~{{count}} result", "resultCount_other": "~{{count}} results"
  • sv: "resultCount_one": "~{{count}} träff", "resultCount_other": "~{{count}} träffar" No code change in search-panel.tsx (the key already interpolates count).

Fixtures + tests

  • web/src/test/fixtures.ts searchHits: add recording_date to the first hit (e.g. "1962-04-03"); the rest can stay recording_date: null (or omit — it's optional).
  • web/src/search/search.test.tsx: assert the first result row shows the date (getByText("1962-04-03") or within the row); assert the count copy includes the ~ (getByText(/~\s*25 results/i) — note the existing /25 results/i assertion still matches the new copy; tighten it to require the ~).

Data flow

DB object → build_document (now copies recording_date) → Meili doc → search_objects hit → API SearchHitViewuseSearchSearchResultRow renders the date. The count is the same estimated_total, now labelled ~.

Error handling / edges

  • recording_date is Option end-to-end → absent on objects without a recording date and on not-yet-reindexed docs; the row simply omits it.
  • Date::to_string() is ISO YYYY-MM-DD (matches AdminObjectView); no locale/timezone formatting.
  • The ~ is cosmetic; pluralization (_one/_other) is unchanged.

Testing

  • Rust: cargo build --workspace; cargo nextest run -p search -p api (stack up) green, incl. the new recording_date assertion + any literal compile-fixes.
  • Frontend: typecheck/lint/test/build/check:size/check:colors green; the new row-date + count tests pass; en/sv parity (the #60 parity test guards the changed values); no codename.
  • No new dependency.

Acceptance criteria

  1. recording_date is projected into the search index (SearchDocument/build_document) and returned through SearchHitSearchHitViewschema.d.ts (SearchHitView.recording_date?: string|null).
  2. The search result row shows the recording date (when present) on its meta line.
  3. The result-count copy reads ~{{count}} … to flag the Meilisearch estimate (en + sv).
  4. Rust (cargo build + search/api tests) green; frontend gate green; en/sv parity; no codename; no new dependency.

Out of scope → follow-ups

  • An object type/classification domain concept + a type facet (no such field exists today).
  • A reindex CLI command to backfill recording_date onto already-indexed objects (new/edited objects index it automatically; reindex_all is the existing rebuild path).
  • Richer faceting; making recording_date filterable/sortable in search.