Models and the Collection Registry
SearchEngine provides a minimal model layer to prepare for future hydration of Typesense documents into Ruby objects. A thread-safe registry maps Typesense collection names to model classes.- Registry:
SearchEngine.register_collection!(name, klass)andSearchEngine.collection_for(name) - Base class:
SearchEngine::Basewithcollectionandattributemacros
Declare a model
Lookup
Convenience: find by id
.find is available on the model class only; it is not delegated to relations.
Errors
- Unknown collection: raises
ArgumentErrorwith a helpful message - Duplicate registration: re-registering the same mapping is a no-op; attempting to map a collection to a different class raises
ArgumentError
Document identity (identify_by)
Every Typesense document must include a unique Stringid. The gem computes this
automatically per record and injects it during indexing.
- Default:
record.id.to_s - Override with
identify_by:
attribute :id, …is invalid and raises. Useidentify_by.- Any
idreturned fromindex -> mapis ignored. - Inspect output renders
idfirst regardless of whether it was declared. - Precedence order: When creating/upserting documents, the id is resolved as:
(1) explicit
:idattribute provided, (2)identify_bystrategy result, (3)record.id.to_sfallback (ActiveRecord sources only). - String coercion: All IDs are coerced to strings internally (Typesense requirement). The gem handles this automatically.
System field: doc_updated_at
- Always injected on create/upsert/update (epoch seconds).
- If declared via
attribute, its type is coerced toint64during schema compilation. - Hydration converts it to
Time(usesTime.zonewhen present). - Cannot be disabled; Typesense requires it for tracking.
Creating documents
UseModel.create(attrs) to insert a single document into the backing collection and get a hydrated instance back.
- Validates required fields (respects
optional:). Unknown fields are rejected whenmapper.strict_unknown_keysis enabled. - Automatically sets
doc_updated_atand hidden flags (*_empty,*_blank) when present in schema. :idmay be provided explicitly; otherwise it is computed viaidentify_by. If absent, Typesense may generate one.- By default, targets the alias (logical) collection; pass
into: “physical_name”to override.
Updating documents
Userecord.update(attrs) to partially update fields of a hydrated record in Typesense.
- Updates are partial; only provided fields are sent.
- Requires the record to have an
id(hydrated or computable). - Returns
1on success,0on failure (failures are logged). - Accepts
into:,partition:,timeout_ms:, andcascade:options.
Mapping source data to model instances (.from)
Use Model.from(data, mode: :instance) to map source data (ActiveRecord instances or SQL results) onto the model’s schema and return hydrated instances or hashes. This uses the same mapping logic defined in your index DSL but does not make any Typesense API calls.
The method accepts input corresponding to your configured source type:
:active_recordsource: Accepts an ActiveRecord instance or an Array of instances. Output preserves input shape (single instance → single instance, array → array).:sqlsource: Accepts a SQL String, executes it internally to fetch rows, and always returns an Array of results (even when a single row is returned).
ActiveRecord source examples
SQL source examples
Hash mode
Passmode: :hash to return HashWithIndifferentAccess documents instead of model instances. This is useful when you need raw hash data or want to avoid hydration overhead.
Behavior and validation
- Mapping: Uses the same
mapblock and schema validation as defined in yourindexDSL. - No Typesense calls: This method only performs local mapping and validation; it does not interact with Typesense.
- Schema validation: Validates mapped documents against the compiled schema (required fields, types, etc.).
- Shape preservation: For ActiveRecord sources, single input → single output, array input → array output. For SQL sources, output is always an array.
- Source requirement: The model must have a
sourcedefined in itsindexDSL (:active_recordor:sql). Models using onlypartition_fetchwithout asourcedeclaration will raise an error.
Error cases
- Missing mapper: Raises
SearchEngine::Errors::InvalidParamsif nomapblock is defined. - Missing source: Raises
SearchEngine::Errors::InvalidParamsif nosourceis defined in theindexDSL. - Invalid mode: Raises
SearchEngine::Errors::InvalidOptionifmodeis not:instanceor:hash. - Type mismatch: For ActiveRecord sources, raises
SearchEngine::Errors::InvalidParamsif input is not an instance of the configured model class. - Unsupported source: Raises
SearchEngine::Errors::InvalidOptionfor source types other than:active_recordor:sql.
Custom instance methods
Hydrated results are instances of your model class, so any instance methods you define are available on results.- Declared attributes get
attr_readers automatically (e.g.,category_id). - Boolean attributes automatically get a question-mark alias method (e.g.,
available?forattribute :available, :boolean). - See also: Relation, Materializers
Instance association readers (joins)
When you declarebelongs_to, has_one, or has_many on a model, an instance method with the same name is defined to resolve referenced records:
belongs_to :author→book.authorreturns a single record ornil.has_many :orders→book.ordersreturns a chainableRelation.
Inheritance
Attributes declared in a parent class are inherited by subclasses. A subclass may override an attribute by redeclaring it:Flow (registry lookup)
See also: Materializers.Thread-safety and reload-friendliness
The collection registry uses a copy-on-write Hash guarded by a small Mutex. Reads are lock-free and writes are atomic, which makes it safe under concurrency and friendly to Rails code reloading in development. See also: Relation.Nested fields (object / object[])
When you need to store nested objects:- Declare the base attribute as
:object(single) or[:object](array of objects). - Prefer declaring subfields inline via the
nested:option onattribute.
- If base is
:object, subfields compile to scalar Typesense types (e.g.,float). - If base is
[:object], subfields compile to array Typesense types (e.g.,float[]). - The schema compiler auto-enables
enable_nested_fieldsat collection level when any object/object[] is present.
Model scopes (ActiveRecord-like)
Define reusable, chainable scopes on your model. Scopes are evaluated against a fresh Relation (all) and must return a SearchEngine::Relation.
- Scopes are class methods created by
scope :name, ->{ ... }. - The block is executed with
selfset to a fresh Relation; return a Relation (ornilto fallback toall). - You can pass parameters to scopes (e.g.,
by_store(id)).
Per‑model default query_by
You can set a default query_by for a collection at the model level. This value participates in the usual precedence: relation options override model default, which overrides global config (SearchEngine.config.default_query_by).
self to allow macro chaining; when called with no arguments it returns the canonical comma‑separated String or nil if unset.
Backlinks: Configuration, Relation
Attribute options reference
Attributes accept additional options to influence schema and query behavior.Boolean attribute question-mark aliases
When you declare a boolean attribute, a question-mark alias method is automatically created for convenient predicate-style access::boolean (not array types like [:boolean]) receive this alias.
Supported options and constraints:
locale: String— only for:stringor[:string]attributes.sort: true|false— mark field sortable in schema.optional: true|false— marks field optional; enables<name>_blankflag in schema.infix: true|false— enable Typesense infix for this field in schema.empty_filtering: true|false— only for array types (e.g.,[:string]); enables<name>_emptyhidden field for safe empty‑array filtering.facet: true|false— mark field as facetable in schema for faceted search/filtering.index: true|false— whenfalse, the field is omitted from the compiled Typesense schema. You may still send the field in documents; Typesense stores it and returns it with hits, but it is not indexed in memory. Nested subfields under an unindexed base object are also omitted from schema.
locale, sort, optional, infix).
Declaring nested subfields with nested
As an alternative to inline nested: on attribute, you can declare subfields after the base attribute is defined as :object or [:object]:
:object, nested subfield types are scalar (e.g., :float). When the base is [:object], nested subfield types are arrays (e.g., [:float]).
Backlinks: Schema, Field Selection, Indexer