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 CatalogueObject → SearchDocument (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)
- Thread
recording_date(YYYY-MM-DD) throughSearchDocument→SearchHit→SearchHitView; show it on the row when present. - Soften the count to
~{{count}}(it's an estimate). - 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>,toSearchDocument. - In
build_document's returned struct:recording_date: object.recording_date.map(|d| d.to_string()),(domain::Date'sDisplayis ISOYYYY-MM-DD, matchingAdminObjectView). 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>,toSearchHit. - In
search_objects's hit map:recording_date: doc.recording_date,(the executedSearchDocumentdeserialization carries it; old index docs missing the field deserialize toNonebecause it'sOption— graceful).
SearchHitView (crates/api/src/admin_search.rs)
- Add
pub recording_date: Option<String>,toSearchHitView. - In the map closure (
:100):recording_date: h.recording_date,.
Indexing / backfill
- New & edited objects get
recording_dateautomatically via the existing on-writesync_object. - Already-indexed objects return
recording_date: Noneuntil areindex_all(the existing rebuild path) runs — a graceful, opt-in backfill; areindexCLI command is a follow-up, not in scope.
Tests (Rust — need the docker stack: Postgres :5442, Meilisearch :7700)
- Update any direct
SearchDocument/SearchHitstruct literals incrates/search/tests/*/crates/api/tests/*to includerecording_date: None(compile fix). (Object fixtures constructCatalogueObject, which already hasrecording_date— unaffected.) - Extend
crates/search/tests/search.rs(thesearch_objects_returns_hits_…test): give one seeded object arecording_dateand assert the returned hit'srecording_dateis theYYYY-MM-DDstring (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 insearch-panel.tsx(the key already interpolatescount).
Fixtures + tests
web/src/test/fixtures.tssearchHits: addrecording_dateto the first hit (e.g."1962-04-03"); the rest can stayrecording_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/iassertion 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
SearchHitView → useSearch → SearchResultRow renders the date. The count is the same
estimated_total, now labelled ~.
Error handling / edges
recording_dateisOptionend-to-end → absent on objects without a recording date and on not-yet-reindexed docs; the row simply omits it.Date::to_string()is ISOYYYY-MM-DD(matchesAdminObjectView); 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:colorsgreen; 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
recording_dateis projected into the search index (SearchDocument/build_document) and returned throughSearchHit→SearchHitView→schema.d.ts(SearchHitView.recording_date?: string|null).- The search result row shows the recording date (when present) on its meta line.
- The result-count copy reads
~{{count}} …to flag the Meilisearch estimate (en + sv). - 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
reindexCLI command to backfillrecording_dateonto already-indexed objects (new/edited objects index it automatically;reindex_allis the existing rebuild path). - Richer faceting; making
recording_datefilterable/sortable in search.