Skip to content
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

Adding serialization for filter field in KnnQueryBuilder #564

Conversation

martin-gaievski
Copy link
Member

@martin-gaievski martin-gaievski commented Sep 28, 2022

Signed-off-by: Martin Gaievski [email protected]

Description

Adding serialization for filter in Knn query. Without the change multi-node cluster may be in inconsistent state, meaning some shards will execute knn query without the filter and search result will be irrelevant.

Special case for rolling and restart upgrades. Let's say we do have existing cluster on 2.3 (we call it old) and we want to upgrade to 3.0 (we call it new). Nodes need to handle following cases for success run:

  • old -> old just existing cluster works before the upgrade. This should already work
  • old -> new this is part of restart and rolling upgrades
  • new -> old part of rolling upgrade
  • new -> new upgraded cluster works

The easiest way to satisfy all criteria is to only write and read filter field in the last case of new->new. In all other cases input stream will be corrupted and logic after knn query will fail. To address this we introducing the cluster min deployed version concept that is set as a lowest out of all versions deployed in the cluster.

One more added check is for manual k-NN search query, it minimal cluster version is below one that supports filtering feature (3.0.0 for this branch), query will be rejected with 4xx Bad request error. This is to address cluster upgrades, we need to avoid accepting queries with filter until upgrade is completed. Otherwise only some of the cluster nodes will use filter and this affects recall/relevance of the result.

Issues Resolved

#376

Check List

  • New functionality includes testing.
    • All tests pass
  • Commits are signed as per the DCO using --signoff

By submitting this pull request, I confirm that my contribution is made under the terms of the Apache 2.0 license.
For more information on following Developer Certificate of Origin and signing off your commits, please check here.

@martin-gaievski martin-gaievski added Enhancements Increases software capabilities beyond original client specifications 2.4.0 feature branch labels Sep 28, 2022
@martin-gaievski martin-gaievski requested a review from a team September 28, 2022 01:10
@codecov-commenter
Copy link

codecov-commenter commented Sep 28, 2022

Codecov Report

Merging #564 (af6ad48) into feature/efficient-filtering (ce68025) will increase coverage by 0.38%.
The diff coverage is 97.22%.

@@                        Coverage Diff                        @@
##             feature/efficient-filtering     #564      +/-   ##
=================================================================
+ Coverage                          83.72%   84.10%   +0.38%     
- Complexity                          1035     1049      +14     
=================================================================
  Files                                148      149       +1     
  Lines                               4282     4316      +34     
  Branches                             379      384       +5     
=================================================================
+ Hits                                3585     3630      +45     
+ Misses                               520      509      -11     
  Partials                             177      177              
Impacted Files Coverage Δ
...rg/opensearch/knn/index/query/KNNQueryBuilder.java 84.10% <92.85%> (+9.28%) ⬆️
...va/org/opensearch/knn/index/KNNClusterContext.java 100.00% <100.00%> (ø)
...main/java/org/opensearch/knn/plugin/KNNPlugin.java 100.00% <100.00%> (ø)

Help us with your feedback. Take ten seconds to tell us how you rate us. Have a feature suggestion? Share it here.

