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 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)); | ||
| } |
| "[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 2 resolved / 2 findingsHardens 2.0.0 search ranking by using constant-score deterministic stages, rebalanced signals, and improved syntax detection to ensure identity matches outrank usage. UI regressions are resolved, and mapping updates for keyword fields are included. ✅ 2 resolved✅ Bug: Top-hit lookup can throw when _source is absent (NLP path)
✅ 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 |
Code Review ✅ Approved 2 resolved / 2 findingsHardens 2.0.0 search ranking by using constant-score deterministic stages, rebalanced signals, and improved syntax detection to ensure identity matches outrank usage. UI regressions are resolved, and mapping updates for keyword fields are included. ✅ 2 resolved✅ Bug: Top-hit lookup can throw when _source is absent (NLP path)
✅ 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 search ranking relevance by making exact/phrase stages use
constant_score(deterministic boost), introducing per-tokenTOKEN_COVERAGEqueries, wiringsignals.fieldsas an allow-list for term/value boost functions, and rebalancing default signal weights (Tier,entityStatus, percentile-rank usage preferred over raw counts). It also fixes auto-tab selection to prefer the entity type of the top-ranked hit rather than the bucket with the most results, and adds multilingual stopword defaults andfqnPartskeyword mappings.constant_scorefor deterministic scoring;TOKEN_COVERAGEis implemented as per-token multi-match;stageFieldWeightsnormalizes configured boosts into the[0.75, 1.25]range;signals.fieldsgates which term/value functions run.fetchEntityDatanow requests one result from the count query (pageSize: 1, 1-indexed) to identify the top hit's entity type, whichfindActiveSearchIndexuses as an override over the highest-count bucket.isReadOnly+excludeFromTabOrderso screen readers and keyboard navigation correctly reflect their state-indicator role.Confidence Score: 5/5
Safe to merge; changes are well-scoped to search ranking configuration and UI state management with no data mutations or schema changes.
The ranking logic changes are isolated to query-building paths, covered by unit tests (SearchRankingHelperTest, SearchSourceBuilderFactoryTest, ExplorePureUtils.test.ts, ExploreUtils.test.ts), and the only behavioral change visible to users — which tab auto-selects on an unscoped search — is intentional and correct. The dual-implementation duplication and minor count-query inefficiency are style concerns that do not affect correctness.
No files require special attention. The TOKEN_COVERAGE builder in both ElasticSearchSourceBuilderFactory and OpenSearchSourceBuilderFactory is duplicated and should be kept in sync on future changes.
Important Files Changed
Sequence Diagram
%%{init: {'theme': 'neutral'}}%% sequenceDiagram participant UI as ExplorePageV1 participant FE as fetchEntityData participant SA as searchAPI participant ES as Elasticsearch/OpenSearch UI->>FE: fetch(query, tab?, searchIndex) alt tab is set par Count + Results in parallel FE->>SA: countPayload (pageSize:1, fetchSource:true, entityType) SA->>ES: DATA_ASSET index agg query (from:0, size:1) ES-->>SA: aggregations + 1 hit SA-->>FE: applyHitCounts returns discarded topHitSearchIndex and FE->>SA: resultsQuery(searchIndex) SA->>ES: typed index query ES-->>SA: hits SA-->>FE: searchResults end else no tab FE->>SA: countPayload (pageSize:1, fetchSource:true, entityType) SA->>ES: DATA_ASSET index agg query (from:0, size:1) ES-->>SA: aggregations + top hit SA-->>FE: counts + topHitSearchIndex FE->>FE: findActiveSearchIndex(counts, tabsInfo, topHitSearchIndex) Note over FE: topHitIndex wins over max-count tab FE->>UI: setAutoSelectedSearchIndex(effectiveIndex) FE->>SA: resultsQuery(effectiveIndex) SA->>ES: effectiveIndex query ES-->>SA: hits SA-->>FE: searchResults end FE->>UI: setSearchHitCounts, setSearchResults, setUpdatedAggregations%%{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 ExplorePageV1 participant FE as fetchEntityData participant SA as searchAPI participant ES as Elasticsearch/OpenSearch UI->>FE: fetch(query, tab?, searchIndex) alt tab is set par Count + Results in parallel FE->>SA: countPayload (pageSize:1, fetchSource:true, entityType) SA->>ES: DATA_ASSET index agg query (from:0, size:1) ES-->>SA: aggregations + 1 hit SA-->>FE: applyHitCounts returns discarded topHitSearchIndex and FE->>SA: resultsQuery(searchIndex) SA->>ES: typed index query ES-->>SA: hits SA-->>FE: searchResults end else no tab FE->>SA: countPayload (pageSize:1, fetchSource:true, entityType) SA->>ES: DATA_ASSET index agg query (from:0, size:1) ES-->>SA: aggregations + top hit SA-->>FE: counts + topHitSearchIndex FE->>FE: findActiveSearchIndex(counts, tabsInfo, topHitSearchIndex) Note over FE: topHitIndex wins over max-count tab FE->>UI: setAutoSelectedSearchIndex(effectiveIndex) FE->>SA: resultsQuery(effectiveIndex) SA->>ES: effectiveIndex query ES-->>SA: hits SA-->>FE: searchResults end FE->>UI: setSearchHitCounts, setSearchResults, setUpdatedAggregationsReviews (25): Last reviewed commit: "Merge branch 'main' into harshach/search..." | Re-trigger Greptile