From 737e8839318d741dc832fcc12aa64868e1b03681 Mon Sep 17 00:00:00 2001 From: anuj-kumary Date: Thu, 8 Oct 2026 20:46:59 +0530 Subject: [PATCH 1/4] Fixes #34954: expand nested glossary search matches and keep their ancestors - TreeAsyncSelectList: expand every level of the search results, not only the glossary roots, so a match nested under parent terms is visible. - Search managers (ES + OS): a getHierarchy page can rank a hit's ancestors below the size cut, orphaning the hit. Fetch the missing ancestors by FQN before building the tree. Co-Authored-By: Claude Opus 5.5 --- .../ElasticSearchSearchManager.java | 55 ++++++++++++++++++- .../opensearch/OpenSearchSearchManager.java | 55 ++++++++++++++++++- .../TreeAsyncSelectList.test.tsx | 46 +++++++++++++++- .../AsyncSelectList/TreeAsyncSelectList.tsx | 10 +++- 4 files changed, 157 insertions(+), 9 deletions(-) diff --git a/openmetadata-service/src/main/java/org/openmetadata/service/search/elasticsearch/ElasticSearchSearchManager.java b/openmetadata-service/src/main/java/org/openmetadata/service/search/elasticsearch/ElasticSearchSearchManager.java index 34eea727a875..3a0a10c9ea53 100644 --- a/openmetadata-service/src/main/java/org/openmetadata/service/search/elasticsearch/ElasticSearchSearchManager.java +++ b/openmetadata-service/src/main/java/org/openmetadata/service/search/elasticsearch/ElasticSearchSearchManager.java @@ -1619,7 +1619,8 @@ private ElasticSearchRequestBuilder buildHierarchyQuery( private List buildSearchHierarchy( org.openmetadata.schema.search.SearchRequest request, SearchResponse searchResponse, - String clusterAlias) { + String clusterAlias) + throws IOException { List response = new ArrayList<>(); String indexName = request.getIndex(); @@ -1634,7 +1635,7 @@ private List buildSearchHierarchy( if (indexName.equalsIgnoreCase(glossaryTermIndex) || indexName.equalsIgnoreCase(glossaryTermAlias)) { - response = buildGlossaryTermSearchHierarchy(searchResponse); + response = buildGlossaryTermSearchHierarchy(indexName, searchResponse); } else if (indexName.equalsIgnoreCase(domainIndex) || indexName.equalsIgnoreCase(domainAlias)) { response = buildDomainSearchHierarchy(searchResponse); } @@ -1642,7 +1643,7 @@ private List buildSearchHierarchy( } private List buildGlossaryTermSearchHierarchy( - SearchResponse searchResponse) { + String indexName, SearchResponse searchResponse) throws IOException { Map termMap = new LinkedHashMap<>(); Map rootTerms = new LinkedHashMap<>(); @@ -1666,6 +1667,7 @@ private List buildGlossaryTermSearchHierarchy( termMap.putIfAbsent(term.getFullyQualifiedName(), term); } + addMissingAncestors(indexName, termMap); termMap.putAll(rootTerms); termMap @@ -1693,6 +1695,53 @@ private List buildGlossaryTermSearchHierarchy( return new ArrayList<>(rootTerms.values()); } + // Ancestors that ranked below the page cut would orphan the hits beneath them + private void addMissingAncestors(String indexName, Map termMap) + throws IOException { + List missing = + termMap.keySet().stream() + .flatMap(fqn -> ancestorFqns(fqn).stream()) + .filter(fqn -> !termMap.containsKey(fqn)) + .distinct() + .map(FieldValue::of) + .toList(); + if (!missing.isEmpty()) { + for (EntityHierarchy term : fetchTermsByFqn(indexName, missing)) { + term.setChildren(new ArrayList<>()); + termMap.putIfAbsent(term.getFullyQualifiedName(), term); + } + } + } + + private List fetchTermsByFqn(String indexName, List fqns) + throws IOException { + SearchResponse response = + client.search( + s -> + s.index(indexName) + .size(fqns.size()) + .query( + q -> + q.terms( + t -> t.field("fullyQualifiedName").terms(tv -> tv.value(fqns)))), + JsonData.class); + return response.hits().hits().stream() + .filter(hit -> hit.source() != null) + .map(hit -> JsonUtils.readValue(hit.source().toJson().toString(), EntityHierarchy.class)) + .toList(); + } + + // Term ancestors only: the glossary (single-part FQN) is the root and lives in another index + private static List ancestorFqns(String fqn) { + List ancestors = new ArrayList<>(); + for (String parent = getParentFQN(fqn); + parent != null && getParentFQN(parent) != null; + parent = getParentFQN(parent)) { + ancestors.add(parent); + } + return ancestors; + } + private List buildDomainSearchHierarchy( SearchResponse searchResponse) { Map entityHierarchyMap = diff --git a/openmetadata-service/src/main/java/org/openmetadata/service/search/opensearch/OpenSearchSearchManager.java b/openmetadata-service/src/main/java/org/openmetadata/service/search/opensearch/OpenSearchSearchManager.java index 9bcacd7c178a..85b3d6033627 100644 --- a/openmetadata-service/src/main/java/org/openmetadata/service/search/opensearch/OpenSearchSearchManager.java +++ b/openmetadata-service/src/main/java/org/openmetadata/service/search/opensearch/OpenSearchSearchManager.java @@ -1701,7 +1701,8 @@ private static SearchException buildSearchException(OpenSearchException e) { private List buildSearchHierarchy( org.openmetadata.schema.search.SearchRequest request, SearchResponse searchResponse, - String clusterAlias) { + String clusterAlias) + throws IOException { List response = new ArrayList<>(); String indexName = request.getIndex(); @@ -1716,7 +1717,7 @@ private List buildSearchHierarchy( if (indexName.equalsIgnoreCase(glossaryTermIndex) || indexName.equalsIgnoreCase(glossaryTermAlias)) { - response = buildGlossaryTermSearchHierarchy(searchResponse); + response = buildGlossaryTermSearchHierarchy(indexName, searchResponse); } else if (indexName.equalsIgnoreCase(domainIndex) || indexName.equalsIgnoreCase(domainAlias)) { response = buildDomainSearchHierarchy(searchResponse); } @@ -1724,7 +1725,7 @@ private List buildSearchHierarchy( } private List buildGlossaryTermSearchHierarchy( - SearchResponse searchResponse) { + String indexName, SearchResponse searchResponse) throws IOException { Map termMap = new LinkedHashMap<>(); Map rootTerms = new LinkedHashMap<>(); @@ -1748,6 +1749,7 @@ private List buildGlossaryTermSearchHierarchy( termMap.putIfAbsent(term.getFullyQualifiedName(), term); } + addMissingAncestors(indexName, termMap); termMap.putAll(rootTerms); termMap @@ -1775,6 +1777,53 @@ private List buildGlossaryTermSearchHierarchy( return new ArrayList<>(rootTerms.values()); } + // Ancestors that ranked below the page cut would orphan the hits beneath them + private void addMissingAncestors(String indexName, Map termMap) + throws IOException { + List missing = + termMap.keySet().stream() + .flatMap(fqn -> ancestorFqns(fqn).stream()) + .filter(fqn -> !termMap.containsKey(fqn)) + .distinct() + .map(FieldValue::of) + .toList(); + if (!missing.isEmpty()) { + for (EntityHierarchy term : fetchTermsByFqn(indexName, missing)) { + term.setChildren(new ArrayList<>()); + termMap.putIfAbsent(term.getFullyQualifiedName(), term); + } + } + } + + private List fetchTermsByFqn(String indexName, List fqns) + throws IOException { + SearchResponse response = + client.search( + s -> + s.index(indexName) + .size(fqns.size()) + .query( + q -> + q.terms( + t -> t.field("fullyQualifiedName").terms(tv -> tv.value(fqns)))), + JsonData.class); + return response.hits().hits().stream() + .filter(hit -> hit.source() != null) + .map(hit -> JsonUtils.readValue(hit.source().toJson().toString(), EntityHierarchy.class)) + .toList(); + } + + // Term ancestors only: the glossary (single-part FQN) is the root and lives in another index + private static List ancestorFqns(String fqn) { + List ancestors = new ArrayList<>(); + for (String parent = getParentFQN(fqn); + parent != null && getParentFQN(parent) != null; + parent = getParentFQN(parent)) { + ancestors.add(parent); + } + return ancestors; + } + private List buildDomainSearchHierarchy( SearchResponse searchResponse) { Map entityHierarchyMap = diff --git a/openmetadata-ui/src/main/resources/ui/src/components/common/AsyncSelectList/TreeAsyncSelectList.test.tsx b/openmetadata-ui/src/main/resources/ui/src/components/common/AsyncSelectList/TreeAsyncSelectList.test.tsx index 4656e43692a8..dccf1109b8df 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/common/AsyncSelectList/TreeAsyncSelectList.test.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/common/AsyncSelectList/TreeAsyncSelectList.test.tsx @@ -10,8 +10,10 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { render, screen } from '@testing-library/react'; +import { act, fireEvent, render, screen } from '@testing-library/react'; import { TagSource } from '../../../generated/type/tagLabel'; +import { searchGlossaryTerms } from '../../../rest/glossaryAPI'; +import { convertGlossaryTermsToTreeOptions } from '../../../utils/GlossaryUtils'; import { SelectOption } from './AsyncSelectList.interface'; import TreeAsyncSelectList from './TreeAsyncSelectList'; @@ -56,4 +58,46 @@ describe('TreeAsyncSelectList', () => { it('should render the component', () => { expect(screen.getByTestId('tag-selector')).toBeInTheDocument(); }); + + it('should expand every level of the search results', async () => { + jest.useFakeTimers(); + (convertGlossaryTermsToTreeOptions as jest.Mock).mockImplementation( + jest.requireActual('../../../utils/GlossaryUtils') + .convertGlossaryTermsToTreeOptions + ); + (searchGlossaryTerms as jest.Mock).mockResolvedValue([ + { + id: 'g', + name: 'Business', + fullyQualifiedName: 'Business', + children: [ + { + id: 'c', + name: 'Customer', + fullyQualifiedName: 'Business.Customer', + children: [ + { + id: 't', + name: 'Churn', + fullyQualifiedName: 'Business.Customer.Churn', + }, + ], + }, + ], + }, + ]); + + fireEvent.change(screen.getByRole('combobox'), { + target: { value: 'Churn' }, + }); + await act(async () => { + jest.advanceTimersByTime(300); + }); + + expect( + await screen.findByTestId('tag-Business.Customer.Churn') + ).toBeInTheDocument(); + + jest.useRealTimers(); + }); }); diff --git a/openmetadata-ui/src/main/resources/ui/src/components/common/AsyncSelectList/TreeAsyncSelectList.tsx b/openmetadata-ui/src/main/resources/ui/src/components/common/AsyncSelectList/TreeAsyncSelectList.tsx index d6a15877c888..3df7afc20daa 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/common/AsyncSelectList/TreeAsyncSelectList.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/common/AsyncSelectList/TreeAsyncSelectList.tsx @@ -52,6 +52,7 @@ import { import { getEntityName } from '../../../utils/EntityNameUtils'; import { filterTreeNodeOptions, + findExpandableKeysForArray, findItemByFqn, } from '../../../utils/GlossaryPureUtils'; import { convertGlossaryTermsToTreeOptions } from '../../../utils/GlossaryUtils'; @@ -433,9 +434,14 @@ const TreeAsyncSelectList: FC = ({ const encodedValue = getEncodedFqn(escapeESReservedCharacters(value)); const results: Glossary[] = await searchGlossaryTerms(encodedValue); - setSearchOptions(filterTreeNodeOptions(results, filterOptions)); + const filteredResults = filterTreeNodeOptions(results, filterOptions); + + setSearchOptions(filteredResults); + // Expand every level so matches nested under parent terms are visible setExpandedRowKeys( - results.map((result) => result.fullyQualifiedName as string) + findExpandableKeysForArray( + filteredResults as unknown as ModifiedGlossaryTerm[] + ) ); setIsLoading(false); } else { From c34a5e702d3552e61c682580187465ef379fe542 Mon Sep 17 00:00:00 2001 From: anuj-kumary Date: Thu, 8 Oct 2026 21:54:36 +0530 Subject: [PATCH 2/4] Only backfill Approved ancestors in glossary hierarchy search The ancestor backfill ran a bare terms query, so an Approved term under a Draft/In Review/Rejected parent pulled that parent into the picker. Apply the same entityStatus=Approved gate buildHierarchyQuery uses. Co-Authored-By: Claude Opus 5.5 --- .../elasticsearch/ElasticSearchSearchManager.java | 9 +++++---- .../search/opensearch/OpenSearchSearchManager.java | 10 ++++++---- 2 files changed, 11 insertions(+), 8 deletions(-) diff --git a/openmetadata-service/src/main/java/org/openmetadata/service/search/elasticsearch/ElasticSearchSearchManager.java b/openmetadata-service/src/main/java/org/openmetadata/service/search/elasticsearch/ElasticSearchSearchManager.java index 3a0a10c9ea53..11706f5a6a8b 100644 --- a/openmetadata-service/src/main/java/org/openmetadata/service/search/elasticsearch/ElasticSearchSearchManager.java +++ b/openmetadata-service/src/main/java/org/openmetadata/service/search/elasticsearch/ElasticSearchSearchManager.java @@ -1715,15 +1715,16 @@ private void addMissingAncestors(String indexName, Map private List fetchTermsByFqn(String indexName, List fqns) throws IOException { + Query byFqn = + Query.of(q -> q.terms(t -> t.field("fullyQualifiedName").terms(tv -> tv.value(fqns)))); + // Same status gate as buildHierarchyQuery + Query approved = Query.of(q -> q.match(m -> m.field("entityStatus").query("Approved"))); SearchResponse response = client.search( s -> s.index(indexName) .size(fqns.size()) - .query( - q -> - q.terms( - t -> t.field("fullyQualifiedName").terms(tv -> tv.value(fqns)))), + .query(q -> q.bool(b -> b.filter(List.of(byFqn, approved)))), JsonData.class); return response.hits().hits().stream() .filter(hit -> hit.source() != null) diff --git a/openmetadata-service/src/main/java/org/openmetadata/service/search/opensearch/OpenSearchSearchManager.java b/openmetadata-service/src/main/java/org/openmetadata/service/search/opensearch/OpenSearchSearchManager.java index 85b3d6033627..d2b4fe566ab8 100644 --- a/openmetadata-service/src/main/java/org/openmetadata/service/search/opensearch/OpenSearchSearchManager.java +++ b/openmetadata-service/src/main/java/org/openmetadata/service/search/opensearch/OpenSearchSearchManager.java @@ -1797,15 +1797,17 @@ private void addMissingAncestors(String indexName, Map private List fetchTermsByFqn(String indexName, List fqns) throws IOException { + Query byFqn = + Query.of(q -> q.terms(t -> t.field("fullyQualifiedName").terms(tv -> tv.value(fqns)))); + // Same status gate as buildHierarchyQuery + Query approved = + Query.of(q -> q.match(m -> m.field("entityStatus").query(FieldValue.of("Approved")))); SearchResponse response = client.search( s -> s.index(indexName) .size(fqns.size()) - .query( - q -> - q.terms( - t -> t.field("fullyQualifiedName").terms(tv -> tv.value(fqns)))), + .query(q -> q.bool(b -> b.filter(List.of(byFqn, approved)))), JsonData.class); return response.hits().hits().stream() .filter(hit -> hit.source() != null) From 7465e192eb3724edb2f28d8264bc045b71f9d2e8 Mon Sep 17 00:00:00 2001 From: sonika-shah <58761340+sonika-shah@users.noreply.github.com> Date: Mon, 12 Oct 2026 01:27:54 +0530 Subject: [PATCH 3/4] Keep parent terms from outranking matches in glossary hierarchy search Parent-term clauses added for the hierarchy scored like real matches. In large glossaries the fqnParts aggregation hits its 1000-bucket cap, some matches lose their own-FQN clause, and parents can take page slots from them. Score the parent clauses 0 for glossary terms: matches rank purely by relevance, parents fill any leftover slots, and the ones that don't fit are backfilled by addMissingAncestors. Domain search is unchanged. --- .../ElasticSearchSearchManager.java | 18 +++++++++++++++++- .../opensearch/OpenSearchSearchManager.java | 18 +++++++++++++++++- 2 files changed, 34 insertions(+), 2 deletions(-) diff --git a/openmetadata-service/src/main/java/org/openmetadata/service/search/elasticsearch/ElasticSearchSearchManager.java b/openmetadata-service/src/main/java/org/openmetadata/service/search/elasticsearch/ElasticSearchSearchManager.java index 11706f5a6a8b..4647beee1f37 100644 --- a/openmetadata-service/src/main/java/org/openmetadata/service/search/elasticsearch/ElasticSearchSearchManager.java +++ b/openmetadata-service/src/main/java/org/openmetadata/service/search/elasticsearch/ElasticSearchSearchManager.java @@ -1597,7 +1597,12 @@ private ElasticSearchRequestBuilder buildHierarchyQuery( // Add the original query as a should clause to preserve high scores b.should(originalQuery); // Add parent term queries as should clauses to fetch hierarchy - parentTermQueries.forEach(b::should); + if (indexName.equalsIgnoreCase(glossaryTermIndex) + || indexName.equalsIgnoreCase(glossaryTermAlias)) { + b.should(withZeroScore(parentTermQueries)); + } else { + parentTermQueries.forEach(b::should); + } if (indexName.equalsIgnoreCase(glossaryTermIndex)) { b.must(m -> m.match(ma -> ma.field("entityStatus").query("Approved"))); } @@ -1695,6 +1700,17 @@ private List buildGlossaryTermSearchHierarchy( return new ArrayList<>(rootTerms.values()); } + // Parents only draw the tree, so they must not take page slots from real matches (missing ones + // are backfilled by addMissingAncestors) + private static Query withZeroScore(List parentTermQueries) { + return Query.of( + q -> + q.constantScore( + c -> + c.filter(f -> f.bool(b -> b.should(parentTermQueries).minimumShouldMatch("1"))) + .boost(0f))); + } + // Ancestors that ranked below the page cut would orphan the hits beneath them private void addMissingAncestors(String indexName, Map termMap) throws IOException { diff --git a/openmetadata-service/src/main/java/org/openmetadata/service/search/opensearch/OpenSearchSearchManager.java b/openmetadata-service/src/main/java/org/openmetadata/service/search/opensearch/OpenSearchSearchManager.java index d2b4fe566ab8..80d749991a9d 100644 --- a/openmetadata-service/src/main/java/org/openmetadata/service/search/opensearch/OpenSearchSearchManager.java +++ b/openmetadata-service/src/main/java/org/openmetadata/service/search/opensearch/OpenSearchSearchManager.java @@ -1661,7 +1661,12 @@ private OpenSearchRequestBuilder buildHierarchyQuery( // Add the original query as a should clause to preserve high scores b.should(originalQuery); // Add parent term queries as should clauses to fetch hierarchy - parentTermQueries.forEach(b::should); + if (indexName.equalsIgnoreCase(glossaryTermIndex) + || indexName.equalsIgnoreCase(glossaryTermAlias)) { + b.should(withZeroScore(parentTermQueries)); + } else { + parentTermQueries.forEach(b::should); + } if (indexName.equalsIgnoreCase(glossaryTermIndex)) { b.must( m -> @@ -1777,6 +1782,17 @@ private List buildGlossaryTermSearchHierarchy( return new ArrayList<>(rootTerms.values()); } + // Parents only draw the tree, so they must not take page slots from real matches (missing ones + // are backfilled by addMissingAncestors) + private static Query withZeroScore(List parentTermQueries) { + return Query.of( + q -> + q.constantScore( + c -> + c.filter(f -> f.bool(b -> b.should(parentTermQueries).minimumShouldMatch("1"))) + .boost(0f))); + } + // Ancestors that ranked below the page cut would orphan the hits beneath them private void addMissingAncestors(String indexName, Map termMap) throws IOException { From dc3fdbc5811ba0d57c2b6e69b5447b2b3db3a5c5 Mon Sep 17 00:00:00 2001 From: anuj-kumary Date: Mon, 12 Oct 2026 10:18:50 +0530 Subject: [PATCH 4/4] Drop backend search-manager changes; keep UI-only fix The hierarchy-ancestor fix in ElasticSearchSearchManager/OpenSearchSearchManager lands separately on main and gets cherry-picked to 2.0. This PR keeps only the TreeAsyncSelectList change, which is client-side and independent of it. Co-Authored-By: Claude Opus 5 (1M context) --- .../ElasticSearchSearchManager.java | 74 +----------------- .../opensearch/OpenSearchSearchManager.java | 75 +------------------ 2 files changed, 8 insertions(+), 141 deletions(-) diff --git a/openmetadata-service/src/main/java/org/openmetadata/service/search/elasticsearch/ElasticSearchSearchManager.java b/openmetadata-service/src/main/java/org/openmetadata/service/search/elasticsearch/ElasticSearchSearchManager.java index 4647beee1f37..34eea727a875 100644 --- a/openmetadata-service/src/main/java/org/openmetadata/service/search/elasticsearch/ElasticSearchSearchManager.java +++ b/openmetadata-service/src/main/java/org/openmetadata/service/search/elasticsearch/ElasticSearchSearchManager.java @@ -1597,12 +1597,7 @@ private ElasticSearchRequestBuilder buildHierarchyQuery( // Add the original query as a should clause to preserve high scores b.should(originalQuery); // Add parent term queries as should clauses to fetch hierarchy - if (indexName.equalsIgnoreCase(glossaryTermIndex) - || indexName.equalsIgnoreCase(glossaryTermAlias)) { - b.should(withZeroScore(parentTermQueries)); - } else { - parentTermQueries.forEach(b::should); - } + parentTermQueries.forEach(b::should); if (indexName.equalsIgnoreCase(glossaryTermIndex)) { b.must(m -> m.match(ma -> ma.field("entityStatus").query("Approved"))); } @@ -1624,8 +1619,7 @@ private ElasticSearchRequestBuilder buildHierarchyQuery( private List buildSearchHierarchy( org.openmetadata.schema.search.SearchRequest request, SearchResponse searchResponse, - String clusterAlias) - throws IOException { + String clusterAlias) { List response = new ArrayList<>(); String indexName = request.getIndex(); @@ -1640,7 +1634,7 @@ private List buildSearchHierarchy( if (indexName.equalsIgnoreCase(glossaryTermIndex) || indexName.equalsIgnoreCase(glossaryTermAlias)) { - response = buildGlossaryTermSearchHierarchy(indexName, searchResponse); + response = buildGlossaryTermSearchHierarchy(searchResponse); } else if (indexName.equalsIgnoreCase(domainIndex) || indexName.equalsIgnoreCase(domainAlias)) { response = buildDomainSearchHierarchy(searchResponse); } @@ -1648,7 +1642,7 @@ private List buildSearchHierarchy( } private List buildGlossaryTermSearchHierarchy( - String indexName, SearchResponse searchResponse) throws IOException { + SearchResponse searchResponse) { Map termMap = new LinkedHashMap<>(); Map rootTerms = new LinkedHashMap<>(); @@ -1672,7 +1666,6 @@ private List buildGlossaryTermSearchHierarchy( termMap.putIfAbsent(term.getFullyQualifiedName(), term); } - addMissingAncestors(indexName, termMap); termMap.putAll(rootTerms); termMap @@ -1700,65 +1693,6 @@ private List buildGlossaryTermSearchHierarchy( return new ArrayList<>(rootTerms.values()); } - // Parents only draw the tree, so they must not take page slots from real matches (missing ones - // are backfilled by addMissingAncestors) - private static Query withZeroScore(List parentTermQueries) { - return Query.of( - q -> - q.constantScore( - c -> - c.filter(f -> f.bool(b -> b.should(parentTermQueries).minimumShouldMatch("1"))) - .boost(0f))); - } - - // Ancestors that ranked below the page cut would orphan the hits beneath them - private void addMissingAncestors(String indexName, Map termMap) - throws IOException { - List missing = - termMap.keySet().stream() - .flatMap(fqn -> ancestorFqns(fqn).stream()) - .filter(fqn -> !termMap.containsKey(fqn)) - .distinct() - .map(FieldValue::of) - .toList(); - if (!missing.isEmpty()) { - for (EntityHierarchy term : fetchTermsByFqn(indexName, missing)) { - term.setChildren(new ArrayList<>()); - termMap.putIfAbsent(term.getFullyQualifiedName(), term); - } - } - } - - private List fetchTermsByFqn(String indexName, List fqns) - throws IOException { - Query byFqn = - Query.of(q -> q.terms(t -> t.field("fullyQualifiedName").terms(tv -> tv.value(fqns)))); - // Same status gate as buildHierarchyQuery - Query approved = Query.of(q -> q.match(m -> m.field("entityStatus").query("Approved"))); - SearchResponse response = - client.search( - s -> - s.index(indexName) - .size(fqns.size()) - .query(q -> q.bool(b -> b.filter(List.of(byFqn, approved)))), - JsonData.class); - return response.hits().hits().stream() - .filter(hit -> hit.source() != null) - .map(hit -> JsonUtils.readValue(hit.source().toJson().toString(), EntityHierarchy.class)) - .toList(); - } - - // Term ancestors only: the glossary (single-part FQN) is the root and lives in another index - private static List ancestorFqns(String fqn) { - List ancestors = new ArrayList<>(); - for (String parent = getParentFQN(fqn); - parent != null && getParentFQN(parent) != null; - parent = getParentFQN(parent)) { - ancestors.add(parent); - } - return ancestors; - } - private List buildDomainSearchHierarchy( SearchResponse searchResponse) { Map entityHierarchyMap = diff --git a/openmetadata-service/src/main/java/org/openmetadata/service/search/opensearch/OpenSearchSearchManager.java b/openmetadata-service/src/main/java/org/openmetadata/service/search/opensearch/OpenSearchSearchManager.java index 80d749991a9d..9bcacd7c178a 100644 --- a/openmetadata-service/src/main/java/org/openmetadata/service/search/opensearch/OpenSearchSearchManager.java +++ b/openmetadata-service/src/main/java/org/openmetadata/service/search/opensearch/OpenSearchSearchManager.java @@ -1661,12 +1661,7 @@ private OpenSearchRequestBuilder buildHierarchyQuery( // Add the original query as a should clause to preserve high scores b.should(originalQuery); // Add parent term queries as should clauses to fetch hierarchy - if (indexName.equalsIgnoreCase(glossaryTermIndex) - || indexName.equalsIgnoreCase(glossaryTermAlias)) { - b.should(withZeroScore(parentTermQueries)); - } else { - parentTermQueries.forEach(b::should); - } + parentTermQueries.forEach(b::should); if (indexName.equalsIgnoreCase(glossaryTermIndex)) { b.must( m -> @@ -1706,8 +1701,7 @@ private static SearchException buildSearchException(OpenSearchException e) { private List buildSearchHierarchy( org.openmetadata.schema.search.SearchRequest request, SearchResponse searchResponse, - String clusterAlias) - throws IOException { + String clusterAlias) { List response = new ArrayList<>(); String indexName = request.getIndex(); @@ -1722,7 +1716,7 @@ private List buildSearchHierarchy( if (indexName.equalsIgnoreCase(glossaryTermIndex) || indexName.equalsIgnoreCase(glossaryTermAlias)) { - response = buildGlossaryTermSearchHierarchy(indexName, searchResponse); + response = buildGlossaryTermSearchHierarchy(searchResponse); } else if (indexName.equalsIgnoreCase(domainIndex) || indexName.equalsIgnoreCase(domainAlias)) { response = buildDomainSearchHierarchy(searchResponse); } @@ -1730,7 +1724,7 @@ private List buildSearchHierarchy( } private List buildGlossaryTermSearchHierarchy( - String indexName, SearchResponse searchResponse) throws IOException { + SearchResponse searchResponse) { Map termMap = new LinkedHashMap<>(); Map rootTerms = new LinkedHashMap<>(); @@ -1754,7 +1748,6 @@ private List buildGlossaryTermSearchHierarchy( termMap.putIfAbsent(term.getFullyQualifiedName(), term); } - addMissingAncestors(indexName, termMap); termMap.putAll(rootTerms); termMap @@ -1782,66 +1775,6 @@ private List buildGlossaryTermSearchHierarchy( return new ArrayList<>(rootTerms.values()); } - // Parents only draw the tree, so they must not take page slots from real matches (missing ones - // are backfilled by addMissingAncestors) - private static Query withZeroScore(List parentTermQueries) { - return Query.of( - q -> - q.constantScore( - c -> - c.filter(f -> f.bool(b -> b.should(parentTermQueries).minimumShouldMatch("1"))) - .boost(0f))); - } - - // Ancestors that ranked below the page cut would orphan the hits beneath them - private void addMissingAncestors(String indexName, Map termMap) - throws IOException { - List missing = - termMap.keySet().stream() - .flatMap(fqn -> ancestorFqns(fqn).stream()) - .filter(fqn -> !termMap.containsKey(fqn)) - .distinct() - .map(FieldValue::of) - .toList(); - if (!missing.isEmpty()) { - for (EntityHierarchy term : fetchTermsByFqn(indexName, missing)) { - term.setChildren(new ArrayList<>()); - termMap.putIfAbsent(term.getFullyQualifiedName(), term); - } - } - } - - private List fetchTermsByFqn(String indexName, List fqns) - throws IOException { - Query byFqn = - Query.of(q -> q.terms(t -> t.field("fullyQualifiedName").terms(tv -> tv.value(fqns)))); - // Same status gate as buildHierarchyQuery - Query approved = - Query.of(q -> q.match(m -> m.field("entityStatus").query(FieldValue.of("Approved")))); - SearchResponse response = - client.search( - s -> - s.index(indexName) - .size(fqns.size()) - .query(q -> q.bool(b -> b.filter(List.of(byFqn, approved)))), - JsonData.class); - return response.hits().hits().stream() - .filter(hit -> hit.source() != null) - .map(hit -> JsonUtils.readValue(hit.source().toJson().toString(), EntityHierarchy.class)) - .toList(); - } - - // Term ancestors only: the glossary (single-part FQN) is the root and lives in another index - private static List ancestorFqns(String fqn) { - List ancestors = new ArrayList<>(); - for (String parent = getParentFQN(fqn); - parent != null && getParentFQN(parent) != null; - parent = getParentFQN(parent)) { - ancestors.add(parent); - } - return ancestors; - } - private List buildDomainSearchHierarchy( SearchResponse searchResponse) { Map entityHierarchyMap =