浏览代码

Add support for indexing byte-sized knn vectors (#90774)

This change adds an element_type as an optional mapping parameter for dense vector fields as 
described in #89784. This also adds a byte element_type for dense vector fields that supports storing 
dense vectors using only 8-bits per dimension. This is only supported when the mapping parameter 
index is set to true.

The code follows a similar pattern to our NumberFieldMapper where we have an enum for 
ElementType, and it has methods that DenseVectorFieldType and DenseVectorMapper can delegate to 
to support each available type (just float and byte for now).
Jack Conradson 3 年之前
父节点
当前提交
f28ae4b288

+ 5 - 0
docs/changelog/90774.yaml

@@ -0,0 +1,5 @@
+pr: 90774
+summary: Add support for indexing byte-sized knn vectors
+area: Vector Search
+type: feature
+issues: []

+ 28 - 5
docs/reference/mapping/types/dense-vector.asciidoc

@@ -5,7 +5,7 @@
 <titleabbrev>Dense vector</titleabbrev>
 ++++
 
-The `dense_vector` field type stores dense vectors of float values. Dense
+The `dense_vector` field type stores dense vectors of numeric values. Dense
 vector fields can be used in the following ways:
 
 * In <<query-dsl-script-score-query,`script_score`>> queries, to score
@@ -14,8 +14,12 @@ documents matching a filter
 vectors to a query vector
 
 The `dense_vector` type does not support aggregations or sorting.
+When <<dense-vector-params, `element_type`>> is `byte`
+<<query-dsl-script-score-query,`script_score`>> is not supported.
 
-You add a `dense_vector` field as an array of floats:
+You add a `dense_vector` field as an array of numeric values
+based on <<dense-vector-params, `element_type`>> with
+`float` by default:
 
 [source,console]
 --------------------------------------------------
@@ -104,6 +108,16 @@ Dense vector fields cannot be indexed if they are within
 
 The following mapping parameters are accepted:
 
+`element_type`::
+(Optional, string)
+The data type used to encode vectors. The supported data types are
+`float` (default) and `byte`. `float` indexes a 4-byte floating-point
+value per dimension. `byte` indexes a 1-byte integer value per dimension.
+`byte` requires `index` to be `true`. Using `byte` can result in a
+substantially smaller index size with the trade off of lower
+precision. Vectors using `byte` require dimensions with integer values
+between -128 to 127, inclusive for both indexing and searching.
+
 `dims`::
 (Required, integer)
 Number of vector dimensions. Can't exceed `1024` for indexed vectors
@@ -134,9 +148,18 @@ distance) between the vectors. The document `_score` is computed as
 
 `dot_product`:::
 Computes the dot product of two vectors. This option provides an optimized way
-to perform cosine similarity. In order to use it, all vectors must be of unit
-length, including both document and query vectors. The document `_score` is
-computed as `(1 + dot_product(query, vector)) / 2`.
+to perform cosine similarity. The constraints and computed score are defined
+by `element_type`.
++
+When `element_type` is `float`, all vectors must be unit length, including both
+document and query vectors. The document `_score` is computed as
+`(1 + dot_product(query, vector)) / 2`.
++
+When `element_type` is `byte`, all vectors must have the same
+length including both document and query vectors or results will be inaccurate.
+The document `_score` is computed as
+`0.5 + (dot_product(query, vector) / (32768 * dims))`
+where `dims` is the number of dimensions per vector.
 
 `cosine`:::
 Computes the cosine similarity. Note that the most efficient way to perform

+ 70 - 0
docs/reference/search/search-your-data/knn-search.asciidoc

@@ -248,6 +248,76 @@ search has a higher probability of finding the true `k` top nearest neighbors.
 Similarly, you can decrease `num_candidates` for faster searches with
 potentially less accurate results.
 
+[discrete]
+[[approximate-knn-using-byte-vectors]]
+==== Approximate kNN using byte vectors
+
+The approximate kNN search API supports `byte` value vectors in
+addition to `float` value vectors. Use the <<search-api-knn, `knn` option>>
+to search a `dense_vector` field with <<dense-vector-params, `element_type`>> set to
+`byte` and indexing enabled.
+
+. Explicitly map one or more `dense_vector` fields with
+<<dense-vector-params, `element_type`>> set to `byte` and indexing enabled.
++
+[source,console]
+----
+PUT byte-image-index
+{
+  "mappings": {
+    "properties": {
+      "byte-image-vector": {
+        "type": "dense_vector",
+        "element_type": "byte",
+        "dims": 2,
+        "index": true,
+        "similarity": "cosine"
+      },
+      "title": {
+        "type": "text"
+      }
+    }
+  }
+}
+----
+// TEST[continued]
+
+. Index your data ensuring all vector values
+are integers within the range [-128, 127].
++
+[source,console]
+----
+POST byte-image-index/_bulk?refresh=true
+{ "index": { "_id": "1" } }
+{ "byte-image-vector": [5, -20], "title": "moose family" }
+{ "index": { "_id": "2" } }
+{ "byte-image-vector": [8, -15], "title": "alpine lake" }
+{ "index": { "_id": "3" } }
+{ "byte-image-vector": [11, 23], "title": "full moon" }
+----
+//TEST[continued]
+
+. Run the search using the <<search-api-knn, `knn` option>>
+ensuring the `query_vector` values are integers within the
+range [-128, 127].
++
+[source,console]
+----
+POST byte-image-index/_search
+{
+  "knn": {
+    "field": "byte-image-vector",
+    "query_vector": [-5, 9],
+    "k": 10,
+    "num_candidates": 100
+  },
+  "fields": [ "title" ]
+}
+----
+// TEST[continued]
+// TEST[s/"k": 10/"k": 3/]
+// TEST[s/"num_candidates": 100/"num_candidates": 3/]
+
 [discrete]
 [[knn-search-filter-example]]
 ==== Filtered kNN search

+ 180 - 0
rest-api-spec/src/yamlRestTest/resources/rest-api-spec/test/search.vectors/45_knn_search_byte.yml

@@ -0,0 +1,180 @@
+setup:
+  - skip:
+      version: ' - 8.5.99'
+      reason: 'byte-sized kNN search added in 8.6'
+
+  - do:
+      indices.create:
+        index: test
+        body:
+          settings:
+            number_of_replicas: 0
+          mappings:
+            properties:
+              name:
+                type: keyword
+              vector:
+                type: dense_vector
+                element_type: byte
+                dims: 5
+                index: true
+                similarity: cosine
+
+  - do:
+      index:
+        index: test
+        id: "1"
+        body:
+          name: cow.jpg
+          vector: [2, -1, 1, 4, -3]
+
+  - do:
+      index:
+        index: test
+        id: "2"
+        body:
+          name: moose.jpg
+          vector: [127.0, -128.0, 0.0, 1.0, -1.0]
+
+  - do:
+      index:
+        index: test
+        id: "3"
+        body:
+          name: rabbit.jpg
+          vector: [5, 4.0, 3, 2.0, 127]
+
+  - do:
+      indices.refresh: {}
+
+---
+"kNN search only":
+  - do:
+      search:
+        index: test
+        body:
+          fields: [ "name" ]
+          knn:
+            field: vector
+            query_vector: [127, 127, -128, -128, 127]
+            k: 2
+            num_candidates: 3
+
+  - match: {hits.hits.0._id: "3"}
+  - match: {hits.hits.0.fields.name.0: "rabbit.jpg"}
+
+  - match: {hits.hits.1._id: "2"}
+  - match: {hits.hits.1.fields.name.0: "moose.jpg"}
+
+---
+"kNN search plus query":
+  - do:
+      search:
+        index: test
+        body:
+          fields: [ "name" ]
+          knn:
+            field: vector
+            query_vector: [127.0, -128.0, 0.0, 1.0, -1.0]
+            k: 2
+            num_candidates: 3
+          query:
+            term:
+              name: rabbit.jpg
+
+  - match: {hits.hits.0._id: "2"}
+  - match: {hits.hits.0.fields.name.0: "moose.jpg"}
+
+  - match: {hits.hits.1._id: "3"}
+  - match: {hits.hits.1.fields.name.0: "rabbit.jpg"}
+
+  - match: {hits.hits.2._id: "1"}
+  - match: {hits.hits.2.fields.name.0: "cow.jpg"}
+
+---
+"kNN search with filter":
+  - do:
+      search:
+        index: test
+        body:
+          fields: [ "name" ]
+          knn:
+            field: vector
+            query_vector: [5.0, 4, 3.0, 2, 127.0]
+            k: 2
+            num_candidates: 3
+
+            filter:
+              term:
+                name: "rabbit.jpg"
+
+  - match: {hits.total.value: 1}
+  - match: {hits.hits.0._id: "3"}
+  - match: {hits.hits.0.fields.name.0: "rabbit.jpg"}
+
+  - do:
+      search:
+        index: test
+        body:
+          fields: [ "name" ]
+          knn:
+            field: vector
+            query_vector: [2, -1, 1, 4, -3]
+            k: 2
+            num_candidates: 3
+            filter:
+              - term:
+                  name: "rabbit.jpg"
+              - term:
+                  _id: 2
+
+  - match: {hits.total.value: 0}
+
+---
+"kNN search with explicit search_type":
+  - do:
+      catch: bad_request
+      search:
+        index: test
+        search_type: query_then_fetch
+        body:
+          fields: [ "name" ]
+          knn:
+            field: vector
+            query_vector: [-0.5, 90.0, -10, 14.8, -156.0]
+            k: 2
+            num_candidates: 3
+
+  - match: { error.root_cause.0.type: "illegal_argument_exception" }
+  - match: { error.root_cause.0.reason: "cannot set [search_type] when using [knn] search, since the search type is determined automatically" }
+
+---
+"Test nonexistent field":
+  - do:
+      catch: bad_request
+      search:
+        index: test
+        body:
+          fields: [ "name" ]
+          knn:
+            field: nonexistent
+            query_vector: [ 1, 0, 0, 0, -1 ]
+            k: 2
+            num_candidates: 3
+  - match: { error.root_cause.0.type: "query_shard_exception" }
+  - match: { error.root_cause.0.reason: "failed to create query: field [nonexistent] does not exist in the mapping" }
+
+---
+"Direct kNN queries are disallowed":
+  - do:
+      catch: bad_request
+      search:
+        index: test
+        body:
+          query:
+            knn:
+              field: vector
+              query_vector: [ -1, 0, 1, 2, 3 ]
+              num_candidates: 1
+  - match: { error.root_cause.0.type: "illegal_argument_exception" }
+  - match: { error.root_cause.0.reason: "[knn] queries cannot be provided directly, use the [knn] body parameter instead" }

+ 249 - 37
server/src/main/java/org/elasticsearch/index/mapper/vectors/DenseVectorFieldMapper.java

@@ -21,6 +21,7 @@ import org.apache.lucene.search.FieldExistsQuery;
 import org.apache.lucene.search.KnnVectorQuery;
 import org.apache.lucene.search.Query;
 import org.apache.lucene.util.BytesRef;
+import org.apache.lucene.util.VectorUtil;
 import org.elasticsearch.Version;
 import org.elasticsearch.common.xcontent.support.XContentMapValues;
 import org.elasticsearch.index.fielddata.FieldDataContext;
@@ -60,13 +61,15 @@ public class DenseVectorFieldMapper extends FieldMapper {
 
     public static final String CONTENT_TYPE = "dense_vector";
     public static short MAX_DIMS_COUNT = 2048; // maximum allowed number of dimensions
-    private static final byte INT_BYTES = 4;
+    private static final int MAGNITUDE_BYTES = 4;
 
     private static DenseVectorFieldMapper toType(FieldMapper in) {
         return (DenseVectorFieldMapper) in;
     }
 
     public static class Builder extends FieldMapper.Builder {
+
+        private final Parameter<ElementType> elementType;
         private final Parameter<Integer> dims = new Parameter<>(
             "dims",
             false,
@@ -91,7 +94,6 @@ public class DenseVectorFieldMapper extends FieldMapper {
                 );
             }
         });
