{"record":{"id":"994a5f519468d934","repo":"ankane/searchkick","slug":"must-specify-a-distance-for-opensearch","errorCode":null,"errorMessage":"Must specify a distance for OpenSearch","messagePattern":"Must specify a distance for OpenSearch","errorType":"exception","errorClass":"ArgumentError","httpStatus":null,"severity":"error","filePath":"lib/searchkick/index_options.rb","lineNumber":441,"sourceCode":"      (options[:locations] || []).map(&:to_s).each do |field|\n        mapping[field] = {\n          type: \"geo_point\"\n        }\n      end\n\n      options[:geo_shape] = options[:geo_shape].product([{}]).to_h if options[:geo_shape].is_a?(Array)\n      (options[:geo_shape] || {}).each do |field, shape_options|\n        mapping[field] = shape_options.merge(type: \"geo_shape\")\n      end\n\n      (options[:knn] || []).each do |field, knn_options|\n        distance = knn_options[:distance]\n        quantization = knn_options[:quantization]\n\n        if Searchkick.opensearch?\n          if distance.nil?\n            # avoid server crash if method not specified\n            raise ArgumentError, \"Must specify a distance for OpenSearch\"\n          end\n\n          vector_options = {\n            type: \"knn_vector\",\n            dimension: knn_options[:dimensions]\n          }\n\n          if !distance.nil?\n            space_type =\n              case distance\n              when \"cosine\"\n                \"cosinesimil\"\n              when \"euclidean\"\n                \"l2\"\n              when \"inner_product\"\n                \"innerproduct\"\n              else\n                raise ArgumentError, \"Unknown distance: #{distance}\"","sourceCodeStart":423,"sourceCodeEnd":459,"githubUrl":"https://github.com/ankane/searchkick/blob/93e901a75b11a25101668a616e006b158251b16e/lib/searchkick/index_options.rb#L423-L459","documentation":"For vector (kNN) search, OpenSearch requires a `method` block with a `space_type` on every `knn_vector` field; without it the server can crash. Searchkick derives `space_type` from the `distance` you pass per field, so when `Searchkick.opensearch?` is true and `knn_options[:distance]` is nil it raises `ArgumentError` at index build time as a protective measure.","triggerScenarios":"`searchkick knn: {embedding: {dimensions: 768}}` (no `distance:`) while connected to OpenSearch, then `Model.reindex` or any index creation. Works silently on Elasticsearch because the ES branch builds `similarity` only `if !distance.nil?`.","commonSituations":"Developing against Elasticsearch and deploying to OpenSearch (or flipping the `Searchkick.opensearch = true` / opensearch-version client env); copying an ES-oriented knn config from the README into an OpenSearch setup; upgrading searchkick and adding vector search without reading the OpenSearch-specific notes.","solutions":["Add `distance:` to every knn entry: `searchkick knn: {embedding: {dimensions: 768, distance: \"cosine\"}}`.","Use only the supported values `\"cosine\"`, `\"euclidean\"`, or `\"inner_product\"` (mapped to cosinesimil/l2/innerproduct for OpenSearch).","If the config must be shared, branch it: `knn: {embedding: {dimensions: 768}.merge(OpenSearchClient? ? {distance: \"cosine\"} : {})}` or a helper that detects `Searchkick.opensearch?`.","Reindex after the change so the mapping with `method: {name: \"hnsw\", ...}` is written."],"exampleFix":"# before\nsearchkick knn: {embedding: {dimensions: 768}}\n# on OpenSearch => ArgumentError: Must specify a distance for OpenSearch\n\n# after\nsearchkick knn: {embedding: {dimensions: 768, distance: \"cosine\"}}","handlingStrategy":"validation","validationCode":"knn = {embedding: {dimensions: 768}}\nknn.each_value { |opts| opts[:distance] ||= \"cosine\" } # OpenSearch requires it\nraise ArgumentError, \"knn field missing distance (required for OpenSearch)\" if Searchkick.opensearch? && knn.any? { |_, o| o[:distance].nil? }\nclass Product < ApplicationRecord\n  searchkick knn: knn\nend","typeGuard":"def knn_config_valid?(knn, opensearch:)\n  knn.all? do |_, opts|\n    opts[:dimensions].is_a?(Integer) && (!opensearch || !opts[:distance].nil?)\n  end\nend","tryCatchPattern":null,"preventionTips":["Always set `distance:` on every knn entry even on Elasticsearch — it is required the moment you point at OpenSearch.","Assert `Searchkick.opensearch?` matches your intended engine in a boot check to catch environment mismatches early.","Cover index creation with a test that runs `Model.reindex` in both engine modes when supporting both."],"tags":["searchkick","opensearch","knn","vector-search","mapping"],"backgroundTag":"missing-required-option","analyzedSha":"93e901a75b11a25101668a616e006b158251b16e","analyzedAt":"2026-08-21T19:06:23.767Z","schemaVersion":2},"datasetVersion":"2026-08-21T23:17:16.201Z"}