Skip to main content
Related: Schema, Indexer, Joins, Observability, References & JOINs — Deep dive

Cascading updates for referenced collections

Keep referencing collections consistent when a referenced document changes. This page covers discovery of references, partial vs full reindex, usage, and emitted instrumentation.

Overview

  • Discovery: Build a reverse reference graph from Typesense live schemas (preferred) using field-level reference (e.g., “authors.id”). Falls back to compiled local schemas from SearchEngine models when Typesense is unavailable.
  • Scope: Single-hop only. Immediate cycles (A ↔ B) are detected and skipped.
  • Reindex: On document update, attempt a targeted partial reindex of referencing collections when safe; otherwise perform a full reindex of each referencing collection.

Reference discovery

  • From Typesense: GET /collections → for each GET /collections/; inspect each field’s reference value.
  • Fallback: compile each registered model schema and use its reference annotations generated from belongs_to :assoc (stored as “<collection>.<foreign_key>”, or ;async variant when enabled).
Notes:
  • Only single-hop references are considered. Multi-hop is not supported.
  • Immediate cycles (e.g., A references B and B references A) are skipped to avoid ping-pong reindex.
Manual targeted rebuild (when you opt into bi‑directional joins and still need a refresh):
# Refresh referencers by foreign key (example keys)
SearchEngine::Indexer.rebuild_partition!(
  SearchEngine::Book,
  partition: { author_id: [42] }
)

Partial vs full reindex

  • Partial reindex is attempted only when both conditions hold:
    • Referencing collection uses the ActiveRecord source adapter, and
    • No custom Partitioner is configured for that collection.
  • Partial strategy: call Indexer.rebuild_partition!(Referrer, partition: { foreign_key =&gt; ids }) so the ActiveRecord source applies where(foreign_key: ids).
  • Full fallback:
    • Triggered when partial is not eligible, or raises unexpectedly.
    • Always used when context: :full.

Partitioned referencers and parallelism

  • When a referencer collection defines partitioning via the index DSL (partitions, partition_fetch), cascade full reindex will honor partition_max_parallel and process partitions concurrently, mirroring full indexation behavior. If partition_max_parallel <= 1 or there is only one partition, processing remains sequential.

Usage

  • Opt-in cascade on instance update:
record.update(name: "New Name", cascade: true)
  • Explicit API:
# After updating an Author with id "42":
SearchEngine::Cascade.cascade_reindex!(source: Author, ids: ["42"], context: :update)

# After a full collection rebuild of Authors:
SearchEngine::Cascade.cascade_reindex!(source: Author, ids: nil, context: :full)
Behavior:
  • context: :update: prefer partial reindex; fallback to full per referencer.
  • context: :full: always full reindex for all referencers.

Instrumentation

Emits search_engine.cascade.run via ActiveSupport::Notifications with keys:
  • source_collection (String)
  • ids_count (Integer)
  • context (:update | :full)
  • targets_total (Integer)
  • partial_count, full_count (Integers)
  • skipped_unregistered (Integer)
  • skipped_cycles (Array of pairs)
  • outcomes (Array): { collection, mode: :partial | :full | :skipped_unregistered | :skipped_cycle }

Caveats

  • Single-hop only; deeper paths are out of scope.
  • Partial reindex requires the ActiveRecord source and no Partitioner.
  • Cascade runs best-effort; errors are swallowed when triggered from Base#update to preserve update semantics.
Backlinks: Schema · Indexer · Joins · Observability