{"record":{"id":"97643175f7da9e27","repo":"ankane/searchkick","slug":"search-must-be-called-on-model-not-relation","errorCode":null,"errorMessage":"search must be called on model, not relation","messagePattern":"search must be called on model, not relation","errorType":"exception","errorClass":"Searchkick::Error","httpStatus":null,"severity":"error","filePath":"lib/searchkick/model.rb","lineNumber":71,"sourceCode":"          data\n        end unless base.method_defined?(:search_data)\n\n        def should_index?\n          true\n        end unless base.method_defined?(:should_index?)\n      end\n\n      class_eval do\n        cattr_reader :searchkick_options, :searchkick_klass, instance_reader: false\n\n        class_variable_set :@@searchkick_options, options.dup\n        class_variable_set :@@searchkick_klass, self\n        class_variable_set :@@searchkick_index_cache, Searchkick::IndexCache.new\n\n        class << self\n          def searchkick_search(term = \"*\", **options, &block)\n            if Searchkick.relation?(self)\n              raise Searchkick::Error, \"search must be called on model, not relation\"\n            end\n\n            Searchkick.search(term, model: self, **options, &block)\n          end\n          alias_method Searchkick.search_method_name, :searchkick_search if Searchkick.search_method_name\n\n          def searchkick_index(name: nil)\n            index_name = name || searchkick_klass.searchkick_index_name\n            index_name = index_name.call if index_name.respond_to?(:call)\n            index_cache = class_variable_get(:@@searchkick_index_cache)\n            index_cache.fetch(index_name) { Searchkick::Index.new(index_name, searchkick_options) }\n          end\n          alias_method :search_index, :searchkick_index unless method_defined?(:search_index)\n\n          def searchkick_reindex(method_name = nil, **options)\n            searchkick_index.reindex(self, method_name: method_name, **options)\n          end\n          alias_method :reindex, :searchkick_reindex unless method_defined?(:reindex)","sourceCodeStart":53,"sourceCodeEnd":89,"githubUrl":"https://github.com/ankane/searchkick/blob/93e901a75b11a25101668a616e006b158251b16e/lib/searchkick/model.rb#L53-L89","documentation":"`searchkick_search` is defined on the model's eigenclass and first checks `Searchkick.relation?(self)`, which is true when `klass.current_scope` is non-nil — i.e. when the method is invoked while an ActiveRecord scope is active (Relation#method_missing delegates `search` to the class inside `scoping { }`). Because searchkick deliberately ignores scopes when querying (it filters only via `where:`), calling search under an active scope would silently drop the scope's conditions, so it raises `Searchkick::Error` instead.","triggerScenarios":"`Product.where(active: true).search(\"milk\")`, `Product.published.search(\"milk\")`, calling `search` inside `Model.scoping { }` / another scope's body, or on a model with a `default_scope` and no `unscope: true` option. Also Mongoid criteria scopes via the Mongoid branch of `relation?`.","commonSituations":"New teams chaining searchkick onto relation chains like any other scope (`Model.where(...).limit(5).search`); models with `default_scope` where every search raises; refactoring from `Model.search` to chained query builders; Mongoid apps using criteria.","solutions":["Call search on the model class and move scope conditions into the query: `Product.search(\"milk\", where: {active: true})`.","For chained scopes, extract the scope's conditions or use `unscoped`: `Product.unscoped.search(\"milk\")` (note: this drops the scope on purpose).","If the model has a `default_scope` you never want applied to search, add `unscope: true` to `searchkick` options so searchkick unscopes internally.","For Mongoid criteria, call `.search` on the document class, not a `Criteria` object."],"exampleFix":"# before\nProduct.where(active: true).search(\"milk\")\n# => Searchkick::Error: search must be called on model, not relation\n\n# after\nProduct.search(\"milk\", where: {active: true})","handlingStrategy":"validation","validationCode":"def safe_search(klass, term = \"*\", **options)\n  raise Searchkick::Error, \"call search on #{klass}, not a relation/scope\" if Searchkick.relation?(klass)\n  klass.search(term, **options)\nend\n\n# usage: safe_search(Product, \"milk\", where: {active: true})","typeGuard":"def searchable_receiver?(klass)\n  !Searchkick.relation?(klass) # true when no scope is active on klass\nend","tryCatchPattern":null,"preventionTips":["Wrap search calls in one helper/query object so the model-vs-relation rule is enforced in one place.","Translate scopes into `where:` conditions; searchkick never applies scopes to queries by design.","Add `unscope: true` to models with a `default_scope` you don't want affecting searches."],"tags":["searchkick","activerecord","relation","scope","api-misuse"],"backgroundTag":"method-called-on-wrong-receiver","analyzedSha":"93e901a75b11a25101668a616e006b158251b16e","analyzedAt":"2026-08-21T19:06:23.767Z","schemaVersion":2},"datasetVersion":"2026-08-21T23:17:16.201Z"}