Overview
- Pin hits by ID to the top of results (stable first-occurrence order)
- Hide hits by ID (hide-wins when an ID is both pinned and hidden)
- Curation tags are optional body-only tags
- Filter flag
filter_curated_hitsoptionally excludes hidden hits from the curated view - Composes with selection, presets, grouping, pagination; does not alter URL/common params
DSL
Immutable chainers onRelation (copy-on-write). Inputs are normalized (coerced to String, blank dropped), arrays flattened one level, and lists de-duplicated while preserving first occurrence order.
pin(*ids)— append to pinned list (stable-dedupe)hide(*ids)— append to hidden list (set semantics)curate(pin: [], hide: [], curation_tags: [], filter_curated_hits: nil|true|false)— replace provided keys; omit to retainclear_curation— remove all curation state from the relation
-
pinned: Array<String> -
hidden: Array<String> -
curation_tags: Array<String> -
filter_curated_hits: true | false | nil
inspectemits a compact token only when non-empty, e.g.curation=p:[p_12,p_34]|h:[p_99]|tags:[homepage]|fch:falseexplainadds a concise curation summary and a conflicts line when overlaps/limits occur
Compiler mapping
Curation state maps to Typesense body params and never appears in URL/common params. Empty arrays are omitted;filter_curated_hits is omitted when nil.
| State key | Example value | Param key | Encoded value |
|---|---|---|---|
pinned | ["p_1","p_2"] | pinned_hits | "p_1,p_2" |
hidden | "p9" | hidden_hits | "p9" |
curation_tags | ["homepage","campaign"] | curation_tags | "homepage,campaign" |
filter_curated_hits | true | filter_curated_hits | true |
- Keys are omitted when arrays are empty or when
filter_curated_hitsisnil - Ordering is deterministic;
pinnedpreserves first-occurrence order
Guardrails & errors
Validation is applied after normalization. Overlaps and limits are recorded forexplain and observability.
Rules
| Rule | Behavior |
|---|---|
| ID format | SearchEngine.config.curation.id_regex (default /\A[\w-:.]+\z/) applied to curated IDs and curation tags |
| Deduplication | pinned stable-dedupes (first occurrence wins); hidden set-dedupes (first-seen order preserved) |
| Limits | max_pins (default 50) and max_hidden (default 200) enforced post-normalization |
| Precedence | When an ID exists in both lists, hide wins (removed from pinned, recorded as conflict) |
Errors
| Error | When |
|---|---|
InvalidCuratedId | Curated ID fails the allowed pattern |
CurationLimitExceeded | Pinned or hidden list exceeds configured limit |
InvalidCurationTag | Curation tag is blank or fails the allowed pattern |
Multi-search
Per-search independence: eachm.add relation carries its own curation keys in its body. Pinned order is preserved; omission rules apply; filter_curated_hits is scoped per entry.
Insert:
Materializers & explain
Materializers reuse the memoized single response and apply curation in-memory.- Ordering: pins first (declared order, present IDs only), then remainder in original order. Hide-wins.
- Filtering: when
filter_curated_hits: true, hidden hits are excluded from iteration and counts. - Counts: when filtering is on,
countreflects the curated view size;exists?follows server totals. To check curated emptiness, usecount > 0. explainadds a curation summary and a conflicts line.
Observability
Events are counts/flags only; IDs/tags are redacted. A compact logging subscriber appends a short curation segment to single-search lines and structured JSON fields when present.search_engine.curation.compile— once per compile when curation state exists- Payload:
pinned_count,hidden_count,has_curation_tags,filter_curated_hits
- Payload:
search_engine.curation.conflict— emitted when overlaps or limits are detected; at most once per compile- Payload:
type(:overlap|:limit_exceeded),count, optionallimit
- Payload:
- Text:
[se.search] collection=products status=200 duration=12.3ms cu=p:2|h:1|f:false|t:1 - JSON:
{ "event":"search", "collection":"books", "curation_pinned_count":2, "curation_hidden_count":1, "curation_has_curation_tags":true, "curation_filter_flag":false }