Skip to content

Commit 83cd071

Browse files
author
Dooyong Kim
committed
Adding basic building blocks for MemoryOptimizedSearch. At the moment, only FAISS is supporing this.
Signed-off-by: Dooyong Kim <[email protected]>
1 parent c7ac05c commit 83cd071

8 files changed

+258
-1
lines changed

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

+82
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111

1212
package org.opensearch.knn.index.codec.KNN990Codec;
1313

14+
import lombok.extern.slf4j.Slf4j;
1415
import org.apache.lucene.codecs.KnnVectorsReader;
1516
import org.apache.lucene.codecs.hnsw.FlatVectorsReader;
1617
import org.apache.lucene.index.ByteVectorValues;
@@ -22,12 +23,16 @@
2223
import org.apache.lucene.search.TopDocs;
2324
import org.apache.lucene.search.TotalHits;
2425
import org.apache.lucene.util.Bits;
26+
import org.apache.lucene.util.IOSupplier;
2527
import org.apache.lucene.util.IOUtils;
2628
import org.opensearch.common.UUIDs;
2729
import org.opensearch.knn.index.codec.util.KNNCodecUtil;
2830
import org.opensearch.knn.index.codec.util.NativeMemoryCacheKeyHelper;
31+
import org.opensearch.knn.index.engine.KNNEngine;
2932
import org.opensearch.knn.index.memory.NativeMemoryCacheManager;
3033
import org.opensearch.knn.index.quantizationservice.QuantizationService;
34+
import org.opensearch.knn.memoryoptsearch.MemoryOptimizedSearcher;
35+
import org.opensearch.knn.memoryoptsearch.MemoryOptimizedSearcherFactory;
3136
import org.opensearch.knn.quantization.models.quantizationState.QuantizationState;
3237
import org.opensearch.knn.quantization.models.quantizationState.QuantizationStateCacheManager;
3338
import org.opensearch.knn.quantization.models.quantizationState.QuantizationStateReadConfig;
@@ -37,23 +42,89 @@
3742
import java.util.HashMap;
3843
import java.util.List;
3944
import java.util.Map;
45+
import java.util.Objects;
46+
47+
import static org.opensearch.knn.common.KNNConstants.KNN_ENGINE;
48+
import static org.opensearch.knn.index.mapper.KNNVectorFieldMapper.KNN_FIELD;
4049

