Script score query

edit

Uses a script to provide a custom score for returned documents.

The script_score query is useful if, for example, a scoring function is expensive and you only need to calculate the score of a filtered set of documents.

Example request

edit

The following script_score query assigns each returned document a score equal to the my-int field value divided by 10.

resp = client.search(
    query={
        "script_score": {
            "query": {
                "match": {
                    "message": "elasticsearch"
                }
            },
            "script": {
                "source": "doc['my-int'].value / 10 "
            }
        }
    },
)
print(resp)
response = client.search(
  body: {
    query: {
      script_score: {
        query: {
          match: {
            message: 'elasticsearch'
          }
        },
        script: {
          source: "doc['my-int'].value / 10 "
        }
      }
    }
  }
)
puts response
const response = await client.search({
  query: {
    script_score: {
      query: {
        match: {
          message: "elasticsearch",
        },
      },
      script: {
        source: "doc['my-int'].value / 10 ",
      },
    },
  },
});
console.log(response);
GET /_search
{
  "query": {
    "script_score": {
      "query": {
        "match": { "message": "elasticsearch" }
      },
      "script": {
        "source": "doc['my-int'].value / 10 "
      }
    }
  }
}

Top-level parameters for script_score

edit
query
(Required, query object) Query used to return documents.
script

(Required, script object) Script used to compute the score of documents returned by the query.

Final relevance scores from the script_score query cannot be negative. To support certain search optimizations, Lucene requires scores be positive or 0.

min_score
(Optional, float) Documents with a score lower than this floating point number are excluded from the search results.
boost
(Optional, float) Documents' scores produced by script are multiplied by boost to produce final documents' scores. Defaults to 1.0.

Notes

edit

Use relevance scores in a script

edit

Within a script, you can access the _score variable which represents the current relevance score of a document.

Use term statistics in a script

edit

Within a script, you can access the _termStats variable which provides statistical information about the terms used in the child query of the script_score query.

Predefined functions

edit

You can use any of the available painless functions in your script. You can also use the following predefined functions to customize scoring:

We suggest using these predefined functions instead of writing your own. These functions take advantage of efficiencies from Elasticsearch' internal mechanisms.

Saturation
edit

saturation(value,k) = value/(k + value)

"script" : {
    "source" : "saturation(doc['my-int'].value, 1)"
}
Sigmoid
edit

sigmoid(value, k, a) = value^a/ (k^a + value^a)

"script" : {
    "source" : "sigmoid(doc['my-int'].value, 2, 1)"
}
Random score function
edit

random_score function generates scores that are uniformly distributed from 0 up to but not including 1.

randomScore function has the following syntax: randomScore(<seed>, <fieldName>). It has a required parameter - seed as an integer value, and an optional parameter - fieldName as a string value.

"script" : {
    "source" : "randomScore(100, '_seq_no')"
}

If the fieldName parameter is omitted, the internal Lucene document ids will be used as a source of randomness. This is very efficient, but unfortunately not reproducible since documents might be renumbered by merges.

"script" : {
    "source" : "randomScore(100)"
}

Note that documents that are within the same shard and have the same value for field will get the same score, so it is usually desirable to use a field that has unique values for all documents across a shard. A good default choice might be to use the _seq_no field, whose only drawback is that scores will change if the document is updated since update operations also update the value of the _seq_no field.

Decay functions for numeric fields
edit

You can read more about decay functions here.

  • double decayNumericLinear(double origin, double scale, double offset, double decay, double docValue)
  • double decayNumericExp(double origin, double scale, double offset, double decay, double docValue)
  • double decayNumericGauss(double origin, double scale, double offset, double decay, double docValue)
"script" : {
    "source" : "decayNumericLinear(params.origin, params.scale, params.offset, params.decay, doc['dval'].value)",
    "params": { 
        "origin": 20,
        "scale": 10,
        "decay" : 0.5,
        "offset" : 0
    }
}

Using params allows to compile the script only once, even if params change.

Decay functions for geo fields
edit
  • double decayGeoLinear(String originStr, String scaleStr, String offsetStr, double decay, GeoPoint docValue)
  • double decayGeoExp(String originStr, String scaleStr, String offsetStr, double decay, GeoPoint docValue)
  • double decayGeoGauss(String originStr, String scaleStr, String offsetStr, double decay, GeoPoint docValue)
"script" : {
    "source" : "decayGeoExp(params.origin, params.scale, params.offset, params.decay, doc['location'].value)",
    "params": {
        "origin": "40, -70.12",
        "scale": "200km",
        "offset": "0km",
        "decay" : 0.2
    }
}
Decay functions for date fields
edit
  • double decayDateLinear(String originStr, String scaleStr, String offsetStr, double decay, JodaCompatibleZonedDateTime docValueDate)
  • double decayDateExp(String originStr, String scaleStr, String offsetStr, double decay, JodaCompatibleZonedDateTime docValueDate)
  • double decayDateGauss(String originStr, String scaleStr, String offsetStr, double decay, JodaCompatibleZonedDateTime docValueDate)
"script" : {
    "source" : "decayDateGauss(params.origin, params.scale, params.offset, params.decay, doc['date'].value)",
    "params": {
        "origin": "2008-01-01T01:00:00Z",
        "scale": "1h",
        "offset" : "0",
        "decay" : 0.5
    }
}

Decay functions on dates are limited to dates in the default format and default time zone. Also calculations with now are not supported.

Functions for vector fields
edit

Functions for vector fields are accessible through script_score query.

Allow expensive queries

edit

Script score queries will not be executed if search.allow_expensive_queries is set to false.

Faster alternatives

edit

The script_score query calculates the score for every matching document, or hit. There are faster alternative query types that can efficiently skip non-competitive hits:

  • If you want to boost documents on some static fields, use the rank_feature query.
  • If you want to boost documents closer to a date or geographic point, use the distance_feature query.

Transition from the function score query

edit

We recommend using the script_score query instead of function_score query for the simplicity of the script_score query.

You can implement the following functions of the function_score query using the script_score query:

script_score
edit

What you used in script_score of the Function Score query, you can copy into the Script Score query. No changes here.

weight
edit

weight function can be implemented in the Script Score query through the following script:

"script" : {
    "source" : "params.weight * _score",
    "params": {
        "weight": 2
    }
}
random_score
edit

Use randomScore function as described in random score function.

field_value_factor
edit

field_value_factor function can be easily implemented through script:

"script" : {
    "source" : "Math.log10(doc['field'].value * params.factor)",
    "params" : {
        "factor" : 5
    }
}

For checking if a document has a missing value, you can use doc['field'].size() == 0. For example, this script will use a value 1 if a document doesn’t have a field field:

"script" : {
    "source" : "Math.log10((doc['field'].size() == 0 ? 1 : doc['field'].value()) * params.factor)",
    "params" : {
        "factor" : 5
    }
}

This table lists how field_value_factor modifiers can be implemented through a script:

Modifier Implementation in Script Score

none

-

log

Math.log10(doc['f'].value)

log1p

Math.log10(doc['f'].value + 1)

log2p

Math.log10(doc['f'].value + 2)

ln

Math.log(doc['f'].value)

ln1p

Math.log(doc['f'].value + 1)

ln2p

Math.log(doc['f'].value + 2)

square

Math.pow(doc['f'].value, 2)

sqrt

Math.sqrt(doc['f'].value)

reciprocal

1.0 / doc['f'].value

decay functions
edit

The script_score query has equivalent decay functions that can be used in scripts.

Functions for vector fields

edit

During vector functions' calculation, all matched documents are linearly scanned. Thus, expect the query time grow linearly with the number of matched documents. For this reason, we recommend to limit the number of matched documents with a query parameter.

This is the list of available vector functions and vector access methods:

  1. cosineSimilarity – calculates cosine similarity
  2. dotProduct – calculates dot product
  3. l1norm – calculates L1 distance
  4. hamming – calculates Hamming distance
  5. l2norm - calculates L2 distance
  6. doc[<field>].vectorValue – returns a vector’s value as an array of floats
  7. doc[<field>].magnitude – returns a vector’s magnitude

The cosineSimilarity function is not supported for bit vectors.

The recommended way to access dense vectors is through the cosineSimilarity, dotProduct, l1norm or l2norm functions. Please note however, that you should call these functions only once per script. For example, don’t use these functions in a loop to calculate the similarity between a document vector and multiple other vectors. If you need that functionality, reimplement these functions yourself by accessing vector values directly.

Let’s create an index with a dense_vector mapping and index a couple of documents into it.

resp = client.indices.create(
    index="my-index-000001",
    mappings={
        "properties": {
            "my_dense_vector": {
                "type": "dense_vector",
                "index": False,
                "dims": 3
            },
            "my_byte_dense_vector": {
                "type": "dense_vector",
                "index": False,
                "dims": 3,
                "element_type": "byte"
            },
            "status": {
                "type": "keyword"
            }
        }
    },
)
print(resp)

resp1 = client.index(
    index="my-index-000001",
    id="1",
    document={
        "my_dense_vector": [
            0.5,
            10,
            6
        ],
        "my_byte_dense_vector": [
            0,
            10,
            6
        ],
        "status": "published"
    },
)
print(resp1)

resp2 = client.index(
    index="my-index-000001",
    id="2",
    document={
        "my_dense_vector": [
            -0.5,
            10,
            10
        ],
        "my_byte_dense_vector": [
            0,
            10,
            10
        ],
        "status": "published"
    },
)
print(resp2)

resp3 = client.indices.refresh(
    index="my-index-000001",
)
print(resp3)
const response = await client.indices.create({
  index: "my-index-000001",
  mappings: {
    properties: {
      my_dense_vector: {
        type: "dense_vector",
        index: false,
        dims: 3,
      },
      my_byte_dense_vector: {
        type: "dense_vector",
        index: false,
        dims: 3,
        element_type: "byte",
      },
      status: {
        type: "keyword",
      },
    },
  },
});
console.log(response);

const response1 = await client.index({
  index: "my-index-000001",
  id: 1,
  document: {
    my_dense_vector: [0.5, 10, 6],
    my_byte_dense_vector: [0, 10, 6],
    status: "published",
  },
});
console.log(response1);

const response2 = await client.index({
  index: "my-index-000001",
  id: 2,
  document: {
    my_dense_vector: [-0.5, 10, 10],
    my_byte_dense_vector: [0, 10, 10],
    status: "published",
  },
});
console.log(response2);

const response3 = await client.indices.refresh({
  index: "my-index-000001",
});
console.log(response3);
PUT my-index-000001
{
  "mappings": {
    "properties": {
      "my_dense_vector": {
        "type": "dense_vector",
        "index": false,
        "dims": 3
      },
      "my_byte_dense_vector": {
        "type": "dense_vector",
        "index": false,
        "dims": 3,
        "element_type": "byte"
      },
      "status" : {
        "type" : "keyword"
      }
    }
  }
}

PUT my-index-000001/_doc/1
{
  "my_dense_vector": [0.5, 10, 6],
  "my_byte_dense_vector": [0, 10, 6],
  "status" : "published"
}

PUT my-index-000001/_doc/2
{
  "my_dense_vector": [-0.5, 10, 10],
  "my_byte_dense_vector": [0, 10, 10],
  "status" : "published"
}

POST my-index-000001/_refresh
Cosine similarity
edit

The cosineSimilarity function calculates the measure of cosine similarity between a given query vector and document vectors.

resp = client.search(
    index="my-index-000001",
    query={
        "script_score": {
            "query": {
                "bool": {
                    "filter": {
                        "term": {
                            "status": "published"
                        }
                    }
                }
            },
            "script": {
                "source": "cosineSimilarity(params.query_vector, 'my_dense_vector') + 1.0",
                "params": {
                    "query_vector": [
                        4,
                        3.4,
                        -0.2
                    ]
                }
            }
        }
    },
)
print(resp)
response = client.search(
  index: 'my-index-000001',
  body: {
    query: {
      script_score: {
        query: {
          bool: {
            filter: {
              term: {
                status: 'published'
              }
            }
          }
        },
        script: {
          source: "cosineSimilarity(params.query_vector, 'my_dense_vector') + 1.0",
          params: {
            query_vector: [
              4,
              3.4,
              -0.2
            ]
          }
        }
      }
    }
  }
)
puts response
const response = await client.search({
  index: "my-index-000001",
  query: {
    script_score: {
      query: {
        bool: {
          filter: {
            term: {
              status: "published",
            },
          },
        },
      },
      script: {
        source:
          "cosineSimilarity(params.query_vector, 'my_dense_vector') + 1.0",
        params: {
          query_vector: [4, 3.4, -0.2],
        },
      },
    },
  },
});
console.log(response);
GET my-index-000001/_search
{
  "query": {
    "script_score": {
      "query" : {
        "bool" : {
          "filter" : {
            "term" : {
              "status" : "published" 
            }
          }
        }
      },
      "script": {
        "source": "cosineSimilarity(params.query_vector, 'my_dense_vector') + 1.0", 
        "params": {
          "query_vector": [4, 3.4, -0.2]  
        }
      }
    }
  }
}

To restrict the number of documents on which script score calculation is applied, provide a filter.

The script adds 1.0 to the cosine similarity to prevent the score from being negative.

To take advantage of the script optimizations, provide a query vector as a script parameter.

If a document’s dense vector field has a number of dimensions different from the query’s vector, an error will be thrown.

Dot product
edit

The dotProduct function calculates the measure of dot product between a given query vector and document vectors.

resp = client.search(
    index="my-index-000001",
    query={
        "script_score": {
            "query": {
                "bool": {
                    "filter": {
                        "term": {
                            "status": "published"
                        }
                    }
                }
            },
            "script": {
                "source": "\n          double value = dotProduct(params.query_vector, 'my_dense_vector');\n          return sigmoid(1, Math.E, -value); \n        ",
                "params": {
                    "query_vector": [
                        4,
                        3.4,
                        -0.2
                    ]
                }
            }
        }
    },
)
print(resp)
response = client.search(
  index: 'my-index-000001',
  body: {
    query: {
      script_score: {
        query: {
          bool: {
            filter: {
              term: {
                status: 'published'
              }
            }
          }
        },
        script: {
          source: "\n          double value = dotProduct(params.query_vector, 'my_dense_vector');\n          return sigmoid(1, Math.E, -value); \n        ",
          params: {
            query_vector: [
              4,
              3.4,
              -0.2
            ]
          }
        }
      }
    }
  }
)
puts response
const response = await client.search({
  index: "my-index-000001",
  query: {
    script_score: {
      query: {
        bool: {
          filter: {
            term: {
              status: "published",
            },
          },
        },
      },
      script: {
        source:
          "\n          double value = dotProduct(params.query_vector, 'my_dense_vector');\n          return sigmoid(1, Math.E, -value); \n        ",
        params: {
          query_vector: [4, 3.4, -0.2],
        },
      },
    },
  },
});
console.log(response);
GET my-index-000001/_search
{
  "query": {
    "script_score": {
      "query" : {
        "bool" : {
          "filter" : {
            "term" : {
              "status" : "published"
            }
          }
        }
      },
      "script": {
        "source": """
          double value = dotProduct(params.query_vector, 'my_dense_vector');
          return sigmoid(1, Math.E, -value); 
        """,
        "params": {
          "query_vector": [4, 3.4, -0.2]
        }
      }
    }
  }
}

Using the standard sigmoid function prevents scores from being negative.

L1 distance (Manhattan distance)
edit

The l1norm function calculates L1 distance (Manhattan distance) between a given query vector and document vectors.

resp = client.search(
    index="my-index-000001",
    query={
        "script_score": {
            "query": {
                "bool": {
                    "filter": {
                        "term": {
                            "status": "published"
                        }
                    }
                }
            },
            "script": {
                "source": "1 / (1 + l1norm(params.queryVector, 'my_dense_vector'))",
                "params": {
                    "queryVector": [
                        4,
                        3.4,
                        -0.2
                    ]
                }
            }
        }
    },
)
print(resp)
response = client.search(
  index: 'my-index-000001',
  body: {
    query: {
      script_score: {
        query: {
          bool: {
            filter: {
              term: {
                status: 'published'
              }
            }
          }
        },
        script: {
          source: "1 / (1 + l1norm(params.queryVector, 'my_dense_vector'))",
          params: {
            "queryVector": [
              4,
              3.4,
              -0.2
            ]
          }
        }
      }
    }
  }
)
puts response
const response = await client.search({
  index: "my-index-000001",
  query: {
    script_score: {
      query: {
        bool: {
          filter: {
            term: {
              status: "published",
            },
          },
        },
      },
      script: {
        source: "1 / (1 + l1norm(params.queryVector, 'my_dense_vector'))",
        params: {
          queryVector: [4, 3.4, -0.2],
        },
      },
    },
  },
});
console.log(response);
GET my-index-000001/_search
{
  "query": {
    "script_score": {
      "query" : {
        "bool" : {
          "filter" : {
            "term" : {
              "status" : "published"
            }
          }
        }
      },
      "script": {
        "source": "1 / (1 + l1norm(params.queryVector, 'my_dense_vector'))", 
        "params": {
          "queryVector": [4, 3.4, -0.2]
        }
      }
    }
  }
}