-
         private final Parameter<Boolean> indexed = Parameter.indexParam(m -> toType(m).indexed, false);
         private final Parameter<VectorSimilarity> similarity = Parameter.enumParam(
             "similarity",
@@ -122,11 +124,25 @@ public class DenseVectorFieldMapper extends FieldMapper {
             this.similarity.requiresParameter(indexed);
             this.indexOptions.requiresParameter(indexed);
             this.indexOptions.setSerializerCheck((id, ic, v) -> v != null);
+
+            this.elementType = new Parameter<>("element_type", false, () -> ElementType.FLOAT, (n, c, o) -> {
+                ElementType elementType = namesToElementType.get((String) o);
+                if (elementType == null) {
+                    throw new MapperParsingException(
+                        "invalid element_type [" + o + "]; available types are " + namesToElementType.keySet()
+                    );
+                }
+                return elementType;
+            }, m -> toType(m).elementType, XContentBuilder::field, Objects::toString).addValidator(e -> {
+                if (e == ElementType.BYTE && indexed.getValue() == false) {
+                    throw new IllegalArgumentException("index must be [true] when element_type is [" + e + "]");
+                }
+            });
         }
 
         @Override
         protected Parameter<?>[] getParameters() {
-            return new Parameter<?>[] { dims, indexed, similarity, indexOptions, meta };
+            return new Parameter<?>[] { elementType, dims, indexed, similarity, indexOptions, meta };
         }
 
         @Override
@@ -136,11 +152,13 @@ public class DenseVectorFieldMapper extends FieldMapper {
                 new DenseVectorFieldType(
                     context.buildFullName(name),
                     indexVersionCreated,
+                    elementType.getValue(),
                     dims.getValue(),
                     indexed.getValue(),
                     similarity.getValue(),
                     meta.getValue()
                 ),
+                elementType.getValue(),
                 dims.getValue(),
                 indexed.getValue(),
                 similarity.getValue(),
@@ -152,6 +170,213 @@ public class DenseVectorFieldMapper extends FieldMapper {
         }
     }
 
+    enum ElementType {
+
+        BYTE(1) {
+
+            @Override
+            public String toString() {
+                return "byte";
+            }
+
+            @Override
+            KnnVectorField createKnnVectorField(String name, float[] vector, VectorSimilarityFunction function) {
+                return new KnnVectorField(name, VectorUtil.toBytesRef(vector), function);
+            }
+
+            @Override
+            IndexFieldData.Builder fielddataBuilder(DenseVectorFieldType denseVectorFieldType, FieldDataContext fieldDataContext) {
+                throw new IllegalArgumentException(
+                    "Fielddata is not supported on field ["
+                        + name()
+                        + "] of type ["
+                        + denseVectorFieldType.typeName()
+                        + "] "
+                        + "with element_type ["
+                        + this
+                        + "]"
+                );
+            }
+
+            @Override
+            void checkVectorBounds(float[] vector) {
+                checkNanAndInfinite(vector);
+
+                StringBuilder errorBuilder = null;
+
+                for (int index = 0; index < vector.length; ++index) {
+                    float value = vector[index];
+
+                    if (value % 1.0f != 0.0f) {
+                        errorBuilder = new StringBuilder(
+                            "element_type ["
+                                + this
+                                + "] vectors only support non-decimal values but found decimal value ["
+                                + value
+                                + "] at dim ["
+                                + index
+                                + "];"
+                        );
+                        break;
+                    }
+
+                    if (value < Byte.MIN_VALUE || value > Byte.MAX_VALUE) {
+                        errorBuilder = new StringBuilder(
+                            "element_type ["
+                                + this
+                                + "] vectors only support integers between ["
+                                + Byte.MIN_VALUE
+                                + ", "
+                                + Byte.MAX_VALUE
+                                + "] but found ["
+                                + value
+                                + "] at dim ["
+                                + index
+                                + "];"
+                        );
+                        break;
+                    }
+                }
+
+                if (errorBuilder != null) {
+                    throw new IllegalArgumentException(appendErrorElements(errorBuilder, vector).toString());
+                }
+            }
+
+            @Override
+            void checkVectorMagnitude(VectorSimilarity similarity, float[] vector, float squaredMagnitude) {
+                StringBuilder errorBuilder = null;
+
+                if (similarity == VectorSimilarity.cosine && Math.sqrt(squaredMagnitude) == 0.0f) {
+                    errorBuilder = new StringBuilder(
+                        "The [" + VectorSimilarity.cosine.name() + "] similarity does not support vectors with zero magnitude."
+                    );
+                }
+
+                if (errorBuilder != null) {
+                    throw new IllegalArgumentException(appendErrorElements(errorBuilder, vector).toString());
+                }
+            }
+        },
+
+        FLOAT(4) {
+
+            @Override
+            public String toString() {
+                return "float";
+            }
+
+            @Override
+            KnnVectorField createKnnVectorField(String name, float[] vector, VectorSimilarityFunction function) {
+                return new KnnVectorField(name, vector, function);
+            }
+
+            @Override
+            IndexFieldData.Builder fielddataBuilder(DenseVectorFieldType denseVectorFieldType, FieldDataContext fieldDataContext) {
+                return new VectorIndexFieldData.Builder(
+                    denseVectorFieldType.name(),
+                    CoreValuesSourceType.KEYWORD,
+                    denseVectorFieldType.indexVersionCreated,
+                    denseVectorFieldType.dims,
+                    denseVectorFieldType.indexed
+                );
+            }
+
+            @Override
+            void checkVectorBounds(float[] vector) {
+                checkNanAndInfinite(vector);
+            }
+
+            @Override
+            void checkVectorMagnitude(VectorSimilarity similarity, float[] vector, float squaredMagnitude) {
+                StringBuilder errorBuilder = null;
+
+                if (similarity == VectorSimilarity.dot_product && Math.abs(squaredMagnitude - 1.0f) > 1e-4f) {
+                    errorBuilder = new StringBuilder(
+                        "The [" + VectorSimilarity.dot_product.name() + "] similarity can only be used with unit-length vectors."
+                    );
+                } else if (similarity == VectorSimilarity.cosine && Math.sqrt(squaredMagnitude) == 0.0f) {
+                    errorBuilder = new StringBuilder(
+                        "The [" + VectorSimilarity.cosine.name() + "] similarity does not support vectors with zero magnitude."
+                    );
+                }
+
+                if (errorBuilder != null) {
+                    throw new IllegalArgumentException(appendErrorElements(errorBuilder, vector).toString());
+                }
+            }
+        };
+
+        final int elementBytes;
+
+        ElementType(int elementBytes) {
+            this.elementBytes = elementBytes;
+        }
+
+        abstract KnnVectorField createKnnVectorField(String name, float[] vector, VectorSimilarityFunction function);
+
+        abstract IndexFieldData.Builder fielddataBuilder(DenseVectorFieldType denseVectorFieldType, FieldDataContext fieldDataContext);
+
+        abstract void checkVectorBounds(float[] vector);
+
+        abstract void checkVectorMagnitude(VectorSimilarity similarity, float[] vector, float squaredMagnitude);
+
+        void checkNanAndInfinite(float[] vector) {
+            StringBuilder errorBuilder = null;
+
+            for (int index = 0; index < vector.length; ++index) {
+                float value = vector[index];
+
+                if (Float.isNaN(value)) {
+                    errorBuilder = new StringBuilder(
+                        "element_type [" + this + "] vectors do not support NaN values but found [" + value + "] at dim [" + index + "];"
+                    );
+                    break;
+                }
+
+                if (Float.isInfinite(value)) {
+                    errorBuilder = new StringBuilder(
+                        "element_type ["
+                            + this
+                            + "] vectors do not support infinite values but found ["
+                            + value
+                            + "] at dim ["
+                            + index
+                            + "];"
+                    );
+                    break;
+                }
+            }
+
+            if (errorBuilder != null) {
+                throw new IllegalArgumentException(appendErrorElements(errorBuilder, vector).toString());
+            }
+        }
+
+        StringBuilder appendErrorElements(StringBuilder errorBuilder, float[] vector) {
+            // Include the first five elements of the invalid vector in the error message
+            errorBuilder.append(" Preview of invalid vector: [");
+            for (int i = 0; i < Math.min(5, vector.length); i++) {
+                if (i > 0) {
+                    errorBuilder.append(", ");
+                }
+                errorBuilder.append(vector[i]);
+            }
+            if (vector.length >= 5) {
+                errorBuilder.append(", ...");
+            }
+            errorBuilder.append("]");
+            return errorBuilder;
+        }
+    }
+
+    static final Map<String, ElementType> namesToElementType = Map.of(
+        ElementType.BYTE.toString(),
+        ElementType.BYTE,
+        ElementType.FLOAT.toString(),
+        ElementType.FLOAT
+    );
+
     enum VectorSimilarity {
         l2_norm(VectorSimilarityFunction.EUCLIDEAN),
         cosine(VectorSimilarityFunction.COSINE),
@@ -232,6 +457,7 @@ public class DenseVectorFieldMapper extends FieldMapper {
     );
 
     public static final class DenseVectorFieldType extends SimpleMappedFieldType {
+        private final ElementType elementType;
         private final int dims;
         private final boolean indexed;
         private final VectorSimilarity similarity;
@@ -240,12 +466,14 @@ public class DenseVectorFieldMapper extends FieldMapper {
         public DenseVectorFieldType(
             String name,
             Version indexVersionCreated,
+            ElementType elementType,
             int dims,
             boolean indexed,
             VectorSimilarity similarity,
             Map<String, String> meta
         ) {
             super(name, indexed, false, indexed == false, TextSearchInfo.NONE, meta);
+            this.elementType = elementType;
             this.dims = dims;
             this.indexed = indexed;
             this.similarity = similarity;
@@ -284,7 +512,7 @@ public class DenseVectorFieldMapper extends FieldMapper {
 
         @Override
         public IndexFieldData.Builder fielddataBuilder(FieldDataContext fieldDataContext) {
-            return new VectorIndexFieldData.Builder(name(), CoreValuesSourceType.KEYWORD, indexVersionCreated, dims, indexed);
+            return elementType.fielddataBuilder(this, fieldDataContext);
         }
 
         @Override
@@ -310,46 +538,21 @@ public class DenseVectorFieldMapper extends FieldMapper {
                 );
             }
 
+            elementType.checkVectorBounds(queryVector);
+
             if (similarity == VectorSimilarity.dot_product || similarity == VectorSimilarity.cosine) {
                 float squaredMagnitude = 0.0f;
                 for (float e : queryVector) {
                     squaredMagnitude += e * e;
                 }
-                checkVectorMagnitude(queryVector, squaredMagnitude);
+                elementType.checkVectorMagnitude(similarity, queryVector, squaredMagnitude);
             }
-            return new KnnVectorQuery(name(), queryVector, numCands, filter);
-        }
 
-        private void checkVectorMagnitude(float[] vector, float squaredMagnitude) {
-            StringBuilder errorBuilder = null;
-            if (similarity == VectorSimilarity.dot_product && Math.abs(squaredMagnitude - 1.0f) > 1e-4f) {
-                errorBuilder = new StringBuilder(
-                    "The [" + VectorSimilarity.dot_product.name() + "] similarity can only be used with unit-length vectors."
-                );
-            } else if (similarity == VectorSimilarity.cosine && Math.sqrt(squaredMagnitude) == 0.0f) {
-                errorBuilder = new StringBuilder(
-                    "The [" + VectorSimilarity.cosine.name() + "] similarity does not support vectors with zero magnitude."
-                );
-            }
-
-            if (errorBuilder != null) {
-                // Include the first five elements of the invalid vector in the error message
-                errorBuilder.append(" Preview of invalid vector: [");
-                for (int i = 0; i < Math.min(5, vector.length); i++) {
-                    if (i > 0) {
-                        errorBuilder.append(", ");
-                    }
-                    errorBuilder.append(vector[i]);
-                }
-                if (vector.length >= 5) {
-                    errorBuilder.append(", ...");
-                }
-                errorBuilder.append("]");
-                throw new IllegalArgumentException(errorBuilder.toString());
-            }
+            return new KnnVectorQuery(name(), queryVector, numCands, filter);
         }
     }
 
+    private final ElementType elementType;
     private final int dims;
     private final boolean indexed;
     private final VectorSimilarity similarity;
@@ -359,6 +562,7 @@ public class DenseVectorFieldMapper extends FieldMapper {
     private DenseVectorFieldMapper(
         String simpleName,
         MappedFieldType mappedFieldType,
+        ElementType elementType,
         int dims,
         boolean indexed,
         VectorSimilarity similarity,
@@ -368,6 +572,7 @@ public class DenseVectorFieldMapper extends FieldMapper {
         CopyTo copyTo
     ) {
         super(simpleName, mappedFieldType, multiFields, copyTo);
+        this.elementType = elementType;
         this.dims = dims;
         this.indexed = indexed;
         this.similarity = similarity;
@@ -414,14 +619,21 @@ public class DenseVectorFieldMapper extends FieldMapper {
             squaredMagnitude += value * value;
         }
         checkDimensionMatches(index, context);
-        fieldType().checkVectorMagnitude(vector, squaredMagnitude);
-        return new KnnVectorField(fieldType().name(), vector, similarity.function);
+        elementType.checkVectorBounds(vector);
+        elementType.checkVectorMagnitude(similarity, vector, squaredMagnitude);
+        return elementType.createKnnVectorField(fieldType().name(), vector, similarity.function);
     }
 
     private Field parseBinaryDocValuesVector(DocumentParserContext context) throws IOException {
+        // ensure byte vectors are always indexed
+        // (should be caught during mapping creation)
+        assert elementType != ElementType.BYTE;
+
         // encode array of floats as array of integers and store into buf
         // this code is here and not int the VectorEncoderDecoder so not to create extra arrays
-        byte[] bytes = indexCreatedVersion.onOrAfter(Version.V_7_5_0) ? new byte[dims * INT_BYTES + INT_BYTES] : new byte[dims * INT_BYTES];
+        byte[] bytes = indexCreatedVersion.onOrAfter(Version.V_7_5_0)
+            ? new byte[dims * elementType.elementBytes + MAGNITUDE_BYTES]
+            : new byte[dims * elementType.elementBytes];
 
         ByteBuffer byteBuffer = ByteBuffer.wrap(bytes);
         double dotProduct = 0f;

+ 247 - 6
server/src/test/java/org/elasticsearch/index/mapper/vectors/DenseVectorFieldMapperTests.java

@@ -31,6 +31,7 @@ import org.elasticsearch.index.mapper.MapperService;
 import org.elasticsearch.index.mapper.MapperTestCase;
 import org.elasticsearch.index.mapper.ParsedDocument;
 import org.elasticsearch.index.mapper.vectors.DenseVectorFieldMapper.DenseVectorFieldType;
+import org.elasticsearch.index.mapper.vectors.DenseVectorFieldMapper.ElementType;
 import org.elasticsearch.index.mapper.vectors.DenseVectorFieldMapper.VectorSimilarity;
 import org.elasticsearch.test.ESTestCase;
 import org.elasticsearch.xcontent.XContentBuilder;
@@ -48,17 +49,23 @@ import static org.hamcrest.Matchers.equalTo;
 import static org.hamcrest.Matchers.instanceOf;
 
 public class DenseVectorFieldMapperTests extends MapperTestCase {
+
+    private final ElementType elementType;
     private final boolean indexed;
     private final boolean indexOptionsSet;
 
     public DenseVectorFieldMapperTests() {
-        this.indexed = randomBoolean();
-        this.indexOptionsSet = randomBoolean();
+        this.elementType = randomFrom(ElementType.BYTE, ElementType.FLOAT);
+        this.indexed = elementType == ElementType.BYTE || randomBoolean();
+        this.indexOptionsSet = this.indexed && randomBoolean();
     }
 
     @Override
     protected void minimalMapping(XContentBuilder b) throws IOException {
         b.field("type", "dense_vector").field("dims", 4);
+        if (elementType != ElementType.FLOAT) {
+            b.field("element_type", elementType.toString());
+        }
         if (indexed) {
             b.field("index", true).field("similarity", "dot_product");
             if (indexOptionsSet) {
@@ -73,7 +80,7 @@ public class DenseVectorFieldMapperTests extends MapperTestCase {
 
     @Override
     protected Object getSampleValueForDocument() {
-        return List.of(0.5, 0.5, 0.5, 0.5);
+        return elementType == ElementType.BYTE ? List.of((byte) 1, (byte) 1, (byte) 1, (byte) 1) : List.of(0.5, 0.5, 0.5, 0.5);
     }
 
     @Override
@@ -93,6 +100,23 @@ public class DenseVectorFieldMapperTests extends MapperTestCase {
             fieldMapping(b -> b.field("type", "dense_vector").field("dims", 4).field("index", true).field("similarity", "dot_product")),
             fieldMapping(b -> b.field("type", "dense_vector").field("dims", 4).field("index", false))
         );
+        checker.registerConflictCheck(
+            "element_type",
+            fieldMapping(
+                b -> b.field("type", "dense_vector")
+                    .field("dims", 4)
+                    .field("index", true)
+                    .field("similarity", "dot_product")
+                    .field("element_type", "byte")
+            ),
+            fieldMapping(
+                b -> b.field("type", "dense_vector")
+                    .field("dims", 4)
+                    .field("index", true)
+                    .field("similarity", "dot_product")
+                    .field("element_type", "float")
+            )
+        );
         checker.registerConflictCheck(
             "index_options",
             fieldMapping(b -> b.field("type", "dense_vector").field("dims", 4).field("index", true).field("similarity", "dot_product")),
@@ -175,7 +199,6 @@ public class DenseVectorFieldMapperTests extends MapperTestCase {
     }
 
     public void testDefaults() throws Exception {
-
         DocumentMapper mapper = createDocumentMapper(fieldMapping(b -> b.field("type", "dense_vector").field("dims", 3)));
 
         float[] validVector = { -12.1f, 100.7f, -4 };
@@ -215,6 +238,35 @@ public class DenseVectorFieldMapperTests extends MapperTestCase {
         assertEquals(similarity.function, vectorField.fieldType().vectorSimilarityFunction());
     }
 
+    public void testIndexedByteVector() throws Exception {
+        VectorSimilarity similarity = RandomPicks.randomFrom(random(), VectorSimilarity.values());
+        DocumentMapper mapper = createDocumentMapper(
+            fieldMapping(
+                b -> b.field("type", "dense_vector")
+                    .field("dims", 3)
+                    .field("index", true)
+                    .field("similarity", similarity.name())
+                    .field("element_type", "byte")
+            )
+        );
+
+        float[] vector = { (byte) -1, (byte) 1, (byte) 127 };
+        ParsedDocument doc1 = mapper.parse(source(b -> b.array("field", vector)));
+
+        IndexableField[] fields = doc1.rootDoc().getFields("field");
+        assertEquals(1, fields.length);
+        assertThat(fields[0], instanceOf(KnnVectorField.class));
+
+        KnnVectorField vectorField = (KnnVectorField) fields[0];
+        vectorField.binaryValue();
+        assertEquals(
+            "Parsed vector is not equal to original.",
+            new BytesRef(new byte[] { (byte) -1, (byte) 1, (byte) 127 }),
+            vectorField.binaryValue()
+        );
+        assertEquals(similarity.function, vectorField.fieldType().vectorSimilarityFunction());
+    }
+
     public void testDotProductWithInvalidNorm() throws Exception {
         DocumentMapper mapper = createDocumentMapper(
             fieldMapping(
@@ -265,6 +317,27 @@ public class DenseVectorFieldMapperTests extends MapperTestCase {
         );
     }
 
+    public void testCosineWithZeroByteVector() throws Exception {
+        DocumentMapper mapper = createDocumentMapper(
+            fieldMapping(
+                b -> b.field("type", "dense_vector")
+                    .field("dims", 3)
+                    .field("index", true)
+                    .field("similarity", VectorSimilarity.cosine)
+                    .field("element_type", "byte")
+            )
+        );
+        float[] vector = { -0.0f, 0.0f, 0.0f };
+        MapperParsingException e = expectThrows(MapperParsingException.class, () -> mapper.parse(source(b -> b.array("field", vector))));
+        assertNotNull(e.getCause());
+        assertThat(
+            e.getCause().getMessage(),
+            containsString(
+                "The [cosine] similarity does not support vectors with zero magnitude. Preview of invalid vector: [-0.0, 0.0, 0.0]"
+            )
+        );
+    }
+
     public void testInvalidParameters() {
         MapperParsingException e = expectThrows(
             MapperParsingException.class,
@@ -342,6 +415,18 @@ public class DenseVectorFieldMapperTests extends MapperTestCase {
             )
         );
         assertThat(e.getMessage(), containsString("[index_options] of type [hnsw] requires field [ef_construction] to be configured"));
+
+        e = expectThrows(
+            MapperParsingException.class,
+            () -> createDocumentMapper(fieldMapping(b -> b.field("type", "dense_vector").field("dims", 3).field("element_type", "bytes")))
+        );
+        assertThat(e.getMessage(), containsString("invalid element_type [bytes]; available types are "));
+
+        e = expectThrows(
+            MapperParsingException.class,
+            () -> createDocumentMapper(fieldMapping(b -> b.field("type", "dense_vector").field("dims", 3).field("element_type", "byte")))
+        );
+        assertThat(e.getMessage(), containsString("index must be [true] when element_type is [byte]"));
     }
 
     public void testAddDocumentsToIndexBefore_V_7_5_0() throws Exception {
@@ -448,6 +533,154 @@ public class DenseVectorFieldMapperTests extends MapperTestCase {
         assertThat(e.getMessage(), containsString("[dense_vector] fields cannot be indexed if they're within [nested] mappings"));
     }
 
+    public void testByteVectorIndexBoundaries() throws IOException {
+        DocumentMapper mapper = createDocumentMapper(
+            fieldMapping(
+                b -> b.field("type", "dense_vector")
+                    .field("element_type", "byte")
+                    .field("dims", 3)
+                    .field("index", true)
+                    .field("similarity", VectorSimilarity.cosine)
+            )
+        );
+
+        Exception e = expectThrows(
+            MapperParsingException.class,
+            () -> mapper.parse(source(b -> b.array("field", new float[] { 128, 0, 0 })))
+        );
+        assertThat(
+            e.getCause().getMessage(),
+            containsString("element_type [byte] vectors only support integers between [-128, 127] but found [128.0] at dim [0];")
+        );
+
+        e = expectThrows(
+            MapperParsingException.class,
+            () -> mapper.parse(source(b -> b.array("field", new float[] { 0.0f, 0.0f, -129.0f })))
+        );
+        assertThat(
+            e.getCause().getMessage(),
+            containsString("element_type [byte] vectors only support integers between [-128, 127] but found [-129.0] at dim [2];")
+        );
+    }
+
+    public void testByteVectorQueryBoundaries() throws IOException {
+        MapperService mapperService = createMapperService(fieldMapping(b -> {
+            b.field("type", "dense_vector");
+            b.field("element_type", "byte");
+            b.field("dims", 3);
+            b.field("index", true);
+            b.field("similarity", "dot_product");
+            b.startObject("index_options");
+            b.field("type", "hnsw");
+            b.field("m", 3);
+            b.field("ef_construction", 10);
+            b.endObject();
+        }));
+
+        DenseVectorFieldType denseVectorFieldType = (DenseVectorFieldType) mapperService.fieldType("field");
+
+        Exception e = expectThrows(
+            IllegalArgumentException.class,
+            () -> denseVectorFieldType.createKnnQuery(new float[] { 128, 0, 0 }, 3, null)
+        );
+        assertThat(
+            e.getMessage(),
+            containsString("element_type [byte] vectors only support integers between [-128, 127] but found [128.0] at dim [0];")
+        );
+
+        e = expectThrows(
+            IllegalArgumentException.class,
+            () -> denseVectorFieldType.createKnnQuery(new float[] { 0.0f, 0f, -129.0f }, 3, null)
+        );
+        assertThat(
+            e.getMessage(),
+            containsString("element_type [byte] vectors only support integers between [-128, 127] but found [-129.0] at dim [2];")
+        );
+
+        e = expectThrows(
+            IllegalArgumentException.class,
+            () -> denseVectorFieldType.createKnnQuery(new float[] { 0.0f, 0.5f, 0.0f }, 3, null)
+        );
+        assertThat(
+            e.getMessage(),
+            containsString("element_type [byte] vectors only support non-decimal values but found decimal value [0.5] at dim [1];")
+        );
+
+        e = expectThrows(
+            IllegalArgumentException.class,
+            () -> denseVectorFieldType.createKnnQuery(new float[] { 0, 0.0f, -0.25f }, 3, null)
+        );
+        assertThat(
+            e.getMessage(),
+            containsString("element_type [byte] vectors only support non-decimal values but found decimal value [-0.25] at dim [2];")
+        );
+
+        e = expectThrows(
+            IllegalArgumentException.class,
+            () -> denseVectorFieldType.createKnnQuery(new float[] { Float.NaN, 0f, 0.0f }, 3, null)
+        );
+        assertThat(e.getMessage(), containsString("element_type [byte] vectors do not support NaN values but found [NaN] at dim [0];"));
+
+        e = expectThrows(
+            IllegalArgumentException.class,
+            () -> denseVectorFieldType.createKnnQuery(new float[] { Float.POSITIVE_INFINITY, 0f, 0.0f }, 3, null)
+        );
+        assertThat(
+            e.getMessage(),
+            containsString("element_type [byte] vectors do not support infinite values but found [Infinity] at dim [0];")
+        );
+
+        e = expectThrows(
+            IllegalArgumentException.class,
+            () -> denseVectorFieldType.createKnnQuery(new float[] { 0, Float.NEGATIVE_INFINITY, 0.0f }, 3, null)
+        );
+        assertThat(
+            e.getMessage(),
+            containsString("element_type [byte] vectors do not support infinite values but found [-Infinity] at dim [1];")
+        );
+    }
+
+    public void testFloatVectorQueryBoundaries() throws IOException {
+        MapperService mapperService = createMapperService(fieldMapping(b -> {
+            b.field("type", "dense_vector");
+            b.field("element_type", "float");
+            b.field("dims", 3);
+            b.field("index", true);
+            b.field("similarity", "dot_product");
+            b.startObject("index_options");
+            b.field("type", "hnsw");
+            b.field("m", 3);
+            b.field("ef_construction", 10);
+            b.endObject();
+        }));
+
+        DenseVectorFieldType denseVectorFieldType = (DenseVectorFieldType) mapperService.fieldType("field");
+
+        Exception e = expectThrows(
+            IllegalArgumentException.class,
+            () -> denseVectorFieldType.createKnnQuery(new float[] { Float.NaN, 0f, 0.0f }, 3, null)
+        );
+        assertThat(e.getMessage(), containsString("element_type [float] vectors do not support NaN values but found [NaN] at dim [0];"));
+
+        e = expectThrows(
+            IllegalArgumentException.class,
+            () -> denseVectorFieldType.createKnnQuery(new float[] { Float.POSITIVE_INFINITY, 0f, 0.0f }, 3, null)
+        );
+        assertThat(
+            e.getMessage(),
+            containsString("element_type [float] vectors do not support infinite values but found [Infinity] at dim [0];")
+        );
+
+        e = expectThrows(
+            IllegalArgumentException.class,
+            () -> denseVectorFieldType.createKnnQuery(new float[] { 0, Float.NEGATIVE_INFINITY, 0.0f }, 3, null)
+        );
+        assertThat(
+            e.getMessage(),
+            containsString("element_type [float] vectors do not support infinite values but found [-Infinity] at dim [1];")
+        );
+    }
+
     public void testKnnVectorsFormat() throws IOException {
         final int m = randomIntBetween(1, DEFAULT_MAX_CONN + 10);
         final int efConstruction = randomIntBetween(1, DEFAULT_BEAM_WIDTH + 10);
@@ -492,18 +725,26 @@ public class DenseVectorFieldMapperTests extends MapperTestCase {
 
     private static class DenseVectorSyntheticSourceSupport implements SyntheticSourceSupport {
         private final int dims = between(5, 1000);
-        private final boolean indexed = randomBoolean();
+        private final ElementType elementType = randomFrom(ElementType.BYTE, ElementType.FLOAT);
+        private final boolean indexed = elementType == ElementType.BYTE || randomBoolean();
         private final boolean indexOptionsSet = indexed && randomBoolean();
 
         @Override
         public SyntheticSourceExample example(int maxValues) throws IOException {
-            List<Float> value = randomList(dims, dims, ESTestCase::randomFloat);
+            List<Float> value = randomList(dims, dims, this::randomValue);
             return new SyntheticSourceExample(value, value, this::mapping);
         }
 
+        private float randomValue() {
+            return elementType == ElementType.BYTE ? ESTestCase.randomByte() : ESTestCase.randomFloat();
+        }
+
         private void mapping(XContentBuilder b) throws IOException {
             b.field("type", "dense_vector");
             b.field("dims", dims);
+            if (elementType == ElementType.BYTE || randomBoolean()) {
+                b.field("element_type", elementType.toString());
+            }
             if (indexed) {
                 b.field("index", true);
                 b.field("similarity", "l2_norm");

+ 84 - 17
server/src/test/java/org/elasticsearch/index/mapper/vectors/DenseVectorFieldTypeTests.java

@@ -29,51 +29,87 @@ public class DenseVectorFieldTypeTests extends FieldTypeTestCase {
         this.indexed = randomBoolean();
     }
 
-    private DenseVectorFieldType createFieldType() {
-        return new DenseVectorFieldType("f", Version.CURRENT, 5, indexed, VectorSimilarity.cosine, Collections.emptyMap());
+    private DenseVectorFieldType createFloatFieldType() {
+        return new DenseVectorFieldType(
+            "f",
+            Version.CURRENT,
+            DenseVectorFieldMapper.ElementType.FLOAT,
+            5,
+            indexed,
+            VectorSimilarity.cosine,
+            Collections.emptyMap()
+        );
+    }
+
+    private DenseVectorFieldType createByteFieldType() {
+        return new DenseVectorFieldType(
+            "f",
+            Version.CURRENT,
+            DenseVectorFieldMapper.ElementType.BYTE,
+            5,
+            true,
+            VectorSimilarity.cosine,
+            Collections.emptyMap()
+        );
     }
 
     public void testHasDocValues() {
-        DenseVectorFieldType ft = createFieldType();
-        assertNotEquals(indexed, ft.hasDocValues());
+        DenseVectorFieldType fft = createFloatFieldType();
+        assertNotEquals(indexed, fft.hasDocValues());
+        DenseVectorFieldType bft = createByteFieldType();
+        assertFalse(bft.hasDocValues());
     }
 
     public void testIsIndexed() {
-        DenseVectorFieldType ft = createFieldType();
-        assertEquals(indexed, ft.isIndexed());
+        DenseVectorFieldType fft = createFloatFieldType();
+        assertEquals(indexed, fft.isIndexed());
+        DenseVectorFieldType bft = createByteFieldType();
+        assertTrue(bft.isIndexed());
     }
 
     public void testIsSearchable() {
-        DenseVectorFieldType ft = createFieldType();
-        assertEquals(indexed, ft.isSearchable());
+        DenseVectorFieldType fft = createFloatFieldType();
+        assertEquals(indexed, fft.isSearchable());
+        DenseVectorFieldType bft = createByteFieldType();
+        assertTrue(bft.isSearchable());
     }
 
     public void testIsAggregatable() {
-        DenseVectorFieldType ft = createFieldType();
-        assertFalse(ft.isAggregatable());
+        DenseVectorFieldType fft = createFloatFieldType();
+        assertFalse(fft.isAggregatable());
+        DenseVectorFieldType bft = createByteFieldType();
+        assertFalse(bft.isAggregatable());
     }
 
     public void testFielddataBuilder() {
-        DenseVectorFieldType ft = createFieldType();
+        DenseVectorFieldType fft = createFloatFieldType();
         FieldDataContext fdc = new FieldDataContext("test", () -> null, Set::of, MappedFieldType.FielddataOperation.SCRIPT);
-        assertNotNull(ft.fielddataBuilder(fdc));
+        assertNotNull(fft.fielddataBuilder(fdc));
+
+        DenseVectorFieldType bft = createByteFieldType();
+        expectThrows(IllegalArgumentException.class, () -> bft.fielddataBuilder(fdc));
     }
 
     public void testDocValueFormat() {
-        DenseVectorFieldType ft = createFieldType();
-        expectThrows(IllegalArgumentException.class, () -> ft.docValueFormat(null, null));
+        DenseVectorFieldType fft = createFloatFieldType();
+        expectThrows(IllegalArgumentException.class, () -> fft.docValueFormat(null, null));
+        DenseVectorFieldType bft = createByteFieldType();
+        expectThrows(IllegalArgumentException.class, () -> bft.docValueFormat(null, null));
     }
 
     public void testFetchSourceValue() throws IOException {
-        DenseVectorFieldType ft = createFieldType();
+        DenseVectorFieldType fft = createFloatFieldType();
         List<Double> vector = List.of(0.0, 1.0, 2.0, 3.0, 4.0);
-        assertEquals(vector, fetchSourceValue(ft, vector));
+        assertEquals(vector, fetchSourceValue(fft, vector));
+        DenseVectorFieldType bft = createByteFieldType();
+        assertEquals(vector, fetchSourceValue(bft, vector));
     }
 
-    public void testCreateKnnQuery() {
+    public void testFloatCreateKnnQuery() {
         DenseVectorFieldType unindexedField = new DenseVectorFieldType(
             "f",
             Version.CURRENT,
+            DenseVectorFieldMapper.ElementType.FLOAT,
             3,
             false,
             VectorSimilarity.cosine,
@@ -88,6 +124,7 @@ public class DenseVectorFieldTypeTests extends FieldTypeTestCase {
         DenseVectorFieldType dotProductField = new DenseVectorFieldType(
             "f",
             Version.CURRENT,
+            DenseVectorFieldMapper.ElementType.FLOAT,
             3,
             true,
             VectorSimilarity.dot_product,
@@ -99,6 +136,36 @@ public class DenseVectorFieldTypeTests extends FieldTypeTestCase {
         DenseVectorFieldType cosineField = new DenseVectorFieldType(
             "f",
             Version.CURRENT,
+            DenseVectorFieldMapper.ElementType.FLOAT,
+            3,
+            true,
+            VectorSimilarity.cosine,
+            Collections.emptyMap()
+        );
+        e = expectThrows(IllegalArgumentException.class, () -> cosineField.createKnnQuery(new float[] { 0.0f, 0.0f, 0.0f }, 10, null));
+        assertThat(e.getMessage(), containsString("The [cosine] similarity does not support vectors with zero magnitude."));
+    }
+
+    public void testByteCreateKnnQuery() {
+        DenseVectorFieldType unindexedField = new DenseVectorFieldType(
+            "f",
+            Version.CURRENT,
+            DenseVectorFieldMapper.ElementType.BYTE,
+            3,
+            false,
+            VectorSimilarity.cosine,
+            Collections.emptyMap()
+        );
+        IllegalArgumentException e = expectThrows(
+            IllegalArgumentException.class,
+            () -> unindexedField.createKnnQuery(new float[] { 0.3f, 0.1f, 1.0f }, 10, null)
+        );
+        assertThat(e.getMessage(), containsString("to perform knn search on field [f], its mapping must have [index] set to [true]"));
+
+        DenseVectorFieldType cosineField = new DenseVectorFieldType(
+            "f",
+            Version.CURRENT,
+            DenseVectorFieldMapper.ElementType.BYTE,
             3,
             true,
             VectorSimilarity.cosine,