> ## Documentation Index
> Fetch the complete documentation index at: https://nikita-shkoda.mintlify.site/llms.txt
> Use this file to discover all available pages before exploring further.

# Models

> Minimal model layer, collection registry, identity, instance helpers, nested fields, scopes, and attribute options.

Related: <a href="/projects/search-engine-for-typesense/v30/relation">Relation</a>, <a href="/projects/search-engine-for-typesense/v30/materializers">Materializers</a>, <a href="/projects/search-engine-for-typesense/v30/upsert">Upsert</a>

## Models and the Collection Registry

<strong>SearchEngine</strong> 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.

* <strong>Registry</strong>: <code>SearchEngine.register\_collection!(name, klass)</code> and <code>SearchEngine.collection\_for(name)</code>
* <strong>Base class</strong>: <code>SearchEngine::Base</code> with <code>collection</code> and <code>attribute</code> macros

### Declare a model

```ruby theme={null}
class SearchEngine::Book < SearchEngine::Base
  collection :books

  # id is reserved and is not declared via `attribute`
  attribute :title, :string
end
```

### Lookup

```ruby theme={null}
SearchEngine.collection_for(:books)
#=> SearchEngine::Book
```

#### Convenience: find by id

```ruby theme={null}
SearchEngine::Book.find("BOOK-1")
# same as:
SearchEngine::Book.find_by(id: "BOOK-1")
```

Note: <code>.find</code> is available on the model class only; it is not delegated to relations.

### Errors

* Unknown collection: raises <code>ArgumentError</code> with a helpful message
* Duplicate registration: re-registering the same mapping is a no-op; attempting to map a collection to a different class raises <code>ArgumentError</code>

### Document identity (identify\_by)

Every Typesense document must include a unique String <code>id</code>. The gem computes this
automatically per record and injects it during indexing.

<Warning>
  When <code>source</code> is not <code>:active\_record</code>, you must define
  <code>identify\_by</code>. SQL and lambda sources yield rows that do not expose
  <code>id</code>, and the mapper ignores any <code>id</code> returned from <code>map</code>.
  See <a href="/projects/search-engine-for-typesense/v30/troubleshooting">Troubleshooting</a> for
  missing id errors during indexing.
</Warning>

* Default: <code>record.id.to\_s</code>
* Override with <code>identify\_by</code>:

```ruby theme={null}
class SearchEngine::Book < SearchEngine::Base
  collection :books

  # Use a method name on the source record
  identify_by :isbn

  # or a custom proc
  # identify_by ->(r) { "#{r.publisher_id}-#{r.id}" }

  index do
    source :active_record, model: ::Book
    map do |r|
      { title: r.title } # any provided :id here will be ignored
    end
  end
end
```

Rules and behavior:

