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

[FEATURE] Add z-score for the normalization processor #376 #468

Closed
wants to merge 5 commits into from

Conversation

sam-herman
Copy link

Description

This change implements #376

  • Add z-score for hybrid query normalization processor
  • Add IT that test normalization end to end

Issues Resolved

Resolving #376

Check List

  • [x ] New functionality includes testing.
    • [ x] All tests pass
  • [x ] New functionality has been documented.
    • [ x] New functionality has javadoc added
  • [ x] 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.

Signed-off-by: Samuel Herman <[email protected]>
Signed-off-by: Samuel Herman <[email protected]>
Signed-off-by: Samuel Herman <[email protected]>
@navneet1v
Copy link
Collaborator

@samuel-oci thanks for creating the PR. But given that this is a new feature, the recommendation is to put this code in feature branch. This is to ensure that main branch is not blocked.

I will go ahead and create a feature branch for this change.

As you have already written down the full code, its a good time to start doing the performance testing and search relevancy testing for this feature.

@navneet1v
Copy link
Collaborator

Created a feature branch from main, for this feature: feature/z-score-normalization

Please raise the PR against that branch. Also can you add a entry in the CHANGELOG.md file for this change.

@@ -52,6 +52,7 @@ public void execute(
final ScoreNormalizationTechnique normalizationTechnique,
final ScoreCombinationTechnique combinationTechnique
) {
log.info("Entering normalization processor workflow");
Copy link
Collaborator

Choose a reason for hiding this comment

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

we can remove this.

@@ -26,8 +26,6 @@
@Log4j2
public class ScoreCombiner {

private static final Float ZERO_SCORE = 0.0f;
Copy link
Collaborator

Choose a reason for hiding this comment

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

any reason why we are removing this?

Copy link
Author

Choose a reason for hiding this comment

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

It's not in use anywhere in the code

*/
/*
TODO: Some todo items that apply here but also on the original normalization techniques on which it is modeled {@link L2ScoreNormalizationTechnique} and {@link MinMaxScoreNormalizationTechnique}
1. Random access to abstract list object is a bad practice both stylistically and from performance perspective and should be removed
Copy link
Collaborator

Choose a reason for hiding this comment

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

Can you please provide an alternative what should be used?

As per my understanding, random access on the List is bad if List concrete implementation is LinkedList. But what I have seen generally is we use ArrayList which is backed by arrays, hence random access is done in constant time.

Copy link
Member

Choose a reason for hiding this comment

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

It should be fine if we know the exact implementation of List, as Navneet mentioned. But with list we can use functional style easier, without expensive conversion array -> stream, that was a reason why we switched to a List.

Copy link
Author

Choose a reason for hiding this comment

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

Usually it is highly discouraged to do List.get() for an abstract List object because it could be an implementation that doesn't support random access efficiently (e.g. LinkedList). Suggested alternative is to enforce that this is explicitly declared as an ArrayList object throughout the hot path that require random access.

Copy link
Member

Choose a reason for hiding this comment

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

I'm ok to switch from using general List to ArrayList, that still works with stream API and keep our requirements to a caller code cleaner. I expect that change will affect a lot of classes, thus I prefer to see it as a separate refactoring PR.

Copy link
Author

Choose a reason for hiding this comment

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

@martin-gaievski same here, I added the comment out of intention to propose as a separate refactoring PR.

/*
TODO: Some todo items that apply here but also on the original normalization techniques on which it is modeled {@link L2ScoreNormalizationTechnique} and {@link MinMaxScoreNormalizationTechnique}
1. Random access to abstract list object is a bad practice both stylistically and from performance perspective and should be removed
2. Identical sub queries and their distribution between shards is currently completely implicit based on ordering and should be explicit based on identifier
Copy link
Collaborator

@navneet1v navneet1v Oct 19, 2023

Choose a reason for hiding this comment

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

This is really a good thought, but problem is none of the query clauses in Opensearch supports identifiers. During the implementation this was discussed. The problem is the way after QueryPhase the results are returned. They are returned in a ScoreDocs array which doesn't support identifiers.

We can go around that but it will require changes in interface of OpenSearch Core. Hence we decided against it to make sure that we are compatible with OpenSearch core.

If there is an alternative supported in opensearch please let us know, may be we are missing something

Copy link
Author

Choose a reason for hiding this comment

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

sounds good @navneet1v I will give it some thought and will come up with suggestion. In any case not planning to do as part of this change. Can keep it for now and can suggest refactor or just remove if not achievable.

TODO: Some todo items that apply here but also on the original normalization techniques on which it is modeled {@link L2ScoreNormalizationTechnique} and {@link MinMaxScoreNormalizationTechnique}
1. Random access to abstract list object is a bad practice both stylistically and from performance perspective and should be removed
2. Identical sub queries and their distribution between shards is currently completely implicit based on ordering and should be explicit based on identifier
3. Weird calculation of numOfSubQueries instead of having a more explicit indicator
Copy link
Collaborator

Choose a reason for hiding this comment

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

same as above.

Comment on lines 36 to 37
// why are we doing that? is List<CompoundTopDocs> the list of subqueries for a single shard? or a global list of all subqueries across shards?
// If a subquery comes from each shard then when is it combined? that seems weird that combination will do combination of normalized results that each is normalized just based on shard level result
Copy link
Collaborator

Choose a reason for hiding this comment

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

Lets talk about these on the github issue and not on the PR.

Copy link
Author

Choose a reason for hiding this comment

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

ack, I think this comment is no longer relevant I put it there and forgot to remove so feel free to ignore this one.

Comment on lines 123 to 125
//TODO: make this better, currently
// this is a horrible implementation in particular when it comes to the topDocsPerSubQuery.get(j)
// which does a random search on an abstract list type.
Copy link
Collaborator

Choose a reason for hiding this comment

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

Please provide reason why this is bad and how it can be improved.

Copy link
Collaborator

Choose a reason for hiding this comment

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

Also, let's avoid using subject word like 'horrible'.

Copy link
Author

Choose a reason for hiding this comment

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

@heemin32 my apologies, will avoid it in the future.

public static final String TECHNIQUE_NAME = "z_score";
private static final float SINGLE_RESULT_SCORE = 1.0f;
@Override
public void normalize(List<CompoundTopDocs> queryTopDocs) {
Copy link
Member

Choose a reason for hiding this comment

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

please make all args of all public methods final

.findAny()
.get()
.getTopDocs()
.size();
Copy link
Member

Choose a reason for hiding this comment

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

Can you please add more checks for nulls and empty objects. I think we're assuming a lot, e.g. topDocs.getTopDocs() is not null, .findAny() will always return something etc.

Copy link
Author

Choose a reason for hiding this comment

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

yeah agreed, I will add checks, currently it's modeled on existing normalization techniques (MinMax/L2) which use similar code.
Also I'm not sure if there could also be a potential issue with implicit assumption that is made regarding the way we discover the number of sub queries. Currently it assumes that all none empty shard results will have all subqueries returned with 0 hits. I didn't confirm if this assumption always hold, but it would be nice if we had someway of passing it as metadata from upstream instead of making implicit assumptions. So just placing it here as a food for thought in case there is interest to explore that.

* SPDX-License-Identifier: Apache-2.0
*/

package org.opensearch.neuralsearch.query;
Copy link
Member

Choose a reason for hiding this comment

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

I think this integ test belongs to a processor package, as we're mainly testing normalization technique that is part of the processor.

Copy link
Author

Choose a reason for hiding this comment

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

sounds good, will move to processor


private Optional<Float> getMaxScore(Map<String, Object> searchResponseAsMap) {
Map<String, Object> hitsMap = (Map<String, Object>) searchResponseAsMap.get("hits");
return hitsMap.get("max_score") == null ? Optional.empty() : Optional.of(((Double) hitsMap.get("max_score")).floatValue());
Copy link
Member

Choose a reason for hiding this comment

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

there 3 methods are copied from https://github.com/opensearch-project/neural-search/blob/main/src/test/java/org/opensearch/neuralsearch/query/HybridQueryIT.java#L271-L283, can we refactor code and pull them to a base class or a utility class for tests

new TopDocs(new TotalHits(0, TotalHits.Relation.EQUAL_TO), new ScoreDoc[0]),
new TopDocs(
new TotalHits(3, TotalHits.Relation.EQUAL_TO),
new ScoreDoc[] { new ScoreDoc(3, 0.98058068f), new ScoreDoc(4, 0.39223227f), new ScoreDoc(2, -1.37281295f) }
Copy link
Member

Choose a reason for hiding this comment

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

can you please add a function or description with a formula of how those expected scores are calculated?

Also why we have a negative score value for one of ScoreDocs?

Copy link
Author

Choose a reason for hiding this comment

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

@martin-gaievski will add documentation. Regarding negatives, z-scores can also be negative:
https://www.z-table.com/
Btw, I should have mentioned in the comments that I wanted to bring up that the combiner is not good at dealing with negative values, but that would be adding additional scope.

Comment on lines 123 to 125
//TODO: make this better, currently
// this is a horrible implementation in particular when it comes to the topDocsPerSubQuery.get(j)
// which does a random search on an abstract list type.
Copy link
Collaborator

Choose a reason for hiding this comment

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

Also, let's avoid using subject word like 'horrible'.

}
}

static private float[] findScoreSumPerSubQuery(final List<CompoundTopDocs> queryTopDocs, final int numOfScores) {
Copy link
Collaborator

Choose a reason for hiding this comment

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

nit. private would be better unless you have specific reason this to be static. Better way would be moving all these methods to another class to make it easier to write unit test.

Copy link
Author

Choose a reason for hiding this comment

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

convention I was following is that if method is not dependent on any instance object it should be static.
Regarding refactoring method out to utility class, are there any other classes that can use it right now or in the future? Ideally I would like to avoid creating unnecessary abstraction.

@sam-herman
Copy link
Author

Created a feature branch from main, for this feature: feature/z-score-normalization

Please raise the PR against that branch. Also can you add a entry in the CHANGELOG.md file for this change.

Thank you @navneet1v @heemin32 @martin-gaievski for reviewing, I will create a new PR against the feature branch with your comments addressed.

Signed-off-by: Samuel Herman <[email protected]>
@sam-herman
Copy link
Author

As you have already written down the full code, its a good time to start doing the performance testing and search relevancy testing for this feature.

@navneet1v can you point me to how to run the performance and search relevancy tests? Is there a ready workflow with existing benchmarks?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants