Relation for selecting or excluding fields, with support for nested join fields and normalization.
Overview
select(*fields): add fields to include list (root and nested); immutable, deduped, order preserved by first mention.exclude(*fields): add fields to exclude list (root and nested); immutable, deduped, order preserved by first mention.reselect(*fields): clear prior include/exclude state and set a new include list.
DSL
- Root fields: symbols/strings (e.g.,
:id, “title”) - Nested fields:
{ assoc => [:field, ...] } reselectreplaces both include and exclude state
Usage
Normalization & Precedence
- Inputs accept symbols/strings and arrays; nested via
{ assoc => [:field, ...] }. - All names are coerced to symbols/strings consistently; blanks rejected; duplicates removed with first-mention preserved.
- Precedence:
- When include is empty, effective selection is “all fields” (when attributes are known) minus explicit excludes.
- When include is non-empty, effective selection is
include − exclude(applied for root and each nested association). reselectclears both include and exclude state.
Nested joins
- Nested shapes require
joins(:assoc)beforehand. - For nested paths without explicit includes, the engine attempts to derive “all fields” from the joined collection’s declared
attributesand subtract explicit excludes. If unknown, nested excludes may be emitted viaexclude_fields.
Inspect / Explain
inspectincludes compact tokens for current selection state, e.g.sel=”…” xsel=”…”.explainprints human-readableselect:andexclude:lines when present and a compact one-line summary of the effective selection after precedence.
State → Params mapping
Compiler Mapping (Typesense params)
- include_fields: base tokens and nested joins encoded as
$assoc(field1,field2). - exclude_fields: base tokens and nested joins encoded similarly.
- Precedence: final effective set is
include − excludeper path (root and each association). Exclude wins. Empty groups are omitted.
Mapping table
| Normalized state | include_fields | exclude_fields |
|---|---|---|
include: [:id, :name] | id,name | — |
exclude: [:legacy] | — | legacy |
include: [:id], include_nested: { authors: ["first_name","last_name"] } | $authors(first_name,last_name),id | — |
exclude_nested: { brands: ["internal_score"] } | — | $brands(internal_score) |
include: [:id,:title], include_nested: { authors: ["first_name","last_name"] }, exclude: [:legacy], exclude_nested: { brands: ["internal_score"] } | id,title,$authors(first_name,last_name) | legacy,$brands(internal_score) |
include_nested: { authors: ["first_name","last_name"] }, exclude_nested: { authors: ["last_name"] } | $authors(first_name) | — |
Flow
Example
Strict vs Lenient selection
Hydration respects selection by assigning only attributes present in each hit. Missing attributes are never synthesized.- Lenient (default): Missing requested fields are left unset; readers should return
nilif they rely on ivars. - Strict: If a requested field is absent in the hit, hydration raises
SearchEngine::Errors::MissingFieldwith guidance.
- Per‑relation override via
options(selection: { strict_missing: true }) - Global default via
SearchEngine.configure { |c| c.selection = OpenStruct.new(strict_missing: false) }
Propagation and enforcement
- During compile, the effective base selection is captured as
requested_rootand the strict flag is recorded. - During hydration,
Resultcomputesmissing = requested_root − present_keysfor each hit; when strict, it raisesMissingFieldwith a helpful message and a pointer back to these docs. - When includes are empty (effective “all fields”), no
requested_rootis set and strict enforcement is a no‑op.
pluck, and Materializers for hydration flow.
Guardrails & errors
Validation happens during chaining (after normalization, before mutating state) and raises actionable errors early. Suggestions are provided when attribute registries are available.- UnknownField: base attribute not declared on the model.
- UnknownJoinField: nested attribute not declared on the given association.
- ConflictingSelection: invalid/ambiguous shapes that cannot be normalized deterministically.
- Suggestion source: Levenshtein/prefix against the relevant registry (top 1–3, stable order).
- Overlap between include and exclude is allowed; precedence still applies. Conflicts are only about malformed shapes.