> ## 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.

# ActiveRecordSyncable

> Keep a Typesense collection in sync with your ActiveRecord model. Upserts on create/update and deletes on destroy.

Related: <a href="/projects/search-engine-for-typesense/v30/models">Models</a>, <a href="/projects/search-engine-for-typesense/v30/indexer">Indexer</a>, <a href="/projects/search-engine-for-typesense/v30/deletion">Deletion</a>, <a href="/projects/search-engine-for-typesense/v30/client">Client</a>

## ActiveRecordSyncable (ActiveRecord Concern)

Keep a Typesense collection in sync with your ActiveRecord model. When enabled, the concern upserts on create/update and deletes on destroy so your Typesense documents mirror your database rows.

<Info>
  Note: Formerly named <code>SearchEngine::Syncable</code>. It has been renamed to <code>SearchEngine::ActiveRecordSyncable</code>.
</Info>

### Quick start

```ruby theme={null}
class Product < ApplicationRecord
  include SearchEngine::ActiveRecordSyncable
  # Defaults: on: %i[create update destroy], collection: :books
  search_engine_syncable
end
```

<Info>
  Callbacks run synchronously using `after_create_commit`/`after_update_commit`/`after_destroy_commit` by default (post-transaction). `SearchEngine.config.indexer.dispatch` does **not** affect these hooks. Use the background-job pattern below if you need async behavior.
</Info>

With explicit options:

```ruby theme={null}
class Order < ApplicationRecord
  include SearchEngine::ActiveRecordSyncable
  search_engine_syncable collection: :orders, on: %w[create destroy]
end
```

### Defaults and validation

* <strong>collection</strong>: inferred from model name (<code>tableize</code>, e.g., <code>Product</code> → "books").
  * Upserts require a <code>SearchEngine::\<Model></code> mapping for that logical collection.
  * If the mapping is missing at boot, a warning is logged and the concern will attempt lazy resolution later. When still unavailable at upsert time, it logs and skips upsert (no exception).
* <strong>on</strong>: defaults to <code>\[:create, :update, :destroy]</code>.
  * Accepts a single symbol/string or an array; values are normalized case‑insensitively.

### What the concern does

* Registers callbacks on your AR model (installed once per class reload):
  * <code>after\_create\_commit</code> → upsert document (post-transaction)
  * <code>after\_update\_commit</code> → upsert document (post-transaction)
  * <code>after\_destroy\_commit</code> → delete document (post-transaction)
* Upserts call your SearchEngine model: <code>SearchEngine::\<Model>.upsert(record: ...)</code> using your mapping (see <a href="/projects/search-engine-for-typesense/v30/indexer">Indexer</a> and <a href="/projects/search-engine-for-typesense/v30/models">Models</a>).
* Deletes compute the document id using the SearchEngine model’s <code>identify\_by</code> when present, otherwise fall back to <code>record.id</code>.
* Target collection resolution prefers alias → physical mapping when available (see <a href="/projects/search-engine-for-typesense/v30/schema">Schema</a>).

```mermaid theme={null}
sequenceDiagram
  participant AR as ActiveRecord
  participant SE as SearchEngine::<Model>
  participant TS as Typesense

  AR->>AR: after_create_commit / after_update_commit
  AR->>SE: upsert(record)
  SE->>TS: POST /collections/:into/documents/import (action=upsert)

  AR->>AR: after_destroy_commit
  AR->>TS: DELETE /collections/:into/documents/:id
```

### Lazy resolution & error handling

* Mapping resolution is best‑effort at boot; if unavailable, a one‑time warning is logged. The concern resolves the SearchEngine model lazily on first use.
* Upsert/delete errors are logged and swallowed to avoid interrupting AR lifecycle callbacks (see logs under <code>search\_engine\_syncable</code>).

### Instance helpers

* <code>record.search\_engine\_record</code> → fetch the associated Typesense document as a hydrated <code>SearchEngine::\<Model></code> instance.
  * Uses the SearchEngine model’s <code>identify\_by</code> to compute id; falls back to <code>record.id</code> when not defined.
  * Returns <code>nil</code> when the model mapping is unavailable or the document is missing.

```ruby theme={null}
doc = Book.first.search_engine_record
# => #<SearchEngine::Book ...> or nil
```

* <code>record.sync\_search\_engine\_record</code> → map and upsert this ActiveRecord instance to the collection.
  * Returns <code>1</code> when upserted, <code>0</code> on error; failures are logged.

```ruby theme={null}
Book.first.sync_search_engine_record
# => 1
```

### Performance & Async

The default `after_*_commit` callbacks execute **synchronously** but only after the database transaction commits.

* **Configuration Ignored**: The `SearchEngine.config.indexer.dispatch` setting (`:active_job` etc.) **does not apply** here. That setting controls bulk rebuilds only.
* **For High Throughput**: If you need background processing to avoid blocking user requests, disable the concern's auto-sync and manually enqueue a job.

```ruby theme={null}
class Product < ApplicationRecord
  # Disable auto-sync
  include SearchEngine::ActiveRecordSyncable
  search_engine_syncable on: []

  after_commit :enqueue_search_update, on: %i[create update destroy]

  def enqueue_search_update
    SearchIndexJob.perform_later(self.class.name, id)
  end
end

# app/jobs/search_index_job.rb
class SearchIndexJob < ApplicationJob
  def perform(class_name, id)
    record = class_name.constantize.find_by(id: id)
    return unless record

    # Manual sync
    record.sync_search_engine_record
  end
end
```

### Callback timing

By default, `ActiveRecordSyncable` uses `after_create_commit` / `after_update_commit` / `after_destroy_commit` callbacks, which fire after the database transaction commits. This is safer — it avoids syncing documents for records that may be rolled back.

To restore the legacy in-transaction behavior (`after_create` / `after_update` / `after_destroy`):

```ruby theme={null}
SearchEngine.configure do |c|
  c.syncable_callback_timing = :after_save
end
```

### Migration from Syncable

* Replace:

```ruby theme={null}
include SearchEngine::Syncable
```

with:

```ruby theme={null}
include SearchEngine::ActiveRecordSyncable
```

No other API changes are required; keep using <code>search\_engine\_syncable</code>.

### Troubleshooting

* "Upsert skipped: no SearchEngine model": create a SearchEngine model and mapping for the collection; see <a href="/projects/search-engine-for-typesense/v30/models">Models</a> and <a href="/projects/search-engine-for-typesense/v30/indexer">Indexer</a>.
* "Cannot delete without id": ensure your SearchEngine model defines <code>identify\_by</code> (or that the AR record’s <code>id</code> is usable).
* Unknown collection behavior: the concern resolves the target via alias or logical name; override <code>collection:</code> when needed.

### Reference

* Upserting helpers: <a href="/projects/search-engine-for-typesense/v30/upsert">Upsert</a>
* Deletion helpers: <a href="/projects/search-engine-for-typesense/v30/deletion">Deletion</a>
* Client wrapper: <a href="/projects/search-engine-for-typesense/v30/client">Client</a>
