Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix #19116 : Backend support for domain hierarchy listing #19191

Open
wants to merge 5 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,15 @@
import static org.openmetadata.service.Entity.DOMAIN;
import static org.openmetadata.service.Entity.FIELD_ASSETS;

import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.UUID;
import java.util.stream.Collectors;
import lombok.extern.slf4j.Slf4j;
import org.jdbi.v3.sqlobject.transaction.Transaction;
import org.openmetadata.schema.EntityInterface;
import org.openmetadata.schema.entity.data.EntityHierarchy;
import org.openmetadata.schema.entity.domains.Domain;
import org.openmetadata.schema.type.EntityReference;
import org.openmetadata.schema.type.Include;
Expand All @@ -30,8 +35,11 @@
import org.openmetadata.schema.type.api.BulkOperationResult;
import org.openmetadata.service.Entity;
import org.openmetadata.service.resources.domains.DomainResource;
import org.openmetadata.service.util.EntityUtil;
import org.openmetadata.service.util.EntityUtil.Fields;
import org.openmetadata.service.util.FullyQualifiedName;
import org.openmetadata.service.util.JsonUtils;
import org.openmetadata.service.util.ResultList;

@Slf4j
public class DomainRepository extends EntityRepository<Domain> {
Expand Down Expand Up @@ -140,6 +148,47 @@ public EntityInterface getParentEntity(Domain entity, String fields) {
: null;
}

public List<EntityHierarchy> buildHierarchy(String fieldsParam, int limit) {
fieldsParam = EntityUtil.addField(fieldsParam, Entity.FIELD_PARENT);
Fields fields = getFields(fieldsParam);
ResultList<Domain> resultList = listAfter(null, fields, new ListFilter(null), limit, null);
List<Domain> domains = resultList.getData();

/*
Maintaining hierarchy in terms of EntityHierarchy to get all other fields of Domain like style,
which would have been restricted if built using hierarchy of Domain, as Domain.getChildren() returns List<EntityReference>
and EntityReference does not support additional properties
*/
List<EntityHierarchy> rootDomains = new ArrayList<>();

Map<UUID, EntityHierarchy> entityHierarchyMap =
domains.stream()
.collect(
Collectors.toMap(
Domain::getId,
domain -> {
EntityHierarchy entityHierarchy =
JsonUtils.readValue(JsonUtils.pojoToJson(domain), EntityHierarchy.class);
entityHierarchy.setChildren(new ArrayList<>());
return entityHierarchy;
}));

for (Domain domain : domains) {
EntityHierarchy entityHierarchy = entityHierarchyMap.get(domain.getId());

if (domain.getParent() != null) {
EntityHierarchy parentHierarchy = entityHierarchyMap.get(domain.getParent().getId());
if (parentHierarchy != null) {
parentHierarchy.getChildren().add(entityHierarchy);
}
} else {
rootDomains.add(entityHierarchy);
}
}

return rootDomains;
}

public class DomainUpdater extends EntityUpdater {
public DomainUpdater(Domain original, Domain updated, Operation operation) {
super(original, updated, operation);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2635,7 +2635,7 @@ public boolean isDelete() {
* version goes to v-1 and new version v0 replaces v1 for the entity.
* </ol>
*
* @see TableRepository.TableUpdater#entitySpecificUpdate() for example.
* @see TableRepository.TableUpdater #entitySpecificUpdate() for example.
*/
public class EntityUpdater {
private static volatile long sessionTimeoutMillis = 10L * 60 * 1000; // 10 minutes
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@
import javax.ws.rs.core.UriInfo;
import lombok.extern.slf4j.Slf4j;
import org.openmetadata.schema.api.domains.CreateDomain;
import org.openmetadata.schema.entity.data.EntityHierarchy;
import org.openmetadata.schema.entity.domains.Domain;
import org.openmetadata.schema.type.ChangeEvent;
import org.openmetadata.schema.type.EntityHistory;
Expand All @@ -57,6 +58,7 @@
import org.openmetadata.service.resources.Collection;
import org.openmetadata.service.resources.EntityResource;
import org.openmetadata.service.security.Authorizer;
import org.openmetadata.service.util.EntityHierarchyList;
import org.openmetadata.service.util.ResultList;

@Slf4j
Expand Down Expand Up @@ -433,4 +435,32 @@ public Response delete(
String name) {
return deleteByName(uriInfo, securityContext, name, true, true);
}

@GET
@Path("/hierarchy")
@Operation(
operationId = "listDomainsHierarchy",
summary = "List domains in hierarchical order",
description = "Get a list of Domains in hierarchical order.",
responses = {
@ApiResponse(
responseCode = "200",
description = "List of Domains in hierarchical order",
content =
@Content(
mediaType = "application/json",
schema = @Schema(implementation = EntityHierarchyList.class)))
})
public ResultList<EntityHierarchy> listHierarchy(
@Context UriInfo uriInfo,
@Context SecurityContext securityContext,
@Parameter(
description = "Fields requested in the returned resource",
schema = @Schema(type = "string", example = FIELDS))
@QueryParam("fields")
String fieldsParam,
@DefaultValue("10") @Min(0) @Max(1000000) @QueryParam("limit") int limitParam) {

return new EntityHierarchyList(repository.buildHierarchy(fieldsParam, limitParam));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,7 @@
import java.util.Optional;
import java.util.Set;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import javax.json.JsonObject;
import javax.net.ssl.SSLContext;
Expand Down Expand Up @@ -468,50 +469,10 @@ public Response search(SearchRequest request, SubjectContext subjectContext) thr
.getIndexMapping(GLOSSARY_TERM)
.getIndexName(clusterAlias))) {
searchSourceBuilder.query(QueryBuilders.boolQuery().must(searchSourceBuilder.query()));

if (request.isGetHierarchy()) {
QueryBuilder baseQuery =
QueryBuilders.boolQuery()
.should(searchSourceBuilder.query())
.should(QueryBuilders.matchPhraseQuery("fullyQualifiedName", request.getQuery()))
.should(QueryBuilders.matchPhraseQuery("name", request.getQuery()))
.should(QueryBuilders.matchPhraseQuery("displayName", request.getQuery()))
.should(
QueryBuilders.matchPhraseQuery(
"glossary.fullyQualifiedName", request.getQuery()))
.should(QueryBuilders.matchPhraseQuery("glossary.displayName", request.getQuery()))
.must(QueryBuilders.matchQuery("status", "Approved"))
.minimumShouldMatch(1);
searchSourceBuilder.query(baseQuery);

SearchResponse searchResponse =
client.search(
new es.org.elasticsearch.action.search.SearchRequest(request.getIndex())
.source(searchSourceBuilder),
RequestOptions.DEFAULT);

// Extract parent terms from aggregation
BoolQueryBuilder parentTermQueryBuilder = QueryBuilders.boolQuery();
Terms parentTerms = searchResponse.getAggregations().get("fqnParts_agg");

// Build es query to get parent terms for the user input query , to build correct hierarchy
if (!parentTerms.getBuckets().isEmpty() && !request.getQuery().equals("*")) {
parentTerms.getBuckets().stream()
.map(Terms.Bucket::getKeyAsString)
.forEach(
parentTerm ->
parentTermQueryBuilder.should(
QueryBuilders.matchQuery("fullyQualifiedName", parentTerm)));

searchSourceBuilder.query(
parentTermQueryBuilder
.minimumShouldMatch(1)
.must(QueryBuilders.matchQuery("status", "Approved")));
}
searchSourceBuilder.sort(SortBuilders.fieldSort("fullyQualifiedName").order(SortOrder.ASC));
}
}

buildHierarchyQuery(request, searchSourceBuilder, client);

/* for performance reasons ElasticSearch doesn't provide accurate hits
if we enable trackTotalHits parameter it will try to match every result, count and return hits
however in most cases for search results an approximate value is good enough.
Expand Down Expand Up @@ -579,15 +540,89 @@ public Response getDocByID(String indexName, String entityId) throws IOException
return getResponse(NOT_FOUND, "Document not found.");
}

private void buildHierarchyQuery(
SearchRequest request, SearchSourceBuilder searchSourceBuilder, RestHighLevelClient client)
throws IOException {

if (!request.isGetHierarchy()) {
return;
}

String indexName = request.getIndex();
String glossaryTermIndex =
Entity.getSearchRepository().getIndexMapping(GLOSSARY_TERM).getIndexName(clusterAlias);
String domainIndex =
Entity.getSearchRepository().getIndexMapping(DOMAIN).getIndexName(clusterAlias);

BoolQueryBuilder baseQuery =
QueryBuilders.boolQuery()
.should(searchSourceBuilder.query())
.should(QueryBuilders.matchPhraseQuery("fullyQualifiedName", request.getQuery()))
.should(QueryBuilders.matchPhraseQuery("name", request.getQuery()))
.should(QueryBuilders.matchPhraseQuery("displayName", request.getQuery()));

if (indexName.equalsIgnoreCase(glossaryTermIndex)) {
baseQuery
.should(QueryBuilders.matchPhraseQuery("glossary.fullyQualifiedName", request.getQuery()))
.should(QueryBuilders.matchPhraseQuery("glossary.displayName", request.getQuery()))
.must(QueryBuilders.matchQuery("status", "Approved"));
} else if (indexName.equalsIgnoreCase(domainIndex)) {
baseQuery
.should(QueryBuilders.matchPhraseQuery("parent.fullyQualifiedName", request.getQuery()))
.should(QueryBuilders.matchPhraseQuery("parent.displayName", request.getQuery()));
}

baseQuery.minimumShouldMatch(1);
searchSourceBuilder.query(baseQuery);

SearchResponse searchResponse =
client.search(
new es.org.elasticsearch.action.search.SearchRequest(request.getIndex())
.source(searchSourceBuilder),
RequestOptions.DEFAULT);

Terms parentTerms = searchResponse.getAggregations().get("fqnParts_agg");

// Build es query to get parent terms for the user input query , to build correct hierarchy
// In case of default search , no need to get parent terms they are already present in the
// response
if (parentTerms != null
&& !parentTerms.getBuckets().isEmpty()
&& !request.getQuery().equals("*")) {
BoolQueryBuilder parentTermQueryBuilder = QueryBuilders.boolQuery();

parentTerms.getBuckets().stream()
.map(Terms.Bucket::getKeyAsString)
.forEach(
parentTerm ->
parentTermQueryBuilder.should(
QueryBuilders.matchQuery("fullyQualifiedName", parentTerm)));
if (indexName.equalsIgnoreCase(glossaryTermIndex)) {
parentTermQueryBuilder
.minimumShouldMatch(1)
.must(QueryBuilders.matchQuery("status", "Approved"));
} else {
parentTermQueryBuilder.minimumShouldMatch(1);
}
searchSourceBuilder.query(parentTermQueryBuilder);
}

searchSourceBuilder.sort(SortBuilders.fieldSort("fullyQualifiedName").order(SortOrder.ASC));
}

public List<?> buildSearchHierarchy(SearchRequest request, SearchResponse searchResponse) {
List<?> response = new ArrayList<>();
if (request
.getIndex()
.equalsIgnoreCase(
Entity.getSearchRepository()
.getIndexMapping(GLOSSARY_TERM)
.getIndexName(clusterAlias))) {

String indexName = request.getIndex();
String glossaryTermIndex =
Entity.getSearchRepository().getIndexMapping(GLOSSARY_TERM).getIndexName(clusterAlias);
String domainIndex =
Entity.getSearchRepository().getIndexMapping(DOMAIN).getIndexName(clusterAlias);

if (indexName.equalsIgnoreCase(glossaryTermIndex)) {
response = buildGlossaryTermSearchHierarchy(searchResponse);
} else if (indexName.equalsIgnoreCase(domainIndex)) {
response = buildDomainSearchHierarchy(searchResponse);
}
return response;
}
Expand Down Expand Up @@ -643,6 +678,38 @@ public List<EntityHierarchy> buildGlossaryTermSearchHierarchy(SearchResponse sea
return new ArrayList<>(rootTerms.values());
}

public List<EntityHierarchy> buildDomainSearchHierarchy(SearchResponse searchResponse) {
Map<String, EntityHierarchy> entityHierarchyMap =
Arrays.stream(searchResponse.getHits().getHits())
.map(hit -> JsonUtils.readValue(hit.getSourceAsString(), EntityHierarchy.class))
.collect(
Collectors.toMap(
EntityHierarchy::getFullyQualifiedName,
entity -> {
entity.setChildren(new ArrayList<>());
return entity;
},
(existing, replacement) -> existing,
LinkedHashMap::new));

List<EntityHierarchy> rootDomains = new ArrayList<>();

entityHierarchyMap
.values()
.forEach(
entity -> {
String parentFqn = getParentFQN(entity.getFullyQualifiedName());
EntityHierarchy parentEntity = entityHierarchyMap.get(parentFqn);
if (parentEntity != null) {
parentEntity.getChildren().add(entity);
} else {
rootDomains.add(entity);
}
});

return rootDomains;
}

@Override
public SearchResultListMapper listWithOffset(
String filter,
Expand Down Expand Up @@ -1797,7 +1864,10 @@ private static SearchSourceBuilder buildDomainsSearch(String query, int from, in
buildSearchQueryBuilder(query, DomainIndex.getFields());
FunctionScoreQueryBuilder queryBuilder = boostScore(queryStringBuilder);
HighlightBuilder hb = buildHighlights(new ArrayList<>());
return searchBuilder(queryBuilder, hb, from, size);
SearchSourceBuilder searchSourceBuilder = searchBuilder(queryBuilder, hb, from, size);
searchSourceBuilder.aggregation(
AggregationBuilders.terms("fqnParts_agg").field("fqnParts").size(1000));
return addAggregation(searchSourceBuilder);
}

private static SearchSourceBuilder buildCostAnalysisReportDataSearch(
Expand Down
Loading
Loading