Skip to main content
Related: Relation, Joins, Materializers A concise, immutable DSL on 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.
Nested fields are addressed via association-name keyed Hashes and require the association to be joined first.
# Internal example state
{ include: Set[:id, :name], include_nested: { authors: Set[:first_name, :last_name] },
  exclude: Set[:legacy], exclude_nested: { brands: Set[:internal_score] } }

DSL

  • Root fields: symbols/strings (e.g., :id, “title”)
  • Nested fields: { assoc => [:field, ...] }
  • reselect replaces both include and exclude state
See also: Relation Guide and Joins for DSL context.

Usage

SearchEngine::Book
  .select(:id, :name)
  .exclude(:internal_score)
SearchEngine::Book
  .joins(:authors)
  .select(:id, :title, authors: [:first_name, :last_name])
  .exclude(authors: [:middle_name])

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).
    • reselect clears 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 attributes and subtract explicit excludes. If unknown, nested excludes may be emitted via exclude_fields.

Inspect / Explain

  • inspect includes compact tokens for current selection state, e.g. sel=”…” xsel=”…”.
  • explain prints human-readable select: and exclude: 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 − exclude per path (root and each association). Exclude wins. Empty groups are omitted.

Mapping table

Normalized stateinclude_fieldsexclude_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

rel = SearchEngine::Book
        .joins(:authors, :publishers)
        .select(:id, :title, authors: [:first_name, :last_name])
        .exclude(:legacy, publishers: [:internal_score])
rel.to_typesense_params[:include_fields]
# => "id,title,$authors(first_name,last_name)"
rel.to_typesense_params[:exclude_fields]
# => "legacy,$publishers(internal_score)"

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 nil if they rely on ivars.
  • Strict: If a requested field is absent in the hit, hydration raises SearchEngine::Errors::MissingField with guidance.
Backed by:
  • Per‑relation override via options(selection: { strict_missing: true })
  • Global default via SearchEngine.configure { |c| c.selection = OpenStruct.new(strict_missing: false) }
# initializer
SearchEngine.configure { |c| c.selection = OpenStruct.new(strict_missing: false) }
# per relation
rel = SearchEngine::Book.select(:id).options(selection: { strict_missing: true })

Propagation and enforcement

  • During compile, the effective base selection is captured as requested_root and the strict flag is recorded.
  • During hydration, Result computes missing = requested_root − present_keys for each hit; when strict, it raises MissingField with a helpful message and a pointer back to these docs.
  • When includes are empty (effective “all fields”), no requested_root is set and strict enforcement is a no‑op.
See also: Materializers for validation on 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.
Example:
UnknownJoinField: :middle_name is not declared on association :authors for SearchEngine::Book
Notes:
  • 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.

Hydration decision (strict vs lenient)