Skip to content

Block hybrid query when no search pipeline is configured - #1924

Open
AdityaWaskar wants to merge 2 commits into
opensearch-project:mainfrom
AdityaWaskar:fix/1922-block-hybrid-query-no-pipeline
Open

Block hybrid query when no search pipeline is configured#1924
AdityaWaskar wants to merge 2 commits into
opensearch-project:mainfrom
AdityaWaskar:fix/1922-block-hybrid-query-no-pipeline

Conversation

@AdityaWaskar

@AdityaWaskar AdityaWaskar commented Aug 2, 2026

Copy link
Copy Markdown

Description

Hybrid query currently executes even when no search pipeline is configured for the request, silently returning results in an internal, unnormalized format instead of failing clearly. This adds a coordinator-level check (in HybridQuerySearchRequestFilter) that rejects hybrid query requests when no search pipeline can be resolved — neither via the request (inline or search_pipeline param) nor as every target index's default search pipeline.

Related Issues

Resolves #1922

Signed-off-by: AdityaWaskar <adityawaskar05@gmail.com>
@github-actions

github-actions Bot commented Aug 2, 2026

Copy link
Copy Markdown

PR Reviewer Guide 🔍

(Review updated until commit 023078f)

Here are some key observations to aid the review process:

🧪 PR contains tests
🔒 No security concerns identified
✅ No TODO sections
🔀 No multiple PR themes
⚡ Recommended focus areas for review

Inline pipeline not detected

hasResolvableSearchPipeline only considers searchRequest.pipeline() (the pipeline id / request-parameter case) and the index's default search pipeline. It does not check for an inline search_pipeline defined in the request source (SearchSourceBuilder#pipeline/searchPipelineSource). The PR description and Javadoc claim inline pipelines are honored, but a request that supplies an inline pipeline definition in the body with no search_pipeline parameter and no index default will be incorrectly rejected with "hybrid query requires a search pipeline...".

private boolean hasResolvableSearchPipeline(SearchRequest searchRequest) {
    String requestPipeline = searchRequest.pipeline();
    if (isConfiguredPipeline(requestPipeline)) {
        return true;
    }

    List<IndexMetadata> indexMetadataList = NeuralSearchClusterUtil.instance().getIndexMetadataList(searchRequest);
    return indexMetadataList.isEmpty() == false && indexMetadataList.stream().allMatch(this::hasDefaultSearchPipeline);
}
Pipeline existence/validity not verified

isConfiguredPipeline only checks the id is non-blank and not NOOP_PIPELINE_ID; it does not verify the pipeline actually exists in the cluster or contains a normalization processor. A request specifying a nonexistent search_pipeline id, or an index with a default_search_pipeline pointing at a deleted/misconfigured pipeline, will pass this check and still produce the unnormalized results the PR aims to prevent.

private boolean isConfiguredPipeline(String pipelineId) {
    return Objects.nonNull(pipelineId)
        && pipelineId.isBlank() == false
        && SearchPipelineService.NOOP_PIPELINE_ID.equals(pipelineId) == false;
}
All-indices AND semantics

The check requires every resolved target index to have a default search pipeline (allMatch). For a multi-index request where some indices have a default pipeline configured and others do not, the request will be rejected even though the user may reasonably expect the configured indices' pipelines to apply. This differs from OpenSearch's per-shard resolution behavior and may reject previously-working requests once this filter is enabled.

List<IndexMetadata> indexMetadataList = NeuralSearchClusterUtil.instance().getIndexMetadataList(searchRequest);
return indexMetadataList.isEmpty() == false && indexMetadataList.stream().allMatch(this::hasDefaultSearchPipeline);

@github-actions

github-actions Bot commented Aug 2, 2026

Copy link
Copy Markdown

PR Code Suggestions ✨

Latest suggestions up to 023078f
Explore these optional code suggestions:

CategorySuggestion                                                                                                                                    Impact
Possible issue
Also accept inline search pipelines

The check does not account for an inline search pipeline defined in the request
source (searchRequest.source().searchPipelineSource()). Requests using inline
pipelines will be incorrectly rejected. Include a check for a non-null/non-empty
inline pipeline source before falling back to index-default resolution.

src/main/java/org/opensearch/neuralsearch/search/HybridQuerySearchRequestFilter.java [143-151]

 private boolean hasResolvableSearchPipeline(SearchRequest searchRequest) {
     String requestPipeline = searchRequest.pipeline();
     if (isConfiguredPipeline(requestPipeline)) {
+        return true;
+    }
+
+    if (searchRequest.source() != null
+        && searchRequest.source().searchPipelineSource() != null
+        && searchRequest.source().searchPipelineSource().isEmpty() == false) {
         return true;
     }
 
     List<IndexMetadata> indexMetadataList = NeuralSearchClusterUtil.instance().getIndexMetadataList(searchRequest);
     return indexMetadataList.isEmpty() == false && indexMetadataList.stream().allMatch(this::hasDefaultSearchPipeline);
 }
Suggestion importance[1-10]: 8

__

Why: Valid concern - inline pipelines defined via searchPipelineSource() in the request body would be incorrectly rejected by the current check, which only inspects the pipeline id and index defaults.

Medium
General
Validate pipeline existence, not just id

Treating any non-noop, non-blank pipeline id as "resolvable" does not verify the
pipeline actually exists or contains a normalization processor. A misspelled or
missing pipeline id will pass validation but still yield the unnormalized response
the PR aims to prevent. Consider validating pipeline existence and/or presence of a
normalization processor via SearchPipelineService.

src/main/java/org/opensearch/neuralsearch/search/HybridQuerySearchRequestFilter.java [158-162]

 private boolean isConfiguredPipeline(String pipelineId) {
     return Objects.nonNull(pipelineId)
         && pipelineId.isBlank() == false
         && SearchPipelineService.NOOP_PIPELINE_ID.equals(pipelineId) == false;
+    // TODO: also verify pipeline is registered and contains a normalization processor
 }
Suggestion importance[1-10]: 3

__

Why: The observation is reasonable but the improved_code is identical to existing_code (only a TODO comment is added), so it doesn't actually implement the fix and provides limited value.

Low

Previous suggestions

Suggestions up to commit 4ea01d6
CategorySuggestion                                                                                                                                    Impact
Possible issue
Detect inline search pipeline in source

The check only considers the named request pipeline but ignores inline search
pipelines defined in the request source (via
SearchSourceBuilder.searchPipelineSource()), even though the Javadoc explicitly
claims inline pipelines are supported. This will incorrectly reject valid hybrid
queries that carry an inline pipeline. Also consider inspecting the source for an
inline pipeline before falling back to index defaults.

src/main/java/org/opensearch/neuralsearch/search/HybridQuerySearchRequestFilter.java [144-147]

 String requestPipeline = searchRequest.pipeline();
 if (isConfiguredPipeline(requestPipeline)) {
     return true;
 }
+if (searchRequest.source() != null && searchRequest.source().searchPipelineSource() != null
+    && searchRequest.source().searchPipelineSource().isEmpty() == false) {
+    return true;
+}
Suggestion importance[1-10]: 8

__

Why: This is a valid issue: the Javadoc explicitly claims inline pipelines are supported, but the code doesn't check searchPipelineSource(). This could cause valid hybrid queries with inline pipelines to be incorrectly rejected.

Medium
General
Relax multi-index pipeline resolution check

Requiring that every target index has a default search pipeline is too strict and
will reject valid hybrid queries against multi-index/wildcard patterns where only
some indices have a default pipeline configured. Consider using anyMatch instead of
allMatch, or at minimum document/rethink this behavior, since a single index without
a default pipeline in a wildcard expansion will block the entire request even if a
valid pipeline is resolvable for other targets.

src/main/java/org/opensearch/neuralsearch/search/HybridQuerySearchRequestFilter.java [149-150]

 List<IndexMetadata> indexMetadataList = NeuralSearchClusterUtil.instance().getIndexMetadataList(searchRequest);
-return indexMetadataList.isEmpty() == false && indexMetadataList.stream().allMatch(this::hasDefaultSearchPipeline);
+return indexMetadataList.isEmpty() == false && indexMetadataList.stream().anyMatch(this::hasDefaultSearchPipeline);
Suggestion importance[1-10]: 5

__

Why: Valid concern about the strictness of allMatch for wildcard/multi-index requests, but changing to anyMatch could be equally problematic (allowing requests where some indices lack a pipeline). The correct behavior is debatable and warrants discussion.

Low

Signed-off-by: AdityaWaskar <adityawaskar05@gmail.com>
@AdityaWaskar

Copy link
Copy Markdown
Author

One design decision worth confirming: for requests spanning multiple indices (or an alias/wildcard), this PR requires every matched index to have a default search pipeline configured (or the request itself to set one) before a hybrid query is allowed to proceed — checked via NeuralSearchClusterUtil#getIndexMetadataList.

I wasn't sure whether that's the right semantics, or whether the default-search-pipeline fallback should only apply to single-index requests (mirroring how I believe SearchPipelineService resolves default pipelines elsewhere). Happy to adjust based on what's intended here.

@github-actions

github-actions Bot commented Aug 2, 2026

Copy link
Copy Markdown

Persistent review updated to latest commit 023078f

@codecov

codecov Bot commented Aug 3, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 84.61538% with 2 lines in your changes missing coverage. Please review.
✅ Project coverage is 83.41%. Comparing base (5facc2e) to head (023078f).

Files with missing lines Patch % Lines
...lsearch/search/HybridQuerySearchRequestFilter.java 84.61% 0 Missing and 2 partials ⚠️
Additional details and impacted files
@@             Coverage Diff              @@
##               main    #1924      +/-   ##
============================================
- Coverage     83.45%   83.41%   -0.04%     
- Complexity     3884     3889       +5     
============================================
  Files           291      291              
  Lines         13819    13832      +13     
  Branches       2294     2299       +5     
============================================
+ Hits          11532    11538       +6     
- Misses         1454     1456       +2     
- Partials        833      838       +5     

☔ View full report in Codecov by Harness.
📢 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.

* @param searchRequest the search request to check
* @return true if a non-noop pipeline can be resolved for this request
*/
private boolean hasResolvableSearchPipeline(SearchRequest searchRequest) {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

I think we should inspect the pipeline to confirm the normalization and combination processors are configured otherwise a search pipeline without those processors still cannot return the right hybrid query result.

@owaiskazi19 owaiskazi19 Aug 3, 2026

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

+1 here. Please add a validation to check the normalization and combination processors attached and an associated test

@martin-gaievski martin-gaievski left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Overall that's the right direction but you need to address comments, in addition to that add a single integ tests for failure path

* @param searchRequest the search request to check
* @return true if a non-noop pipeline can be resolved for this request
*/
private boolean hasResolvableSearchPipeline(SearchRequest searchRequest) {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

this logic diverges from SearchPipelineService. Core method resolvePipeline checks source().searchPipelineSource() first, before the named param.

When Core resolves an inline-body ad-hoc pipeline, request's searchPipelineSource map is not drained — meaning it is still intact when this ActionFilter runs (the filter runs before resolvePipeline). An inline-object body sets searchPipelineSource (SearchSourceBuilder.java) and leaves searchRequest.pipeline() null (RestSearchAction only populates pipeline() from the ?search_pipeline= param or the string form of source().pipeline()). So isConfiguredPipeline(null) is false, no index default exists, and the request is wrongly rejected with a 400, even core would build and run the inline normalization pipeline correctly.

Example of failing request, where index has no default pipeline, no ?search_pipeline= param:

POST /my-index/_search
{
  "search_pipeline": {
    "phase_results_processors": [
      { "normalization-processor": {
          "normalization": { "technique": "min_max" },
          "combination":  { "technique": "arithmetic_mean" } } }
    ]
  },
  "query": { "hybrid": { "queries": [ {"match":{"text":"hello"}}, {"term":{"text":"place"}} ] } }
}

You can fail fast on the inline body before the named/default checks, mirroring core branch.

}

List<IndexMetadata> indexMetadataList = NeuralSearchClusterUtil.instance().getIndexMetadataList(searchRequest);
return indexMetadataList.isEmpty() == false && indexMetadataList.stream().allMatch(this::hasDefaultSearchPipeline);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

this logic is different in core, and fails in multiple scenarios:

  • it collapses to _noneNO_OP_PIPELINE → no normalization runs → exact unnormalized/internal-format response we're trying to address. Intended behavior is to fail fast
  • multi-index mixed defaults → false rejection (over-strict vs. core). with your change we're skipping indices that have no explicit default rather than treating them as _none.

I suggest you don't allMatch over raw .get() values.
Most robust approach is to delegate to core so the filter cannot diverge.
Alternatively fold per-index defaults with core's exact semantics — use .exists() to skip indices with no default, take the first default, collapse to _none if a later index has a different pipeline, then accept only if the folded id is a configured (non-_none) pipeline.

return;
}
if (hasResolvableSearchPipeline(searchRequest) == false) {
listener.onFailure(new IllegalArgumentException(HybridQueryUtil.HYBRID_QUERY_REQUIRES_SEARCH_PIPELINE_MESSAGE));

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

provided error message is over promising - we are checking for a simple search pipeline presence, not normalization related processors. Either add check for processor or relax the message and drop mentions of processors (which is much easier fix I believe)

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

Labels

good first issue Good for newcomers

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[PROPOSAL] Block hybrid query in case there are no search pipeline

4 participants