Skip to content

Add hybrid query and score/rank based normalization processor stats #1326

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

Merged

Conversation

q-andy
Copy link
Contributor

@q-andy q-andy commented May 15, 2025

PR to add initial stats for hybrid query, normalization processor, and RRF processor

Normalization processor event stats:

  • normalization_processor_executions
  • comb_arithmetic_executions
  • comb_geometric_executions
  • comb_harmonic_executions
  • comb_rrf_executions
  • norm_l2_executions
  • norm_minmax_executions
  • norm_zscore_executions

Normalization processor info stats:

  • normalization_processors
  • comb_arithmetic_processors
  • comb_geometric_processors
  • comb_harmonic_processors
  • norm_l2_processors
  • norm_minmax_processors
  • norm_zscore_processors

RRF processor event stats:

  • rank_based_normalization_processor_executions

RRF processor info stats:

  • rank_based_normalization_processors
  • comb_rrf_processors

Hybrid query event stats

  • hybrid_query_requests
  • hybrid_query_with_filter_requests
  • hybrid_query_with_inner_hits_requests
  • hybrid_query_with_pagination_requests

Example response:

	"info": {
		"cluster_version": "3.1.0",
		"processors": {
			"search": {
				"hybrid": {
					"comb_geometric_processors": 0,
					"comb_rrf_processors": 0,
					"norm_l2_processors": 0,
					"norm_minmax_processors": 0,
					"comb_harmonic_processors": 0,
					"comb_arithmetic_processors": 0,
					"norm_zscore_processors": 0,
					"rank_based_normalization_processors": 0,
					"normalization_processors": 0
				}
			},
			"ingest": {
				"text_chunking_delimiter_processors": 0,
				"text_embedding_processors_in_pipelines": 0,
				"text_chunking_fixed_length_processors": 0,
				"text_chunking_processors": 0
			}
		}
	},
	"all_nodes": {
		"query": {
			"hybrid": {
				"hybrid_query_with_pagination_requests": 0,
				"hybrid_query_with_filter_requests": 0,
				"hybrid_query_with_inner_hits_requests": 0,
				"hybrid_query_requests": 0
			}
		},
		"semantic_highlighting": {
			"semantic_highlighting_request_count": 0
		},
		"processors": {
			"search": {
				"hybrid": {
					"comb_harmonic_executions": 0,
					"norm_zscore_executions": 0,
					"norm_l2_executions": 0,
					"comb_rrf_executions": 0,
					"rank_based_normalization_processor_executions": 0,
					"comb_arithmetic_executions": 0,
					"normalization_processor_executions": 0,
					"comb_geometric_executions": 0,
					"norm_minmax_executions": 0
				}
			},
			"ingest": {
				"text_chunking_executions": 0,
				"text_embedding_executions": 0,
				"text_chunking_fixed_length_executions": 0,
				"text_chunking_delimiter_executions": 0
			}
		}
	},
....

Related Issues

Resolves #1146

Check List

  • New functionality includes testing.
  • New functionality has been documented.
  • API changes companion pull request created. <- Since we are backfilling stats for previous features, we will add them all to the spec and documentation near 3.1 release
  • Commits are signed per the DCO using --signoff.
  • Public documentation issue/PR created. <- Since we are backfilling stats for previous features, we will add them all to the spec and documentation near 3.1 release

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.

@q-andy q-andy force-pushed the stats-normalization-processor branch 4 times, most recently from 966d125 to 6797f1d Compare May 16, 2025 23:32
@@ -70,6 +72,7 @@ <Result extends SearchPhaseResult> void hybridizeScores(
Optional<FetchSearchResult> fetchSearchResult = getFetchSearchResults(searchPhaseResult);
boolean explain = Objects.nonNull(searchPhaseContext.getRequest().source().explain())
&& searchPhaseContext.getRequest().source().explain();
EventStatsManager.increment(EventStatName.RRF_PROCESSOR_EXECUTIONS);
Copy link
Member

Choose a reason for hiding this comment

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

for rank based normalization you also need to increment counter for combination technique calls, currently we do support "rrf" https://docs.opensearch.org/docs/latest/search-plugins/search-pipelines/score-ranker-processor/

Copy link
Contributor Author

@q-andy q-andy May 19, 2025

Choose a reason for hiding this comment

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

Do we plan to add more combination techniques for rank based normalization in the future? We could also wait until then to add more granular breakdowns, otherwise the stats will just be duplicated right? I added anyways for now

Copy link
Member

Choose a reason for hiding this comment

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

no in near future, my worry was more about consistent result format, because for RRF we have "rrf" as both normalization technique (hidden), and combination technique.

@q-andy q-andy force-pushed the stats-normalization-processor branch 5 times, most recently from b9765c8 to c6f1ace Compare May 19, 2025 22:21
@q-andy q-andy marked this pull request as ready for review May 19, 2025 22:23
@q-andy q-andy changed the title Add normalization processor and RRF processor stats Add hybrid query and score/rank based normalization processor stats May 20, 2025
@q-andy q-andy force-pushed the stats-normalization-processor branch from c6f1ace to c7ea569 Compare May 20, 2025 00:23
@@ -40,6 +49,24 @@ public class NormalizationProcessor extends AbstractScoreHybridizationProcessor
private final ScoreCombinationTechnique combinationTechnique;
private final NormalizationProcessorWorkflow normalizationWorkflow;

private final Map<String, Runnable> normTechniqueIncrementers = Map.of(
Copy link
Contributor

Choose a reason for hiding this comment

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

looks like we have switch/case to increment for some processors, and runnable as map values for others. We should have one standard approach

Copy link
Member

Choose a reason for hiding this comment

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

+1. I am more inclined towards using map.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Sure, can refactor the others as maps.

Copy link
Member

Choose a reason for hiding this comment

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

this map can be static, same for the second map combTechniqueIncrementers

for (QueryBuilder query : queries) {
if (filter == null) {
compoundQueryBuilder.add(query);
} else {
compoundQueryBuilder.add(query.filter(filter));
}

// Check if children have inner hits for stats
if (hasInnerHits == false) {
Copy link
Contributor

Choose a reason for hiding this comment

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

is this the right comparison?

if children have inner hits, shouldn't this be true?


// Check if children have inner hits for stats
if (hasInnerHits == false) {
Map<String, InnerHitContextBuilder> innerHits = new HashMap<>();
Copy link
Contributor

Choose a reason for hiding this comment

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

shouldn't we emit stats for the inner hits? seems like we're checking for inner hits without doing anything to them. hasInnerHits can be flipped between true/false in the for loop, but we're only emitting once after the loop finishes in line 295. Is this valid?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

The thought here is that we care about the stats at the level of the hybrid query. If at least one child query has inner hits then that means the hybrid query is an inner hits query. So we check each child, if at least one has inner hits, then we set hasInnerHits = true and we don't have to keep checking the rest.

@@ -40,6 +49,24 @@ public class NormalizationProcessor extends AbstractScoreHybridizationProcessor
private final ScoreCombinationTechnique combinationTechnique;
private final NormalizationProcessorWorkflow normalizationWorkflow;

private final Map<String, Runnable> normTechniqueIncrementers = Map.of(
Copy link
Member

Choose a reason for hiding this comment

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

+1. I am more inclined towards using map.


private void recordStats(ScoreCombinationTechnique combinationTechnique) {
EventStatsManager.increment(EventStatName.RRF_PROCESSOR_EXECUTIONS);
Optional.of(combTechniqueIncrementers.get(combinationTechnique.techniqueName())).ifPresent(Runnable::run);
Copy link
Member

Choose a reason for hiding this comment

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

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I added this since @martin-gaievski requested, but my opinion is if currently RRF processor only works works with RRF combination technique and RRF normalization technique, then functionally the stats will be identical, and I don't think there's a point to including a breakdown, it will just be duplicated. Combination is configurable with a single option, but normalization technique isn't a configurable in processor config: https://docs.opensearch.org/docs/latest/search-plugins/search-pipelines/score-ranker-processor/

If in the future there are more normalizaiton/score techniques added we can add this granularity then.

Copy link
Member

Choose a reason for hiding this comment

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

Having RRF as processor and as technique gives more dimensions for reporting. For instance, with original version you can generate report with breakdown "by processor type". But report with breakdown on something like "by combination technique" will be harder because for RRF you raw data will have combination technique metric with empty value.

Copy link
Contributor Author

@q-andy q-andy May 22, 2025

Choose a reason for hiding this comment

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

I see your point, my concern is having duplicated information in the stats API makes it less readable and less performant. Especially as we add more features, we are concerned with possible response bloat, similar the issues core is facing with the size of _nodes/info and _nodes/stats responses causing slowdowns on large clusters. And adding stats is one-way door in the sense that it's difficult to justify removing them since it's a breaking change. Ideally we save granularity for when it is needed and don't add more stats unless necessary

From a report perspective, I'm thinking in terms of what kind of insight a breakdown would give you: If you are trying to determine which score combination techniques are seeing more usage, perhaps proportion of RRF to zscore or minmax isn't comparable since they're categorically different, e.g. used in different processors in different contexts. And if needed, the information is available implicitly from looking at RRF processor stats directly.

for (QueryBuilder query : queries) {
if (filter == null) {
compoundQueryBuilder.add(query);
} else {
compoundQueryBuilder.add(query.filter(filter));
}

// Check if children have inner hits for stats
if (hasInnerHits == false) {
Copy link
Member

Choose a reason for hiding this comment

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

Where are we changing this parameter value?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

It's set inside the loop, see my other comment for explanation. Basically my thought is we want to increment the stat once if at least one child query has inner hits, which means the hybrid query as a whole is an inner hits hybrid query.

@@ -40,6 +49,24 @@ public class NormalizationProcessor extends AbstractScoreHybridizationProcessor
private final ScoreCombinationTechnique combinationTechnique;
private final NormalizationProcessorWorkflow normalizationWorkflow;

private final Map<String, Runnable> normTechniqueIncrementers = Map.of(
Copy link
Member

Choose a reason for hiding this comment

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

this map can be static, same for the second map combTechniqueIncrementers

break;
case RRFProcessor.TYPE:
countRRFProcessorStats(stats, processorConfig);
break;
Copy link
Member

Choose a reason for hiding this comment

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

please add default case and throw one of runtime exceptions in case we reach it

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Not sure if that makes sense here? Here we are iterating through all processors in the pipeline, which may contain processors from core or other plugins. If we encounter an MLInferenceRequestProcessor for example, we would not want to record stats for it, and that would trigger the default case and throw an exception, breaking the API.

@q-andy q-andy force-pushed the stats-normalization-processor branch 3 times, most recently from ebd6ef6 to 0eac607 Compare May 22, 2025 22:14
@q-andy q-andy force-pushed the stats-normalization-processor branch from c7f39e9 to bfedad1 Compare June 2, 2025 23:06
@q-andy q-andy mentioned this pull request Jun 2, 2025
5 tasks
Copy link
Member

@martin-gaievski martin-gaievski 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 to me, thanks!

@vibrantvarun
Copy link
Member

LGTM

q-andy added 5 commits June 4, 2025 15:49
Signed-off-by: Andy Qin <[email protected]>

# Conflicts:
#	CHANGELOG.md
#	src/main/java/org/opensearch/neuralsearch/stats/events/EventStatName.java

# Conflicts:
#	CHANGELOG.md
#	src/main/java/org/opensearch/neuralsearch/stats/events/EventStatName.java
#	src/main/java/org/opensearch/neuralsearch/stats/info/InfoStatName.java
Signed-off-by: Andy Qin <[email protected]>

# Conflicts:
#	CHANGELOG.md
Signed-off-by: Andy Qin <[email protected]>
@q-andy q-andy force-pushed the stats-normalization-processor branch from bfedad1 to 788edd7 Compare June 4, 2025 22:50
@heemin32 heemin32 merged commit f6236b7 into opensearch-project:main Jun 5, 2025
82 of 91 checks passed
Copy link

codecov bot commented Jun 5, 2025

Codecov Report

All modified and coverable lines are covered by tests ✅

Project coverage is 0.00%. Comparing base (2928d83) to head (788edd7).
Report is 2 commits behind head on main.

Additional details and impacted files
@@             Coverage Diff              @@
##               main   #1326       +/-   ##
============================================
- Coverage     82.71%       0   -82.72%     
============================================
  Files           149       0      -149     
  Lines          7383       0     -7383     
  Branches       1192       0     -1192     
============================================
- Hits           6107       0     -6107     
+ Misses          821       0      -821     
+ Partials        455       0      -455     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

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

Successfully merging this pull request may close these issues.

[FEATURE] Create stats api to capture hybrid query stats
5 participants