Unlike cosineSimilarity that represent similarity, l1norm and l2norm shown below represent distances or differences. This means, that the more similar the vectors are, the lower the scores will be that are produced by the l1norm and l2norm functions. Thus, as we need more similar vectors to score higher, we reversed the output from l1norm and l2norm. Also, to avoid division by 0 when a document vector matches the query exactly, we added 1 in the denominator.

Hamming distance
edit

The hamming function calculates Hamming distance between a given query vector and document vectors. It is only available for byte and bit vectors.

resp = client.search(
    index="my-index-000001",
    query={
        "script_score": {
            "query": {
                "bool": {
                    "filter": {
                        "term": {
                            "status": "published"
                        }
                    }
                }
            },
            "script": {
                "source": "(24 - hamming(params.queryVector, 'my_byte_dense_vector')) / 24",
                "params": {
                    "queryVector": [
                        4,
                        3,
                        0
                    ]
                }
            }
        }
    },
)
print(resp)
const response = await client.search({
  index: "my-index-000001",
  query: {
    script_score: {
      query: {
        bool: {
          filter: {
            term: {
              status: "published",
            },
          },
        },
      },
      script: {
        source:
          "(24 - hamming(params.queryVector, 'my_byte_dense_vector')) / 24",
        params: {
          queryVector: [4, 3, 0],
        },
      },
    },
  },
});
console.log(response);
GET my-index-000001/_search
{
  "query": {
    "script_score": {
      "query" : {
        "bool" : {
          "filter" : {
            "term" : {
              "status" : "published"
            }
          }
        }
      },
      "script": {
        "source": "(24 - hamming(params.queryVector, 'my_byte_dense_vector')) / 24", 
        "params": {
          "queryVector": [4, 3, 0]
        }
      }
    }
  }
}

Calculate the Hamming distance and normalize it by the bits to get a score between 0 and 1.

L2 distance (Euclidean distance)
edit

The l2norm function calculates L2 distance (Euclidean distance) between a given query vector and document vectors.

resp = client.search(
    index="my-index-000001",
    query={
        "script_score": {
            "query": {
                "bool": {
                    "filter": {
                        "term": {
                            "status": "published"
                        }
                    }
                }
            },
            "script": {
                "source": "1 / (1 + l2norm(params.queryVector, 'my_dense_vector'))",
                "params": {
                    "queryVector": [
                        4,
                        3.4,
                        -0.2
                    ]
                }
            }
        }
    },
)
print(resp)
response = client.search(
  index: 'my-index-000001',
  body: {
    query: {
      script_score: {
        query: {
          bool: {
            filter: {
              term: {
                status: 'published'
              }
            }
          }
        },
        script: {
          source: "1 / (1 + l2norm(params.queryVector, 'my_dense_vector'))",
          params: {
            "queryVector": [
              4,
              3.4,
              -0.2
            ]
          }
        }
      }
    }
  }
)
puts response
const response = await client.search({
  index: "my-index-000001",
  query: {
    script_score: {
      query: {
        bool: {
          filter: {
            term: {
              status: "published",
            },
          },
        },
      },
      script: {
        source: "1 / (1 + l2norm(params.queryVector, 'my_dense_vector'))",
        params: {
          queryVector: [4, 3.4, -0.2],
        },
      },
    },
  },
});
console.log(response);
GET my-index-000001/_search
{
  "query": {
    "script_score": {
      "query" : {
        "bool" : {
          "filter" : {
            "term" : {
              "status" : "published"
            }
          }
        }
      },
      "script": {
        "source": "1 / (1 + l2norm(params.queryVector, 'my_dense_vector'))",
        "params": {
          "queryVector": [4, 3.4, -0.2]
        }
      }
    }
  }
}
Checking for missing values
edit

If a document doesn’t have a value for a vector field on which a vector function is executed, an error will be thrown.

You can check if a document has a value for the field my_vector with doc['my_vector'].size() == 0. Your overall script can look like this:

"source": "doc['my_vector'].size() == 0 ? 0 : cosineSimilarity(params.queryVector, 'my_vector')"
Accessing vectors directly
edit

