Skip to content

Harden search ranking relevance#29903

Open
harshach wants to merge 20 commits into
mainfrom
harshach/search-ranking-review
Open

Harden search ranking relevance#29903
harshach wants to merge 20 commits into
mainfrom
harshach/search-ranking-review

Conversation

@harshach

@harshach harshach commented Jul 9, 2026

Copy link
Copy Markdown
Collaborator

Describe your changes:

Related to #29853

Fixes search ranking settings parity and hardens the 2.0.0 default ranking behavior so name/identity matches consistently outrank weak usage-heavy matches.

This PR also addresses the open review-comment items from #29853:

  • UI-derived partialName stages now keep .ngram fields.
  • Explore tools toggles have accessible labels and read-only state indicators.
  • Search settings test helper disables ranking through typed POJOs.
  • Stale Table Data Quality page JavaDoc is corrected.

Type of change:

  • Bug fix
  • Improvement

High-level design:

Ranking hardening:

  • exact-stage matching now tries raw query variants and stopword-stripped variants, so names containing stopwords like cost_of_goods_sold can still land in exactName.
  • exact and phrase ranking stages use constant_score so identity-stage precedence is deterministic instead of BM25/idf dependent.
  • tokenCoverage is implemented as per-token coverage wrapped in constant_score, rather than compiling the same as standard multi-match.
  • configured searchFields boosts are honored inside ranked text stages after normalization, keeping stage weight dominant.
  • ranking.signals.fields now gates which term/value functions are applied.
  • default signals are rebalanced: Tier is visible, raw usage is reduced, percentile usage is preferred, and entityStatus gives a small tie-breaker advantage to active/non-deprecated statuses.
  • syntax detection keeps ranked search for pure quoted queries and intra-word hyphenated identifiers.
  • multilingual stopword defaults are populated for de/es/fr/pt/ru/zh/jp/ja.
  • table fqnParts is explicitly mapped as keyword for en/ru; zh/jp already had it.

No schemaChanges.sql change is included. For 2.0.0, fresh/default settings are registered from searchSettings.json; existing beta was manually patched already.

Tests:

Use cases covered

  • Search settings UI derives partialName stages from .ngram name fields instead of analyzed name fields.
  • Explore tools menu exposes read-only, labeled toggle state indicators for ranking details and deleted results.
  • Search settings integration helpers disable ranking through typed POJOs instead of JSON tree mutation.
  • Exact ranking includes raw and stopword-stripped query variants.
  • Ranked search remains enabled for pure quoted and intra-word hyphenated queries.
  • Signal allow-list gates ranking functions.
  • Search field boost configuration contributes to ranked stage field weights.

Unit tests

  • I added unit tests for the new/changed logic.
  • Files updated: SearchSettingsUtils.test.ts, ExploreV1.test.tsx, SearchRankingHelperTest.java, SearchSourceBuilderFactoryTest.java
  • Coverage %: Not measured.

Backend integration tests

  • Existing integration test helpers compile with typed POJO changes.
  • Files updated: SearchSettingsTestHelper.java

Ingestion integration tests

  • Not applicable (no ingestion changes).

Playwright (UI) tests

  • Not applicable (no Playwright workflow changes).
  • Files updated: TableDataQualityPage.java

Manual testing performed

  • PATH=/opt/homebrew/opt/node@22/bin:$PATH yarn test SearchSettingsUtils.test.ts ExploreV1.test.tsx --runInBand
  • UI checkstyle sequence for changed UI files: organize-imports-cli, eslint --fix, prettier --write
  • mvn -pl openmetadata-integration-tests spotless:apply
  • mvn -pl openmetadata-sdk -am -DskipTests install
  • mvn -pl openmetadata-integration-tests -DskipTests test-compile
  • mvn -pl openmetadata-service spotless:apply
  • mvn -pl openmetadata-service -Dtest=SearchRankingHelperTest,SearchSourceBuilderFactoryTest test
  • jq empty openmetadata-service/src/main/resources/json/data/settings/searchSettings.json openmetadata-spec/src/main/resources/elasticsearch/{en,ru,zh,jp}/table_index_mapping.json
  • git diff --check

Not in this PR: P3 click/LTR feedback wiring and a full offline relevance harness. Those need product/data plumbing beyond the 2.0.0 ranking migration/default-settings fix.

UI screen recording / screenshots:

Not attached — UI change is accessibility/state semantics only with no intended visual change.

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.
  • For JSON Schema changes: I updated the migration scripts or explained why it is not needed.
  • For UI changes: I attached a screen recording and/or screenshots above.
  • 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.
  • I have added tests around the new logic.

