{"record":{"id":"f5456276dae77359","repo":"ankane/searchkick","slug":"can-t-pass-both-language-and-stemmer","errorCode":null,"errorMessage":"Can't pass both language and stemmer","messagePattern":"Can't pass both language and stemmer","errorType":"exception","errorClass":"ArgumentError","httpStatus":null,"severity":"error","filePath":"lib/searchkick/index_options.rb","lineNumber":154,"sourceCode":"            },\n            searchkick_stemmer: {\n              # use stemmer if language is lowercase, snowball otherwise\n              type: language == language.to_s.downcase ? \"stemmer\" : \"snowball\",\n              language: language || \"English\"\n            }\n          },\n          char_filter: {\n            # https://www.elastic.co/guide/en/elasticsearch/guide/current/custom-analyzers.html\n            # &_to_and\n            ampersand: {\n              type: \"mapping\",\n              mappings: [\"&=> and \"]\n            }\n          }\n        }\n      }\n\n      raise ArgumentError, \"Can't pass both language and stemmer\" if options[:stemmer] && language\n      update_language(settings, language)\n      update_stemming(settings)\n\n      if Searchkick.env == \"test\"\n        settings[:number_of_shards] = 1\n        settings[:number_of_replicas] = 0\n      end\n\n      if options[:similarity]\n        settings[:similarity] = {default: {type: options[:similarity]}}\n      end\n\n      settings[:index] = {\n        max_ngram_diff: 49,\n        max_shingle_diff: 4\n      }\n\n      if options[:knn]","sourceCodeStart":136,"sourceCodeEnd":172,"githubUrl":"https://github.com/ankane/searchkick/blob/93e901a75b11a25101668a616e006b158251b16e/lib/searchkick/index_options.rb#L136-L172","documentation":"search calls return a lazy Searchkick::Relation; once results are materialized (results, each, to_a, hits) the relation is in a loaded state. Later chainable calls (where, where_not, limit, order, page, per_page, aggs, ...) all pass through check_loaded, which raises Error 'Relation loaded' instead of silently building a divergent query from the executed one.","triggerScenarios":"r = Product.search('milk'); r.results; r.where(in_stock: true) - or any hits/results/map first, then page(2)/order(:price) chained on the same relation object.","commonSituations":"Memoizing a relation in a controller, rendering results, then trying to refine or paginate it later; view helpers that both iterate and mutate; copying ActiveRecord habits where chaining after load builds a new relation.","solutions":["Chain all conditions before reading: r = Product.search('milk').where(in_stock: true).page(2); then call r.results","Start a fresh relation for the variant query instead of reusing the loaded object","Check r.loaded? before attempting to chain more options"],"exampleFix":"# before\nr = Product.search('milk')\nr.results # loads the relation\nr.where(in_stock: true) # => Searchkick::Error: Relation loaded\n\n# after\nr = Product.search('milk').where(in_stock: true)\nr.results","handlingStrategy":"validation","validationCode":"def refine(relation)\n  raise Searchkick::Error, 'Relation loaded' if relation.loaded?\n  relation\nend\n\nr = refine(Product.search('milk')).where(in_stock: true)\nr.results","typeGuard":"def chainable?(relation)\n  !relation.loaded?\nend","tryCatchPattern":null,"preventionTips":["Build the full chain (where/order/page) before touching results or hits","Never memoize a relation you plan to mutate later - memoize its results or build a new relation","Treat loaded? relations as immutable; copy options into a fresh search for variants"],"tags":["searchkick","relation","lazy-loading","frozen-query"],"backgroundTag":"invalid-state-transition","analyzedSha":"93e901a75b11a25101668a616e006b158251b16e","analyzedAt":"2026-08-21T19:06:23.767Z","schemaVersion":2},"datasetVersion":"2026-08-21T23:17:16.201Z"}