* <code>attribute :id, ...</code> is invalid and raises. Use <code>identify\_by</code>.
* Any <code>id</code> returned from <code>index -> map</code> is ignored.
* Inspect output renders <code>id</code> first regardless of whether it was declared.
* **Precedence order**: When creating/upserting documents, the id is resolved as:
  (1) explicit <code>:id</code> attribute provided, (2) <code>identify\_by</code> strategy
  result, (3) <code>record.id.to\_s</code> fallback (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 to `int64` during schema compilation.
* Hydration converts it to `Time` (uses `Time.zone` when present).
* Cannot be disabled; Typesense requires it for tracking.

### Creating documents

Use <code>Model.create(attrs)</code> to insert a single document into the backing collection and get a hydrated instance back.

```ruby theme={null}
class SearchEngine::Book < SearchEngine::Base
  collection :books
  attribute :title, :string
  identify_by :isbn
end

book = SearchEngine::Book.create(title: "The Ruby Guide", isbn: "978-1234567890")
#=> #<SearchEngine::Book @id="978-1234567890" @title="The Ruby Guide" @doc_updated_at=...>
```

Notes:

* Validates required fields (respects <code>optional:</code>). Unknown fields are rejected when <code>mapper.strict\_unknown\_keys</code> is enabled.
* Automatically sets <code>doc\_updated\_at</code> and hidden flags (`*_empty`, `*_blank`) when present in schema.
* <code>:id</code> may be provided explicitly; otherwise it is computed via <code>identify\_by</code>. If absent, Typesense may generate one.
* By default, targets the alias (logical) collection; pass <code>into: "physical\_name"</code> to override.

### Updating documents

Use <code>record.update(attrs)</code> to partially update fields of a hydrated record in Typesense.

```ruby theme={null}
book = SearchEngine::Book.find("978-1234567890")
book.update(title: "New Title", price: 19.99)
# => 1
```

Notes:

* Updates are partial; only provided fields are sent.
* Requires the record to have an <code>id</code> (hydrated or computable).
* Returns <code>1</code> on success, <code>0</code> on failure (failures are logged).
* Accepts <code>into:</code>, <code>partition:</code>, <code>timeout\_ms:</code>, and <code>cascade:</code> options.

### Mapping source data to model instances (`.from`)

Use <code>Model.from(data, mode: :instance)</code> 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 <code>index</code> DSL but does not make any Typesense API calls.

The method accepts input corresponding to your configured source type:

* <strong><code>:active\_record</code> source</strong>: Accepts an ActiveRecord instance or an Array of instances. Output preserves input shape (single instance → single instance, array → array).
* <strong><code>:sql</code> source</strong>: 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

```ruby theme={null}
class SearchEngine::Book < SearchEngine::Base
  collection :books
  attribute :title, :string
  attribute :author_id, :integer

  index do
    source :active_record, model: ::Book
    map do |r|
      { title: r.title, author_id: r.author_id }
    end
  end
end

# Single instance → single instance
book = SearchEngine::Book.from(::Book.first)
#=> #<SearchEngine::Book @id="1" @title="The Ruby Guide" @author_id=123>

# Array → array
books = SearchEngine::Book.from([::Book.first, ::Book.second])
#=> [#<SearchEngine::Book ...>, #<SearchEngine::Book ...>]
```

#### SQL source examples

```ruby theme={null}
class SearchEngine::Book < SearchEngine::Base
  collection :books
  attribute :title, :string
  attribute :author_id, :integer
  identify_by ->(row) { row['id'] }

  index do
    source :sql, sql: "SELECT id, title, author_id FROM books"
    map do |row|
      { title: row['title'], author_id: row['author_id'] }
    end
  end
end

# SQL always returns an array, even for single-row results
books = SearchEngine::Book.from("SELECT id, title, author_id FROM books WHERE id = 1")
#=> [#<SearchEngine::Book @id="1" @title="The Ruby Guide" @author_id=123>]
```

#### Hash mode

Pass <code>mode: :hash</code> to return <code>HashWithIndifferentAccess</code> documents instead of model instances. This is useful when you need raw hash data or want to avoid hydration overhead.

```ruby theme={null}
# Returns HashWithIndifferentAccess with both symbol and string keys
doc = SearchEngine::Book.from(::Book.first, mode: :hash)
#=> {"id"=>"1", :id=>"1", "title"=>"The Ruby Guide", :title=>"The Ruby Guide", ...}

# Access with either key type
doc[:title]   #=> "The Ruby Guide"
doc['title']  #=> "The Ruby Guide"
```

#### Behavior and validation

* <strong>Mapping</strong>: Uses the same <code>map</code> block and schema validation as defined in your <code>index</code> DSL.
* <strong>No Typesense calls</strong>: This method only performs local mapping and validation; it does not interact with Typesense.
* <strong>Schema validation</strong>: Validates mapped documents against the compiled schema (required fields, types, etc.).
* <strong>Shape preservation</strong>: For ActiveRecord sources, single input → single output, array input → array output. For SQL sources, output is always an array.
* <strong>Source requirement</strong>: The model must have a <code>source</code> defined in its <code>index</code> DSL (<code>:active\_record</code> or <code>:sql</code>). Models using only <code>partition\_fetch</code> without a <code>source</code> declaration will raise an error.

#### Error cases

* Missing mapper: Raises <code>SearchEngine::Errors::InvalidParams</code> if no <code>map</code> block is defined.
* Missing source: Raises <code>SearchEngine::Errors::InvalidParams</code> if no <code>source</code> is defined in the <code>index</code> DSL.
* Invalid mode: Raises <code>SearchEngine::Errors::InvalidOption</code> if <code>mode</code> is not <code>:instance</code> or <code>:hash</code>.
* Type mismatch: For ActiveRecord sources, raises <code>SearchEngine::Errors::InvalidParams</code> if input is not an instance of the configured model class.
* Unsupported source: Raises <code>SearchEngine::Errors::InvalidOption</code> for source types other than <code>:active\_record</code> or <code>:sql</code>.

Backlinks: <a href="/projects/search-engine-for-typesense/v30/indexer">Indexer</a>, <a href="/projects/search-engine-for-typesense/v30/schema">Schema</a>

### Custom instance methods

Hydrated results are instances of your model class, so any instance methods you define are available on results.

```ruby theme={null}
class SearchEngine::Publisher < SearchEngine::Base
  collection :publishers

  attribute :category_id, :integer

  index do
    # ... indexation logic ...
  end

  def category_name
    @category_name ||= ::Category.find_by(id: category_id).name
  end
end

SearchEngine::Publisher.first.category_name
# => "Technical Publishing"
```

Notes:

* Declared attributes get <code>attr\_reader</code>s automatically (e.g., <code>category\_id</code>).
* Boolean attributes automatically get a question-mark alias method (e.g., <code>available?</code> for <code>attribute :available, :boolean</code>).
* See also: <a href="/projects/search-engine-for-typesense/v30/relation">Relation</a>, <a href="/projects/search-engine-for-typesense/v30/materializers">Materializers</a>

### Instance association readers (joins)

When you declare <code>belongs\_to</code>, <code>has\_one</code>, or <code>has\_many</code> on a model, an instance method with the same name is defined to resolve referenced records:

* <code>belongs\_to :author</code> → <code>book.author</code> returns a single record or <code>nil</code>.
* <code>has\_many :orders</code> → <code>book.orders</code> returns a chainable <code>Relation</code>.

See details and examples in <a href="/projects/search-engine-for-typesense/v30/joins#instance-association-readers-ar-like-hop">Joins</a>.

### Inheritance

Attributes declared in a parent class are inherited by subclasses. A subclass may override an attribute by redeclaring it:

```ruby theme={null}
class SearchEngine::Item < SearchEngine::Base
  attribute :name, :string
end

class SearchEngine::Book < SearchEngine::Item
  attribute :name, :text # overrides only for Book
end
```

### Flow (registry lookup)

```mermaid theme={null}
flowchart LR
  A[Collection name] --> B[SearchEngine registry]
  B -->|hit| C[Model class]
  B -->|miss| D[[Error: Unregistered collection]]
  C --> E[Future: hydration from Typesense document]
```

See also: <a href="/projects/search-engine-for-typesense/v30/materializers">Materializers</a>.

### 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: <a href="/projects/search-engine-for-typesense/v30/relation">Relation</a>.

## Nested fields (object / object\[])

When you need to store nested objects:

* Declare the base attribute as <code>:object</code> (single) or <code>\[:object]</code> (array of objects).
* Prefer declaring subfields inline via the <code>nested:</code> option on <code>attribute</code>.

Rules:

* If base is <code>:object</code>, subfields compile to scalar Typesense types (e.g., <code>float</code>).
* If base is <code>\[:object]</code>, subfields compile to array Typesense types (e.g., <code>float\[]</code>).
* The schema compiler auto-enables <code>enable\_nested\_fields</code> at collection level when any object/object\[] is present.

Example (inline nested):

```ruby theme={null}
class SearchEngine::Book < SearchEngine::Base
  collection :books

  attribute :prices, [:object], nested: {
    current_price: :float,
    list_price: :float,
    discount_percent: :float,
    minimum_quantity: :integer,
    price_type: :string,
    start_date: :string,
    end_date: :string
  }
end
```

Backlinks: <a href="/projects/search-engine-for-typesense/v30/schema">Schema</a>, <a href="/projects/search-engine-for-typesense/v30/index">Home</a>

## Model scopes (ActiveRecord-like)

Define reusable, chainable scopes on your model. Scopes are evaluated against a fresh Relation (<code>all</code>) and must return a <code>SearchEngine::Relation</code>.

```ruby theme={null}
class SearchEngine::Book < SearchEngine::Base
  collection :books

  scope :published, -> { where(published: true) }
  scope :featured, -> { where(published: true, featured: true) }
  scope :by_publisher, ->(publisher_id) { where(publisher_id: publisher_id) }
end

# Usage
SearchEngine::Book
  .published
  .featured
  .by_publisher(params[:publisher_id])
  .search(params[:search_query])
```

Notes:

* Scopes are class methods created by `scope :name, -&gt;{ ... }`.
* The block is executed with <code>self</code> set to a fresh Relation; return a Relation (or <code>nil</code> to fallback to <code>all</code>).
* You can pass parameters to scopes (e.g., <code>by\_store(id)</code>).

## Per‑model default `query_by`

You can set a default <code>query\_by</code> for a collection at the model level. This value participates in the usual precedence: relation options override model default, which overrides global config (<code>SearchEngine.config.default\_query\_by</code>).

```ruby theme={null}
class SearchEngine::Book < SearchEngine::Base
  collection :books
  query_by :title, :description
  # also accepts a comma string: query_by "title, description"
end

# Effective precedence inside compile
# relation.options(query_by: ...) > model.query_by > config.default_query_by
```

Return value on write is <code>self</code> to allow macro chaining; when called with no arguments it returns the canonical comma‑separated String or <code>nil</code> if unset.

Backlinks: <a href="/projects/search-engine-for-typesense/v30/configuration">Configuration</a>, <a href="/projects/search-engine-for-typesense/v30/relation">Relation</a>

## Attribute options reference

Attributes accept additional options to influence schema and query behavior.

```ruby theme={null}
class SearchEngine::Article < SearchEngine::Base
  collection :articles

  attribute :title, :string, sort: true, optional: false, infix: true
  attribute :tags,  [:string], empty_filtering: true
  attribute :name,  :string, locale: "en"
  attribute :category, :string, facet: true
  attribute :published, :boolean
end
```

### Boolean attribute question-mark aliases

When you declare a boolean attribute, a question-mark alias method is automatically created for convenient predicate-style access:

```ruby theme={null}
class SearchEngine::Article < SearchEngine::Base
  collection :articles

  attribute :published, :boolean
  attribute :featured, :boolean
end

article = SearchEngine::Article.from_document(
  id: '1',
  published: true,
  featured: false
)

# Both forms work identically
article.published   # => true
article.published? # => true

article.featured   # => false
article.featured?  # => false
```

The question-mark alias is a simple method alias; it returns the same value as the regular reader with no additional logic. Only attributes declared with type <code>:boolean</code> (not array types like <code>\[:boolean]</code>) receive this alias.

Supported options and constraints:

* <code>locale: String</code> — only for <code>:string</code> or <code>\[:string]</code> attributes.
* <code>sort: true|false</code> — mark field sortable in schema.
* <code>optional: true|false</code> — marks field optional; enables <code>\<name>\_blank</code> flag in schema.
* <code>infix: true|false</code> — enable Typesense infix for this field in schema.
* <code>empty\_filtering: true|false</code> — only for array types (e.g., <code>\[:string]</code>); enables <code>\<name>\_empty</code> hidden field for safe empty‑array filtering.
* <code>facet: true|false</code> — mark field as facetable in schema for faceted search/filtering.
* <code>index: true|false</code> — when <code>false</code>, the field is generally 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. **Exception:** fields referenced by an embedding's <code>from:</code> list are kept in the compiled schema with Typesense-native <code>"index": false</code> and <code>"optional": true</code>, so the embedding model can read their values without keyword-indexing them.

Example (display‑only attribute that is not indexed):

```ruby theme={null}
class SearchEngine::Article < SearchEngine::Base
  collection :articles

  attribute :slug, :string
  attribute :headline, :string
  attribute :body_html, :string, index: false # stored & returned, but not indexed
end

# Articles#create/upsert may include :body_html, and hydrated results render it in console output/inspect like any other declared attribute.
```

See also how these options appear in the compiled schema and diffs in <a href="/projects/search-engine-for-typesense/v30/schema">Schema</a> (keys: <code>locale</code>, <code>sort</code>, <code>optional</code>, <code>infix</code>).

### Embedding fields (vector search)

Use the `embedding` macro to declare auto-embedding or external-embedding vector fields. The macro registers a `:vector` attribute internally and builds the Typesense `embed` block at schema compile time.

```ruby theme={null}
class SearchEngine::Product < SearchEngine::Base
  collection 'products'

  attribute :name, :string, sort: true
  attribute :description, :string

  embedding from: %i[name description]
end
```

See <a href="/projects/search-engine-for-typesense/v30/vector-search">Vector Search</a> for all `embedding` forms, name resolution rules, configuration, and query DSL.

### Declaring nested subfields with `nested`

As an alternative to inline <code>nested:</code> on <code>attribute</code>, you can declare subfields after the base attribute is defined as <code>:object</code> or <code>\[:object]</code>:

```ruby theme={null}
attribute :prices, [:object]
nested :prices,
  current_price: :float,
  price_type: :string
```

When the base is <code>:object</code>, nested subfield types are scalar (e.g., <code>:float</code>). When the base is <code>\[:object]</code>, nested subfield types are arrays (e.g., <code>\[:float]</code>).

Backlinks: <a href="/projects/search-engine-for-typesense/v30/schema">Schema</a>, <a href="/projects/search-engine-for-typesense/v30/field-selection">Field Selection</a>, <a href="/projects/search-engine-for-typesense/v30/indexer">Indexer</a>
