{"record":{"id":"c24eb0f7402eb2d1","repo":"ankane/searchkick","slug":"could-not-find-class-class-name","errorCode":null,"errorMessage":"Could not find class: #{class_name}","messagePattern":"Could not find class: #(.+?)","errorType":"exception","errorClass":"Searchkick::Error","httpStatus":null,"severity":"error","filePath":"lib/searchkick.rb","lineNumber":304,"sourceCode":"    relation =\n      if relation.respond_to?(:primary_key)\n        primary_key = relation.primary_key\n        raise Error, \"Need primary key to load records\" if !primary_key\n\n        relation.where(primary_key => ids)\n      elsif relation.respond_to?(:queryable)\n        relation.queryable.for_ids(ids)\n      end\n\n    raise Error, \"Not sure how to load records\" if !relation\n\n    relation\n  end\n\n  # public (for reindexing conversions)\n  def self.load_model(class_name, allow_child: false)\n    model = class_name.safe_constantize\n    raise Error, \"Could not find class: #{class_name}\" unless model\n    if allow_child\n      unless model.respond_to?(:searchkick_klass)\n        raise Error, \"#{class_name} is not a searchkick model\"\n      end\n    else\n      unless Searchkick.models.include?(model)\n        raise Error, \"#{class_name} is not a searchkick model\"\n      end\n    end\n    model\n  end\n\n  # private\n  def self.indexer\n    Thread.current[:searchkick_indexer] ||= Indexer.new\n  end\n\n  # private","sourceCodeStart":286,"sourceCodeEnd":322,"githubUrl":"https://github.com/ankane/searchkick/blob/93e901a75b11a25101668a616e006b158251b16e/lib/searchkick.rb#L286-L322","documentation":"On Elasticsearch 9.0.0+ exact kNN, if a field was mapped with cosine distance (via searchkick options) but the query requests a different distance, Searchkick raises ArgumentError 'distance must match searchkick options'. The guard exists to prevent incorrect distances/results seen with Elasticsearch 9.0.0-rc1 when exact script scoring uses a non-cosine metric on a cosine-mapped field.","triggerScenarios":"Model mapped with searchkick knn: {embedding: {distance: 'cosine'}}, an Elasticsearch 9.x server, and Product.search('*', knn: {field: :embedding, vector: vec, distance: 'euclidean', exact: true}).","commonSituations":"Upgrading the cluster to Elasticsearch 9 while existing code used per-query distance overrides for exact search; reusing a metric override that worked on ES 8; benchmarking different metrics with exact: true.","solutions":["Use distance 'cosine' for exact queries on cosine-mapped fields under ES 9 - drop the override or set it to 'cosine'","If another metric is truly required, change the field's distance in searchkick options and reindex so it is mapped that way","Treat staying on Elasticsearch < 9 only as a stopgap - the guard keeps firing on 9+"],"exampleFix":"# before (ES 9.x, field mapped cosine)\nProduct.search('*', knn: {field: :v, vector: vec, distance: 'euclidean', exact: true})\n# => ArgumentError: distance must match searchkick options\n\n# after\nProduct.search('*', knn: {field: :v, vector: vec, distance: 'cosine', exact: true})","handlingStrategy":"validation","validationCode":"def exact_knn(vector, field: :embedding)\n  mapped = Product.searchkick_options.dig(:knn, field)&.[](:distance)\n  on_es9 = !Searchkick.server_below?('9.0.0')\n  distance = on_es9 && mapped == 'cosine' ? 'cosine' : (mapped || 'cosine')\n  Product.search('*', knn: {field: field, vector: vector, distance: distance, exact: true})\nend","typeGuard":null,"tryCatchPattern":null,"preventionTips":["For exact search, default the metric to the field's mapped distance instead of overriding it","Track Elasticsearch major upgrades as a release step: re-run kNN integration tests against the new version","Centralize version-conditional behavior in helpers, not scattered call sites"],"tags":["searchkick","elasticsearch","knn","distance-metric","elasticsearch-9"],"backgroundTag":"config-mismatch","analyzedSha":"93e901a75b11a25101668a616e006b158251b16e","analyzedAt":"2026-08-21T19:06:23.767Z","schemaVersion":2},"datasetVersion":"2026-08-21T23:17:16.201Z"}