You can access vector values directly through the following functions:

  • doc[<field>].vectorValue – returns a vector’s value as an array of floats

For bit vectors, it does return a float[], where each element represents 8 bits.

  • doc[<field>].magnitude – returns a vector’s magnitude as a float (for vectors created prior to version 7.5 the magnitude is not stored. So this function calculates it anew every time it is called).

For bit vectors, this is just the square root of the sum of 1 bits.

For example, the script below implements a cosine similarity using these two functions:

resp = client.search(
    index="my-index-000001",
    query={
        "script_score": {
            "query": {
                "bool": {
                    "filter": {
                        "term": {
                            "status": "published"
                        }
                    }
                }
            },
            "script": {
                "source": "\n          float[] v = doc['my_dense_vector'].vectorValue;\n          float vm = doc['my_dense_vector'].magnitude;\n          float dotProduct = 0;\n          for (int i = 0; i < v.length; i++) {\n            dotProduct += v[i] * params.queryVector[i];\n          }\n          return dotProduct / (vm * (float) params.queryVectorMag);\n        ",
                "params": {
                    "queryVector": [
                        4,
                        3.4,
                        -0.2
                    ],
                    "queryVectorMag": 5.25357
                }
            }
        }
    },
)
print(resp)
response = client.search(
  index: 'my-index-000001',
  body: {
    query: {
      script_score: {
        query: {
          bool: {
            filter: {
              term: {
                status: 'published'
              }
            }
          }
        },
        script: {
          source: "\n          float[] v = doc['my_dense_vector'].vectorValue;\n          float vm = doc['my_dense_vector'].magnitude;\n          float dotProduct = 0;\n          for (int i = 0; i < v.length; i++) {\n            dotProduct += v[i] * params.queryVector[i];\n          }\n          return dotProduct / (vm * (float) params.queryVectorMag);\n        ",
          params: {
            "queryVector": [
              4,
              3.4,
              -0.2
            ],
            "queryVectorMag": 5.25357
          }
        }
      }
    }
  }
)
puts response
const response = await client.search({
  index: "my-index-000001",
  query: {
    script_score: {
      query: {
        bool: {
          filter: {
            term: {
              status: "published",
            },
          },
        },
      },
      script: {
        source:
          "\n          float[] v = doc['my_dense_vector'].vectorValue;\n          float vm = doc['my_dense_vector'].magnitude;\n          float dotProduct = 0;\n          for (int i = 0; i < v.length; i++) {\n            dotProduct += v[i] * params.queryVector[i];\n          }\n          return dotProduct / (vm * (float) params.queryVectorMag);\n        ",
        params: {
          queryVector: [4, 3.4, -0.2],
          queryVectorMag: 5.25357,
        },
      },
    },
  },
});
console.log(response);
GET my-index-000001/_search
{
  "query": {
    "script_score": {
      "query" : {
        "bool" : {
          "filter" : {
            "term" : {
              "status" : "published"
            }
          }
        }
      },
      "script": {
        "source": """
          float[] v = doc['my_dense_vector'].vectorValue;
          float vm = doc['my_dense_vector'].magnitude;
          float dotProduct = 0;
          for (int i = 0; i < v.length; i++) {
            dotProduct += v[i] * params.queryVector[i];
          }
          return dotProduct / (vm * (float) params.queryVectorMag);
        """,
        "params": {
          "queryVector": [4, 3.4, -0.2],
          "queryVectorMag": 5.25357
        }
      }
    }
  }
}
Bit vectors and vector functions
edit

When using bit vectors, not all the vector functions are available. The supported functions are:

  • hamming – calculates Hamming distance, the sum of the bitwise XOR of the two vectors
  • l1norm – calculates L1 distance, this is simply the hamming distance
  • l2norm - calculates L2 distance, this is the square root of the hamming distance
  • dotProduct – calculates dot product. When comparing two bit vectors, this is the sum of the bitwise AND of the two vectors. If providing float[] or byte[], who has dims number of elements, as a query vector, the dotProduct is the sum of the floating point values using the stored bit vector as a mask.

Here is an example of using dot-product with bit vectors.

resp = client.indices.create(
    index="my-index-bit-vectors",
    mappings={
        "properties": {
            "my_dense_vector": {
                "type": "dense_vector",
                "index": False,
                "element_type": "bit",
                "dims": 40
            }
        }
    },
)
print(resp)

