Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
* Fix exact search and rescore scoring innerproduct and cosinesimil fields with L2 on model based and 2.17 to 2.19 indices [#3537](https://github.com/opensearch-project/k-NN/pull/3537)
* Fix native memory leak in MemOptimizedNativeIndexBuildStrategy on failed merges [#3593](https://github.com/opensearch-project/k-NN/pull/3593)
* Return model not found instead of the internal model index when getting a model before any model is trained [#3598](https://github.com/opensearch-project/k-NN/pull/3598)
* Support rescoring with `expand_nested_docs` on the Lucene engine, so the final reduction to k counts parent documents instead of child documents [#3579](https://github.com/opensearch-project/k-NN/pull/3579)
* Fix double free of off-heap vectors when a native index build fails [#3621](https://github.com/opensearch-project/k-NN/pull/3621)
* Honor query cancellation and timeout in memory-optimized search [#3620](https://github.com/opensearch-project/k-NN/pull/3620)

Expand All @@ -57,3 +58,4 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
* Add native SIMD cosine scoring for FP16 and SQ formats, removing post-hoc score conversion [#3386](https://github.com/opensearch-project/k-NN/pull/3386)
* Skip warmup for warm-tier indices to avoid unnecessary graph loading from remote store [#3565](https://github.com/opensearch-project/k-NN/pull/3565)
* Avoid FP16 -> FP32 -> FP16 round trip when merging `half_float` segments [#3610](https://github.com/opensearch-project/k-NN/pull/3610)
* Report `exact_search` timings in the Profile API for nested k-NN queries with `expand_nested_docs` on the Lucene engine [#3579](https://github.com/opensearch-project/k-NN/pull/3579)
Original file line number Diff line number Diff line change
Expand Up @@ -153,16 +153,18 @@ public static Query create(CreateQueryRequest createQueryRequest) {
)
);

// Skip wrapping with RescoreKNNVectorQuery for expandNested as it will reduce the results to k after rescoring
// which will not return all the child documents
// TODO: Skip retrieving child docs in ExpandNestedDocsQuery if rescoring is enabled and instead retrieve them after rescoring and
// reducing to top K.
if (needsRescore && expandNested) {
log.warn("Rescoring is not supported when [{}] is set to true", EXPAND_NESTED);
if (needsRescore == false) {
return luceneKnnQuery;
}
return needsRescore ? new RescoreKNNVectorQuery(luceneKnnQuery, fieldName, k, vector, shardId) : luceneKnnQuery;

// ExpandNestedDocsQuery owns the rescore stage itself, because rescoring has to happen before the child
// documents are expanded. Wrapping it here would instead cut to k after the expansion, which counts child
// documents rather than parent documents and drops parents that belong in the result.
if (expandNested) {
return luceneKnnQuery;
}

return new RescoreKNNVectorQuery(luceneKnnQuery, fieldName, k, vector, shardId);
}

private static int getDimension(float[] floatQueryVector, byte[] byteQueryVector) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@
import com.google.common.annotations.VisibleForTesting;
import lombok.extern.log4j.Log4j2;
import org.apache.lucene.index.LeafReaderContext;
import org.apache.lucene.search.DocIdSetIterator;
import org.apache.lucene.search.IndexSearcher;
import org.apache.lucene.search.MatchNoDocsQuery;
import org.apache.lucene.search.Query;
Expand All @@ -25,7 +24,6 @@
import org.opensearch.knn.index.query.exactsearch.ExactSearcher;
import org.opensearch.knn.indices.ModelDao;
import org.opensearch.knn.profile.KNNProfileUtil;
import org.opensearch.knn.profile.query.KNNQueryTimingType;
import org.opensearch.search.profile.ContextualProfileBreakdown;
import org.opensearch.search.profile.query.QueryProfiler;

Expand Down Expand Up @@ -115,21 +113,15 @@ private TopDocs searchLeaf(
if (scorer == null) {
return TopDocsCollector.EMPTY_TOPDOCS;
}
DocIdSetIterator iterator = scorer.iterator();
final ExactSearcher.ExactSearcherContext exactSearcherContext = ExactSearcher.ExactSearcherContext.builder()
.matchedDocsIterator(iterator)
.numberOfMatchedDocs(iterator.cost())
// setting to false because in re-scoring we want to do exact search on full precision vectors
.useQuantizedVectorsForSearch(false)
.k(k)
.field(field)
.floatQueryVector(queryVector)
.build();
TopDocs results = (TopDocs) KNNProfileUtil.profileBreakdown(
TopDocs results = QueryUtils.rescoreLeafWithFullPrecision(
searcher,
profile,
leafReaderContext,
KNNQueryTimingType.EXACT_SEARCH,
() -> searcher.searchLeaf(leafReaderContext, exactSearcherContext)
field,
queryVector,
scorer.iterator(),
k,
null
);
if (leafReaderContext.docBase > 0) {
for (ScoreDoc scoreDoc : results.scoreDocs) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,13 @@
import org.apache.lucene.util.BitSet;
import org.apache.lucene.util.BitSetIterator;
import org.apache.lucene.util.Bits;
import org.opensearch.common.Nullable;
import org.opensearch.knn.index.query.KNNWeight;
import org.opensearch.knn.index.query.exactsearch.ExactSearcher;
import org.opensearch.knn.index.query.iterators.GroupedNestedDocIdSetIterator;
import org.opensearch.knn.profile.KNNProfileUtil;
import org.opensearch.knn.profile.query.KNNQueryTimingType;
import org.opensearch.search.profile.ContextualProfileBreakdown;

import java.io.IOException;
import java.util.ArrayList;
Expand Down Expand Up @@ -183,4 +188,58 @@ protected boolean match(int doc) {
};
return BitSet.of(filterIterator, maxDoc);
}

/**
* Re-scores an already gathered candidate set of a single leaf against full precision vectors.
*
* The approximate search may have walked the graph over quantized vectors, so its scores are inexact. This
* runs an exact search restricted to the candidates, always over full precision vectors, and reports the
* time it took under {@link KNNQueryTimingType#EXACT_SEARCH} when the search is being profiled.
*
* Only the float, non-radial rescore path is covered, which is what the Lucene engine queries need. The
* native engine queries carry extra context (radius, byte vectors, memory optimized search) and go through
* {@link org.opensearch.knn.index.query.KNNWeight#exactSearch} instead.
*
* Deliberately static rather than an instance method: callers that mock this class to stub out the
* surrounding search steps still exercise the real context construction here.
*
* @param exactSearcher the searcher performing the exact search
* @param profile breakdown to report the exact search time to, or null when the search is not being profiled
* @param leafReaderContext the leaf reader context
* @param field the vector field being searched
* @param floatQueryVector the full precision query vector
* @param matchedDocs the candidates to re-score
* @param k the number of results to keep; pass the candidate count to keep all of them
* @param parentsFilter when non-null, collapses each parent group to its best scoring child before the
* top k cut, so k counts parent documents rather than nested field documents
* @return the re-scored documents, sorted by descending score, with leaf local document IDs
* @throws IOException
*/
public static TopDocs rescoreLeafWithFullPrecision(
final ExactSearcher exactSearcher,
@Nullable final ContextualProfileBreakdown profile,
final LeafReaderContext leafReaderContext,
final String field,
final float[] floatQueryVector,
final DocIdSetIterator matchedDocs,
final int k,
@Nullable final BitSetProducer parentsFilter
) throws IOException {
final ExactSearcher.ExactSearcherContext exactSearcherContext = ExactSearcher.ExactSearcherContext.builder()
.matchedDocsIterator(matchedDocs)
.numberOfMatchedDocs(matchedDocs.cost())
// setting to false because in re-scoring we want to do exact search on full precision vectors
.useQuantizedVectorsForSearch(false)
.k(k)
.field(field)
.floatQueryVector(floatQueryVector)
.parentsFilter(parentsFilter)
.build();
return (TopDocs) KNNProfileUtil.profileBreakdown(
profile,
leafReaderContext,
KNNQueryTimingType.EXACT_SEARCH,
() -> exactSearcher.searchLeaf(leafReaderContext, exactSearcherContext)
);
}
}
Loading
Loading