Greptile Summary

This PR hardens the 2.0.0 default search ranking by making identity-match stages (exact, phrase) use constant_score for deterministic stage precedence, implementing TOKEN_COVERAGE as a per-token coverage query, honoring configured field boosts inside ranked stages, and gating signal functions through a signals.fields allow-list. It also fixes syntax-detection edge cases (intra-word hyphens, pure quoted queries, escaped characters), populates multilingual stopword defaults, and adds fqnParts: keyword to en/ru table index mappings.

  • Backend ranking overhaul: exact and phrase stages now use constant_score wrappers; TOKEN_COVERAGE is split into per-token sub-queries; stageFieldWeights normalises configured searchFields boosts to a [0.75, 1.25] weight range; unescapePlainTextQuery strips UI-applied Lucene escapes before ranked query building.
  • Settings / config changes: searchSettings.json rebalances Tier boosts (10x), dramatically reduces raw usage count factors, adds entityStatus term boosts, and populates multilingual stopword defaults for de/es/fr/pt/ru/zh/jp/ja.
  • UI and test improvements: findActiveSearchIndex now picks the tab with the most hits instead of the first populated tab; ExploreV1 toggle labels and read-only semantics are fixed for accessibility; SearchSettingsTestHelper migrates to typed POJOs.

Confidence Score: 5/5

Safe to merge; changes are additive ranking improvements with good test coverage across unit and integration tests for all new code paths.

All new methods have direct unit tests, ES/OpenSearch factories are kept in parity, and UI utils changes are covered by updated tests. The most impactful changes are intentional and documented.

searchSettings.json (large boost factor rebalancing warrants production traffic validation) and SearchRankingHelper.java (stageFieldWeights normalisation inversion for small configured boosts).

Important Files Changed

Filename Overview
openmetadata-service/src/main/java/org/openmetadata/service/search/SearchRankingHelper.java Core ranking logic: adds exactMatchTexts(List), queryTerms, unescapePlainTextQuery, signalFieldEnabled, and stageFieldWeights; weight normalization formula ranges [0.75, 1.25] with a subtle side-effect for fields with small positive boosts
openmetadata-service/src/main/java/org/openmetadata/service/search/SearchSourceBuilderFactory.java Syntax detection now handles escaped characters, pure quoted queries, and intra-word hyphens/plusses; range-query detection also checks isEscaped; logic is well-tested
openmetadata-service/src/main/java/org/openmetadata/service/search/elasticsearch/ElasticSearchSourceBuilderFactory.java Exact and phrase ranking stages now use constant_score; TOKEN_COVERAGE implemented as per-term sub-queries; unescapePlainTextQuery applied at entry points; signal field filtering added to collectBoostFunctionsV2
openmetadata-service/src/main/java/org/openmetadata/service/search/opensearch/OpenSearchSourceBuilderFactory.java Mirrors ElasticSearchSourceBuilderFactory changes; parity is maintained between both engines
openmetadata-service/src/main/resources/json/data/settings/searchSettings.json Significant rebalancing: Tier1 boost 0.05 to 0.5, raw usage count factor 40x reduction, percentile rank factor 10x increase, entityStatus term boosts added, multilingual stopwords populated
openmetadata-ui/src/main/resources/ui/src/utils/ExplorePureUtils.ts findActiveSearchIndex changed from first-with-hits to most-hits; intentional UX improvement with correct tie-breaking and test coverage
openmetadata-ui/src/main/resources/ui/src/utils/SearchSettingsUtils.ts Adds deriveNgramNameFields for partialName/ngram stages so UI-derived stage fields correctly use .ngram variants
openmetadata-spec/src/main/resources/elasticsearch/en/table_index_mapping.json Adds fqnParts as keyword field; needs reindex or manual mapping update for existing en/ru indices but PR explains why migration is deferred for 2.0.0
openmetadata-integration-tests/src/test/java/org/openmetadata/it/search/SearchSettingsTestHelper.java withRankingDisabled refactored from JsonNode tree mutation to typed POJO; copyOf performs deep copy via JSON serialization round-trip ensuring no shared-state mutations

Sequence Diagram

