From 71fff475cd76f3fe7fd195a15f54138c9127ad6a Mon Sep 17 00:00:00 2001 From: Naveen Tatikonda Date: Mon, 22 Jul 2024 19:28:42 -0500 Subject: [PATCH] Add support for Lucene inbuilt Scalar Quantizer (#1848) * Add support for Lucene Inbuilt Scalar Quantizer Signed-off-by: Naveen Tatikonda * Refactor code Signed-off-by: Naveen Tatikonda * Add Tests Signed-off-by: Naveen Tatikonda * Address Review Comments Signed-off-by: Naveen Tatikonda * Refactoring changes Signed-off-by: Naveen Tatikonda * Remove compress as an input parameter and set default as true Signed-off-by: Naveen Tatikonda * Add Constructor overloading and other refactoring changes Signed-off-by: Naveen Tatikonda * Add more unit tests Signed-off-by: Naveen Tatikonda * Set default encoder as encoder flat Signed-off-by: Naveen Tatikonda --------- Signed-off-by: Naveen Tatikonda --- .../opensearch-knn.release-notes-2.16.0.0.md | 1 + .../opensearch/knn/common/KNNConstants.java | 6 + .../org/opensearch/knn/index/Parameter.java | 81 +++++++ .../codec/BasePerFieldKnnVectorsFormat.java | 88 +++++--- .../KNN920PerFieldKnnVectorsFormat.java | 5 +- .../KNN940PerFieldKnnVectorsFormat.java | 5 +- .../KNN950PerFieldKnnVectorsFormat.java | 5 +- .../KNN990PerFieldKnnVectorsFormat.java | 16 +- ...KNNScalarQuantizedVectorsFormatParams.java | 82 +++++++ .../codec/params/KNNVectorsFormatParams.java | 45 ++++ .../index/mapper/KNNVectorFieldMapper.java | 4 +- .../org/opensearch/knn/index/util/Lucene.java | 39 ++++ .../opensearch/knn/index/LuceneEngineIT.java | 200 ++++++++++++++++++ .../opensearch/knn/index/ParameterTests.java | 34 +++ ...alarQuantizedVectorsFormatParamsTests.java | 110 ++++++++++ .../params/KNNVectorsFormatParamsTests.java | 56 +++++ 16 files changed, 746 insertions(+), 31 deletions(-) create mode 100644 src/main/java/org/opensearch/knn/index/codec/params/KNNScalarQuantizedVectorsFormatParams.java create mode 100644 src/main/java/org/opensearch/knn/index/codec/params/KNNVectorsFormatParams.java create mode 100644 src/test/java/org/opensearch/knn/index/codec/params/KNNScalarQuantizedVectorsFormatParamsTests.java create mode 100644 src/test/java/org/opensearch/knn/index/codec/params/KNNVectorsFormatParamsTests.java diff --git a/release-notes/opensearch-knn.release-notes-2.16.0.0.md b/release-notes/opensearch-knn.release-notes-2.16.0.0.md index c24d3552b..da60c2cbf 100644 --- a/release-notes/opensearch-knn.release-notes-2.16.0.0.md +++ b/release-notes/opensearch-knn.release-notes-2.16.0.0.md @@ -10,6 +10,7 @@ Compatible with OpenSearch 2.16.0 * Add script scoring support for knn field with binary data type [#1826](https://github.com/opensearch-project/k-NN/pull/1826) * Add painless script support for hamming with binary vector data type [#1839](https://github.com/opensearch-project/k-NN/pull/1839) * Add binary format support with IVF method in Faiss Engine [#1784](https://github.com/opensearch-project/k-NN/pull/1784) +* Add support for Lucene inbuilt Scalar Quantizer [#1848](https://github.com/opensearch-project/k-NN/pull/1848) ### Enhancements * Switch from byte stream to byte ref for serde [#1825](https://github.com/opensearch-project/k-NN/pull/1825) ### Bug Fixes diff --git a/src/main/java/org/opensearch/knn/common/KNNConstants.java b/src/main/java/org/opensearch/knn/common/KNNConstants.java index 77aae79c3..56f9ffaf8 100644 --- a/src/main/java/org/opensearch/knn/common/KNNConstants.java +++ b/src/main/java/org/opensearch/knn/common/KNNConstants.java @@ -74,6 +74,12 @@ public class KNNConstants { // Lucene specific constants public static final String LUCENE_NAME = "lucene"; + public static final String LUCENE_SQ_CONFIDENCE_INTERVAL = "confidence_interval"; + public static final int DYNAMIC_CONFIDENCE_INTERVAL = 0; + public static final double MINIMUM_CONFIDENCE_INTERVAL = 0.9; + public static final double MAXIMUM_CONFIDENCE_INTERVAL = 1.0; + public static final String LUCENE_SQ_BITS = "bits"; + public static final int LUCENE_SQ_DEFAULT_BITS = 7; // nmslib specific constants public static final String NMSLIB_NAME = "nmslib"; diff --git a/src/main/java/org/opensearch/knn/index/Parameter.java b/src/main/java/org/opensearch/knn/index/Parameter.java index a4520636e..50d792ebd 100644 --- a/src/main/java/org/opensearch/knn/index/Parameter.java +++ b/src/main/java/org/opensearch/knn/index/Parameter.java @@ -14,7 +14,9 @@ import org.opensearch.common.ValidationException; import org.opensearch.knn.training.VectorSpaceInfo; +import java.util.Locale; import java.util.Map; +import java.util.Objects; import java.util.function.BiFunction; import java.util.function.Predicate; @@ -204,6 +206,85 @@ public ValidationException validateWithData(Object value, VectorSpaceInfo vector } } + /** + * Double method parameter + */ + public static class DoubleParameter extends Parameter { + public DoubleParameter(String name, Double defaultValue, Predicate validator) { + super(name, defaultValue, validator); + } + + public DoubleParameter( + String name, + Double defaultValue, + Predicate validator, + BiFunction validatorWithData + ) { + super(name, defaultValue, validator, validatorWithData); + } + + @Override + public ValidationException validate(Object value) { + if (Objects.isNull(value)) { + String validationErrorMsg = String.format(Locale.ROOT, "Null value provided for Double " + "parameter \"%s\".", getName()); + return getValidationException(validationErrorMsg); + } + if (value.equals(0)) value = 0.0; + + if (!(value instanceof Double)) { + String validationErrorMsg = String.format( + Locale.ROOT, + "Value not of type Double for Double " + "parameter \"%s\".", + getName() + ); + return getValidationException(validationErrorMsg); + } + + if (!validator.test((Double) value)) { + String validationErrorMsg = String.format( + Locale.ROOT, + "Parameter validation failed for Double " + "parameter \"%s\".", + getName() + ); + return getValidationException(validationErrorMsg); + } + return null; + } + + @Override + public ValidationException validateWithData(Object value, VectorSpaceInfo vectorSpaceInfo) { + if (Objects.isNull(value)) { + String validationErrorMsg = String.format(Locale.ROOT, "Null value provided for Double " + "parameter \"%s\".", getName()); + return getValidationException(validationErrorMsg); + } + + if (!(value instanceof Double)) { + String validationErrorMsg = String.format( + Locale.ROOT, + "value is not an instance of Double for Double parameter [%s].", + getName() + ); + return getValidationException(validationErrorMsg); + } + + if (validatorWithData == null) { + return null; + } + + if (!validatorWithData.apply((Double) value, vectorSpaceInfo)) { + String validationErrorMsg = String.format(Locale.ROOT, "parameter validation failed for Double parameter [%s].", getName()); + return getValidationException(validationErrorMsg); + } + return null; + } + + private ValidationException getValidationException(String validationErrorMsg) { + ValidationException validationException = new ValidationException(); + validationException.addValidationError(validationErrorMsg); + return validationException; + } + } + /** * String method parameter */ diff --git a/src/main/java/org/opensearch/knn/index/codec/BasePerFieldKnnVectorsFormat.java b/src/main/java/org/opensearch/knn/index/codec/BasePerFieldKnnVectorsFormat.java index b9e280e2e..f3738452a 100644 --- a/src/main/java/org/opensearch/knn/index/codec/BasePerFieldKnnVectorsFormat.java +++ b/src/main/java/org/opensearch/knn/index/codec/BasePerFieldKnnVectorsFormat.java @@ -10,14 +10,19 @@ import org.apache.lucene.codecs.KnnVectorsFormat; import org.apache.lucene.codecs.perfield.PerFieldKnnVectorsFormat; import org.opensearch.index.mapper.MapperService; -import org.opensearch.knn.common.KNNConstants; +import org.opensearch.knn.index.codec.params.KNNScalarQuantizedVectorsFormatParams; +import org.opensearch.knn.index.codec.params.KNNVectorsFormatParams; import org.opensearch.knn.index.mapper.KNNVectorFieldMapper; +import org.opensearch.knn.index.util.KNNEngine; -import java.util.Map; import java.util.Optional; -import java.util.function.BiFunction; +import java.util.function.Function; import java.util.function.Supplier; +import static org.opensearch.knn.common.KNNConstants.LUCENE_SQ_BITS; +import static org.opensearch.knn.common.KNNConstants.LUCENE_SQ_CONFIDENCE_INTERVAL; +import static org.opensearch.knn.common.KNNConstants.METHOD_ENCODER_PARAMETER; + /** * Base class for PerFieldKnnVectorsFormat, builds KnnVectorsFormat based on specific Lucene version */ @@ -29,15 +34,34 @@ public abstract class BasePerFieldKnnVectorsFormat extends PerFieldKnnVectorsFor private final int defaultMaxConnections; private final int defaultBeamWidth; private final Supplier defaultFormatSupplier; - private final BiFunction formatSupplier; + private final Function vectorsFormatSupplier; + private Function scalarQuantizedVectorsFormatSupplier; + private static final String MAX_CONNECTIONS = "max_connections"; + private static final String BEAM_WIDTH = "beam_width"; + + public BasePerFieldKnnVectorsFormat( + Optional mapperService, + int defaultMaxConnections, + int defaultBeamWidth, + Supplier defaultFormatSupplier, + Function vectorsFormatSupplier + ) { + this.mapperService = mapperService; + this.defaultMaxConnections = defaultMaxConnections; + this.defaultBeamWidth = defaultBeamWidth; + this.defaultFormatSupplier = defaultFormatSupplier; + this.vectorsFormatSupplier = vectorsFormatSupplier; + } @Override public KnnVectorsFormat getKnnVectorsFormatForField(final String field) { if (isKnnVectorFieldType(field) == false) { log.debug( - "Initialize KNN vector format for field [{}] with default params [max_connections] = \"{}\" and [beam_width] = \"{}\"", + "Initialize KNN vector format for field [{}] with default params [{}] = \"{}\" and [{}] = \"{}\"", field, + MAX_CONNECTIONS, defaultMaxConnections, + BEAM_WIDTH, defaultBeamWidth ); return defaultFormatSupplier.get(); @@ -48,15 +72,43 @@ public KnnVectorsFormat getKnnVectorsFormatForField(final String field) { ) ).fieldType(field); var params = type.getKnnMethodContext().getMethodComponentContext().getParameters(); - int maxConnections = getMaxConnections(params); - int beamWidth = getBeamWidth(params); + + if (type.getKnnMethodContext().getKnnEngine() == KNNEngine.LUCENE + && params != null + && params.containsKey(METHOD_ENCODER_PARAMETER)) { + KNNScalarQuantizedVectorsFormatParams knnScalarQuantizedVectorsFormatParams = new KNNScalarQuantizedVectorsFormatParams( + params, + defaultMaxConnections, + defaultBeamWidth + ); + if (knnScalarQuantizedVectorsFormatParams.validate(params)) { + log.debug( + "Initialize KNN vector format for field [{}] with params [{}] = \"{}\", [{}] = \"{}\", [{}] = \"{}\", [{}] = \"{}\"", + field, + MAX_CONNECTIONS, + knnScalarQuantizedVectorsFormatParams.getMaxConnections(), + BEAM_WIDTH, + knnScalarQuantizedVectorsFormatParams.getBeamWidth(), + LUCENE_SQ_CONFIDENCE_INTERVAL, + knnScalarQuantizedVectorsFormatParams.getConfidenceInterval(), + LUCENE_SQ_BITS, + knnScalarQuantizedVectorsFormatParams.getBits() + ); + return scalarQuantizedVectorsFormatSupplier.apply(knnScalarQuantizedVectorsFormatParams); + } + + } + + KNNVectorsFormatParams knnVectorsFormatParams = new KNNVectorsFormatParams(params, defaultMaxConnections, defaultBeamWidth); log.debug( - "Initialize KNN vector format for field [{}] with params [max_connections] = \"{}\" and [beam_width] = \"{}\"", + "Initialize KNN vector format for field [{}] with params [{}] = \"{}\" and [{}] = \"{}\"", field, - maxConnections, - beamWidth + MAX_CONNECTIONS, + knnVectorsFormatParams.getMaxConnections(), + BEAM_WIDTH, + knnVectorsFormatParams.getBeamWidth() ); - return formatSupplier.apply(maxConnections, beamWidth); + return vectorsFormatSupplier.apply(knnVectorsFormatParams); } @Override @@ -67,18 +119,4 @@ public int getMaxDimensions(String fieldName) { private boolean isKnnVectorFieldType(final String field) { return mapperService.isPresent() && mapperService.get().fieldType(field) instanceof KNNVectorFieldMapper.KNNVectorFieldType; } - - private int getMaxConnections(final Map params) { - if (params != null && params.containsKey(KNNConstants.METHOD_PARAMETER_M)) { - return (int) params.get(KNNConstants.METHOD_PARAMETER_M); - } - return defaultMaxConnections; - } - - private int getBeamWidth(final Map params) { - if (params != null && params.containsKey(KNNConstants.METHOD_PARAMETER_EF_CONSTRUCTION)) { - return (int) params.get(KNNConstants.METHOD_PARAMETER_EF_CONSTRUCTION); - } - return defaultBeamWidth; - } } diff --git a/src/main/java/org/opensearch/knn/index/codec/KNN920Codec/KNN920PerFieldKnnVectorsFormat.java b/src/main/java/org/opensearch/knn/index/codec/KNN920Codec/KNN920PerFieldKnnVectorsFormat.java index ae1ef206c..7cca04319 100644 --- a/src/main/java/org/opensearch/knn/index/codec/KNN920Codec/KNN920PerFieldKnnVectorsFormat.java +++ b/src/main/java/org/opensearch/knn/index/codec/KNN920Codec/KNN920PerFieldKnnVectorsFormat.java @@ -22,7 +22,10 @@ public KNN920PerFieldKnnVectorsFormat(final Optional mapperServic Lucene92HnswVectorsFormat.DEFAULT_MAX_CONN, Lucene92HnswVectorsFormat.DEFAULT_BEAM_WIDTH, () -> new Lucene92HnswVectorsFormat(), - (maxConnm, beamWidth) -> new Lucene92HnswVectorsFormat(maxConnm, beamWidth) + knnVectorsFormatParams -> new Lucene92HnswVectorsFormat( + knnVectorsFormatParams.getMaxConnections(), + knnVectorsFormatParams.getBeamWidth() + ) ); } } diff --git a/src/main/java/org/opensearch/knn/index/codec/KNN940Codec/KNN940PerFieldKnnVectorsFormat.java b/src/main/java/org/opensearch/knn/index/codec/KNN940Codec/KNN940PerFieldKnnVectorsFormat.java index d9a1a9251..1ed9c929c 100644 --- a/src/main/java/org/opensearch/knn/index/codec/KNN940Codec/KNN940PerFieldKnnVectorsFormat.java +++ b/src/main/java/org/opensearch/knn/index/codec/KNN940Codec/KNN940PerFieldKnnVectorsFormat.java @@ -22,7 +22,10 @@ public KNN940PerFieldKnnVectorsFormat(final Optional mapperServic Lucene94HnswVectorsFormat.DEFAULT_MAX_CONN, Lucene94HnswVectorsFormat.DEFAULT_BEAM_WIDTH, () -> new Lucene94HnswVectorsFormat(), - (maxConnm, beamWidth) -> new Lucene94HnswVectorsFormat(maxConnm, beamWidth) + knnVectorsFormatParams -> new Lucene94HnswVectorsFormat( + knnVectorsFormatParams.getMaxConnections(), + knnVectorsFormatParams.getBeamWidth() + ) ); } } diff --git a/src/main/java/org/opensearch/knn/index/codec/KNN950Codec/KNN950PerFieldKnnVectorsFormat.java b/src/main/java/org/opensearch/knn/index/codec/KNN950Codec/KNN950PerFieldKnnVectorsFormat.java index 05ce7271f..978b22003 100644 --- a/src/main/java/org/opensearch/knn/index/codec/KNN950Codec/KNN950PerFieldKnnVectorsFormat.java +++ b/src/main/java/org/opensearch/knn/index/codec/KNN950Codec/KNN950PerFieldKnnVectorsFormat.java @@ -23,7 +23,10 @@ public KNN950PerFieldKnnVectorsFormat(final Optional mapperServic Lucene95HnswVectorsFormat.DEFAULT_MAX_CONN, Lucene95HnswVectorsFormat.DEFAULT_BEAM_WIDTH, () -> new Lucene95HnswVectorsFormat(), - (maxConnm, beamWidth) -> new Lucene95HnswVectorsFormat(maxConnm, beamWidth) + knnVectorsFormatParams -> new Lucene95HnswVectorsFormat( + knnVectorsFormatParams.getMaxConnections(), + knnVectorsFormatParams.getBeamWidth() + ) ); } diff --git a/src/main/java/org/opensearch/knn/index/codec/KNN990Codec/KNN990PerFieldKnnVectorsFormat.java b/src/main/java/org/opensearch/knn/index/codec/KNN990Codec/KNN990PerFieldKnnVectorsFormat.java index abf40f2ef..e8ecfad18 100644 --- a/src/main/java/org/opensearch/knn/index/codec/KNN990Codec/KNN990PerFieldKnnVectorsFormat.java +++ b/src/main/java/org/opensearch/knn/index/codec/KNN990Codec/KNN990PerFieldKnnVectorsFormat.java @@ -5,6 +5,7 @@ package org.opensearch.knn.index.codec.KNN990Codec; +import org.apache.lucene.codecs.lucene99.Lucene99HnswScalarQuantizedVectorsFormat; import org.apache.lucene.codecs.lucene99.Lucene99HnswVectorsFormat; import org.opensearch.index.mapper.MapperService; import org.opensearch.knn.index.codec.BasePerFieldKnnVectorsFormat; @@ -16,6 +17,7 @@ * Class provides per field format implementation for Lucene Knn vector type */ public class KNN990PerFieldKnnVectorsFormat extends BasePerFieldKnnVectorsFormat { + private static final int NUM_MERGE_WORKERS = 1; public KNN990PerFieldKnnVectorsFormat(final Optional mapperService) { super( @@ -23,7 +25,19 @@ public KNN990PerFieldKnnVectorsFormat(final Optional mapperServic Lucene99HnswVectorsFormat.DEFAULT_MAX_CONN, Lucene99HnswVectorsFormat.DEFAULT_BEAM_WIDTH, () -> new Lucene99HnswVectorsFormat(), - (maxConnm, beamWidth) -> new Lucene99HnswVectorsFormat(maxConnm, beamWidth) + knnVectorsFormatParams -> new Lucene99HnswVectorsFormat( + knnVectorsFormatParams.getMaxConnections(), + knnVectorsFormatParams.getBeamWidth() + ), + knnScalarQuantizedVectorsFormatParams -> new Lucene99HnswScalarQuantizedVectorsFormat( + knnScalarQuantizedVectorsFormatParams.getMaxConnections(), + knnScalarQuantizedVectorsFormatParams.getBeamWidth(), + NUM_MERGE_WORKERS, + knnScalarQuantizedVectorsFormatParams.getBits(), + knnScalarQuantizedVectorsFormatParams.isCompressFlag(), + knnScalarQuantizedVectorsFormatParams.getConfidenceInterval(), + null + ) ); } diff --git a/src/main/java/org/opensearch/knn/index/codec/params/KNNScalarQuantizedVectorsFormatParams.java b/src/main/java/org/opensearch/knn/index/codec/params/KNNScalarQuantizedVectorsFormatParams.java new file mode 100644 index 000000000..79bf1cbdb --- /dev/null +++ b/src/main/java/org/opensearch/knn/index/codec/params/KNNScalarQuantizedVectorsFormatParams.java @@ -0,0 +1,82 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.opensearch.knn.index.codec.params; + +import lombok.Getter; +import org.opensearch.knn.index.MethodComponentContext; + +import java.util.Map; + +import static org.opensearch.knn.common.KNNConstants.ENCODER_SQ; +import static org.opensearch.knn.common.KNNConstants.LUCENE_SQ_BITS; +import static org.opensearch.knn.common.KNNConstants.LUCENE_SQ_CONFIDENCE_INTERVAL; +import static org.opensearch.knn.common.KNNConstants.LUCENE_SQ_DEFAULT_BITS; +import static org.opensearch.knn.common.KNNConstants.METHOD_ENCODER_PARAMETER; + +/** + * Class provides params for LuceneHnswScalarQuantizedVectorsFormat + */ +@Getter +public class KNNScalarQuantizedVectorsFormatParams extends KNNVectorsFormatParams { + private Float confidenceInterval; + private int bits; + private boolean compressFlag; + + public KNNScalarQuantizedVectorsFormatParams(Map params, int defaultMaxConnections, int defaultBeamWidth) { + super(params, defaultMaxConnections, defaultBeamWidth); + MethodComponentContext encoderMethodComponentContext = (MethodComponentContext) params.get(METHOD_ENCODER_PARAMETER); + Map sqEncoderParams = encoderMethodComponentContext.getParameters(); + this.initConfidenceInterval(sqEncoderParams); + this.initBits(sqEncoderParams); + this.initCompressFlag(); + } + + @Override + public boolean validate(Map params) { + if (params.get(METHOD_ENCODER_PARAMETER) == null) { + return false; + } + + // Validate if the object is of type MethodComponentContext before casting it later + if (!(params.get(METHOD_ENCODER_PARAMETER) instanceof MethodComponentContext)) { + return false; + } + MethodComponentContext encoderMethodComponentContext = (MethodComponentContext) params.get(METHOD_ENCODER_PARAMETER); + if (!ENCODER_SQ.equals(encoderMethodComponentContext.getName())) { + return false; + } + + return true; + } + + private void initConfidenceInterval(final Map params) { + + if (params != null && params.containsKey(LUCENE_SQ_CONFIDENCE_INTERVAL)) { + if (params.get(LUCENE_SQ_CONFIDENCE_INTERVAL).equals(0)) { + this.confidenceInterval = (float) 0; + return; + } + this.confidenceInterval = ((Double) params.get(LUCENE_SQ_CONFIDENCE_INTERVAL)).floatValue(); + return; + } + + // If confidence_interval is not provided by user, then it will be set with a default value as null so that + // it will be computed later in Lucene based on the dimension of the vector as 1 - 1/(1 + d) + this.confidenceInterval = null; + } + + private void initBits(final Map params) { + if (params != null && params.containsKey(LUCENE_SQ_BITS)) { + this.bits = (int) params.get(LUCENE_SQ_BITS); + return; + } + this.bits = LUCENE_SQ_DEFAULT_BITS; + } + + private void initCompressFlag() { + this.compressFlag = true; + } +} diff --git a/src/main/java/org/opensearch/knn/index/codec/params/KNNVectorsFormatParams.java b/src/main/java/org/opensearch/knn/index/codec/params/KNNVectorsFormatParams.java new file mode 100644 index 000000000..52134bc7e --- /dev/null +++ b/src/main/java/org/opensearch/knn/index/codec/params/KNNVectorsFormatParams.java @@ -0,0 +1,45 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.opensearch.knn.index.codec.params; + +import lombok.Getter; +import org.opensearch.knn.common.KNNConstants; + +import java.util.Map; + +/** + * Class provides params for LuceneHNSWVectorsFormat + */ +@Getter +public class KNNVectorsFormatParams { + private int maxConnections; + private int beamWidth; + + public KNNVectorsFormatParams(final Map params, int defaultMaxConnections, int defaultBeamWidth) { + initMaxConnections(params, defaultMaxConnections); + initBeamWidth(params, defaultBeamWidth); + } + + public boolean validate(final Map params) { + return true; + } + + private void initMaxConnections(final Map params, int defaultMaxConnections) { + if (params != null && params.containsKey(KNNConstants.METHOD_PARAMETER_M)) { + this.maxConnections = (int) params.get(KNNConstants.METHOD_PARAMETER_M); + return; + } + this.maxConnections = defaultMaxConnections; + } + + private void initBeamWidth(final Map params, int defaultBeamWidth) { + if (params != null && params.containsKey(KNNConstants.METHOD_PARAMETER_EF_CONSTRUCTION)) { + this.beamWidth = (int) params.get(KNNConstants.METHOD_PARAMETER_EF_CONSTRUCTION); + return; + } + this.beamWidth = defaultBeamWidth; + } +} diff --git a/src/main/java/org/opensearch/knn/index/mapper/KNNVectorFieldMapper.java b/src/main/java/org/opensearch/knn/index/mapper/KNNVectorFieldMapper.java index 5c19a4989..026e9f469 100644 --- a/src/main/java/org/opensearch/knn/index/mapper/KNNVectorFieldMapper.java +++ b/src/main/java/org/opensearch/knn/index/mapper/KNNVectorFieldMapper.java @@ -349,7 +349,7 @@ private void validateEncoder(final KNNMethodContext knnMethodContext, final Vect return; } - if (VectorDataType.BINARY != vectorDataType) { + if (VectorDataType.FLOAT == vectorDataType) { return; } @@ -380,7 +380,7 @@ private void validateEncoder(final KNNMethodContext knnMethodContext, final Vect String.format( Locale.ROOT, "%s data type does not support %s encoder", - VectorDataType.BINARY.getValue(), + vectorDataType.getValue(), encoderMethodComponentContext.getName() ) ); diff --git a/src/main/java/org/opensearch/knn/index/util/Lucene.java b/src/main/java/org/opensearch/knn/index/util/Lucene.java index b5bbfca75..caf4200cb 100644 --- a/src/main/java/org/opensearch/knn/index/util/Lucene.java +++ b/src/main/java/org/opensearch/knn/index/util/Lucene.java @@ -7,19 +7,30 @@ import com.google.common.collect.ImmutableMap; import org.apache.lucene.util.Version; +import org.opensearch.knn.common.KNNConstants; import org.opensearch.knn.index.KNNMethod; import org.opensearch.knn.index.KNNSettings; import org.opensearch.knn.index.MethodComponent; +import org.opensearch.knn.index.MethodComponentContext; import org.opensearch.knn.index.Parameter; import org.opensearch.knn.index.SpaceType; +import java.util.Collections; import java.util.List; import java.util.Map; import java.util.function.Function; +import static org.opensearch.knn.common.KNNConstants.DYNAMIC_CONFIDENCE_INTERVAL; +import static org.opensearch.knn.common.KNNConstants.ENCODER_SQ; +import static org.opensearch.knn.common.KNNConstants.LUCENE_SQ_BITS; +import static org.opensearch.knn.common.KNNConstants.LUCENE_SQ_CONFIDENCE_INTERVAL; +import static org.opensearch.knn.common.KNNConstants.LUCENE_SQ_DEFAULT_BITS; +import static org.opensearch.knn.common.KNNConstants.MAXIMUM_CONFIDENCE_INTERVAL; +import static org.opensearch.knn.common.KNNConstants.METHOD_ENCODER_PARAMETER; import static org.opensearch.knn.common.KNNConstants.METHOD_HNSW; import static org.opensearch.knn.common.KNNConstants.METHOD_PARAMETER_EF_CONSTRUCTION; import static org.opensearch.knn.common.KNNConstants.METHOD_PARAMETER_M; +import static org.opensearch.knn.common.KNNConstants.MINIMUM_CONFIDENCE_INTERVAL; /** * KNN Library for Lucene @@ -27,6 +38,30 @@ public class Lucene extends JVMLibrary { Map> distanceTransform; + private static final List LUCENE_SQ_BITS_SUPPORTED = List.of(7); + + private final static MethodComponentContext ENCODER_DEFAULT = new MethodComponentContext( + KNNConstants.ENCODER_FLAT, + Collections.emptyMap() + ); + + private final static Map HNSW_ENCODERS = ImmutableMap.of( + ENCODER_SQ, + MethodComponent.Builder.builder(ENCODER_SQ) + .addParameter( + LUCENE_SQ_CONFIDENCE_INTERVAL, + new Parameter.DoubleParameter( + LUCENE_SQ_CONFIDENCE_INTERVAL, + null, + v -> v == DYNAMIC_CONFIDENCE_INTERVAL || (v >= MINIMUM_CONFIDENCE_INTERVAL && v <= MAXIMUM_CONFIDENCE_INTERVAL) + ) + ) + .addParameter( + LUCENE_SQ_BITS, + new Parameter.IntegerParameter(LUCENE_SQ_BITS, LUCENE_SQ_DEFAULT_BITS, LUCENE_SQ_BITS_SUPPORTED::contains) + ) + .build() + ); final static Map METHODS = ImmutableMap.of( METHOD_HNSW, @@ -44,6 +79,10 @@ public class Lucene extends JVMLibrary { v -> v > 0 ) ) + .addParameter( + METHOD_ENCODER_PARAMETER, + new Parameter.MethodComponentContextParameter(METHOD_ENCODER_PARAMETER, ENCODER_DEFAULT, HNSW_ENCODERS) + ) .build() ).addSpaces(SpaceType.UNDEFINED, SpaceType.L2, SpaceType.COSINESIMIL, SpaceType.INNER_PRODUCT).build() ); diff --git a/src/test/java/org/opensearch/knn/index/LuceneEngineIT.java b/src/test/java/org/opensearch/knn/index/LuceneEngineIT.java index 2170790ca..e7f38787d 100644 --- a/src/test/java/org/opensearch/knn/index/LuceneEngineIT.java +++ b/src/test/java/org/opensearch/knn/index/LuceneEngineIT.java @@ -33,11 +33,20 @@ import java.util.function.Function; import java.util.stream.Collectors; +import static org.opensearch.knn.common.KNNConstants.ENCODER_SQ; +import static org.opensearch.knn.common.KNNConstants.LUCENE_SQ_BITS; +import static org.opensearch.knn.common.KNNConstants.LUCENE_SQ_CONFIDENCE_INTERVAL; +import static org.opensearch.knn.common.KNNConstants.LUCENE_SQ_DEFAULT_BITS; +import static org.opensearch.knn.common.KNNConstants.MAXIMUM_CONFIDENCE_INTERVAL; import static org.opensearch.knn.common.KNNConstants.MAX_DISTANCE; +import static org.opensearch.knn.common.KNNConstants.METHOD_ENCODER_PARAMETER; import static org.opensearch.knn.common.KNNConstants.METHOD_HNSW; import static org.opensearch.knn.common.KNNConstants.METHOD_PARAMETER; +import static org.opensearch.knn.common.KNNConstants.MINIMUM_CONFIDENCE_INTERVAL; import static org.opensearch.knn.common.KNNConstants.MIN_SCORE; +import static org.opensearch.knn.common.KNNConstants.NAME; import static org.opensearch.knn.common.KNNConstants.NMSLIB_NAME; +import static org.opensearch.knn.common.KNNConstants.PARAMETERS; import static org.opensearch.knn.common.KNNConstants.VECTOR_DATA_TYPE_FIELD; public class LuceneEngineIT extends KNNRestTestCase { @@ -466,6 +475,197 @@ public void testRadiusSearch_usingScoreThreshold_withFilter_usingCosineMetrics_u validateRadiusSearchResults(TEST_QUERY_VECTORS, null, score, SpaceType.COSINESIMIL, expectedResults, COLOR_FIELD_NAME, "red", null); } + @SneakyThrows + public void testSQ_withInvalidParams_thenThrowException() { + + // Use invalid number of bits for the bits param which throws an exception + int bits = -1; + expectThrows( + ResponseException.class, + () -> createKnnIndexMappingWithLuceneEngineAndSQEncoder( + DIMENSION, + SpaceType.L2, + VectorDataType.FLOAT, + bits, + MINIMUM_CONFIDENCE_INTERVAL + ) + ); + + // Use invalid value for confidence_interval param which throws an exception + double confidenceInterval = -2.5; + expectThrows( + ResponseException.class, + () -> createKnnIndexMappingWithLuceneEngineAndSQEncoder( + DIMENSION, + SpaceType.L2, + VectorDataType.FLOAT, + LUCENE_SQ_DEFAULT_BITS, + confidenceInterval + ) + ); + + // Use "byte" data_type with sq encoder which throws an exception + expectThrows( + ResponseException.class, + () -> createKnnIndexMappingWithLuceneEngineAndSQEncoder( + DIMENSION, + SpaceType.L2, + VectorDataType.BYTE, + LUCENE_SQ_DEFAULT_BITS, + MINIMUM_CONFIDENCE_INTERVAL + ) + ); + } + + @SneakyThrows + public void testAddDocWithSQEncoder() { + createKnnIndexMappingWithLuceneEngineAndSQEncoder( + DIMENSION, + SpaceType.L2, + VectorDataType.FLOAT, + LUCENE_SQ_DEFAULT_BITS, + MAXIMUM_CONFIDENCE_INTERVAL + ); + Float[] vector = new Float[] { 2.0f, 4.5f, 6.5f }; + addKnnDoc(INDEX_NAME, DOC_ID, FIELD_NAME, vector); + + refreshIndex(INDEX_NAME); + assertEquals(1, getDocCount(INDEX_NAME)); + } + + @SneakyThrows + public void testUpdateDocWithSQEncoder() { + createKnnIndexMappingWithLuceneEngineAndSQEncoder( + DIMENSION, + SpaceType.INNER_PRODUCT, + VectorDataType.FLOAT, + LUCENE_SQ_DEFAULT_BITS, + MAXIMUM_CONFIDENCE_INTERVAL + ); + Float[] vector = { 6.0f, 6.0f, 7.0f }; + addKnnDoc(INDEX_NAME, DOC_ID, FIELD_NAME, vector); + + Float[] updatedVector = { 8.0f, 8.0f, 8.0f }; + updateKnnDoc(INDEX_NAME, DOC_ID, FIELD_NAME, updatedVector); + + refreshIndex(INDEX_NAME); + assertEquals(1, getDocCount(INDEX_NAME)); + } + + @SneakyThrows + public void testDeleteDocWithSQEncoder() { + createKnnIndexMappingWithLuceneEngineAndSQEncoder( + DIMENSION, + SpaceType.INNER_PRODUCT, + VectorDataType.FLOAT, + LUCENE_SQ_DEFAULT_BITS, + MAXIMUM_CONFIDENCE_INTERVAL + ); + Float[] vector = { 6.0f, 6.0f, 7.0f }; + addKnnDoc(INDEX_NAME, DOC_ID, FIELD_NAME, vector); + + deleteKnnDoc(INDEX_NAME, DOC_ID); + + refreshIndex(INDEX_NAME); + assertEquals(0, getDocCount(INDEX_NAME)); + } + + @SneakyThrows + public void testIndexingAndQueryingWithSQEncoder() { + createKnnIndexMappingWithLuceneEngineAndSQEncoder( + DIMENSION, + SpaceType.INNER_PRODUCT, + VectorDataType.FLOAT, + LUCENE_SQ_DEFAULT_BITS, + MAXIMUM_CONFIDENCE_INTERVAL + ); + + int numDocs = 10; + for (int i = 0; i < numDocs; i++) { + float[] indexVector = new float[DIMENSION]; + Arrays.fill(indexVector, (float) i); + addKnnDocWithAttributes(INDEX_NAME, Integer.toString(i), FIELD_NAME, indexVector, ImmutableMap.of("rating", String.valueOf(i))); + } + + // Assert that all docs are ingested + refreshAllNonSystemIndices(); + assertEquals(numDocs, getDocCount(INDEX_NAME)); + + float[] queryVector = new float[DIMENSION]; + Arrays.fill(queryVector, (float) numDocs); + int k = 10; + + Response searchResponse = searchKNNIndex(INDEX_NAME, new KNNQueryBuilder(FIELD_NAME, queryVector, k), k); + List results = parseSearchResponse(EntityUtils.toString(searchResponse.getEntity()), FIELD_NAME); + assertEquals(k, results.size()); + for (int i = 0; i < k; i++) { + assertEquals(numDocs - i - 1, Integer.parseInt(results.get(i).getDocId())); + } + } + + public void testQueryWithFilterUsingSQEncoder() throws Exception { + createKnnIndexMappingWithLuceneEngineAndSQEncoder( + DIMENSION, + SpaceType.INNER_PRODUCT, + VectorDataType.FLOAT, + LUCENE_SQ_DEFAULT_BITS, + MAXIMUM_CONFIDENCE_INTERVAL + ); + + addKnnDocWithAttributes( + DOC_ID, + new float[] { 6.0f, 7.9f, 3.1f }, + ImmutableMap.of(COLOR_FIELD_NAME, "red", TASTE_FIELD_NAME, "sweet") + ); + addKnnDocWithAttributes(DOC_ID_2, new float[] { 3.2f, 2.1f, 4.8f }, ImmutableMap.of(COLOR_FIELD_NAME, "green")); + addKnnDocWithAttributes(DOC_ID_3, new float[] { 4.1f, 5.0f, 7.1f }, ImmutableMap.of(COLOR_FIELD_NAME, "red")); + + refreshIndex(INDEX_NAME); + + final float[] searchVector = { 6.0f, 6.0f, 4.1f }; + List expectedDocIdsKGreaterThanFilterResult = Arrays.asList(DOC_ID, DOC_ID_3); + List expectedDocIdsKLimitsFilterResult = Arrays.asList(DOC_ID); + validateQueryResultsWithFilters(searchVector, 5, 1, expectedDocIdsKGreaterThanFilterResult, expectedDocIdsKLimitsFilterResult); + } + + private void createKnnIndexMappingWithLuceneEngineAndSQEncoder( + int dimension, + SpaceType spaceType, + VectorDataType vectorDataType, + int bits, + double confidenceInterval + ) throws Exception { + XContentBuilder builder = XContentFactory.jsonBuilder() + .startObject() + .startObject(PROPERTIES_FIELD_NAME) + .startObject(FIELD_NAME) + .field(TYPE_FIELD_NAME, KNN_VECTOR_TYPE) + .field(DIMENSION_FIELD_NAME, dimension) + .field(VECTOR_DATA_TYPE_FIELD, vectorDataType) + .startObject(KNNConstants.KNN_METHOD) + .field(KNNConstants.NAME, KNNEngine.LUCENE.getMethod(METHOD_HNSW).getMethodComponent().getName()) + .field(KNNConstants.METHOD_PARAMETER_SPACE_TYPE, spaceType.getValue()) + .field(KNNConstants.KNN_ENGINE, KNNEngine.LUCENE.getName()) + .startObject(KNNConstants.PARAMETERS) + .field(KNNConstants.METHOD_PARAMETER_M, M) + .field(KNNConstants.METHOD_PARAMETER_EF_CONSTRUCTION, EF_CONSTRUCTION) + .startObject(METHOD_ENCODER_PARAMETER) + .field(NAME, ENCODER_SQ) + .startObject(PARAMETERS) + .field(LUCENE_SQ_BITS, bits) + .field(LUCENE_SQ_CONFIDENCE_INTERVAL, confidenceInterval) + .endObject() + .endObject() + .endObject() + .endObject() + .endObject() + .endObject() + .endObject(); + + String mapping = builder.toString(); + createKnnIndex(INDEX_NAME, mapping); + } + private void createKnnIndexMappingWithLuceneEngine(int dimension, SpaceType spaceType, VectorDataType vectorDataType) throws Exception { XContentBuilder builder = XContentFactory.jsonBuilder() .startObject() diff --git a/src/test/java/org/opensearch/knn/index/ParameterTests.java b/src/test/java/org/opensearch/knn/index/ParameterTests.java index 2f3f19727..18b892499 100644 --- a/src/test/java/org/opensearch/knn/index/ParameterTests.java +++ b/src/test/java/org/opensearch/knn/index/ParameterTests.java @@ -125,6 +125,40 @@ public void testStringParameter_validateWithData() { assertNotNull(parameter.validateWithData("test", testVectorSpaceInfo)); } + public void testDoubleParameter_validate() { + final Parameter.DoubleParameter parameter = new Parameter.DoubleParameter("test_parameter", 1.0, v -> v >= 0); + + // valid value + assertNull(parameter.validate(0.9)); + + // Invalid type + assertNotNull(parameter.validate(true)); + + // Invalid type + assertNotNull(parameter.validate(-1)); + + } + + public void testDoubleParameter_validateWithData() { + final Parameter.DoubleParameter parameter = new Parameter.DoubleParameter( + "test", + 1.0, + v -> v > 0, + (v, vectorSpaceInfo) -> v > vectorSpaceInfo.getDimension() + ); + + VectorSpaceInfo testVectorSpaceInfo = new VectorSpaceInfo(0); + + // Invalid type + assertNotNull(parameter.validateWithData("String", testVectorSpaceInfo)); + + // Invalid value + assertNotNull(parameter.validateWithData(-1, testVectorSpaceInfo)); + + // valid value + assertNull(parameter.validateWithData(1.2, testVectorSpaceInfo)); + } + public void testMethodComponentContextParameter_validate() { String methodComponentName1 = "method-1"; String parameterKey1 = "parameter_key_1"; diff --git a/src/test/java/org/opensearch/knn/index/codec/params/KNNScalarQuantizedVectorsFormatParamsTests.java b/src/test/java/org/opensearch/knn/index/codec/params/KNNScalarQuantizedVectorsFormatParamsTests.java new file mode 100644 index 000000000..bcba8ebbd --- /dev/null +++ b/src/test/java/org/opensearch/knn/index/codec/params/KNNScalarQuantizedVectorsFormatParamsTests.java @@ -0,0 +1,110 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + * + * Modifications Copyright OpenSearch Contributors. See + * GitHub history for details. + */ + +package org.opensearch.knn.index.codec.params; + +import junit.framework.TestCase; +import org.opensearch.knn.index.MethodComponentContext; + +import java.util.HashMap; +import java.util.Map; + +import static org.opensearch.knn.common.KNNConstants.ENCODER_SQ; +import static org.opensearch.knn.common.KNNConstants.LUCENE_SQ_CONFIDENCE_INTERVAL; +import static org.opensearch.knn.common.KNNConstants.LUCENE_SQ_DEFAULT_BITS; +import static org.opensearch.knn.common.KNNConstants.METHOD_ENCODER_PARAMETER; +import static org.opensearch.knn.common.KNNConstants.METHOD_PARAMETER_EF_CONSTRUCTION; +import static org.opensearch.knn.common.KNNConstants.METHOD_PARAMETER_M; +import static org.opensearch.knn.common.KNNConstants.MINIMUM_CONFIDENCE_INTERVAL; + +public class KNNScalarQuantizedVectorsFormatParamsTests extends TestCase { + private static final int DEFAULT_MAX_CONNECTIONS = 16; + private static final int DEFAULT_BEAM_WIDTH = 100; + + public void testInitParams_whenCalled_thenReturnDefaultParams() { + KNNScalarQuantizedVectorsFormatParams knnScalarQuantizedVectorsFormatParams = new KNNScalarQuantizedVectorsFormatParams( + getDefaultParamsForConstructor(), + DEFAULT_MAX_CONNECTIONS, + DEFAULT_BEAM_WIDTH + ); + + assertEquals(DEFAULT_MAX_CONNECTIONS, knnScalarQuantizedVectorsFormatParams.getMaxConnections()); + assertEquals(DEFAULT_BEAM_WIDTH, knnScalarQuantizedVectorsFormatParams.getBeamWidth()); + assertNull(knnScalarQuantizedVectorsFormatParams.getConfidenceInterval()); + assertTrue(knnScalarQuantizedVectorsFormatParams.isCompressFlag()); + assertEquals(LUCENE_SQ_DEFAULT_BITS, knnScalarQuantizedVectorsFormatParams.getBits()); + } + + public void testInitParams_whenCalled_thenReturnParams() { + int m = 64; + int efConstruction = 128; + + Map encoderParams = new HashMap<>(); + encoderParams.put(LUCENE_SQ_CONFIDENCE_INTERVAL, MINIMUM_CONFIDENCE_INTERVAL); + MethodComponentContext encoderComponentContext = new MethodComponentContext(ENCODER_SQ, encoderParams); + + Map params = new HashMap<>(); + params.put(METHOD_ENCODER_PARAMETER, encoderComponentContext); + params.put(METHOD_PARAMETER_M, m); + params.put(METHOD_PARAMETER_EF_CONSTRUCTION, efConstruction); + + KNNScalarQuantizedVectorsFormatParams knnScalarQuantizedVectorsFormatParams = new KNNScalarQuantizedVectorsFormatParams( + params, + DEFAULT_MAX_CONNECTIONS, + DEFAULT_BEAM_WIDTH + ); + + assertEquals(m, knnScalarQuantizedVectorsFormatParams.getMaxConnections()); + assertEquals(efConstruction, knnScalarQuantizedVectorsFormatParams.getBeamWidth()); + assertEquals((float) MINIMUM_CONFIDENCE_INTERVAL, knnScalarQuantizedVectorsFormatParams.getConfidenceInterval()); + assertTrue(knnScalarQuantizedVectorsFormatParams.isCompressFlag()); + assertEquals(LUCENE_SQ_DEFAULT_BITS, knnScalarQuantizedVectorsFormatParams.getBits()); + } + + public void testValidate_whenCalled_thenReturnTrue() { + Map params = getDefaultParamsForConstructor(); + KNNScalarQuantizedVectorsFormatParams knnScalarQuantizedVectorsFormatParams = new KNNScalarQuantizedVectorsFormatParams( + params, + DEFAULT_MAX_CONNECTIONS, + DEFAULT_BEAM_WIDTH + ); + assertTrue(knnScalarQuantizedVectorsFormatParams.validate(params)); + } + + public void testValidate_whenCalled_thenReturnFalse() { + KNNScalarQuantizedVectorsFormatParams knnScalarQuantizedVectorsFormatParams = new KNNScalarQuantizedVectorsFormatParams( + getDefaultParamsForConstructor(), + DEFAULT_MAX_CONNECTIONS, + DEFAULT_BEAM_WIDTH + ); + Map params = new HashMap<>(); + + // Return false if encoder value is null + params.put(METHOD_ENCODER_PARAMETER, null); + assertFalse(knnScalarQuantizedVectorsFormatParams.validate(params)); + + // Return false if encoder value is not an instance of MethodComponentContext + params.replace(METHOD_ENCODER_PARAMETER, "dummy string"); + assertFalse(knnScalarQuantizedVectorsFormatParams.validate(params)); + + // Return false if encoder name is not "sq" + MethodComponentContext encoderComponentContext = new MethodComponentContext("invalid encoder name", new HashMap<>()); + params.replace(METHOD_ENCODER_PARAMETER, encoderComponentContext); + assertFalse(knnScalarQuantizedVectorsFormatParams.validate(params)); + } + + private Map getDefaultParamsForConstructor() { + MethodComponentContext encoderComponentContext = new MethodComponentContext(ENCODER_SQ, new HashMap<>()); + Map params = new HashMap<>(); + params.put(METHOD_ENCODER_PARAMETER, encoderComponentContext); + return params; + } +} diff --git a/src/test/java/org/opensearch/knn/index/codec/params/KNNVectorsFormatParamsTests.java b/src/test/java/org/opensearch/knn/index/codec/params/KNNVectorsFormatParamsTests.java new file mode 100644 index 000000000..dca054046 --- /dev/null +++ b/src/test/java/org/opensearch/knn/index/codec/params/KNNVectorsFormatParamsTests.java @@ -0,0 +1,56 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + * + * Modifications Copyright OpenSearch Contributors. See + * GitHub history for details. + */ + +package org.opensearch.knn.index.codec.params; + +import junit.framework.TestCase; + +import java.util.HashMap; +import java.util.Map; + +import static org.opensearch.knn.common.KNNConstants.METHOD_PARAMETER_EF_CONSTRUCTION; +import static org.opensearch.knn.common.KNNConstants.METHOD_PARAMETER_M; + +public class KNNVectorsFormatParamsTests extends TestCase { + private static final int DEFAULT_MAX_CONNECTIONS = 16; + private static final int DEFAULT_BEAM_WIDTH = 100; + + public void testInitParams_whenCalled_thenReturnDefaultParams() { + KNNVectorsFormatParams knnVectorsFormatParams = new KNNVectorsFormatParams( + new HashMap<>(), + DEFAULT_MAX_CONNECTIONS, + DEFAULT_BEAM_WIDTH + ); + assertEquals(DEFAULT_MAX_CONNECTIONS, knnVectorsFormatParams.getMaxConnections()); + assertEquals(DEFAULT_BEAM_WIDTH, knnVectorsFormatParams.getBeamWidth()); + } + + public void testInitParams_whenCalled_thenReturnParams() { + int m = 64; + int efConstruction = 128; + Map params = new HashMap<>(); + params.put(METHOD_PARAMETER_M, m); + params.put(METHOD_PARAMETER_EF_CONSTRUCTION, efConstruction); + + KNNVectorsFormatParams knnVectorsFormatParams = new KNNVectorsFormatParams(params, DEFAULT_MAX_CONNECTIONS, DEFAULT_BEAM_WIDTH); + assertEquals(m, knnVectorsFormatParams.getMaxConnections()); + assertEquals(efConstruction, knnVectorsFormatParams.getBeamWidth()); + } + + public void testValidate_whenCalled_thenReturnTrue() { + KNNVectorsFormatParams knnVectorsFormatParams = new KNNVectorsFormatParams( + new HashMap<>(), + DEFAULT_MAX_CONNECTIONS, + DEFAULT_BEAM_WIDTH + ); + assertTrue(knnVectorsFormatParams.validate(new HashMap<>())); + } +}