Skip to main content
Related: Models, Indexer, Deletion, Client

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.
Note: Formerly named SearchEngine::Syncable. It has been renamed to SearchEngine::ActiveRecordSyncable.

Quick start

class Product < ApplicationRecord
  include SearchEngine::ActiveRecordSyncable
  # Defaults: on: %i[create update destroy], collection: :books
  search_engine_syncable
end
Callbacks run synchronously (after_create/after_update/after_destroy by default); SearchEngine.config.indexer.dispatch does not affect these hooks. Use the background-job pattern below if you need async behavior.
With explicit options:
class Order < ApplicationRecord
  include SearchEngine::ActiveRecordSyncable
  search_engine_syncable collection: :orders, on: %w[create destroy]
end

Defaults and validation

  • collection: inferred from model name (tableize, e.g., Product → “books”).
    • Upserts require a SearchEngine::<Model> 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).
  • on: defaults to [:create, :update, :destroy].
    • 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):
    • after_create → upsert document
    • after_update → upsert document
    • after_destroy → delete document
  • Upserts call your SearchEngine model: SearchEngine::<Model>.upsert(record: …) using your mapping (see Indexer and Models).
  • Deletes compute the document id using the SearchEngine model’s identify_by when present, otherwise fall back to record.id.
  • Target collection resolution prefers alias → physical mapping when available (see Schema).

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 search_engine_syncable).

Instance helpers

  • record.search_engine_record → fetch the associated Typesense document as a hydrated SearchEngine::<Model> instance.
    • Uses the SearchEngine model’s identify_by to compute id; falls back to record.id when not defined.
    • Returns nil when the model mapping is unavailable or the document is missing.
doc = Book.first.search_engine_record
# => #<SearchEngine::Book ...> or nil
  • record.sync_search_engine_record → map and upsert this ActiveRecord instance to the collection.
    • Returns 1 when upserted, 0 on error; failures are logged.
Book.first.sync_search_engine_record
# => 1

Performance & Async

The default after_create/update/destroy callbacks execute synchronously (inline) within the request cycle (and often within the DB transaction).
  • 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.
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

Callbacks timing: after_* vs after_commit

  • Uses after_create/after_update/after_destroy by default for immediacy.
  • If your app requires Typesense writes only after DB commit, switch to after_commit equivalents in your app; the upsert/delete calls are safe to use there as well.

Migration from Syncable

  • Replace:
include SearchEngine::Syncable
with:
include SearchEngine::ActiveRecordSyncable
No other API changes are required; keep using search_engine_syncable.

Troubleshooting

  • “Upsert skipped: no SearchEngine model”: create a SearchEngine model and mapping for the collection; see Models and Indexer.
  • “Cannot delete without id”: ensure your SearchEngine model defines identify_by (or that the AR record’s id is usable).
  • Unknown collection behavior: the concern resolves the target via alias or logical name; override collection: when needed.

Reference