%%{init: {'theme': 'neutral'}}%%
sequenceDiagram
    participant UI as UI / Search Box
    participant SSF as SearchSourceBuilderFactory
    participant RH as SearchRankingHelper
    participant ES as Elasticsearch / OpenSearch

    UI->>SSF: buildDataAssetSearchBuilderV2(query)
    SSF->>SSF: containsQuerySyntax(query)?
    alt has Lucene syntax
        SSF->>ES: simple_query_string / query_string
    else plain text or pure-quoted / hyphenated identifier
        SSF->>SSF: unescapePlainTextQuery(query)
        SSF->>RH: resolveRanking(settings, assetConfig)
        RH-->>SSF: RankingConfiguration (stages)
        loop each RankingStage
            alt EXACT stage
                SSF->>RH: exactMatchTexts([rawQuery, significantQuery])
                RH-->>SSF: term variants
                SSF->>SSF: constantScore(boolQuery(terms), stageWeight)
            else PHRASE stage
                SSF->>SSF: constantScore(matchPhrase, stageWeight)
            else TOKEN_COVERAGE stage
                SSF->>RH: queryTerms(significantQuery)
                loop each token
                    SSF->>SSF: multiMatchQuery(token, fields, AND)
                end
                SSF->>SSF: constantScore(boolQuery(tokens), stageWeight)
            else STANDARD / FUZZY stage
                SSF->>RH: stageFieldWeights(stage, assetConfig)
                RH-->>SSF: normalized weights [0.75-1.25]
                SSF->>SSF: multiMatchQuery(significantQuery, weightedFields)
            end
        end
        SSF->>SSF: disMaxQuery(stageQueries)
        SSF->>SSF: applyFunctionScoringV2 (signals gated)
        SSF->>ES: function_score(disMax(...))
    end
    ES-->>UI: ranked hits
Loading
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
sequenceDiagram
    participant UI as UI / Search Box
    participant SSF as SearchSourceBuilderFactory
    participant RH as SearchRankingHelper
    participant ES as Elasticsearch / OpenSearch

    UI->>SSF: buildDataAssetSearchBuilderV2(query)
    SSF->>SSF: containsQuerySyntax(query)?
    alt has Lucene syntax
        SSF->>ES: simple_query_string / query_string
    else plain text or pure-quoted / hyphenated identifier
        SSF->>SSF: unescapePlainTextQuery(query)
        SSF->>RH: resolveRanking(settings, assetConfig)
        RH-->>SSF: RankingConfiguration (stages)
        loop each RankingStage
            alt EXACT stage
                SSF->>RH: exactMatchTexts([rawQuery, significantQuery])
                RH-->>SSF: term variants
                SSF->>SSF: constantScore(boolQuery(terms), stageWeight)
            else PHRASE stage
                SSF->>SSF: constantScore(matchPhrase, stageWeight)
            else TOKEN_COVERAGE stage
                SSF->>RH: queryTerms(significantQuery)
                loop each token
                    SSF->>SSF: multiMatchQuery(token, fields, AND)
                end
                SSF->>SSF: constantScore(boolQuery(tokens), stageWeight)
            else STANDARD / FUZZY stage
                SSF->>RH: stageFieldWeights(stage, assetConfig)
                RH-->>SSF: normalized weights [0.75-1.25]
                SSF->>SSF: multiMatchQuery(significantQuery, weightedFields)
            end
        end
        SSF->>SSF: disMaxQuery(stageQueries)
        SSF->>SSF: applyFunctionScoringV2 (signals gated)
        SSF->>ES: function_score(disMax(...))
    end
    ES-->>UI: ranked hits
Loading

Reviews (19): Last reviewed commit: "fix(explore): land on the most-matched e..." | Re-trigger Greptile

@github-actions github-actions Bot added backend safe to test Add this label to run secure Github workflows on PRs labels Jul 9, 2026
@harshach
harshach marked this pull request as ready for review July 9, 2026 19:08
@harshach
harshach requested a review from a team as a code owner July 9, 2026 19:08
Copilot AI review requested due to automatic review settings July 9, 2026 19:08

Copilot AI left a comment

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.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@github-actions

github-actions Bot commented Jul 9, 2026

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 commented Jul 9, 2026

Copy link
Copy Markdown
Contributor

Jest test Coverage

UI tests summary

Lines Statements Branches Functions
Coverage: 64%
65.03% (75540/116155) 48.82% (44955/92068) 49.65% (13626/27440)

@harshach harshach changed the title [codex] Fix search ranking settings parity [codex] Harden search ranking relevance Jul 9, 2026
Copilot AI review requested due to automatic review settings July 9, 2026 20:53

Copilot AI left a comment

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.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

Copilot AI review requested due to automatic review settings July 9, 2026 21:13

Copilot AI left a comment

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.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

…ng-review

