|  | @@ -8,6 +8,8 @@
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  |  package org.elasticsearch.xpack.vectors.mapper;
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  | +import org.apache.lucene.codecs.KnnVectorsFormat;
 | 
	
		
			
				|  |  | +import org.apache.lucene.codecs.lucene90.Lucene90HnswVectorsFormat;
 | 
	
		
			
				|  |  |  import org.apache.lucene.document.BinaryDocValuesField;
 | 
	
		
			
				|  |  |  import org.apache.lucene.document.Field;
 | 
	
		
			
				|  |  |  import org.apache.lucene.document.KnnVectorField;
 | 
	
	
		
			
				|  | @@ -16,6 +18,10 @@ import org.apache.lucene.search.DocValuesFieldExistsQuery;
 | 
	
		
			
				|  |  |  import org.apache.lucene.search.Query;
 | 
	
		
			
				|  |  |  import org.apache.lucene.util.BytesRef;
 | 
	
		
			
				|  |  |  import org.elasticsearch.Version;
 | 
	
		
			
				|  |  | +import org.elasticsearch.index.mapper.MappingParser;
 | 
	
		
			
				|  |  | +import org.elasticsearch.index.mapper.PerFieldKnnVectorsFormatFieldMapper;
 | 
	
		
			
				|  |  | +import org.elasticsearch.xcontent.ToXContent;
 | 
	
		
			
				|  |  | +import org.elasticsearch.xcontent.XContentBuilder;
 | 
	
		
			
				|  |  |  import org.elasticsearch.xcontent.XContentParser.Token;
 | 
	
		
			
				|  |  |  import org.elasticsearch.common.xcontent.support.XContentMapValues;
 | 
	
		
			
				|  |  |  import org.elasticsearch.index.fielddata.IndexFieldData;
 | 
	
	
		
			
				|  | @@ -40,6 +46,7 @@ import java.nio.ByteBuffer;
 | 
	
		
			
				|  |  |  import java.time.ZoneId;
 | 
	
		
			
				|  |  |  import java.util.List;
 | 
	
		
			
				|  |  |  import java.util.Map;
 | 
	
		
			
				|  |  | +import java.util.Objects;
 | 
	
		
			
				|  |  |  import java.util.function.Supplier;
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  |  import static org.elasticsearch.common.xcontent.XContentParserUtils.ensureExpectedToken;
 | 
	
	
		
			
				|  | @@ -47,7 +54,7 @@ import static org.elasticsearch.common.xcontent.XContentParserUtils.ensureExpect
 | 
	
		
			
				|  |  |  /**
 | 
	
		
			
				|  |  |   * A {@link FieldMapper} for indexing a dense vector of floats.
 | 
	
		
			
				|  |  |   */
 | 
	
		
			
				|  |  | -public class DenseVectorFieldMapper extends FieldMapper {
 | 
	
		
			
				|  |  | +public class DenseVectorFieldMapper extends FieldMapper implements PerFieldKnnVectorsFormatFieldMapper {
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  |      public static final String CONTENT_TYPE = "dense_vector";
 | 
	
		
			
				|  |  |      public static short MAX_DIMS_COUNT = 2048; //maximum allowed number of dimensions
 | 
	
	
		
			
				|  | @@ -73,6 +80,8 @@ 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", false, m -> toType(m).similarity, null, VectorSimilarity.class);
 | 
	
		
			
				|  |  | +        private final Parameter<IndexOptions> indexOptions = new Parameter<>("index_options", false, () -> null,
 | 
	
		
			
				|  |  | +            (n, c, o) ->  o == null ? null : parseIndexOptions(n, o), m -> toType(m).indexOptions);
 | 
	
		
			
				|  |  |          private final Parameter<Map<String, String>> meta = Parameter.metaParam();
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  |          final Version indexVersionCreated;
 | 
	
	
		
			
				|  | @@ -84,11 +93,13 @@ public class DenseVectorFieldMapper extends FieldMapper {
 | 
	
		
			
				|  |  |              this.indexed.requiresParameters(similarity);
 | 
	
		
			
				|  |  |              this.similarity.setSerializerCheck((id, ic, v) -> v != null);
 | 
	
		
			
				|  |  |              this.similarity.requiresParameters(indexed);
 | 
	
		
			
				|  |  | +            this.indexOptions.requiresParameters(indexed);
 | 
	
		
			
				|  |  | +            this.indexOptions.setSerializerCheck((id, ic, v) -> v != null);
 | 
	
		
			
				|  |  |          }
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  |          @Override
 | 
	
		
			
				|  |  |          protected List<Parameter<?>> getParameters() {
 | 
	
		
			
				|  |  | -            return List.of(dims, indexed, similarity, meta);
 | 
	
		
			
				|  |  | +            return List.of(dims, indexed, similarity, indexOptions, meta);
 | 
	
		
			
				|  |  |          }
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  |          @Override
 | 
	
	
		
			
				|  | @@ -100,6 +111,7 @@ public class DenseVectorFieldMapper extends FieldMapper {
 | 
	
		
			
				|  |  |                  dims.getValue(),
 | 
	
		
			
				|  |  |                  indexed.getValue(),
 | 
	
		
			
				|  |  |                  similarity.getValue(),
 | 
	
		
			
				|  |  | +                indexOptions.getValue(),
 | 
	
		
			
				|  |  |                  indexVersionCreated,
 | 
	
		
			
				|  |  |                  multiFieldsBuilder.build(this, context),
 | 
	
		
			
				|  |  |                  copyTo.build());
 | 
	
	
		
			
				|  | @@ -116,6 +128,67 @@ public class DenseVectorFieldMapper extends FieldMapper {
 | 
	
		
			
				|  |  |          }
 | 
	
		
			
				|  |  |      }
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  | +    private abstract static class IndexOptions implements ToXContent {
 | 
	
		
			
				|  |  | +        final String type;
 | 
	
		
			
				|  |  | +        IndexOptions(String type) {
 | 
	
		
			
				|  |  | +            this.type = type;
 | 
	
		
			
				|  |  | +        }
 | 
	
		
			
				|  |  | +    }
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +    private static class HnswIndexOptions extends IndexOptions {
 | 
	
		
			
				|  |  | +        private final int m;
 | 
	
		
			
				|  |  | +        private final int efConstruction;
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +        static IndexOptions parseIndexOptions(String fieldName, Map<String, ?> indexOptionsMap) {
 | 
	
		
			
				|  |  | +            Object mNode = indexOptionsMap.remove("m");
 | 
	
		
			
				|  |  | +            Object efConstructionNode = indexOptionsMap.remove("ef_construction");
 | 
	
		
			
				|  |  | +            if (mNode == null) {
 | 
	
		
			
				|  |  | +                throw new MapperParsingException("[index_options] of type [hnsw] requires field [m] to be configured");
 | 
	
		
			
				|  |  | +            }
 | 
	
		
			
				|  |  | +            if (efConstructionNode == null) {
 | 
	
		
			
				|  |  | +                throw new MapperParsingException("[index_options] of type [hnsw] requires field [ef_construction] to be configured");
 | 
	
		
			
				|  |  | +            }
 | 
	
		
			
				|  |  | +            int m = XContentMapValues.nodeIntegerValue(mNode);
 | 
	
		
			
				|  |  | +            int efConstruction = XContentMapValues.nodeIntegerValue(efConstructionNode);
 | 
	
		
			
				|  |  | +            MappingParser.checkNoRemainingFields(fieldName, indexOptionsMap);
 | 
	
		
			
				|  |  | +            return new HnswIndexOptions(m, efConstruction);
 | 
	
		
			
				|  |  | +        }
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +        private HnswIndexOptions(int m, int efConstruction) {
 | 
	
		
			
				|  |  | +            super("hnsw");
 | 
	
		
			
				|  |  | +            this.m = m;
 | 
	
		
			
				|  |  | +            this.efConstruction = efConstruction;
 | 
	
		
			
				|  |  | +        }
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +        @Override
 | 
	
		
			
				|  |  | +        public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException {
 | 
	
		
			
				|  |  | +            builder.startObject();
 | 
	
		
			
				|  |  | +            builder.field("type", type);
 | 
	
		
			
				|  |  | +            builder.field("m", m);
 | 
	
		
			
				|  |  | +            builder.field("ef_construction", efConstruction);
 | 
	
		
			
				|  |  | +            builder.endObject();
 | 
	
		
			
				|  |  | +            return builder;
 | 
	
		
			
				|  |  | +        }
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +        @Override
 | 
	
		
			
				|  |  | +        public boolean equals(Object o) {
 | 
	
		
			
				|  |  | +            if (this == o) return true;
 | 
	
		
			
				|  |  | +            if (o == null || getClass() != o.getClass()) return false;
 | 
	
		
			
				|  |  | +            HnswIndexOptions that = (HnswIndexOptions) o;
 | 
	
		
			
				|  |  | +            return m == that.m && efConstruction == that.efConstruction;
 | 
	
		
			
				|  |  | +        }
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +        @Override
 | 
	
		
			
				|  |  | +        public int hashCode() {
 | 
	
		
			
				|  |  | +            return Objects.hash(type, m, efConstruction);
 | 
	
		
			
				|  |  | +        }
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +        @Override
 | 
	
		
			
				|  |  | +        public String toString() {
 | 
	
		
			
				|  |  | +            return "{type=" + type + ", m=" + m + ", ef_construction=" + efConstruction + " }";
 | 
	
		
			
				|  |  | +        }
 | 
	
		
			
				|  |  | +    }
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  |      public static final TypeParser PARSER
 | 
	
		
			
				|  |  |          = new TypeParser((n, c) -> new Builder(n, c.indexVersionCreated()), notInMultiFields(CONTENT_TYPE));
 | 
	
		
			
				|  |  |  
 | 
	
	
		
			
				|  | @@ -185,15 +258,17 @@ public class DenseVectorFieldMapper extends FieldMapper {
 | 
	
		
			
				|  |  |      private final int dims;
 | 
	
		
			
				|  |  |      private final boolean indexed;
 | 
	
		
			
				|  |  |      private final VectorSimilarity similarity;
 | 
	
		
			
				|  |  | +    private final IndexOptions indexOptions;
 | 
	
		
			
				|  |  |      private final Version indexCreatedVersion;
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  | -    private DenseVectorFieldMapper(String simpleName, MappedFieldType mappedFieldType, int dims,
 | 
	
		
			
				|  |  | -                                   boolean indexed, VectorSimilarity similarity,
 | 
	
		
			
				|  |  | +    private DenseVectorFieldMapper(String simpleName, MappedFieldType mappedFieldType, int dims, boolean indexed,
 | 
	
		
			
				|  |  | +                                   VectorSimilarity similarity, IndexOptions indexOptions,
 | 
	
		
			
				|  |  |                                     Version indexCreatedVersion, MultiFields multiFields, CopyTo copyTo) {
 | 
	
		
			
				|  |  |          super(simpleName, mappedFieldType, multiFields, copyTo);
 | 
	
		
			
				|  |  |          this.dims = dims;
 | 
	
		
			
				|  |  |          this.indexed = indexed;
 | 
	
		
			
				|  |  |          this.similarity = similarity;
 | 
	
		
			
				|  |  | +        this.indexOptions = indexOptions;
 | 
	
		
			
				|  |  |          this.indexCreatedVersion = indexCreatedVersion;
 | 
	
		
			
				|  |  |      }
 | 
	
		
			
				|  |  |  
 | 
	
	
		
			
				|  | @@ -289,4 +364,29 @@ public class DenseVectorFieldMapper extends FieldMapper {
 | 
	
		
			
				|  |  |      public FieldMapper.Builder getMergeBuilder() {
 | 
	
		
			
				|  |  |          return new Builder(simpleName(), indexCreatedVersion).init(this);
 | 
	
		
			
				|  |  |      }
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +    private static IndexOptions parseIndexOptions(String fieldName, Object propNode) {
 | 
	
		
			
				|  |  | +        @SuppressWarnings("unchecked")
 | 
	
		
			
				|  |  | +        Map<String, ?> indexOptionsMap = (Map<String, ?>) propNode;
 | 
	
		
			
				|  |  | +        Object typeNode = indexOptionsMap.remove("type");
 | 
	
		
			
				|  |  | +        if (typeNode == null) {
 | 
	
		
			
				|  |  | +            throw new MapperParsingException("[index_options] requires field [type] to be configured");
 | 
	
		
			
				|  |  | +        }
 | 
	
		
			
				|  |  | +        String type = XContentMapValues.nodeStringValue(typeNode);
 | 
	
		
			
				|  |  | +        if (type.equals("hnsw")) {
 | 
	
		
			
				|  |  | +            return HnswIndexOptions.parseIndexOptions(fieldName, indexOptionsMap);
 | 
	
		
			
				|  |  | +        } else {
 | 
	
		
			
				|  |  | +            throw new MapperParsingException("Unknown vector index options type [" + type + "] for field [" + fieldName + "]");
 | 
	
		
			
				|  |  | +        }
 | 
	
		
			
				|  |  | +    }
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +    @Override
 | 
	
		
			
				|  |  | +    public KnnVectorsFormat getKnnVectorsFormatForField() {
 | 
	
		
			
				|  |  | +        if (indexOptions == null) {
 | 
	
		
			
				|  |  | +            return null; // use default format
 | 
	
		
			
				|  |  | +        } else {
 | 
	
		
			
				|  |  | +            HnswIndexOptions hnswIndexOptions = (HnswIndexOptions) indexOptions;
 | 
	
		
			
				|  |  | +            return new Lucene90HnswVectorsFormat(hnswIndexOptions.m, hnswIndexOptions.efConstruction);
 | 
	
		
			
				|  |  | +        }
 | 
	
		
			
				|  |  | +    }
 | 
	
		
			
				|  |  |  }
 |