Skip to content

Add 'knn' section to search endpoint#88002

Merged
jtibshirani merged 14 commits intoelastic:knn-searchfrom
jtibshirani:knn-search-phase
Jun 30, 2022
Merged

Add 'knn' section to search endpoint#88002
jtibshirani merged 14 commits intoelastic:knn-searchfrom
jtibshirani:knn-search-phase

Conversation

@jtibshirani
Copy link
Copy Markdown
Contributor

@jtibshirani jtibshirani commented Jun 24, 2022

This PR adds a new knn section to the search request:

POST products/_search
{
  "query": {
    "multi_match": {
      "query": "black flower dress",
      "fields": ["title", "description"],
    },
    "boost": 0.9
  },
  "knn": {
    "field": "title_vector",
    "query_vector": [0.3f, 0.1f, ...],
    "k": 5,
    "num_candidates": 50,
    "boost": 0.1
  },
  "size": 20
}

As explained in #87625, the search returns a global top k documents across all
shards:

  • In the DFS phase, collect the top k vector matches per shard, combine them
    and keep the global top k.
  • Then move to the query phase. Convert the top k vector matches into a new
    KnnScoreDocQueryBuilder that matches only those top documents. For the final
    query, take a boolean disjunction between this KnnScoreDocQueryBuilder query and
    the search request query.

Commits:

  • Rename KnnSearchRequestBuilder -> KnnSearchRequestParser to avoid confusion with new KnnSearchBuilder
  • Add KnnScoreDocQueryBuilder
  • Implement kNN search using DFS phase

Addresses #87625.

@jtibshirani
Copy link
Copy Markdown
Contributor Author

jtibshirani commented Jun 24, 2022

NOTE: this PR targets a feature branch knn-search. There are still several TODOs (tracked in #87625) like support for filters.

@jtibshirani jtibshirani added >feature :Search/Search Search-related issues that do not fall into other categories labels Jun 24, 2022
@jtibshirani jtibshirani marked this pull request as ready for review June 24, 2022 16:29
@elasticmachine elasticmachine added the Team:Search Meta label for search team label Jun 24, 2022
@elasticmachine
Copy link
Copy Markdown
Collaborator

Pinging @elastic/es-search (Team:Search)

@sethmlarson sethmlarson added the Team:Clients Meta label for clients team label Jun 24, 2022
@elasticmachine
Copy link
Copy Markdown
Collaborator

Pinging @elastic/clients-team (Team:Clients)

static void adjustSearchType(SearchRequest searchRequest) {
// if there's a kNN search, always use DFS_QUERY_THEN_FETCH
if (searchRequest.hasKnnSearch()) {
searchRequest.searchType(DFS_QUERY_THEN_FETCH);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

drive-by comment: do we want to return an error in case search type is explicitly set and differs from dfs query then fetch? Maybe this is something to think about for later, does not look high priority in this PR. Or maybe you already though about it.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks @javanna, I meant to add a comment about this. Yes in a follow-up PR I plan to disallow setting the search type explicitly when knn is provided too.

// Emulate TransportSearchAction logic: first adjust search type, then check minimize roundtrips
TransportSearchAction.adjustSearchType(searchRequest);

// Minimize roundtrips should always be false because kNN uses DFS_QUERY_THEN_FETCH
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

too bad that we can't minimize roundtrips! maybe that's a good reminder to rethink whether we can do something about this, I will try to think about this.

Copy link
Copy Markdown
Contributor Author

@jtibshirani jtibshirani Jun 24, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In a follow-up, I hope to optimize the case where there is only kNN, with no query or aggregations. Then we can run kNN during the main query phase (using QUERY_THEN_FETCH), and it will be possible to minimize roundtrips.

Also, I found this test to be a little fragile/ hacky -- I'm happy to remove it if people don't think it's valuable. I checked that the new CCS REST tests already cover the important cases.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see, agreed that it's a bit hacky because it requires adjustSearchType to be called. I would rather add a very simple unit test around adjustSearchType then. this specific test method already tests the scenario with dfs enabled, so we should just verify that the search type is adjusted when knn is used.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also, Nhat is running benchmarks to asses how bad it is that we don't minimize roundtrips in some cases, in the context of #73971 . I would imagine that the results of the benchmark will also inform whether we need to put more effort into minimizing roundtrips when knn is used.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍 I'll rework this test. Looking forward to seeing Nhat's results.

Copy link
Copy Markdown
Contributor

@sethmlarson sethmlarson left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

API looks good, one thought:

@jtibshirani
Copy link
Copy Markdown
Contributor Author

@elasticmachine run elasticsearch-ci/packaging-tests-unix-sample

@jtibshirani
Copy link
Copy Markdown
Contributor Author

Thanks @mayya-sharipova for the helpful review!

@jpountz do you have time to review just to check the overall strategy looks okay? As a reminder, the PR targets a feature branch knn-search. There are still several TODOs (tracked in #87625) like support for filters.

searchRequest.searchType(QUERY_THEN_FETCH);
}

// if there's a kNN search, always use DFS_QUERY_THEN_FETCH
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In a follow-up I'm thinking of handling this in SearchSourceBuilder#rewrite instead.

Copy link
Copy Markdown
Contributor

@jpountz jpountz left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I only skimmed through the PR. One thing that looks a bit odd to me if I read the code correctly is that we compute global nearest vectors on the coordinating node and then send them all to all shards, where many of them might get ignored as part of QueryBuilder#toQuery. Would it be possible to only send the relevant vectors to each shard?

final SearchPhaseController.ReducedQueryPhase reducedQueryPhase = resultConsumer.reduce();
final boolean queryAndFetchOptimization = queryResults.length() == 1;
final boolean queryAndFetchOptimization = queryResults.length() == 1
&& context.getRequest().searchType() == SearchType.QUERY_THEN_FETCH;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why would we not run the fetch phase and the query phase in the same roundtrip when the search type has a DFS phase?

Copy link
Copy Markdown
Contributor Author

@jtibshirani jtibshirani Jun 29, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Previously, when there was a single shard we would always set the search type to QUERY_THEN_FETCH:

// optimize search type for cases where there is only one shard group to search on
if (shardIterators.size() == 1) {
// if we only have one group, then we always want Q_T_F, no need for DFS, and no need to do THEN since we hit one shard
searchRequest.searchType(QUERY_THEN_FETCH);
}

This makes sense, since you don't need DFS with one shard. Now, we might still execute DFS_QUERY_THEN_FETCH with a single shard, when kNN is used. From failing tests, I saw that we don't apply the "query and fetch" optimization in the query phase when DFS is enabled. (Surprisingly, we use different codepath to execute the query phase when DFS is enabled vs. not. There are two different actions QUERY_ID_ACTION_NAME and QUERY_ACTION_NAME).

Here was my thinking for next steps:

  • In a follow-up, I plan to use QUERY_THEN_FETCH for kNN with a single shard. This will remove the special handling, since we will always know QUERY_THEN_FETCH is used when there's one shard.
  • For now I'll change this check to queryResults.length() == 1 && context.getRequest().hasKnnSearch() == false and add a comment explaining.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for explaining, the proposed follow-up sounds good to me.

@jtibshirani
Copy link
Copy Markdown
Contributor Author

One thing that looks a bit odd to me if I read the code correctly is that we compute global nearest vectors on the coordinating node and then send them all to all shards, where many of them might get ignored as part of QueryBuilder#toQuery.

That's correct! I did this because I thought it was a nice invariant that we always send the same QueryBuilder to each shard. I can't think of a prior case where we've sent a different query to each shard? It didn't seem like a big deal because k is bounded and typically not that large.

@jpountz
Copy link
Copy Markdown
Contributor

jpountz commented Jun 28, 2022

The closest thing that I know of is how searches sorted by descending @timestamp fan out to shards that hold recent data first and then to shards that hold older data with a search_after key that hopefully sometimes helps skip these shards entirely in the common case when the most recent documents were all in the recent shards.

I agree that it shouldn't be a big deal in practice. One could argue that it is k times the number of shards, but the size of these queries is still likely smaller than the responses that shards will send back.

@jtibshirani
Copy link
Copy Markdown
Contributor Author

jtibshirani commented Jun 28, 2022

@jpountz thanks for your comments! It sounds like things look okay overall, no major flags. In the interest of time, I'm going to continue with these PRs (merging into a feature branch) and tag both you and @mayya-sharipova for a final review before merging to main.

@jtibshirani
Copy link
Copy Markdown
Contributor Author

I pushed some updates:

  • Add comment to clarify why the 'query and fetch' optimization doesn't work when kNN is enabled. It'd be best to support this -- I'll address this later when we tackle the TODO item "Optimize the single shard case".
  • Only send the relevant score docs to each shard. This can be more efficient, especially when some shards don't return any kNN results (maybe because the filter doesn't match?) It also made things cleaner since I could avoid the new methods Lucene.writeScoreDocWithShardIndex. We do something similar in SearchQueryThenFetchAsyncAction#rewriteShardSearchRequest, so I used the same method name.

Copy link
Copy Markdown
Contributor

@sethmlarson sethmlarson left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good from an API perspective.

I'm guessing that the filter property of knn is being implemented in a future PR since there are no YAML tests cases with the property? If so that's fine.

Copy link
Copy Markdown
Contributor

@mayya-sharipova mayya-sharipova left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@jtibshirani Thanks, new changes LGTM@

@jtibshirani jtibshirani merged commit d53d01f into elastic:knn-search Jun 30, 2022
@jtibshirani jtibshirani deleted the knn-search-phase branch June 30, 2022 14:55
jtibshirani added a commit that referenced this pull request Jul 13, 2022
This PR adds a new `knn` section to the search request.

As explained in #87625, the search returns a global top k documents across all
shards:
* In the DFS phase, collect the top k vector matches per shard, combine them
and keep the global top k. 
* Then move to the query phase. Convert the top k vector matches into a new
`KnnScoreDocQueryBuilder` that matches only those top documents. For the final
query, take a boolean disjunction between this `KnnScoreDocQueryBuilder` query and
the search request `query`.

Commits:
* Rename KnnSearchRequestBuilder -> KnnSearchRequestParser to avoid confusion with new KnnSearchBuilder
* Add `KnnScoreDocQueryBuilder`
* Implement kNN search using DFS phase

Addresses #87625.
@jtibshirani jtibshirani added :Search Relevance/Vectors Vector search and removed :Search/Search Search-related issues that do not fall into other categories labels Jul 21, 2022
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

>non-issue :Search Relevance/Vectors Vector search Team:Clients Meta label for clients team Team:Search Meta label for search team v8.4.0

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants