{"record":{"id":"eb8b56e79a95a04a","repo":"ankane/searchkick","slug":"no-search-context-found-for-id","errorCode":null,"errorMessage":"No search context found for id","messagePattern":"No search context found for id","errorType":"exception","errorClass":"Searchkick::MissingIndexError","httpStatus":null,"severity":"error","filePath":"lib/searchkick/query.rb","lineNumber":193,"sourceCode":"        puts \"Results\"\n        puts JSON.pretty_generate(response.to_h)\n      end\n\n      # set execute for multi search\n      @execute = Results.new(searchkick_klass, response, opts)\n    end\n\n    def retry_misspellings?(response)\n      @misspellings_below && response[\"error\"].nil? && Results.new(searchkick_klass, response).total_count < @misspellings_below\n    end\n\n    private\n\n    def handle_error(e)\n      status_code = e.message[1..3].to_i\n      if status_code == 404\n        if e.message.include?(\"No search context found for id\")\n          raise MissingIndexError, \"No search context found for id\"\n        else\n          raise MissingIndexError, \"Index missing - run #{reindex_command}\"\n        end\n      elsif status_code == 500 && (\n        e.message.include?(\"IllegalArgumentException[minimumSimilarity >= 1]\") ||\n        e.message.include?(\"No query registered for [multi_match]\") ||\n        e.message.include?(\"[match] query does not support [cutoff_frequency]\") ||\n        e.message.include?(\"No query registered for [function_score]\")\n      )\n\n        raise UnsupportedVersionError\n      elsif status_code == 400\n        if (\n          e.message.include?(\"bool query does not support [filter]\") ||\n          e.message.include?(\"[bool] filter does not support [filter]\")\n        )\n\n          raise UnsupportedVersionError","sourceCodeStart":175,"sourceCodeEnd":211,"githubUrl":"https://github.com/ankane/searchkick/blob/93e901a75b11a25101668a616e006b158251b16e/lib/searchkick/query.rb#L175-L211","documentation":"When a scrolled search returns 404 with 'No search context found for id', the scroll cursor no longer exists on the server — the keep-alive (searchkick default `1m`) expired between batches, the scroll was explicitly cleared, or the node restarted/failed over. `Query#handle_error` maps this to `Searchkick::MissingIndexError` with the message 'No search context found for id'.","triggerScenarios":"`Model.search(\"*\", scroll: \"1m\", batch_size: 1000) { |batch| ... }` where per-batch processing (e.g. slow API calls, large N+1 loading) exceeds the scroll keep-alive; resuming/serializing a scroll cursor after a long pause; scrolling during a node restart or when the search context is evicted (max_open_scroll_context limit hit).","commonSituations":"Bulk export/reindex jobs that do heavy per-batch work; parallel scroll workers sharing cursors; low `search.max_open_scroll_context` defaults causing eviction; ES node maintenance mid-scroll.","solutions":["Increase the keep-alive to comfortably exceed your worst-case per-batch time: `scroll: \"10m\"` (each batch request extends it).","Move expensive per-batch work out of the scroll loop — collect ids in the loop, process afterwards.","Rescue `Searchkick::MissingIndexError` and restart the scroll from the beginning (make the job idempotent, e.g. keyed on updated_at or a cursor table).","If eviction is the cause, raise `search.max_open_scroll_context` on the cluster or ensure scroll cursors are closed promptly."],"exampleFix":"# before\nProduct.search(\"*\", scroll: \"1m\", batch_size: 1000) do |batch|\n  HeavyExport.run(batch) # slower than 1m => MissingIndexError\nend\n\n# after\nProduct.search(\"*\", scroll: \"10m\", batch_size: 1000) do |batch|\n  ids += batch.map(&:id) # cheap loop; heavy work afterwards\nend\nHeavyExport.run_later(ids)","handlingStrategy":"retry","validationCode":"# Before long jobs, size scroll keep-alive to worst-case batch time\nper_batch_seconds = 45\nscroll_keepalive = \"#{(per_batch_seconds * 4 / 60.0).ceil}m\" # 4x headroom\nProduct.search(\"*\", scroll: scroll_keepalive, batch_size: 500) { |batch| process(batch) }","typeGuard":null,"tryCatchPattern":"def with_scroll_retry(klass, attempts: 2)\n  yield\nrescue Searchkick::MissingIndexError => e\n  raise unless e.message.include?(\"No search context found\")\n  retry if (attempts -= 1) > 0 # restart scroll from beginning; job must be idempotent\n  raise\nend\n\nwith_scroll_retry(Product) do\n  Product.search(\"*\", scroll: \"10m\", batch_size: 1000) { |b| Export.run(b) }\nend","preventionTips":["Set `scroll:` well above worst-case batch processing time (e.g. \"10m\" for heavy workloads).","Keep the scroll loop cheap: collect ids, do heavy work after it completes.","Make scroll-driven jobs idempotent (cursor column or updated_at watermark) so a restart from scratch is safe.","Monitor `search.max_open_scroll_context` on the cluster if you run many concurrent scrolls."],"tags":["searchkick","scroll","pagination","elasticsearch","timeout"],"backgroundTag":"expired-scroll-context","analyzedSha":"93e901a75b11a25101668a616e006b158251b16e","analyzedAt":"2026-08-21T19:06:23.767Z","schemaVersion":2},"datasetVersion":"2026-08-21T23:17:16.201Z"}