4150
/**
4251
* Vectors reader class for reading the flat vectors for native engines. The class provides methods for iterating
4352
* over the vectors and retrieving their values.
4453
*/
54+
@Slf4j
4555
public class NativeEngines990KnnVectorsReader extends KnnVectorsReader {
56+
private static final int RESERVE_TWICE_SPACE = 2;
57+
private static final float SUFFICIENT_LOAD_FACTOR = 0.6f;
4658

4759
private final FlatVectorsReader flatVectorsReader;
4860
private Map<String, String> quantizationStateCacheKeyPerField;
4961
private SegmentReadState segmentReadState;
5062
private final List<String> cacheKeys;
63+
private Map<String, MemoryOptimizedSearcher> memoryOptimizedSearchers;
5164

5265
public NativeEngines990KnnVectorsReader(final SegmentReadState state, final FlatVectorsReader flatVectorsReader) {
5366
this.flatVectorsReader = flatVectorsReader;
5467
this.segmentReadState = state;
5568
this.cacheKeys = getVectorCacheKeysFromSegmentReaderState(state);
69+
this.memoryOptimizedSearchers = new HashMap<>(RESERVE_TWICE_SPACE * segmentReadState.fieldInfos.size(), SUFFICIENT_LOAD_FACTOR);
5670
loadCacheKeyMap();
71+
72+
//
73+
// TMP(KDY) : Dynamic update will be covered in part-7. Please refer to
74+
// https://github.com/opensearch-project/k-NN/issues/2401#issuecomment-2699777824
75+
//
76+
final boolean isMemoryOptimizedSearchEnabled = false;
77+
if (isMemoryOptimizedSearchEnabled) {
78+
loadMemoryOptimizedSearcher();
79+
}
80+
}
81+
82+
private IOSupplier<MemoryOptimizedSearcher> getIndexFileNameIfMemoryOptimizedSearchSupported(final FieldInfo fieldInfo) {
83+
// Skip non-knn fields.
84+
final Map<String, String> attributes = fieldInfo.attributes();
85+
if (attributes == null || attributes.containsKey(KNN_FIELD) == false) {
86+
return null;
87+
}
88+
89+
// Get engine
90+
final String engineName = attributes.getOrDefault(KNN_ENGINE, KNNEngine.DEFAULT.getName());
91+
final KNNEngine knnEngine = KNNEngine.getEngine(engineName);
92+
93+
// Get memory optimized searcher from engine
94+
final MemoryOptimizedSearcherFactory searcherFactory = knnEngine.getMemoryOptimizedSearcherFactory();
95+
if (searcherFactory == null) {
96+
// It's not supported
97+
return null;
98+
}
99+
100+
// Start creating searcher
101+
final String fileName = KNNCodecUtil.getNativeEngineFileFromFieldInfo(fieldInfo, segmentReadState.segmentInfo);
102+
if (fileName != null) {
103+
return () -> searcherFactory.createMemoryOptimizedSearcher(segmentReadState.directory, fileName);
104+
}
105+
106+
// Not supported
107+
return null;
108+
}
109+
110+
public void loadMemoryOptimizedSearcher() {
111+
try {
112+
for (FieldInfo fieldInfo : segmentReadState.fieldInfos) {
113+
final IOSupplier<MemoryOptimizedSearcher> searcherSupplier = getIndexFileNameIfMemoryOptimizedSearchSupported(fieldInfo);
114+
if (searcherSupplier != null) {
115+
final MemoryOptimizedSearcher searcher = Objects.requireNonNull(searcherSupplier.get());
116+
memoryOptimizedSearchers.put(fieldInfo.getName(), searcher);
117+
}
118+
}
119+
} catch (Exception e) {
120+
// Close opened searchers first, then suppress
121+
try {
122+
IOUtils.closeWhileHandlingException(memoryOptimizedSearchers.values());
123+
} catch (Exception closeException) {
124+
log.error(closeException.getMessage(), closeException);
125+
}
126+
throw new RuntimeException(e);
127+
}
57128
}
58129

59130
/**
@@ -135,6 +206,14 @@ public void search(String field, float[] target, KnnCollector knnCollector, Bits
135206
((QuantizationConfigKNNCollector) knnCollector).setQuantizationState(quantizationState);
136207
return;
137208
}
209+
210+
// Try with memory optimized searcher
211+
final MemoryOptimizedSearcher memoryOptimizedSearcher = memoryOptimizedSearchers.get(field);
212+
if (memoryOptimizedSearcher != null) {
213+
memoryOptimizedSearcher.search(target, knnCollector, acceptDocs);
214+
return;
215+
}
216+
138217
throw new UnsupportedOperationException("Search functionality using codec is not supported with Native Engine Reader");
139218
}
140219

@@ -197,6 +276,9 @@ public void close() throws IOException {
197276
quantizationStateCacheManager.evict(cacheKey);
198277
}
199278
}
279+
280+
// TODO(KDY)
281+
// Close all memory optimized searchers.
200282
}
201283

202284
private void loadCacheKeyMap() {

src/main/java/org/opensearch/knn/index/engine/KNNEngine.java

+6
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
import com.google.common.collect.ImmutableSet;
99
import org.opensearch.common.ValidationException;
1010
import org.opensearch.knn.index.SpaceType;
11+
import org.opensearch.knn.memoryoptsearch.MemoryOptimizedSearcherFactory;
1112
import org.opensearch.knn.index.engine.faiss.Faiss;
1213
import org.opensearch.knn.index.engine.lucene.Lucene;
1314
import org.opensearch.knn.index.engine.nmslib.Nmslib;
@@ -216,4 +217,9 @@ public ResolvedMethodContext resolveMethod(
216217
public boolean supportsRemoteIndexBuild() {
217218
return knnLibrary.supportsRemoteIndexBuild();
218219
}
220+
221+
@Override
222+
public MemoryOptimizedSearcherFactory getMemoryOptimizedSearcherFactory() {
223+
return knnLibrary.getMemoryOptimizedSearcherFactory();
224+
}
219225
}

src/main/java/org/opensearch/knn/index/engine/KNNLibrary.java

+11-1
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77

88
import org.opensearch.common.ValidationException;
99
import org.opensearch.knn.index.SpaceType;
10+
import org.opensearch.knn.memoryoptsearch.MemoryOptimizedSearcherFactory;
1011

1112
import java.util.Collections;
1213
import java.util.List;
@@ -140,11 +141,20 @@ default List<String> mmapFileExtensions() {
140141
return Collections.emptyList();
141142
}
142143

143-
/**
144+
/*
144145
* Returns whether or not the engine implementation supports remote index build
145146
* @return true if remote index build is supported, false otherwise
146147
*/
147148
default boolean supportsRemoteIndexBuild() {
148149
return false;
149150
}
151+
152+
/**
153+
* Create a new memory optimized searcher factory.
154+
* @return New searcher factory that performs KNN search with optimized memory management.
155+
* If null, it indicates it does not support memory optimized searcher.
156+
*/
157+
default MemoryOptimizedSearcherFactory getMemoryOptimizedSearcherFactory() {
158+
return null;
159+
}
150160
}