@@ -181,6 +184,12 @@ protected void doWriteTo(StreamOutput out) throws IOException {
out.writeString(fieldName);
out.writeFloatArray(vector);
out.writeInt(k);
if (filter != null) {
out.writeBoolean(true);
Copy link
Collaborator

Choose a reason for hiding this comment

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

is this boolean required? can we somehow skip it?

Copy link
Member

Choose a reason for hiding this comment

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

Use writeOptionalNamedWriteable? I think it abstracts this logic.

Copy link
Member Author

Choose a reason for hiding this comment

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

thanks Jack, will update to writeOptionalNamedWriteable

@@ -109,6 +109,9 @@ public KNNQueryBuilder(StreamInput in) throws IOException {
fieldName = in.readString();
vector = in.readFloatArray();
k = in.readInt();
if (in.readBoolean()) {
filter = in.readNamedWriteable(QueryBuilder.class);
}
} catch (IOException ex) {
throw new RuntimeException("[KNN] Unable to create KNNQueryBuilder: " + ex);
Copy link
Collaborator

Choose a reason for hiding this comment

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

can we fix this error also. This exception will eat up the stack trace. I know its not part of your change.

Suggested change
throw new RuntimeException("[KNN] Unable to create KNNQueryBuilder: " + ex);
throw new RuntimeException("[KNN] Unable to create KNNQueryBuilder ", ex);

Copy link
Member Author

Choose a reason for hiding this comment

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

ack


final KNNQueryBuilder knnQueryBuilder = new KNNQueryBuilder(FIELD_NAME, queryVector, K);

try (BytesStreamOutput output = new BytesStreamOutput()) {
Copy link
Collaborator

Choose a reason for hiding this comment

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

Can we extract this block as a private method? I see the only difference is checking if getFilter return null or expected filter which can be handled by parameter?

Copy link
Member Author

Choose a reason for hiding this comment

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

let me think on proper refactoring, it should be possible. I'm adding one more similar block so code duplication is definitely there

@martin-gaievski martin-gaievski force-pushed the feature/efficient-filtering-mg branch from bf7469c to c385555 Compare September 28, 2022 19:06
@martin-gaievski martin-gaievski force-pushed the feature/efficient-filtering-mg branch from 195f025 to 536ed11 Compare September 29, 2022 00:20
}
}

private void assertSerialization(final QueryBuilder deserializedQuery, boolean assertFilterIsNull) {
Copy link
Collaborator

Choose a reason for hiding this comment

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

TERM_QUERY is decided by KNNQueryBuilder but this method does not get the value from the KNNQueryBuilder.
Also I see there is a room for further optimization.

public void testSerialization() throws Exception {
    testSerialization(Version.CURRENT, null);
    testSerialization(Version.CURRENT, TERM_QUERY);
    testSerialization(Version.V_2_3_0, null);
}

Copy link
Member Author

Choose a reason for hiding this comment

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

ack

ImmutableOpenMap<String, DiscoveryNode> clusterDiscoveryNodes = ImmutableOpenMap.of();
log.debug("Reading cluster min version");
try {
clusterDiscoveryNodes = this.clusterService.state().getNodes().getNodes();
Copy link
Collaborator

Choose a reason for hiding this comment

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

Is this method call cheap? Otherwise, calling this method every time when we serialize/deserialize query might lead to performance degradation.

Copy link
Member Author

Choose a reason for hiding this comment

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

It should be cheap, state is cached locally and updated asynchronously if nodes are joining/leaving cluster. I want to run perf tests on multi-node cluster once this is merged to feature branch and compare latencies with previous solution.

@martin-gaievski martin-gaievski force-pushed the feature/efficient-filtering-mg branch 8 times, most recently from 255edae to af6ad48 Compare October 5, 2022 05:13
@martin-gaievski martin-gaievski force-pushed the feature/efficient-filtering-mg branch from af6ad48 to c314b11 Compare October 5, 2022 15:49
@martin-gaievski martin-gaievski merged commit 1729748 into opensearch-project:feature/efficient-filtering Oct 5, 2022
martin-gaievski added a commit that referenced this pull request Oct 14, 2022
* Adding serialization/deserialization for filter field in Lucene knn query

Signed-off-by: Martin Gaievski <[email protected]>
martin-gaievski added a commit to martin-gaievski/k-NN that referenced this pull request Oct 20, 2022
…project#564)

* Adding serialization/deserialization for filter field in Lucene knn query

Signed-off-by: Martin Gaievski <[email protected]>
@heemin32 heemin32 added v2.4.0 'Issues and PRs related to version v2.4.0' and removed 2.4.0 labels Nov 2, 2022
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Enhancements Increases software capabilities beyond original client specifications feature branch v2.4.0 'Issues and PRs related to version v2.4.0'
Projects
None yet
Development

Successfully merging this pull request may close these issues.

6 participants