resp1 = client.index(
    index="my-index-bit-vectors",
    id="1",
    document={
        "my_dense_vector": [
            8,
            5,
            -15,
            1,
            -7
        ]
    },
)
print(resp1)

resp2 = client.index(
    index="my-index-bit-vectors",
    id="2",
    document={
        "my_dense_vector": [
            -1,
            115,
            -3,
            4,
            -128
        ]
    },
)
print(resp2)

resp3 = client.index(
    index="my-index-bit-vectors",
    id="3",
    document={
        "my_dense_vector": [
            2,
            18,
            -5,
            0,
            -124
        ]
    },
)
print(resp3)

resp4 = client.indices.refresh(
    index="my-index-bit-vectors",
)
print(resp4)
const response = await client.indices.create({
  index: "my-index-bit-vectors",
  mappings: {
    properties: {
      my_dense_vector: {
        type: "dense_vector",
        index: false,
        element_type: "bit",
        dims: 40,
      },
    },
  },
});
console.log(response);

const response1 = await client.index({
  index: "my-index-bit-vectors",
  id: 1,
  document: {
    my_dense_vector: [8, 5, -15, 1, -7],
  },
});
console.log(response1);

const response2 = await client.index({
  index: "my-index-bit-vectors",
  id: 2,
  document: {
    my_dense_vector: [-1, 115, -3, 4, -128],
  },
});
console.log(response2);

const response3 = await client.index({
  index: "my-index-bit-vectors",
  id: 3,
  document: {
    my_dense_vector: [2, 18, -5, 0, -124],
  },
});
console.log(response3);

const response4 = await client.indices.refresh({
  index: "my-index-bit-vectors",
});
console.log(response4);
PUT my-index-bit-vectors
{
  "mappings": {
    "properties": {
      "my_dense_vector": {
        "type": "dense_vector",
        "index": false,
        "element_type": "bit",
        "dims": 40 
      }
    }
  }
}

PUT my-index-bit-vectors/_doc/1
{
  "my_dense_vector": [8, 5, -15, 1, -7] 
}

PUT my-index-bit-vectors/_doc/2
{
  "my_dense_vector": [-1, 115, -3, 4, -128]
}

PUT my-index-bit-vectors/_doc/3
{
  "my_dense_vector": [2, 18, -5, 0, -124]
}

POST my-index-bit-vectors/_refresh

The number of dimensions or bits for the bit vector.

This vector represents 5 bytes, or 5 * 8 = 40 bits, which equals the configured dimensions

resp = client.search(
    index="my-index-bit-vectors",
    query={
        "script_score": {
            "query": {
                "match_all": {}
            },
            "script": {
                "source": "dotProduct(params.query_vector, 'my_dense_vector')",
                "params": {
                    "query_vector": [
                        8,
                        5,
                        -15,
                        1,
                        -7
                    ]
                }
            }
        }
    },
)
print(resp)
const response = await client.search({
  index: "my-index-bit-vectors",
  query: {
    script_score: {
      query: {
        match_all: {},
      },
      script: {
        source: "dotProduct(params.query_vector, 'my_dense_vector')",
        params: {
          query_vector: [8, 5, -15, 1, -7],
        },
      },
    },
  },
});
console.log(response);
GET my-index-bit-vectors/_search
{
  "query": {
    "script_score": {
      "query" : {
        "match_all": {}
      },
      "script": {
        "source": "dotProduct(params.query_vector, 'my_dense_vector')",
        "params": {
          "query_vector": [8, 5, -15, 1, -7] 
        }
      }
    }
  }
}

This vector is 40 bits, and thus will compute a bitwise & operation with the stored vectors.

resp = client.search(
    index="my-index-bit-vectors",
    query={
        "script_score": {
            "query": {
                "match_all": {}
            },
            "script": {
                "source": "dotProduct(params.query_vector, 'my_dense_vector')",
                "params": {
                    "query_vector": [
                        0.23,
                        1.45,
                        3.67,
                        4.89,
                        -0.56,
                        2.34,
                        3.21,
                        1.78,
                        -2.45,
                        0.98,
                        -0.12,
                        3.45,
                        4.56,
                        2.78,
                        1.23,
                        0.67,
                        3.89,
                        4.12,
                        -2.34,
                        1.56,
                        0.78,
                        3.21,
                        4.12,
                        2.45,
                        -1.67,
                        0.34,
                        -3.45,
                        4.56,
                        -2.78,
                        1.23,
                        -0.67,
                        3.89,
                        -4.34,
                        2.12,
                        -1.56,
                        0.78,
                        -3.21,
                        4.45,
                        2.12,
                        1.67
                    ]
                }
            }
        }
    },
)
print(resp)
const response = await client.search({
  index: "my-index-bit-vectors",
  query: {
    script_score: {
      query: {
        match_all: {},
      },
      script: {
        source: "dotProduct(params.query_vector, 'my_dense_vector')",
        params: {
          query_vector: [
            0.23, 1.45, 3.67, 4.89, -0.56, 2.34, 3.21, 1.78, -2.45, 0.98, -0.12,
            3.45, 4.56, 2.78, 1.23, 0.67, 3.89, 4.12, -2.34, 1.56, 0.78, 3.21,
            4.12, 2.45, -1.67, 0.34, -3.45, 4.56, -2.78, 1.23, -0.67, 3.89,
            -4.34, 2.12, -1.56, 0.78, -3.21, 4.45, 2.12, 1.67,
          ],
        },
      },
    },
  },
});
console.log(response);
GET my-index-bit-vectors/_search
{
  "query": {
    "script_score": {
      "query" : {
        "match_all": {}
      },
      "script": {
        "source": "dotProduct(params.query_vector, 'my_dense_vector')",
        "params": {
          "query_vector": [0.23, 1.45, 3.67, 4.89, -0.56, 2.34, 3.21, 1.78, -2.45, 0.98, -0.12, 3.45, 4.56, 2.78, 1.23, 0.67, 3.89, 4.12, -2.34, 1.56, 0.78, 3.21, 4.12, 2.45, -1.67, 0.34, -3.45, 4.56, -2.78, 1.23, -0.67, 3.89, -4.34, 2.12, -1.56, 0.78, -3.21, 4.45, 2.12, 1.67] 
        }
      }
    }
  }
}

This vector is 40 individual dimensions, and thus will sum the floating point values using the stored bit vector as a mask.

Currently, the cosineSimilarity function is not supported for bit vectors.

Explain request

edit

Using an explain request provides an explanation of how the parts of a score were computed. The script_score query can add its own explanation by setting the explanation parameter:

resp = client.explain(
    index="my-index-000001",
    id="0",
    query={
        "script_score": {
            "query": {
                "match": {
                    "message": "elasticsearch"
                }
            },
            "script": {
                "source": "\n          long count = doc['count'].value;\n          double normalizedCount = count / 10;\n          if (explanation != null) {\n            explanation.set('normalized count = count / 10 = ' + count + ' / 10 = ' + normalizedCount);\n          }\n          return normalizedCount;\n        "
            }
        }
    },
)
print(resp)
response = client.explain(
  index: 'my-index-000001',
  id: 0,
  body: {
    query: {
      script_score: {
        query: {
          match: {
            message: 'elasticsearch'
          }
        },
        script: {
          source: "\n          long count = doc['count'].value;\n          double normalizedCount = count / 10;\n          if (explanation != nil) {\n            explanation.set('normalized count = count / 10 = ' + count + ' / 10 = ' + normalizedCount);\n          }\n          return normalizedCount;\n        "
        }
      }
    }
  }
)
puts response
const response = await client.explain({
  index: "my-index-000001",
  id: 0,
  query: {
    script_score: {
      query: {
        match: {
          message: "elasticsearch",
        },
      },
      script: {
        source:
          "\n          long count = doc['count'].value;\n          double normalizedCount = count / 10;\n          if (explanation != null) {\n            explanation.set('normalized count = count / 10 = ' + count + ' / 10 = ' + normalizedCount);\n          }\n          return normalizedCount;\n        ",
      },
    },
  },
});
console.log(response);
GET /my-index-000001/_explain/0
{
  "query": {
    "script_score": {
      "query": {
        "match": { "message": "elasticsearch" }
      },
      "script": {
        "source": """
          long count = doc['count'].value;
          double normalizedCount = count / 10;
          if (explanation != null) {
            explanation.set('normalized count = count / 10 = ' + count + ' / 10 = ' + normalizedCount);
          }
          return normalizedCount;
        """
      }
    }
  }
}

Note that the explanation will be null when using in a normal _search request, so having a conditional guard is best practice.