# Conflicts:
#	openmetadata-integration-tests/src/test/java/org/openmetadata/playwright/ui/pages/TableDataQualityPage.java
#	openmetadata-ui/src/main/resources/ui/src/components/ExploreV1/ExploreV1.component.tsx

Copilot AI left a comment

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.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

Copilot AI left a comment

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.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

Copilot AI left a comment

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.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

…ng-review

# Conflicts:
#	openmetadata-integration-tests/src/test/java/org/openmetadata/it/tests/search/TestCaseStaleDefinitionReindexIT.java

Copilot AI left a comment

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.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

Copilot AI left a comment

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.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

…ng-review

Resolved SystemResourceIT.java conflict by adopting main's canonical-driven
fieldValueBoost assertions (from #30060), which validate against the packaged
searchSettings.json instead of hardcoded factors and therefore hold for this
branch's retuned signal values.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

Copilot AI left a comment

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.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

Copilot AI left a comment

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.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

Copilot AI left a comment

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.

Pull request overview

Copilot reviewed 22 out of 22 changed files in this pull request and generated 2 comments.

Comment on lines +231 to +233
private static boolean isTokenLeadingSyntax(String query, int index) {
return index == 0 || Character.isWhitespace(query.charAt(index - 1));
}
Comment on lines 223 to 231
List.of(
"owner:john",
"name : test",
"name:test AND type:table",
"description:\"exact phrase\"",
"[a TO z]",
"-deprecated",
"customer -orders",
"*PII*")

Copilot AI left a comment

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.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

pmbrull
pmbrull previously approved these changes Jul 17, 2026

Copilot AI left a comment

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.

Pull request overview

Copilot reviewed 21 out of 21 changed files in this pull request and generated 1 comment.

Comment on lines 198 to +202
for (int i = 0; i < query.length(); i++) {
char current = query.charAt(i);
if (isSingleCharacterSyntax(current)
|| isFieldQuerySeparator(query, i)
|| isBooleanOperatorAt(query, i)) {
if (!isEscaped(query, i)
&& (isSingleCharacterSyntax(query, i, pureQuotedQuery)
|| isFieldQuerySeparator(query, i)
|| isBooleanOperatorAt(query, i))) {
@sonarqubecloud

Copy link
Copy Markdown

@sonarqubecloud

Copy link
Copy Markdown

A relevance-ranked search query surfaces related entities across several
asset types — e.g. searching a chart's exact name also matches the dashboard
that embeds it. `findActiveSearchIndex` returned the first populated tab in
tab order, so the results panel could open a tab that does not contain the
searched entity (chart name -> Dashboards tab has 1 hit -> chart card absent).

Pick the entity index with the most hits instead, keeping the original tab
order on ties. This restores the behaviour the precise query_string path gave
before the search ranking changes and fixes the "Verify charts are visible in
explore tree" Playwright regression (chart search rendered the embedding
dashboard instead of the chart).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@gitar-bot

gitar-bot Bot commented Jul 17, 2026

Copy link
Copy Markdown
Code Review ✅ Approved 1 resolved / 1 findings

Hardens search ranking relevance by implementing deterministic score-boosting for identity matches and rebalancing usage signals. Resolves prior issues with query escaping and configuration management.

✅ 1 resolved
Edge Case: Exact stage feeds raw quoted query, producing unmatchable term variants

📄 openmetadata-service/src/main/java/org/openmetadata/service/search/elasticsearch/ElasticSearchSourceBuilderFactory.java:674-688 📄 openmetadata-service/src/main/java/org/openmetadata/service/search/opensearch/OpenSearchSourceBuilderFactory.java:1112-1126
buildExactRankingStageQueryV2 now adds originalQuery to the list passed to SearchRankingHelper.exactMatchTexts. For a pure-quoted query (which still reaches the ranked path because containsQuerySyntax no longer treats surrounding quotes as syntax), originalQuery retains its wrapping double quotes, e.g. "customer orders". exactMatchTexts then derives keyword term variants that embed the literal " characters ("customer orders", "customer_orders", etc.). These termQuery clauses run against keyword fields (name/fqnParts) whose indexed values never contain quote characters, so they can never match — harmless but they add dead should-clauses to every exact stage for quoted queries. Consider stripping a single pair of surrounding quotes from originalQuery before building exact-match variants so the intended identity match is produced.

Options

Display: compact → Showing less information.

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

Compact
gitar display:verbose         

Was this helpful? React with 👍 / 👎 | Gitar

Copilot AI left a comment

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.

Pull request overview

Copilot reviewed 23 out of 23 changed files in this pull request and generated no new comments.

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 skip-pr-checks Bypass PR metadata validation check

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants