Skip to content

Fixes #33587: let hybrid ranking ride along with the query instead of a cluster-global pipeline - #33588

Open
mohityadav766 wants to merge 1 commit into
fix/33572-cluster-alias-scoped-search-requestsfrom
fix/33587-inline-hybrid-rrf-pipeline
Open

mohityadav766 wants to merge 1 commit into
fix/33572-cluster-alias-scoped-search-requestsfrom
fix/33587-inline-hybrid-rrf-pipeline

Conversation

@mohityadav766

@mohityadav766 mohityadav766 commented Sep 18, 2026

Copy link
Copy Markdown
Member

Describe your changes:

Fixes #33587

Stacked on #33574 — base is that branch, so the diff here is only the pipeline change. GitHub will retarget this to main automatically once #33574 merges.

Creating the hybrid-rrf search pipeline needs cluster:admin/search/pipeline/put, which an index-scoped search role does not have and which the security plugin cannot pattern-scope. On every deployment whose role is confined to its own <clusterAlias>* prefix the PUT 403s. The reindex survives — ReindexingOrchestrator warns and continues — but the pipeline is never created, so Collate's hybrid queries name a pipeline that does not exist and ranking silently degrades. The only signal is one preflight WARN.

Two further problems with the stored pipeline even where it is permitted: a search pipeline is a cluster-global object and hybrid-rrf is a hardcoded name, while its weights come from per-deployment search settings. On a shared cluster each tenant's reindex overwrote the previous tenant's ranking weights. Granting the privilege would have made that worse, not better.

What changed

  1. OpenSearchVectorService.buildHybridRrfPipelineDefinition(kw, sem) — the RRF body is now a value, used both by the PUT and by callers that inline it into a search request's search_pipeline field. A test pins the two byte-identical, so stored and inline deployments cannot drift into ranking differently.

  2. SearchRepository.getHybridRrfPipelineDefinition() — resolves the effective weights (search settings, falling back to config) per call and returns the body to inline. Resolving per call is also why an admin's weight change now applies to the next query instead of waiting for a reindex to re-PUT.

  3. SystemResource — stop turning a failed pipeline refresh into a SystemSettingsException. That made search settings unsaveable on exactly the deployments this issue affects. The weights being saved are the ones hybrid search reads, so they take effect either way.

The PUT stays as a best-effort legacy path for anything still reading through ?search_pipeline=hybrid-rrf; it can be deleted once no consumer names the stored pipeline.

Why inline is safe

OpenSearch builds ad-hoc pipelines through the same processor factories as stored ones — SearchPipelineService.resolvePipeline passes phaseInjectorProcessorFactories to PipelineWithMetrics.create, and RRFProcessorFactory ignores the pipeline source — so score-ranker-processor behaves identically inline. Note OpenSearch rejects a request that both names a stored pipeline and carries an inline one, so consumers must send exactly one.

Companion change

This PR only exposes the definition. The consumer switch is in Collate (HybridSearchService), which is what actually restores hybrid search; that PR is blocked on this one merging.

Type of change:

  • Bug fix

High-level design:

N/A — small change.

Tests:

Use cases covered

  • The inline and stored pipeline bodies are the same document, so ranking cannot diverge between deployments using either form
  • Saving search settings succeeds on a deployment where the pipeline refresh is not permitted
  • The existing stored-pipeline PUT is unchanged in shape and weights

Unit tests

  • I added unit tests for the new/changed logic.

OpenSearchVectorServiceTest.testInlinePipelineDefinitionIsByteIdenticalToTheStoredPipelineBody — captures the real PUT body and asserts it equals buildHybridRrfPipelineDefinition(0.4, 0.6). The pre-existing pipeline-shape tests (weights, rank_constant, no response_processors/collapse) still pass unchanged, which is the regression guard on the extraction.

Result: Tests run: 173, Failures: 0, Errors: 0 across the touched search suites.

Backend integration tests

  • Not applicable (no API surface change; SystemResource behaviour change is failure-path only).

Ingestion integration tests

  • Not applicable.

Playwright (UI) tests

  • Not applicable.

Manual testing performed

Verified the inline path against upstream source rather than a live restricted cluster:

  • SearchPipelineService.resolvePipeline builds ad-hoc pipelines with the full factory set including phase-results processors, and rejects requests carrying both a named and an inline pipeline
  • RRFProcessorFactory.create places no constraint on Processor.PipelineContext / pipeline source
  • Compiled the Collate consumer against this branch installed locally, and its query-builder suite passes (40 tests)

UI screen recording / screenshots:

Not applicable.

Checklist:

  • I have read the CONTRIBUTING document.
  • My PR title is Fixes <issue-number>: <short explanation>
  • My PR is linked to a GitHub issue via Fixes #<issue-number> above.
  • I have commented on my code, particularly in hard-to-understand areas.
  • I have added tests (unit / integration / Playwright as applicable) and listed them above.
  • I have added a test that covers the exact scenario we are fixing.

🤖 Generated with Claude Code

RetriggerConfidence Score: 4/5

The implementation appears safe to merge, with non-blocking but important regression-test gaps around the settings-save recovery and effective-weight lookup.

Findings

  1. P2 Key behaviors lack coverage
Summary

This PR extracts the OpenSearch RRF pipeline document for request-level use, resolves effective hybrid weights per call, and makes legacy named-pipeline refresh failures non-fatal when saving search settings.

  • Keeps stored and inline pipeline definitions byte-identical.
  • Prevents cluster-scoped pipeline permissions from blocking settings updates.
  • Leaves the named pipeline as a best-effort compatibility path.
  • Needs direct regression coverage for the changed settings-save and weight-resolution behavior.
Diagram
%%{init: {'theme': 'neutral'}}%%
flowchart TD
  A[Admin saves search settings] --> B[Attempt legacy named-pipeline PUT]
  B -->|Success| C[Persist settings]
  B -->|Failure: permission or cluster error| D[Warn]
  D --> C
  C --> E[Next hybrid query]
  E --> F[Resolve settings or config weights]
  F --> G[Build request-level RRF pipeline]
  G --> H[OpenSearch executes hybrid ranking]
Loading

Reviews (1) · Last reviewed commit: "Fixes #33587: let hybrid ranking ride al..."

… a cluster-global pipeline

Creating the hybrid-rrf search pipeline needs cluster:admin/search/pipeline/put,
which an index-scoped search role does not have and which the security plugin
cannot pattern-scope. On every deployment whose role is confined to its own
<clusterAlias>* prefix the PUT 403s. The reindex survives (the orchestrator
warns and continues) but the pipeline is never created, so hybrid queries name
a pipeline that does not exist and ranking silently degrades — the only signal
being one preflight WARN.

Two more things were wrong with the stored pipeline even where it is permitted.
A search pipeline is cluster-global and hybrid-rrf is a hardcoded name, while
its weights come from per-deployment search settings: on a shared cluster each
tenant's reindex overwrote the previous tenant's ranking weights. Granting the
privilege would have made that worse rather than better.

Expose the pipeline as a definition rather than only as a stored object:

- buildHybridRrfPipelineDefinition(kw, sem) is now the single source of truth
  for the RRF body, used both by the PUT and by callers that inline it. A test
  pins the two byte-identical so the stored and inline forms cannot drift into
  ranking differently.
- SearchRepository.getHybridRrfPipelineDefinition() resolves the effective
  weights per call and hands back the body to inline. Resolving per call is
  also why an admin's weight change now applies to the next query instead of
  waiting for a reindex to re-PUT.

OpenSearch builds ad-hoc pipelines through the same processor factories as
stored ones — resolvePipeline passes phaseInjectorProcessorFactories, and
RRFProcessorFactory ignores the pipeline source — so score-ranker-processor
behaves identically inline.

Also stop failing the search-settings save when the pipeline refresh fails.
SystemResource turned that 403 into a SystemSettingsException, which made
search settings unsaveable on exactly the deployments this affects. The
weights being saved are the ones hybrid search reads, so they apply either way.

The PUT stays as a best-effort legacy path for deployments still reading
through ?search_pipeline=hybrid-rrf; it can be removed once no consumer names
the stored pipeline.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@mohityadav766
mohityadav766 requested a review from a team as a code owner September 18, 2026 15:53
@github-actions

Copy link
Copy Markdown
Contributor

❌ PR checklist incomplete

This PR cannot be merged until the following are addressed on its linked issue:

  • No GitHub issue is linked. Link an issue in the Development section of the PR (or add Fixes #12345 to the description). For a same-org cross-repo issue, add Fixes open-metadata/<repo>#123 to the description.

The fields live on the linked issue in the Shipping project (open the issue → right sidebar → Projects). After you set them, re-run this check (or push a commit) — issue/project changes do not re-trigger it automatically.

Maintainers can bypass this check by adding the skip-pr-checks label.

@github-actions github-actions Bot added backend safe to test Add this label to run secure Github workflows on PRs labels Sep 18, 2026
@gitar-bot

gitar-bot Bot commented Sep 18, 2026

Copy link
Copy Markdown
Code Review ✅ Approved

🟡 Medium risk

Moves hybrid RRF pipeline definition inline with search requests instead of storing it globally, eliminating permission issues on role-scoped deployments and preventing weight conflicts on shared clusters. The inline and stored bodies are byte-identical by test, weights resolve per query to reflect current settings, and the stored PUT remains as a legacy path. No issues found.

Review coverage

Rules No rules evaluated

Functional validation Not enabled · Set up

Auto-approval Not enabled · Set up

Options

Display: compact → Counting what did not apply, without listing it.

Comment with these commands to change the behavior for this request:

Compact
gitar display:verbose         

Was this helpful? React with 👍 / 👎 | Powered by Gitar — free for open source

Comment on lines 705 to 715
} catch (Exception e) {
LOG.error("Failed to update hybrid search pipeline", e);
throw new SystemSettingsException(
"Failed to update hybrid search pipeline: " + e.getMessage());
// Refreshing the named pipeline is a cluster-scoped write, so it fails outright on a
// deployment whose search role is confined to its own <clusterAlias>* prefix. Failing
// the whole settings save on that made search settings unsaveable there. The weights
// being saved here are the ones hybrid search reads per query, so they take effect
// regardless of whether the stored pipeline could be refreshed.
LOG.warn(
"Saved search settings but could not refresh the named hybrid search pipeline; "
+ "the new weights still apply to hybrid queries",
e);
}

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.

P2 Key behaviors lack coverage

The added test only compares the extracted definition with the stored PUT body. It does not cover the new behavior where a pipeline refresh fails but the settings update still succeeds, nor does it verify that getHybridRrfPipelineDefinition() uses the configured weights. Without direct tests for these central behaviors, either part of this fix could regress unnoticed.

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

@sonarqubecloud

Copy link
Copy Markdown

@github-actions

Copy link
Copy Markdown
Contributor

✅ Playwright Results — workflow succeeded

Validated commit 47a355467f7bac1230274a3a4387fd674b24c15f in Playwright run 35365168590, attempt 1.

✅ 4471 passed · ❌ 0 failed · 🟡 10 flaky · ⏭️ 1 skipped · 🧰 0 lifecycle flaky

Performance

Blocking targets: ✅ met · Optimization targets: 🟡 in progress

Shard-job maxima below are not the full workflow wall time; the linked run includes build, fixture, planning, and reporting.

🕒 Full workflow signal wall (to summary) 57m 18s

⏱️ Max setup 4m 10s · max shard execution 20m 44s · max shard-job elapsed before upload 24m 8s · reporting 22s

🌐 220.09 requests/attempt · 2.23 app boots/UI scenario · 37.06% common-shard skew

Optimization targets still in progress:

  • Common shard skew was 37.06% (convergence target: at most 15%).
  • Browser traffic was 220.09 requests per attempt (convergence target: fewer than 200).
  • Application boot ratio was 2.23 per UI scenario (10643 boots / 4769 scenarios; convergence target: at most 1).
Shard Passed Failed Flaky Skipped Lifecycle failed Lifecycle flaky
✅ Shard advanced-search-01 64 0 0 0 0 0
✅ Shard advanced-search-02 66 0 0 0 0 0
✅ Shard chromium-01 128 0 0 0 0 0
🟡 Shard chromium-02 150 0 1 0 0 0
✅ Shard chromium-03 199 0 0 0 0 0
✅ Shard chromium-04 168 0 0 0 0 0
🟡 Shard chromium-05 142 0 1 0 0 0
🟡 Shard chromium-06 135 0 1 0 0 0
🟡 Shard chromium-07 146 0 2 1 0 0
🟡 Shard chromium-08 173 0 1 0 0 0
✅ Shard chromium-09 148 0 0 0 0 0
✅ Shard chromium-10 178 0 0 0 0 0
✅ Shard chromium-11 171 0 0 0 0 0
✅ Shard chromium-12 152 0 0 0 0 0
✅ Shard chromium-13 154 0 0 0 0 0
✅ Shard chromium-14 162 0 0 0 0 0
✅ Shard chromium-15 186 0 0 0 0 0
✅ Shard chromium-16 159 0 0 0 0 0
✅ Shard chromium-17 163 0 0 0 0 0
✅ Shard chromium-18 195 0 0 0 0 0
🟡 Shard chromium-19 182 0 2 0 0 0
🟡 Shard chromium-20 167 0 1 0 0 0
✅ Shard chromium-21 170 0 0 0 0 0
✅ Shard chromium-22 162 0 0 0 0 0
✅ Shard chromium-23 153 0 0 0 0 0
🟡 Shard chromium-24 166 0 1 0 0 0
✅ Shard data-asset-rules-01 65 0 0 0 0 0
✅ Shard domain-isolation-01 16 0 0 0 0 0
✅ Shard global-state-01 34 0 0 0 0 0
✅ Shard import-export-01 80 0 0 0 0 0
✅ Shard import-export-02 48 0 0 0 0 0
✅ Shard import-export-03 22 0 0 0 0 0
✅ Shard ingestion-01 56 0 0 0 0 0
✅ Shard ingestion-02 42 0 0 0 0 0
✅ Shard reindex-01 28 0 0 0 0 0
✅ Shard search-01 12 0 0 0 0 0
✅ Shard search-rbac-01 29 0 0 0 0 0
🟡 10 flaky test(s) (passed on retry)
  • Pages/Domains.spec.tsData consumer can manage domain as owner (shard chromium-02, 1 retry)
  • Pages/Glossary.spec.tsDrag and Drop Glossary Term (shard chromium-05, 1 retry)
  • Features/ChangeSummaryBadge.spec.tsAI badge should appear on column description with Suggested source (shard chromium-06, 1 retry)
  • Pages/UserDetails.spec.tsCreate team with domain and verify visibility of inherited domain in user profile after team removal (shard chromium-07, 1 retry)
  • Features/CustomizeDetailPage.spec.tsDashboard - customization should work (shard chromium-07, 1 retry)
  • Pages/ExplorePageRightPanel.spec.tsShould perform CRUD and Removal operations for databaseSchema (shard chromium-08, 1 retry)
  • Features/ContextCenterArticles.spec.tsArticle list basics and creation entrypoints (shard chromium-19, 1 retry)
  • Pages/Lineage/DataAssetLineage.spec.tsColumn lineage for apiEndpoint -> topic (shard chromium-19, 1 retry)
  • Features/ContextCenterArchive.spec.tsfile in deleted folder is absent from search and not added to archive (shard chromium-20, 1 retry)
  • Pages/ExplorePageRightPanel_KnowledgeCenter.spec.tsShould remove user owner for knowledgeCenter (shard chromium-24, 1 retry)

📦 Download artifacts

How to debug locally
# Download playwright-test-results-<shard> artifact and unzip
npx playwright show-trace path/to/trace.zip    # view trace

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

Labels

backend safe to test Add this label to run secure Github workflows on PRs

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant