-
Notifications
You must be signed in to change notification settings - Fork 169
Enable optimistic search to memory optimized search. #2933
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -24,7 +24,6 @@ | |
import org.apache.lucene.util.BitSetIterator; | ||
import org.apache.lucene.util.Bits; | ||
import org.apache.lucene.util.FixedBitSet; | ||
import org.opensearch.common.Nullable; | ||
import org.opensearch.common.StopWatch; | ||
import org.opensearch.common.lucene.Lucene; | ||
import org.opensearch.knn.common.FieldInfoExtractor; | ||
|
@@ -52,6 +51,9 @@ | |
import static org.opensearch.knn.common.KNNConstants.SPACE_TYPE; | ||
import static org.opensearch.knn.common.KNNConstants.VECTOR_DATA_TYPE_FIELD; | ||
|
||
import static org.opensearch.knn.profile.StopWatchUtils.startStopWatch; | ||
import static org.opensearch.knn.profile.StopWatchUtils.stopStopWatchAndLog; | ||
|
||
/** | ||
* {@link KNNWeight} serves as a template for implementing approximate nearest neighbor (ANN) | ||
* and radius search over a native index type, such as Faiss. | ||
|
@@ -298,12 +300,13 @@ public PerLeafResult searchLeaf(LeafReaderContext context, int k) throws IOExcep | |
final SegmentReader reader = Lucene.segmentReader(context.reader()); | ||
final String segmentName = reader.getSegmentName(); | ||
|
||
StopWatch stopWatch = startStopWatch(); | ||
final StopWatch stopWatch = startStopWatch(log); | ||
final BitSet filterBitSet = getFilteredDocsBitSet(context); | ||
stopStopWatchAndLog(stopWatch, "FilterBitSet creation", segmentName); | ||
stopStopWatchAndLog(log, stopWatch, "FilterBitSet creation", knnQuery.getShardId(), segmentName, knnQuery.getField()); | ||
|
||
// Save its cardinality, as the cardinality calculation is expensive. | ||
final int filterCardinality = filterBitSet.cardinality(); | ||
|
||
final int maxDoc = context.reader().maxDoc(); | ||
int filterCardinality = filterBitSet.cardinality(); | ||
// We don't need to go to JNI layer if no documents are found which satisfy the filters | ||
// We should give this condition a deeper look that where it should be placed. For now I feel this is a good | ||
// place, | ||
|
@@ -320,19 +323,19 @@ public PerLeafResult searchLeaf(LeafReaderContext context, int k) throws IOExcep | |
* This improves the recall. | ||
*/ | ||
if (isFilteredExactSearchPreferred(filterCardinality)) { | ||
TopDocs result = doExactSearch(context, new BitSetIterator(filterBitSet, filterCardinality), filterCardinality, k); | ||
return new PerLeafResult(filterWeight == null ? null : filterBitSet, result); | ||
final TopDocs result = doExactSearch(context, new BitSetIterator(filterBitSet, filterCardinality), filterCardinality, k); | ||
return new PerLeafResult( | ||
filterWeight == null ? null : filterBitSet, | ||
filterCardinality, | ||
result, | ||
PerLeafResult.SearchMode.EXACT_SEARCH | ||
); | ||
} | ||
|
||
/* | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This logic has been moved to approximateSearch |
||
* If filters match all docs in this segment, then null should be passed as filterBitSet | ||
* so that it will not do a bitset look up in bottom search layer. | ||
*/ | ||
final BitSet annFilter = (filterWeight != null && filterCardinality == maxDoc) ? null : filterBitSet; | ||
final StopWatch annStopWatch = startStopWatch(log); | ||
final TopDocs topDocs = approximateSearch(context, filterBitSet, filterCardinality, k); | ||
stopStopWatchAndLog(log, stopWatch, "ANN search", knnQuery.getShardId(), segmentName, knnQuery.getField()); | ||
|
||
StopWatch annStopWatch = startStopWatch(); | ||
final TopDocs topDocs = approximateSearch(context, annFilter, filterCardinality, k); | ||
stopStopWatchAndLog(annStopWatch, "ANN search", segmentName); | ||
if (knnQuery.isExplain()) { | ||
knnExplanation.addLeafResult(context.id(), topDocs.scoreDocs.length); | ||
} | ||
|
@@ -341,18 +344,21 @@ public PerLeafResult searchLeaf(LeafReaderContext context, int k) throws IOExcep | |
// results less than K, though we have more than k filtered docs | ||
if (isExactSearchRequire(context, filterCardinality, topDocs.scoreDocs.length)) { | ||
final BitSetIterator docs = filterWeight != null ? new BitSetIterator(filterBitSet, filterCardinality) : null; | ||
TopDocs result = doExactSearch(context, docs, filterCardinality, k); | ||
return new PerLeafResult(filterWeight == null ? null : filterBitSet, result); | ||
final TopDocs result = doExactSearch(context, docs, filterCardinality, k); | ||
return new PerLeafResult( | ||
filterWeight == null ? null : filterBitSet, | ||
filterCardinality, | ||
result, | ||
PerLeafResult.SearchMode.EXACT_SEARCH | ||
); | ||
} | ||
return new PerLeafResult(filterWeight == null ? null : filterBitSet, topDocs); | ||
} | ||
|
||
private void stopStopWatchAndLog(@Nullable final StopWatch stopWatch, final String prefixMessage, String segmentName) { | ||
if (log.isDebugEnabled() && stopWatch != null) { | ||
stopWatch.stop(); | ||
final String logMessage = prefixMessage + " shard: [{}], segment: [{}], field: [{}], time in nanos:[{}] "; | ||
log.debug(logMessage, knnQuery.getShardId(), segmentName, knnQuery.getField(), stopWatch.totalTime().nanos()); | ||
} | ||
return new PerLeafResult( | ||
filterWeight == null ? null : filterBitSet, | ||
filterCardinality, | ||
topDocs, | ||
PerLeafResult.SearchMode.APPROXIMATE_SEARCH | ||
); | ||
} | ||
|
||
protected BitSet getFilteredDocsBitSet(final LeafReaderContext ctx) throws IOException { | ||
|
@@ -413,9 +419,33 @@ private TopDocs doExactSearch( | |
return exactSearch(context, exactSearcherContextBuilder.build()); | ||
} | ||
|
||
protected TopDocs approximateSearch(final LeafReaderContext context, final BitSet filterIdsBitSet, final int cardinality, final int k) | ||
throws IOException { | ||
/** | ||
* Performs an approximate nearest neighbor (ANN) search on the provided index segment. | ||
* <p> | ||
* This method prepares all necessary query metadata before triggering the actual ANN search. | ||
* It extracts the {@code model_id} from field-level attributes if required, retrieves any | ||
* quantization or auxiliary metadata associated with the vector field, and applies quantization | ||
* to the query vector when applicable. After these preprocessing steps, it invokes | ||
* {@code doANNSearch(LeafReaderContext, BitSet, int, int)} to execute the approximate search | ||
* and obtain the top results. | ||
* | ||
* @param context the {@link LeafReaderContext} representing the current index segment | ||
* @param filterIdsBitSet an optional {@link BitSet} indicating document IDs to include in the search; | ||
* may be {@code null} if no filtering is required | ||
* @param filterCardinality the number of documents included in {@code filterIdsBitSet}; | ||
* used to optimize search filtering | ||
* @param k the number of nearest neighbors to retrieve | ||
* @return a {@link TopDocs} object containing the top {@code k} approximate search results | ||
* @throws IOException if an error occurs while reading index data or accessing vector fields | ||
*/ | ||
public TopDocs approximateSearch( | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. why we are making this function public? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We need this particular function in optimistic second search. Otherwise, if using searchLeaf, then we will end up building filter bitset twice. |
||
final LeafReaderContext context, | ||
final BitSet filterIdsBitSet, | ||
final int filterCardinality, | ||
final int k | ||
) throws IOException { | ||
final SegmentReader reader = Lucene.segmentReader(context.reader()); | ||
|
||
FieldInfo fieldInfo = FieldInfoExtractor.getFieldInfo(reader, knnQuery.getField()); | ||
|
||
if (fieldInfo == null) { | ||
|
@@ -465,6 +495,11 @@ protected TopDocs approximateSearch(final LeafReaderContext context, final BitSe | |
// TODO: Change type of vector once more quantization methods are supported | ||
byte[] quantizedVector = maybeQuantizeVector(segmentLevelQuantizationInfo); | ||
float[] transformedVector = maybeTransformVector(segmentLevelQuantizationInfo, spaceType); | ||
/* | ||
* If filters match all docs in this segment, then null should be passed as filterBitSet | ||
* so that it will not do a bitset look up in bottom search layer. | ||
*/ | ||
final BitSet annFilter = filterCardinality == context.reader().maxDoc() ? null : filterIdsBitSet; | ||
|
||
KNNCounter.GRAPH_QUERY_REQUESTS.increment(); | ||
final TopDocs results = doANNSearch( | ||
|
@@ -477,8 +512,8 @@ protected TopDocs approximateSearch(final LeafReaderContext context, final BitSe | |
quantizedVector, | ||
transformedVector, | ||
modelId, | ||
filterIdsBitSet, | ||
cardinality, | ||
annFilter, | ||
filterCardinality, | ||
k | ||
); | ||
|
||
|
@@ -553,10 +588,10 @@ protected void addExplainIfRequired(final TopDocs results, final KNNEngine knnEn | |
*/ | ||
public TopDocs exactSearch(final LeafReaderContext leafReaderContext, final ExactSearcher.ExactSearcherContext exactSearcherContext) | ||
throws IOException { | ||
StopWatch stopWatch = startStopWatch(); | ||
final StopWatch stopWatch = startStopWatch(log); | ||
TopDocs exactSearchResults = exactSearcher.searchLeaf(leafReaderContext, exactSearcherContext); | ||
final SegmentReader reader = Lucene.segmentReader(leafReaderContext.reader()); | ||
stopStopWatchAndLog(stopWatch, "Exact search", reader.getSegmentName()); | ||
stopStopWatchAndLog(log, stopWatch, "Exact search", knnQuery.getShardId(), reader.getSegmentName(), knnQuery.getField()); | ||
return exactSearchResults; | ||
} | ||
|
||
|
@@ -673,13 +708,6 @@ private boolean isMissingNativeEngineFiles(LeafReaderContext context) { | |
return engineFiles.isEmpty(); | ||
} | ||
|
||
private StopWatch startStopWatch() { | ||
if (log.isDebugEnabled()) { | ||
return new StopWatch().start(); | ||
} | ||
return null; | ||
} | ||
|
||
protected int[] getParentIdsArray(final LeafReaderContext context) throws IOException { | ||
if (knnQuery.getParentsFilter() == null) { | ||
return null; | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
do we need this ?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If you want to run search for memory optimized cases then lets create another gradle task and also a new CI that runs that task.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We need this to force it to run 2nd search in optimistic.
Since the 2nd search will kick off only if there's segment whose min score > the min score in merged results, it was tricky for me to make the data.