Skip to content

Commit fbd6e65

Browse files
author
Dooyong Kim
committed
Added index scope setting 'index.knn.memory_optimized_search'
Signed-off-by: Dooyong Kim <[email protected]> Co-authored-by: Dooyong Kim <[email protected]> tmp
1 parent db64b12 commit fbd6e65

11 files changed

+259
-89
lines changed

Diff for: src/main/java/org/opensearch/knn/index/KNNSettings.java

+27-4
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55

66
package org.opensearch.knn.index;
77

8+
import lombok.NonNull;
89
import lombok.extern.log4j.Log4j2;
910
import org.apache.logging.log4j.LogManager;
1011
import org.apache.logging.log4j.Logger;
@@ -96,6 +97,8 @@ public class KNNSettings {
9697
public static final String KNN_DERIVED_SOURCE_ENABLED = "index.knn.derived_source.enabled";
9798
public static final String KNN_INDEX_REMOTE_VECTOR_BUILD = "index.knn.remote_index_build.enabled";
9899
public static final String KNN_REMOTE_VECTOR_REPO = "knn.remote_index_build.vector_repo";
100+
public static final String MEMORY_OPTIMIZED_KNN_SEARCH_MODE = "index.knn.memory_optimized_search";
101+
public static final boolean DEFAULT_MEMORY_OPTIMIZED_KNN_SEARCH_MODE = false;
99102

100103
/**
101104
* Default setting values
@@ -121,9 +124,9 @@ public class KNNSettings {
121124

122125
public static final Integer ADVANCED_FILTERED_EXACT_SEARCH_THRESHOLD_DEFAULT_VALUE = -1;
123126
public static final Integer KNN_DEFAULT_QUANTIZATION_STATE_CACHE_SIZE_LIMIT_PERCENTAGE = 5; // By default, set aside 5% of the JVM for
124-
// the limit
127+
// the limit
125128
public static final Integer KNN_MAX_QUANTIZATION_STATE_CACHE_SIZE_LIMIT_PERCENTAGE = 10; // Quantization state cache limit cannot exceed
126-
// 10% of the JVM heap
129+
// 10% of the JVM heap
127130
public static final Integer KNN_DEFAULT_QUANTIZATION_STATE_CACHE_EXPIRY_TIME_MINUTES = 60;
128131
public static final boolean KNN_DISK_VECTOR_SHARD_LEVEL_RESCORING_DISABLED_VALUE = false;
129132

@@ -285,6 +288,12 @@ public class KNNSettings {
285288
*/
286289
public static final Setting<Boolean> IS_KNN_INDEX_SETTING = Setting.boolSetting(KNN_INDEX, false, IndexScope, Final);
287290

291+
public static final Setting<Boolean> MEMORY_OPTIMIZED_KNN_SEARCH_MODE_SETTING = Setting.boolSetting(
292+
MEMORY_OPTIMIZED_KNN_SEARCH_MODE,
293+
false,
294+
IndexScope
295+
);
296+
288297
/**
289298
* index_thread_quantity - the parameter specifies how many threads the nms library should use to create the graph.
290299
* By default, the nms library sets this value to NUM_CORES. However, because ES can spawn NUM_CORES threads for
@@ -577,7 +586,8 @@ public List<Setting<?>> getSettings() {
577586
KNN_DISK_VECTOR_SHARD_LEVEL_RESCORING_DISABLED_SETTING,
578587
KNN_DERIVED_SOURCE_ENABLED_SETTING,
579588
KNN_INDEX_REMOTE_VECTOR_BUILD_SETTING,
580-
KNN_REMOTE_VECTOR_REPO_SETTING
589+
KNN_REMOTE_VECTOR_REPO_SETTING,
590+
MEMORY_OPTIMIZED_KNN_SEARCH_MODE_SETTING
581591
);
582592
return Stream.concat(settings.stream(), Stream.concat(getFeatureFlags().stream(), dynamicCacheSettings.values().stream()))
583593
.collect(Collectors.toList());
@@ -734,12 +744,25 @@ public static int getEfSearchParam(String index) {
734744
);
735745
}
736746

747+
/**
748+
* Return whether memory optimized search enabled for the given index.
749+
*
750+
* @param indexName The name of target index to test whether if it is on.
751+
* @return True if memory optimized search is enabled, otherwise False.
752+
*/
753+
public static boolean isMemoryOptimizedKnnSearchModeEnabled(@NonNull final String indexName) {
754+
return KNNSettings.state().clusterService.state()
755+
.getMetadata()
756+
.index(indexName)
757+
.getSettings()
758+
.getAsBoolean(MEMORY_OPTIMIZED_KNN_SEARCH_MODE, DEFAULT_MEMORY_OPTIMIZED_KNN_SEARCH_MODE);
759+
}
760+
737761
public void setClusterService(ClusterService clusterService) {
738762
this.clusterService = clusterService;
739763
}
740764

741765
static class SpaceTypeValidator implements Setting.Validator<String> {
742-
743766
@Override
744767
public void validate(String value) {
745768
try {

Diff for: src/main/java/org/opensearch/knn/index/codec/BasePerFieldKnnVectorsFormat.java

+21-2
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
import org.apache.lucene.codecs.hnsw.FlatVectorScorerUtil;
1212
import org.apache.lucene.codecs.lucene99.Lucene99FlatVectorsFormat;
1313
import org.apache.lucene.codecs.perfield.PerFieldKnnVectorsFormat;
14+
import org.opensearch.common.settings.Settings;
1415
import org.opensearch.index.IndexSettings;
1516
import org.opensearch.index.mapper.MapperService;
1617
import org.opensearch.knn.index.KNNSettings;
@@ -31,6 +32,8 @@
3132
import static org.opensearch.knn.common.KNNConstants.LUCENE_SQ_BITS;
3233
import static org.opensearch.knn.common.KNNConstants.LUCENE_SQ_CONFIDENCE_INTERVAL;
3334
import static org.opensearch.knn.common.KNNConstants.METHOD_ENCODER_PARAMETER;
35+
import static org.opensearch.knn.index.KNNSettings.DEFAULT_MEMORY_OPTIMIZED_KNN_SEARCH_MODE;
36+
import static org.opensearch.knn.index.KNNSettings.MEMORY_OPTIMIZED_KNN_SEARCH_MODE;
3437

3538
/**
3639
* Base class for PerFieldKnnVectorsFormat, builds KnnVectorsFormat based on specific Lucene version
@@ -155,14 +158,30 @@ public KnnVectorsFormat getKnnVectorsFormatForField(final String field) {
155158
private NativeEngines990KnnVectorsFormat nativeEngineVectorsFormat() {
156159
// mapperService is already checked for null or valid instance type at caller, hence we don't need
157160
// addition isPresent check here.
158-
int approximateThreshold = getApproximateThresholdValue();
161+
final int approximateThreshold = getApproximateThresholdValue();
162+
final boolean memoryOptimizedSearchSupported = isMemoryOptimizedSearchSupported();
159163
return new NativeEngines990KnnVectorsFormat(
160164
new Lucene99FlatVectorsFormat(FlatVectorScorerUtil.getLucene99FlatVectorsScorer()),
161165
approximateThreshold,
162-
nativeIndexBuildStrategyFactory
166+
nativeIndexBuildStrategyFactory,
167+
memoryOptimizedSearchSupported
163168
);
164169
}
165170

171+
private boolean isMemoryOptimizedSearchSupported() {
172+
if (nativeIndexBuildStrategyFactory != null && nativeIndexBuildStrategyFactory.getIndexSettings() != null) {
173+
final Settings settings = nativeIndexBuildStrategyFactory.getIndexSettings().getSettings();
174+
if (settings != null) {
175+
try {
176+
return settings.getAsBoolean(MEMORY_OPTIMIZED_KNN_SEARCH_MODE, DEFAULT_MEMORY_OPTIMIZED_KNN_SEARCH_MODE);
177+
} catch (Throwable th) {
178+
log.error("Failed to get a bool flag of [{}] from settings.", MEMORY_OPTIMIZED_KNN_SEARCH_MODE);
179+
}
180+
}
181+
}
182+
return false;
183+
}
184+
166185
private int getApproximateThresholdValue() {
167186
// This is private method and mapperService is already checked for null or valid instance type before this call
168187
// at caller, hence we don't need additional isPresent check here.

Diff for: src/main/java/org/opensearch/knn/index/codec/KNN990Codec/NativeEngines990KnnVectorsFormat.java

+6-3
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ public class NativeEngines990KnnVectorsFormat extends KnnVectorsFormat {
3535
private static final String FORMAT_NAME = "NativeEngines990KnnVectorsFormat";
3636
private static int approximateThreshold;
3737
private final NativeIndexBuildStrategyFactory nativeIndexBuildStrategyFactory;
38+
private final boolean memoryOptimizedSearchEnabled;
3839

3940
public NativeEngines990KnnVectorsFormat() {
4041
this(new Lucene99FlatVectorsFormat(new DefaultFlatVectorScorer()));
@@ -49,18 +50,20 @@ public NativeEngines990KnnVectorsFormat(final FlatVectorsFormat flatVectorsForma
4950
}
5051

5152
public NativeEngines990KnnVectorsFormat(final FlatVectorsFormat flatVectorsFormat, int approximateThreshold) {
52-
this(flatVectorsFormat, approximateThreshold, new NativeIndexBuildStrategyFactory());
53+
this(flatVectorsFormat, approximateThreshold, new NativeIndexBuildStrategyFactory(), false);
5354
}
5455

5556
public NativeEngines990KnnVectorsFormat(
5657
final FlatVectorsFormat flatVectorsFormat,
5758
int approximateThreshold,
58-
final NativeIndexBuildStrategyFactory nativeIndexBuildStrategyFactory
59+
final NativeIndexBuildStrategyFactory nativeIndexBuildStrategyFactory,
60+
final boolean memoryOptimizedSearchEnabled
5961
) {
6062
super(FORMAT_NAME);
6163
NativeEngines990KnnVectorsFormat.flatVectorsFormat = flatVectorsFormat;
6264
NativeEngines990KnnVectorsFormat.approximateThreshold = approximateThreshold;
6365
this.nativeIndexBuildStrategyFactory = nativeIndexBuildStrategyFactory;
66+
this.memoryOptimizedSearchEnabled = memoryOptimizedSearchEnabled;
6467
}
6568

6669
/**
@@ -85,7 +88,7 @@ public KnnVectorsWriter fieldsWriter(final SegmentWriteState state) throws IOExc
8588
*/
8689
@Override
8790
public KnnVectorsReader fieldsReader(final SegmentReadState state) throws IOException {
88-
return new NativeEngines990KnnVectorsReader(state, flatVectorsFormat.fieldsReader(state));
91+
return new NativeEngines990KnnVectorsReader(state, flatVectorsFormat.fieldsReader(state), memoryOptimizedSearchEnabled);
8992
}
9093

9194
/**

Diff for: src/main/java/org/opensearch/knn/index/codec/KNN990Codec/NativeEngines990KnnVectorsReader.java

+84-75
Original file line numberDiff line numberDiff line change
@@ -59,85 +59,27 @@ public class NativeEngines990KnnVectorsReader extends KnnVectorsReader {
5959

6060
private final FlatVectorsReader flatVectorsReader;
6161
private Map<String, String> quantizationStateCacheKeyPerField;
62-
private SegmentReadState segmentReadState;
62+
private final SegmentReadState segmentReadState;
6363
private final List<String> cacheKeys;
6464
private volatile Map<String, VectorSearcher> vectorSearchers;
6565

6666
public NativeEngines990KnnVectorsReader(final SegmentReadState state, final FlatVectorsReader flatVectorsReader) {
67+
this(state, flatVectorsReader, false);
68+
}
69+
70+
public NativeEngines990KnnVectorsReader(
71+
final SegmentReadState state,
72+
final FlatVectorsReader flatVectorsReader,
73+
final boolean memoryOptimizedSearchEnabled
74+
) {
6775
this.flatVectorsReader = flatVectorsReader;
6876
this.segmentReadState = state;
6977
this.cacheKeys = getVectorCacheKeysFromSegmentReaderState(state);
70-
this.vectorSearchers = null;
7178

7279
loadCacheKeyMap();
7380

74-
// Memory optimized searcher will be ONLY used for searching, we don't need this during merging.
75-
// TODO(KDY) : Enable this based on setting flag value. e.g. index.knn.memory_optimized_search = True
76-
if (state.context.context() != IOContext.Context.MERGE) {
77-
loadMemoryOptimizedSearcher();
78-
}
79-
}
80-
81-
private IOSupplier<VectorSearcher> getIndexFileNameIfMemoryOptimizedSearchSupported(final FieldInfo fieldInfo) {
82-
// Skip non-knn fields.
83-
final Map<String, String> attributes = fieldInfo.attributes();
84-
if (attributes == null || attributes.containsKey(KNN_FIELD) == false) {
85-
return null;
86-
}
87-
88-
// Get engine
89-
final String engineName = attributes.getOrDefault(KNN_ENGINE, KNNEngine.DEFAULT.getName());
90-
final KNNEngine knnEngine = KNNEngine.getEngine(engineName);
91-
92-
// Get memory optimized searcher from engine
93-
final VectorSearcherFactory searcherFactory = knnEngine.getVectorSearcherFactory();
94-
if (searcherFactory == null) {
95-
// It's not supported
96-
return null;
97-
}
98-
99-
// Start creating searcher
100-
final String fileName = KNNCodecUtil.getNativeEngineFileFromFieldInfo(fieldInfo, segmentReadState.segmentInfo);
101-
if (fileName != null) {
102-
return () -> searcherFactory.createVectorSearcher(segmentReadState.directory, fileName);
103-
}
104-
105-
// Not supported
106-
return null;
107-
}
108-
109-
private synchronized void loadMemoryOptimizedSearcher() {
110-
if (vectorSearchers != null) {
111-
return;
112-
}
113-
114-
final Map<String, VectorSearcher> vectorSearcherPerField = new HashMap<>(
115-
RESERVE_TWICE_SPACE * segmentReadState.fieldInfos.size(),
116-
SUFFICIENT_LOAD_FACTOR
117-
);
118-
119-
try {
120-
for (FieldInfo fieldInfo : segmentReadState.fieldInfos) {
121-
final IOSupplier<VectorSearcher> searcherSupplier = getIndexFileNameIfMemoryOptimizedSearchSupported(fieldInfo);
122-
if (searcherSupplier != null) {
123-
final VectorSearcher searcher = searcherSupplier.get();
124-
if (searcher != null) {
125-
// It's supported. There can be a case where a certain index type underlying is not yet supported while KNNEngine
126-
// itself supports memory optimized searching.
127-
vectorSearcherPerField.put(fieldInfo.getName(), searcher);
128-
}
129-
}
130-
}
131-
132-
vectorSearchers = vectorSearcherPerField;
133-
} catch (Exception e) {
134-
// Close opened searchers first, then suppress
135-
try {
136-
IOUtils.closeWhileHandlingException(vectorSearcherPerField.values());
137-
} catch (Exception closeException) {
138-
log.error(closeException.getMessage(), closeException);
139-
}
140-
throw new RuntimeException(e);
81+
if (memoryOptimizedSearchEnabled && state.context.context() != IOContext.Context.MERGE) {
82+
loadMemoryOptimizedSearcherIfRequired();
14183
}
14284
}
14385

@@ -308,12 +250,7 @@ private boolean trySearchWithMemoryOptimizedSearch(
308250
Bits acceptDocs,
309251
boolean isFloatVector
310252
) throws IOException {
311-
if (vectorSearchers == null) {
312-
// Null `vectorSearchers` indicates that this reader was instantiated during merge.
313-
// In this case, we load the searcher on demand for searching.
314-
// This will not likely happen, by the time searching, this old segment will be merged away anyway.
315-
loadMemoryOptimizedSearcher();
316-
}
253+
loadMemoryOptimizedSearcherIfRequired();
317254

318255
// Try with memory optimized searcher
319256
final VectorSearcher memoryOptimizedSearcher = vectorSearchers.get(field);
@@ -350,4 +287,76 @@ private static List<String> getVectorCacheKeysFromSegmentReaderState(SegmentRead
350287

351288
return cacheKeys;
352289
}
290+
291+
private void loadMemoryOptimizedSearcherIfRequired() {
292+
if (vectorSearchers != null) {
293+
return;
294+
}
295+
296+
synchronized (this) {
297+
if (vectorSearchers != null) {
298+
return;
299+
}
300+
301+
// We need sufficient memory space for this table as it will be queried for every single search.
302+
// Hence, having larger space to approximate a perfect hash here.
303+
final Map<String, VectorSearcher> vectorSearcherPerField = new HashMap<>(
304+
RESERVE_TWICE_SPACE * segmentReadState.fieldInfos.size(),
305+
SUFFICIENT_LOAD_FACTOR
306+
);
307+
308+
try {
309+
for (FieldInfo fieldInfo : segmentReadState.fieldInfos) {
310+
final IOSupplier<VectorSearcher> searcherSupplier = getIndexFileNameIfMemoryOptimizedSearchSupported(fieldInfo);
311+
if (searcherSupplier != null) {
312+
final VectorSearcher searcher = searcherSupplier.get();
313+
if (searcher != null) {
314+
// It's supported. There can be a case where a certain index type underlying is not yet supported while
315+
// KNNEngine
316+
// itself supports memory optimized searching.
317+
vectorSearcherPerField.put(fieldInfo.getName(), searcher);
318+
}
319+
}
320+
}
321+
322+
vectorSearchers = vectorSearcherPerField;
323+
} catch (Exception e) {
324+
// Close opened searchers first, then suppress
325+
try {
326+
IOUtils.closeWhileHandlingException(vectorSearcherPerField.values());
327+
} catch (Exception closeException) {
328+
log.error(closeException.getMessage(), closeException);
329+
}
330+
throw new RuntimeException(e);
331+
}
332+
}
333+
}
334+
335+
private IOSupplier<VectorSearcher> getIndexFileNameIfMemoryOptimizedSearchSupported(final FieldInfo fieldInfo) {
336+
// Skip non-knn fields.
337+
final Map<String, String> attributes = fieldInfo.attributes();
338+
if (attributes == null || attributes.containsKey(KNN_FIELD) == false) {
339+
return null;
340+
}
341+
342+
// Get engine
343+
final String engineName = attributes.getOrDefault(KNN_ENGINE, KNNEngine.DEFAULT.getName());
344+
final KNNEngine knnEngine = KNNEngine.getEngine(engineName);
345+
346+
// Get memory optimized searcher from engine
347+
final VectorSearcherFactory searcherFactory = knnEngine.getVectorSearcherFactory();
348+
if (searcherFactory == null) {
349+
// It's not supported
350+
return null;
351+
}
352+
353+
// Start creating searcher
354+
final String fileName = KNNCodecUtil.getNativeEngineFileFromFieldInfo(fieldInfo, segmentReadState.segmentInfo);
355+
if (fileName != null) {
356+
return () -> searcherFactory.createVectorSearcher(segmentReadState.directory, fileName);
357+
}
358+
359+
// Not supported
360+
return null;
361+
}
353362
}

Diff for: src/main/java/org/opensearch/knn/index/codec/nativeindex/NativeIndexBuildStrategyFactory.java

+2
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55

66
package org.opensearch.knn.index.codec.nativeindex;
77

8+
import lombok.Getter;
89
import org.apache.lucene.index.FieldInfo;
910
import org.opensearch.index.IndexSettings;
1011
import org.opensearch.knn.index.codec.nativeindex.remote.RemoteIndexBuildStrategy;
@@ -22,6 +23,7 @@
2223
public final class NativeIndexBuildStrategyFactory {
2324

2425
private final Supplier<RepositoriesService> repositoriesServiceSupplier;
26+
@Getter
2527
private final IndexSettings indexSettings;
2628

2729
public NativeIndexBuildStrategyFactory() {

0 commit comments

Comments
 (0)