Harden search ranking relevance#29903
Conversation
❌ PR checklist incompleteThis PR cannot be merged until the following are addressed on its linked issue:
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 |
…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
…ng-review # Conflicts: # openmetadata-integration-tests/src/test/java/org/openmetadata/it/tests/search/TestCaseStaleDefinitionReindexIT.java
…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>
| private static boolean isTokenLeadingSyntax(String query, int index) { | ||
| return index == 0 || Character.isWhitespace(query.charAt(index - 1)); | ||
| } |
| List.of( | ||
| "owner:john", | ||
| "name : test", | ||
| "name:test AND type:table", | ||
| "description:\"exact phrase\"", | ||
| "[a TO z]", | ||
| "-deprecated", | ||
| "customer -orders", | ||
| "*PII*") |
| 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))) { |
|
|
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>
Code Review ✅ Approved 1 resolved / 1 findingsHardens 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
OptionsDisplay: compact → Showing less information. Comment with these commands to change the behavior for this request:
Was this helpful? React with 👍 / 👎 | Gitar |



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:
partialNamestages now keep.ngramfields.Type of change:
High-level design:
Ranking hardening:
cost_of_goods_soldcan still land inexactName.constant_scoreso identity-stage precedence is deterministic instead of BM25/idf dependent.tokenCoverageis implemented as per-token coverage wrapped inconstant_score, rather than compiling the same as standard multi-match.searchFieldsboosts are honored inside ranked text stages after normalization, keeping stage weight dominant.ranking.signals.fieldsnow gates which term/value functions are applied.entityStatusgives a small tie-breaker advantage to active/non-deprecated statuses.fqnPartsis explicitly mapped askeywordfor en/ru; zh/jp already had it.No
schemaChanges.sqlchange is included. For 2.0.0, fresh/default settings are registered fromsearchSettings.json; existing beta was manually patched already.Tests:
Use cases covered
partialNamestages from.ngramname fields instead of analyzed name fields.Unit tests
SearchSettingsUtils.test.ts,ExploreV1.test.tsx,SearchRankingHelperTest.java,SearchSourceBuilderFactoryTest.javaBackend integration tests
SearchSettingsTestHelper.javaIngestion integration tests
Playwright (UI) tests
TableDataQualityPage.javaManual testing performed
PATH=/opt/homebrew/opt/node@22/bin:$PATH yarn test SearchSettingsUtils.test.ts ExploreV1.test.tsx --runInBandorganize-imports-cli,eslint --fix,prettier --writemvn -pl openmetadata-integration-tests spotless:applymvn -pl openmetadata-sdk -am -DskipTests installmvn -pl openmetadata-integration-tests -DskipTests test-compilemvn -pl openmetadata-service spotless:applymvn -pl openmetadata-service -Dtest=SearchRankingHelperTest,SearchSourceBuilderFactoryTest testjq empty openmetadata-service/src/main/resources/json/data/settings/searchSettings.json openmetadata-spec/src/main/resources/elasticsearch/{en,ru,zh,jp}/table_index_mapping.jsongit diff --checkNot 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:
Fixes <issue-number>: <short explanation>Fixes #<issue-number>above.Greptile Summary
This PR hardens the 2.0.0 default search ranking by making identity-match stages (exact, phrase) use
constant_scorefor deterministic stage precedence, implementingTOKEN_COVERAGEas a per-token coverage query, honoring configured field boosts inside ranked stages, and gating signal functions through asignals.fieldsallow-list. It also fixes syntax-detection edge cases (intra-word hyphens, pure quoted queries, escaped characters), populates multilingual stopword defaults, and addsfqnParts: keywordto en/ru table index mappings.constant_scorewrappers;TOKEN_COVERAGEis split into per-token sub-queries;stageFieldWeightsnormalises configuredsearchFieldsboosts to a[0.75, 1.25]weight range;unescapePlainTextQuerystrips UI-applied Lucene escapes before ranked query building.searchSettings.jsonrebalances Tier boosts (10x), dramatically reduces raw usage count factors, addsentityStatusterm boosts, and populates multilingual stopword defaults for de/es/fr/pt/ru/zh/jp/ja.findActiveSearchIndexnow picks the tab with the most hits instead of the first populated tab;ExploreV1toggle labels and read-only semantics are fixed for accessibility;SearchSettingsTestHelpermigrates 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
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%%{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 hitsReviews (19): Last reviewed commit: "fix(explore): land on the most-matched e..." | Re-trigger Greptile