ankane/searchkick · warning

Records in search index do not exist in database: #{missing_

Error message

Records in search index do not exist in database: #{missing_records.map { |v| "#{Array(v[:model]).map(&:model_name).sort.join("/")} #{v[:id]}" }.join(", ")}

What it means

When results load, Searchkick pairs each hit with its database record; hits whose `_id` matches no row are collected as `missing_records` (results.rb:268-278) and reported with this warning when hits are built (results.rb:311-315). It means the search index is stale relative to the database: matched documents no longer exist as rows. The missing records are excluded from results - the warning is informational, and `results.missing_records` exposes the details.

Source

Thrown at lib/searchkick/results.rb:312

                highlight = hit["highlight"].to_a.to_h { |k, v| [base_field(k), v.first] }
                options[:highlighted_fields].map { |k| base_field(k) }.each do |k|
                  result["highlighted_#{k}"] ||= (highlight[k] || result[k])
                end
              end

              result["id"] ||= result["_id"] # needed for legacy reasons
              [HashWrapper.new(result), hit]
            end
        end

       [results, missing_records]
      end
    end

    def build_hits
      @build_hits ||= begin
        if missing_records.any?
          Searchkick.warn("Records in search index do not exist in database: #{missing_records.map { |v| "#{Array(v[:model]).map(&:model_name).sort.join("/")} #{v[:id]}" }.join(", ")}")
        end
        with_hit_and_missing_records[0]
      end
    end

    def results_query(records, hits)
      records = Searchkick.scope(records)

      ids = hits.map { |hit| hit["_id"] }
      if options[:includes] || options[:model_includes]
        included_relations = []
        combine_includes(included_relations, options[:includes])
        combine_includes(included_relations, options[:model_includes][records]) if options[:model_includes]

        records = records.includes(included_relations)
      end

      if options[:scope_results]

View on GitHub (pinned to 93e901a75b)

Solutions

  1. Reindex the model to resync: `Product.reindex` (or `reindex(:async)` / `reindex(:queue)` for large sets)
  2. Avoid callback-skipping deletes: use `find_each(&:destroy)`, or run `Product.reindex` immediately after `delete_all`/SQL deletes
  3. Check the background job queue for failed Searchkick reindex/deindex jobs when callbacks are async
  4. Inspect `results.missing_records` after searches in data-sensitive flows and alert or trigger a reindex when it is non-empty

Example fix

# before
Product.where(archived: true).delete_all # skips Searchkick callbacks
Product.search("milk").results # warns: Records in search index do not exist in database

# after
Product.where(archived: true).find_each(&:destroy) # deindexes each record
# or, for bulk SQL deletes, follow with:
Product.where(archived: true).delete_all
Product.reindex
Defensive patterns

Strategy: fallback

Prevention

When it happens

Trigger: `Product.search('milk').results` where matched documents were deleted from the DB but not from the index: `delete_all` or raw SQL deletes that skip Searchkick callbacks; failed or still-queued reindex jobs with `callbacks: :async` or `:queue`; records destroyed while a scroll export was mid-flight.

Common situations: Bulk deletes via `delete_all` in data cleanups; `callbacks: false` used for import speed without a follow-up reindex; silently failing background reindex workers; long-running exports over data being mutated concurrently.

Related errors


AI-assisted analysis of ankane/searchkick@93e901a75b (2026-08-21). Data as JSON: /api/errors/a726af536c7f7874. Report an issue: GitHub.