ankane/searchkick · error · ArgumentError

Must specify a distance for OpenSearch

Error message

Must specify a distance for OpenSearch

What it means

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.

Source

Thrown at lib/searchkick/index_options.rb:441

      (options[:locations] || []).map(&:to_s).each do |field|
        mapping[field] = {
          type: "geo_point"
        }
      end

      options[:geo_shape] = options[:geo_shape].product([{}]).to_h if options[:geo_shape].is_a?(Array)
      (options[:geo_shape] || {}).each do |field, shape_options|
        mapping[field] = shape_options.merge(type: "geo_shape")
      end

      (options[:knn] || []).each do |field, knn_options|
        distance = knn_options[:distance]
        quantization = knn_options[:quantization]

        if Searchkick.opensearch?
          if distance.nil?
            # avoid server crash if method not specified
            raise ArgumentError, "Must specify a distance for OpenSearch"
          end

          vector_options = {
            type: "knn_vector",
            dimension: knn_options[:dimensions]
          }

          if !distance.nil?
            space_type =
              case distance
              when "cosine"
                "cosinesimil"
              when "euclidean"
                "l2"
              when "inner_product"
                "innerproduct"
              else
                raise ArgumentError, "Unknown distance: #{distance}"

View on GitHub (pinned to 93e901a75b)

Solutions

  1. Add `distance:` to every knn entry: `searchkick knn: {embedding: {dimensions: 768, distance: "cosine"}}`.
  2. Use only the supported values `"cosine"`, `"euclidean"`, or `"inner_product"` (mapped to cosinesimil/l2/innerproduct for OpenSearch).
  3. If the config must be shared, branch it: `knn: {embedding: {dimensions: 768}.merge(OpenSearchClient? ? {distance: "cosine"} : {})}` or a helper that detects `Searchkick.opensearch?`.
  4. Reindex after the change so the mapping with `method: {name: "hnsw", ...}` is written.

Example fix

# before
searchkick knn: {embedding: {dimensions: 768}}
# on OpenSearch => ArgumentError: Must specify a distance for OpenSearch

# after
searchkick knn: {embedding: {dimensions: 768, distance: "cosine"}}
Defensive patterns

Strategy: validation

Validate before calling

knn = {embedding: {dimensions: 768}}
knn.each_value { |opts| opts[:distance] ||= "cosine" } # OpenSearch requires it
raise ArgumentError, "knn field missing distance (required for OpenSearch)" if Searchkick.opensearch? && knn.any? { |_, o| o[:distance].nil? }
class Product < ApplicationRecord
  searchkick knn: knn
end

Type guard

def knn_config_valid?(knn, opensearch:)
  knn.all? do |_, opts|
    opts[:dimensions].is_a?(Integer) && (!opensearch || !opts[:distance].nil?)
  end
end

Prevention

When it happens

Trigger: `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?`.

Common situations: 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.

Related errors


AI-assisted analysis of ankane/searchkick@93e901a75b (2026-08-21). Data as JSON: /api/errors/994a5f519468d934. Report an issue: GitHub.