{"record":{"id":"972722af4e46d8c7","repo":"ankane/searchkick","slug":"first-with-error-error-on-item-with-id-fi","errorCode":null,"errorMessage":"#{first_with_error[\"error\"]} on item with id '#{first_with_error[\"_id\"]}'","messagePattern":"#(.+?) on item with id '#(.+?)'","errorType":"exception","errorClass":"Searchkick::ImportError","httpStatus":null,"severity":"error","filePath":"lib/searchkick/indexer.rb","lineNumber":29,"sourceCode":"    def queue(items)\n      @queued_items.concat(items)\n      perform unless Searchkick.callbacks_value == :bulk\n    end\n\n    def perform\n      items = @queued_items\n      @queued_items = []\n\n      return if items.empty?\n\n      response = Searchkick.client.bulk(body: Searchkick.opensearch4_client? ? items.as_json : items)\n      if response[\"errors\"]\n        # note: delete does not set error when item not found\n        first_with_error = response[\"items\"].map do |item|\n          (item[\"index\"] || item[\"delete\"] || item[\"update\"])\n        end.find.with_index { |item, i| item[\"error\"] && !ignore_missing?(items[i], item[\"error\"]) }\n        if first_with_error\n          raise ImportError, \"#{first_with_error[\"error\"]} on item with id '#{first_with_error[\"_id\"]}'\"\n        end\n      end\n\n      # maybe return response in future\n      nil\n    end\n\n    private\n\n    def ignore_missing?(item, error)\n      error[\"type\"] == \"document_missing_exception\" && item.instance_variable_defined?(:@ignore_missing)\n    end\n  end\nend\n","sourceCodeStart":11,"sourceCodeEnd":44,"githubUrl":"https://github.com/ankane/searchkick/blob/93e901a75b11a25101668a616e006b158251b16e/lib/searchkick/indexer.rb#L11-L44","documentation":"The `Searchkick::Indexer` batches records into a single Elasticsearch/OpenSearch `_bulk` request. If the response reports `errors: true`, searchkick walks the per-item results, skips deletes of missing documents and items flagged `ignore_missing`, and raises `Searchkick::ImportError` with the first item's raw error and its `_id`. So this error wraps the server's own per-document failure — the message before ' on item with id' is the authoritative cause (e.g. mapper parse exceptions, version conflicts, bulk rejections).","triggerScenarios":"`Model.reindex`, `model.reindex`, `Searchkick::Index.new(...).import` or any callback-triggered bulk (create/save/destroy with inline/async callbacks) where at least one item fails: a value that doesn't fit the mapping (string into integer field), sending `searchkick_index` data as non-JSON-serializable objects, a stale mapping after changing `searchkick` options without reindexing, or ES bulk queue overflow (429 TOOMANYREQUESTS).","commonSituations":"Running `reindex` after a model/mapping change while old documents contain incompatible data (dates as strings, nil into keyword, arrays into scalar fields); data serialization bugs in `search_data`; bulk indexing faster than the cluster accepts (thread_pool.search.queue_size / bulk queue rejections); document_missing_exception on update callbacks after a record was deleted directly in the database (use `model.reindex(..., ignore_missing: true)` or `delete` instead).","solutions":["Read the error text before ' on item with id' — it names the document and the server-side reason (e.g. 'mapper [price] cannot be changed from [long] to [text]', 'version_conflict_engine_exception', 'es_rejected_execution').","Load the failing record by the reported id and fix its `search_data` output so it matches the current mapping, then re-run the import.","If the error is a mapping/parser conflict after changing `searchkick` options, run a full `Model.reindex` with the new options so the index is recreated before records import.","For update/destroy callbacks racing a direct-DB delete, pass `ignore_missing: true` to `reindex` or rescue `Searchkick::ImportError` and treat document_missing as benign.","For 429/bulk rejections, lower `batch_size` (e.g. `searchkick batch_size: 100`) or use `mode: :async`/`:queue` so retries happen in background jobs."],"exampleFix":"# before\nproduct.update!(price: \"twelve\") # reindex callback fires bulk\n# => Searchkick::ImportError: mapper [price] failed to parse field [...] on item with id '42'\n\n# after\n# fix the data / search_data to match the mapping\nproduct.update!(price: 12.0)\n\n# benign delete races:\nbegin\n  product.reindex(ignore_missing: true)\nrescue Searchkick::ImportError => e\n  raise unless e.message.include?(\"document_missing_exception\")\nend","handlingStrategy":"try-catch","validationCode":null,"typeGuard":null,"tryCatchPattern":"begin\n  Product.reindex(batch_size: 500)\nrescue Searchkick::ImportError => e\n  id = e.message[/on item with id '(.+))'/, 1] || e.message[/on item with id '(.+)'/, 1]\n  reason = e.message.split(\" on item with id\").first\n  Rails.logger.error(\"reindex failed first on id=#{id}: #{reason}\")\n  if reason.include?(\"document_missing_exception\")\n    retry # record already gone; benign\n  else\n    raise # real data/mapping problem — fix search_data for that record\n  end\nend","preventionTips":["Keep `search_data` output strictly JSON-serializable and type-stable (dates as dates, numbers as numbers).","After changing searchkick mapping options, always run a full reindex before relying on callbacks.","Pass `ignore_missing: true` for update-path reindexes that race direct-DB deletes.","Cap `batch_size` and use `mode: :async`/`:queue` for large imports to avoid bulk rejections (429)."],"tags":["searchkick","bulk-index","reindex","import","elasticsearch","opensearch"],"backgroundTag":"bulk-index-partial-failure","analyzedSha":"93e901a75b11a25101668a616e006b158251b16e","analyzedAt":"2026-08-21T19:06:23.767Z","schemaVersion":2},"datasetVersion":"2026-08-21T23:17:16.201Z"}