src/main/java/org/opensearch/knn/index/engine/faiss/Faiss.java

+7
Original file line numberDiff line numberDiff line change
@@ -8,12 +8,14 @@
88
import com.google.common.collect.ImmutableMap;
99
import org.opensearch.knn.common.KNNConstants;
1010
import org.opensearch.knn.index.SpaceType;
11+
import org.opensearch.knn.memoryoptsearch.MemoryOptimizedSearcherFactory;
1112
import org.opensearch.knn.index.engine.KNNMethod;
1213
import org.opensearch.knn.index.engine.KNNMethodConfigContext;
1314
import org.opensearch.knn.index.engine.KNNMethodContext;
1415
import org.opensearch.knn.index.engine.MethodResolver;
1516
import org.opensearch.knn.index.engine.NativeLibrary;
1617
import org.opensearch.knn.index.engine.ResolvedMethodContext;
18+
import org.opensearch.knn.memoryoptsearch.faiss.FaissMemoryOptimizedSearcherFactory;
1719

1820
import java.util.Map;
1921
import java.util.function.Function;
@@ -123,4 +125,9 @@ public ResolvedMethodContext resolveMethod(
123125
public boolean supportsRemoteIndexBuild() {
124126
return true;
125127
}
128+
129+
@Override
130+
public MemoryOptimizedSearcherFactory getMemoryOptimizedSearcherFactory() {
131+
return new FaissMemoryOptimizedSearcherFactory();
132+
}
126133
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
/*
2+
* Copyright OpenSearch Contributors
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
package org.opensearch.knn.memoryoptsearch;
7+
8+
import org.apache.lucene.search.KnnCollector;
9+
import org.apache.lucene.util.Bits;
10+
11+
import java.io.Closeable;
12+
import java.io.IOException;
13+
14+
/**
15+
* Memory optimized searcher that performs vector search with the best efforts to minimize memory pressure.
16+
* Its implementation should only focus on optimizing memory allocations, and must not increase memory pressure in JVM.
17+
* The main focus of the pressure is limited to JVM, and it would be better if it could take a leverage of OS cache memory to improve
18+
* performance internally.
19+
* Although its main focus is on memory pressure, but it does not mean that we sacrifice performance over memory consumption.
20+
* If anything, this searcher must be balanced between two factors, seeking the best performance while minimizing memory pressure.
21+
* This is the goal of this searcher.
22+
*/
23+
public interface MemoryOptimizedSearcher extends Closeable {
24+
25+
/**
26+
* Return the k nearest neighbor documents as determined by comparison of their vector values for
27+
* this field, to the given vector, by the field's similarity function. The score of each document
28+
* is derived from the vector similarity in a way that ensures scores are positive and that a
29+
* larger score corresponds to a higher ranking.
30+
*
31+
* <p>The search is allowed to be approximate, meaning the results are not guaranteed to be the
32+
* true k closest neighbors. For large values of k (for example when k is close to the total
33+
* number of documents), the search may also retrieve fewer than k documents.
34+
*
35+
* @param target the vector-valued float vector query
36+
* @param knnCollector a KnnResults collector and relevant settings for gathering vector results
37+
* @param acceptDocs {@link Bits} that represents the allowed documents to match, or {@code null}
38+
* if they are all allowed to match.
39+
*/
40+
void search(float[] target, KnnCollector knnCollector, Bits acceptDocs) throws IOException;
41+
42+
/**
43+
* Return the k nearest neighbor documents as determined by comparison of their vector values for
44+
* this field, to the given vector, by the field's similarity function. The score of each document
45+
* is derived from the vector similarity in a way that ensures scores are positive and that a
46+
* larger score corresponds to a higher ranking.
47+
*
48+
* <p>The search is allowed to be approximate, meaning the results are not guaranteed to be the
49+
* true k closest neighbors. For large values of k (for example when k is close to the total
50+
* number of documents), the search may also retrieve fewer than k documents.
51+
*
52+
* @param target the vector-valued byte vector query
53+
* @param knnCollector a KnnResults collector and relevant settings for gathering vector results
54+
* @param acceptDocs {@link Bits} that represents the allowed documents to match, or {@code null}
55+
* if they are all allowed to match.
56+
*/
57+
void search(byte[] target, KnnCollector knnCollector, Bits acceptDocs) throws IOException;
58+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
/*
2+
* Copyright OpenSearch Contributors
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
package org.opensearch.knn.memoryoptsearch;
7+
8+
import org.apache.lucene.store.Directory;
9+
10+
import java.io.IOException;
11+
12+
/**
13+
* Factory to create {@link MemoryOptimizedSearcher}.
14+
* Provided parameters will have {@link Directory} and a file name where implementation can rely on it to open an input stream.
15+
*/
16+
public interface MemoryOptimizedSearcherFactory {
17+
/**
18+
* Create a non-null {@link MemoryOptimizedSearcher} with given Lucene's {@link Directory}.
19+
*
20+
* @param directory Lucene's Directory.
21+
* @param fileName Logical file name to load.
22+
* @return It must return a non-null {@link MemoryOptimizedSearcher}
23+
* @throws IOException
24+
*/
25+
MemoryOptimizedSearcher createMemoryOptimizedSearcher(Directory directory, String fileName) throws IOException;
26+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
/*
2+
* Copyright OpenSearch Contributors
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
package org.opensearch.knn.memoryoptsearch.faiss;
7+
8+
import org.apache.lucene.search.KnnCollector;
9+
import org.apache.lucene.store.IndexInput;
10+
import org.apache.lucene.util.Bits;
11+
import org.opensearch.knn.memoryoptsearch.MemoryOptimizedSearcher;
12+
13+
import java.io.IOException;
14+
15+
public class FaissMemoryOptimizedSearcher implements MemoryOptimizedSearcher {
16+
private final IndexInput indexInput;
17+
18+
public FaissMemoryOptimizedSearcher(IndexInput indexInput) {
19+
this.indexInput = indexInput;
20+
}
21+
22+
@Override
23+
public void search(float[] target, KnnCollector knnCollector, Bits acceptDocs) throws IOException {
24+
// TODO(KDY) : This will be covered in subsequent parts.
25+
throw new UnsupportedOperationException("Not implemented yet");
26+
}
27+
28+
@Override
29+
public void search(byte[] target, KnnCollector knnCollector, Bits acceptDocs) throws IOException {
30+
// TODO(KDY) : This will be covered in subsequent parts.
31+
throw new UnsupportedOperationException("Not implemented yet");
32+
}
33+
34+
@Override
35+
public void close() throws IOException {
36+
indexInput.close();
37+
}
38+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
/*
2+
* Copyright OpenSearch Contributors
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
package org.opensearch.knn.memoryoptsearch.faiss;
7+
8+
import org.apache.lucene.store.Directory;
9+
import org.apache.lucene.store.IOContext;
10+
import org.apache.lucene.store.IndexInput;
11+
import org.apache.lucene.store.ReadAdvice;
12+
import org.opensearch.knn.memoryoptsearch.MemoryOptimizedSearcher;
13+
import org.opensearch.knn.memoryoptsearch.MemoryOptimizedSearcherFactory;
14+
15+
import java.io.IOException;
16+
17+
public class FaissMemoryOptimizedSearcherFactory implements MemoryOptimizedSearcherFactory {
18+
@Override
19+
public MemoryOptimizedSearcher createMemoryOptimizedSearcher(final Directory directory, final String fileName) throws IOException {
20+
// Why ReadAdvice.RANDOM?
21+
// We pass `RANDOM` as advice to prevent the underlying storage from performing read-ahead. Since vector search naturally accesses
22+
// random vector locations, read-ahead does not improve performance. By passing the `RANDOM` context, we explicitly indicate that
23+
// this searcher will access vectors randomly.
24+
final IndexInput indexInput = directory.openInput(
25+
fileName,
26+
new IOContext(IOContext.Context.DEFAULT, null, null, ReadAdvice.RANDOM)
27+
);
28+
return new FaissMemoryOptimizedSearcher(indexInput);
29+
}
30+
}

0 commit comments

Comments
 (0)