examples/demo_shop/app/controllers/books_controller.rb.
Overview
- Declare associations on your model with
belongs_to :name,has_one :name, andhas_many :name(options:collection:,local_key:,foreign_key:;async_ref:for belongs_to). - Select joins in queries with
Relation#joins(*assocs); names validated against the model’s registry. - Use nested include fields for joined collections;
where/ordercan target joined fields using$assoc.field. - Observe compile‑time summaries via
search_engine.joins.compilewithout exposing raw literals.
DSL
Declare associations on your model usingbelongs_to / belongs_to_many / has_one / has_many:
name(Symbol/String): logical association name.collection(Symbol/String): target Typesense collection name (auto‑resolved fromname).local_key(Symbol/String): local attribute used as the join key (auto‑resolved; overrideable).foreign_key(Symbol/String): foreign key in the target collection (auto‑resolved; overrideable).async_ref(Boolean, belongs_to / belongs_to_many only): mark the reference asynchronous in schema.
Declaring references
When declaring abelongs_to or belongs_to_many association, the local_key attribute must be declared with type :string or [:string] in your model. Typesense requires reference fields to be strings, and the schema compiler validates this during compilation.
:integer), schema compilation will raise an error with guidance to update the attribute declaration.
Auto‑resolution rules
- belongs_to / belongs_to_many (association‑name based):
collection: plural of the first argumentlocal_key:singular(name)_idswhen the argument is plural; elsesingular(name)_idforeign_key:singular(name)_id
- has_one (deterministic):
collection: plural of the first argumentlocal_key:<current_singular>_idforeign_key:<current_singular>_id
- has_many (deterministic):
collection: plural of the first argumentlocal_key:<current_singular>_idforeign_key:<current_singular>_ids
collection:, local_key:, or foreign_key:.
Instance association readers (AR-like hop)
Declaringbelongs_to, belongs_to_many, has_one, or has_many also defines an instance method with the same name that resolves referenced records using the join config.
Example:
- belongs_to (singular name): returns a single record via
find_by(foreign_key: local_value); returns nil when local value is nil. - belongs_to (plural or when local value is an Array): returns a
Relationscoped withwhere(foreign_key: local_array); empty array yields an empty relation. - has_one (any name): returns a single record via
find_by(foreign_key: local_value); returns nil when local value is nil. - has_many (any name): always returns a
Relationscoped withwhere(foreign_key: local_value); nil local value yields an empty relation. - belongs_to_many (any name): always returns a
Relationscoped withwhere(foreign_key: local_value); nil local value yields an empty relation. Use for shared-key one‑to‑many joins.
- Methods are defined at declaration time; resolution uses the per‑class
join_for(:name)andSearchEngine.collection_for(cfg[:collection]). - Returned
Relationis fully chainable (where,order,joins, etc.). - Asynchronous belongs_to (
async_ref: true) works the same; if the referenced target is missing, the singular call returns nil.
Shared‑key example (generic)
Advanced behavior
- Client-side fallback can run when Typesense references are missing or shared keys are used.
- Asynchronous references are available via
async_ref: trueonbelongs_to.
Quoting and type coercion: Joined-field values are coerced using the target model’s attribute types. If a joined field is typed as
:string, numeric inputs are converted to strings and quoted in filter_by (e.g., $calculated_products(store_id:=“1070”)).See References & JOINs — Deep dive for full rules and caveats.
Relation Usage
UseRelation#joins(*assocs) to select join associations on a query. Names are validated against the model’s joins_config and stored in the relation’s immutable state in the order provided. Multiple calls append:
joinsaccepts symbols/strings; inputs are normalized to symbols.- Unknown names raise
SearchEngine::Errors::UnknownJoinwith an actionable message that lists available associations. - Order is preserved and duplicates are not deduped by default; explicit chaining is honored.
- For debugging,
rel.joins_listreturns the frozen array of association names in state.
Filtering and Ordering on Joined Fields
With joins applied, you can reference joined collection fields inwhere and order using nested hashes. Joined left‑hand‑sides render as $assoc.field.
| Input (Ruby) | Compiled filter_by | Compiled sort_by |
|---|---|---|
where(authors: { last_name: "Rowling" }) | $authors.last_name:=“Rowling” | – |
where(orders: { total_price: 12.34 }) | $orders.total_price:=12.34 | – |
order(authors: { last_name: :asc }) | – | $authors.last_name:asc |
order(“$authors.last_name:asc”) | – | $authors.last_name:asc |
- Base fields continue to work unchanged (e.g.,
where(active: true)). - Mixed base and joined predicates interleave as usual; the compiler preserves grouping semantics.
- Raw
orderstrings are accepted as‑is; ensure you supply valid Typesense fragments.
Nested field selection for joined collections
You can select fields from joined collections using a nested Ruby shape. These compile to Typesenseinclude_fields with $assoc(field,…) segments.
- Input types: mix base fields (
:id, “title”) and nested hashes (authors: [:first_name, :last_name]). - Merging: multiple calls merge and dedupe. First mention wins ordering; later calls append only new fields.
- Ordering policy: nested
$assoc(…)segments are emitted first in association first-mention order, then base fields. - Validation: association keys are validated against
klass.joins_config(UnknownJoinon typos). Calling.joins(:assoc)before selecting nested fields is recommended; the compiler will still emit$assoc(…)even ifjoinswasn’t chained yet.
End-to-end example with filters and sort
:_join key with join context for downstream components. See Compiler for the exact shape.
Common pitfalls
- Declare every association on the model before using
.joins(:assoc). - Use only single-hop paths like
$assoc.field. - Keep joined field names aligned with the target model attributes.
Deep dives
- Reference fields and async refs: References & JOINs — Deep dive
- Joined selection + grouping example: JOINs, Selection, and Grouping
- JOIN instrumentation and logs: Observability