Skip to content

Commit

Permalink
feat(recommendations): Recommendations infra P1 (#3455)
Browse files Browse the repository at this point in the history
  • Loading branch information
jjoyce0510 authored Oct 27, 2021
1 parent 87e93ab commit 2f03ad8
Show file tree
Hide file tree
Showing 107 changed files with 4,334 additions and 305 deletions.
1 change: 1 addition & 0 deletions datahub-graphql-core/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ graphqlCodegen {
"$projectDir/src/main/resources/app.graphql".toString(),
"$projectDir/src/main/resources/search.graphql".toString(),
"$projectDir/src/main/resources/analytics.graphql".toString(),
"$projectDir/src/main/resources/recommendation.graphql".toString(),
]
outputDir = new File("$projectDir/src/mainGeneratedGraphQL/java")
packageName = "com.linkedin.datahub.graphql.generated"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,5 +12,6 @@ public class Constants {
public static final String SEARCH_SCHEMA_FILE = "search.graphql";
public static final String APP_SCHEMA_FILE = "app.graphql";
public static final String ANALYTICS_SCHEMA_FILE = "analytics.graphql";
public static final String RECOMMENDATIONS_SCHEMA_FILE = "recommendation.graphql";
public static final String BROWSE_PATH_DELIMITER = "/";
}
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
import com.linkedin.datahub.graphql.generated.EntityRelationshipLegacy;
import com.linkedin.datahub.graphql.generated.ForeignKeyConstraint;
import com.linkedin.datahub.graphql.generated.MLModelProperties;
import com.linkedin.datahub.graphql.generated.RecommendationContent;
import com.linkedin.datahub.graphql.generated.SearchResult;
import com.linkedin.datahub.graphql.generated.InstitutionalMemoryMetadata;
import com.linkedin.datahub.graphql.generated.UsageQueryResult;
Expand Down Expand Up @@ -65,6 +66,7 @@
import com.linkedin.datahub.graphql.resolvers.policy.ListPoliciesResolver;
import com.linkedin.datahub.graphql.resolvers.config.AppConfigResolver;
import com.linkedin.datahub.graphql.resolvers.policy.UpsertPolicyResolver;
import com.linkedin.datahub.graphql.resolvers.recommendation.ListRecommendationsResolver;
import com.linkedin.datahub.graphql.resolvers.search.SearchAcrossEntitiesResolver;
import com.linkedin.datahub.graphql.resolvers.type.AspectInterfaceTypeResolver;
import com.linkedin.datahub.graphql.resolvers.type.HyperParameterValueTypeResolver;
Expand Down Expand Up @@ -108,6 +110,7 @@
import com.linkedin.datahub.graphql.types.usage.UsageType;
import com.linkedin.entity.client.EntityClient;
import com.linkedin.metadata.entity.EntityService;
import com.linkedin.metadata.recommendation.RecommendationsService;
import com.linkedin.metadata.graph.GraphClient;
import graphql.execution.DataFetcherResult;
import graphql.schema.idl.RuntimeWiring;
Expand Down Expand Up @@ -145,6 +148,7 @@ public class GmsGraphQLEngine {
private final EntityClient entityClient;
private final EntityService entityService;
private final GraphClient graphClient;
private final RecommendationsService recommendationsService;

private final DatasetType datasetType;
private final CorpUserType corpUserType;
Expand Down Expand Up @@ -196,19 +200,22 @@ public class GmsGraphQLEngine {
public final List<BrowsableEntityType<?>> browsableTypes;

public GmsGraphQLEngine() {
this(null, null, null, null);
this(null, null, null, null, null);
}

public GmsGraphQLEngine(
final AnalyticsService analyticsService,
final EntityService entityService,
final GraphClient graphClient,
final EntityClient entityClient
) {
final EntityClient entityClient,
final RecommendationsService recommendationsService

) {
this.analyticsService = analyticsService;
this.entityClient = entityClient;
this.entityService = entityService;
this.graphClient = graphClient;
this.entityClient = entityClient;
this.recommendationsService = recommendationsService;

this.datasetType = new DatasetType(entityClient);
this.corpUserType = new CorpUserType(entityClient);
Expand Down Expand Up @@ -299,6 +306,18 @@ public static String analyticsSchema() {
return analyticsSchemaString;
}

public static String recommendationsSchema() {
String recommendationsSchemaString;
try {
InputStream is = Thread.currentThread().getContextClassLoader().getResourceAsStream(RECOMMENDATIONS_SCHEMA_FILE);
recommendationsSchemaString = IOUtils.toString(is, StandardCharsets.UTF_8);
is.close();
} catch (IOException e) {
throw new RuntimeException("Failed to find GraphQL Schema with name " + RECOMMENDATIONS_SCHEMA_FILE, e);
}
return recommendationsSchemaString;
}

/**
* Returns a {@link Supplier} responsible for creating a new {@link DataLoader} from
* a {@link LoadableType}.
Expand Down Expand Up @@ -337,6 +356,7 @@ public GraphQLEngine.Builder builder() {
.addSchema(searchSchema())
.addSchema(appSchema())
.addSchema(analyticsSchema())
.addSchema(recommendationsSchema())
.addDataLoaders(loaderSuppliers(loadableTypes))
.addDataLoader("Aspect", (context) -> createAspectLoader(context))
.addDataLoader("UsageQueryResult", (context) -> createUsageLoader(context))
Expand Down Expand Up @@ -420,6 +440,8 @@ private void configureQueryResolvers(final RuntimeWiring.Builder builder) {
new ListUsersResolver(GmsClientFactory.getEntitiesClient()))
.dataFetcher("listGroups",
new ListGroupsResolver(GmsClientFactory.getEntitiesClient()))
.dataFetcher("listRecommendations",
new ListRecommendationsResolver(recommendationsService))
.dataFetcher("getEntityCounts",
new EntityCountsResolver(GmsClientFactory.getEntitiesClient()))
);
Expand Down Expand Up @@ -469,6 +491,11 @@ private void configureGenericEntityResolvers(final RuntimeWiring.Builder builder
entityTypes.stream().collect(Collectors.toList()),
(env) -> ((AggregationMetadata) env.getSource()).getEntity()))
)
.type("RecommendationContent", typeWiring -> typeWiring
.dataFetcher("entity", new EntityTypeResolver(
entityTypes.stream().collect(Collectors.toList()),
(env) -> ((RecommendationContent) env.getSource()).getEntity()))
)
.type("BrowseResults", typeWiring -> typeWiring
.dataFetcher("entities", new AuthenticatedResolver<>(
new EntityTypeBatchResolver(
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,165 @@
package com.linkedin.datahub.graphql.resolvers.recommendation;

import com.linkedin.common.urn.Urn;
import com.linkedin.datahub.graphql.generated.ContentParams;
import com.linkedin.datahub.graphql.generated.EntityProfileParams;
import com.linkedin.datahub.graphql.generated.Filter;
import com.linkedin.datahub.graphql.generated.ListRecommendationsInput;
import com.linkedin.datahub.graphql.generated.ListRecommendationsResult;
import com.linkedin.datahub.graphql.generated.RecommendationContent;
import com.linkedin.datahub.graphql.generated.RecommendationModule;
import com.linkedin.datahub.graphql.generated.RecommendationParams;
import com.linkedin.datahub.graphql.generated.RecommendationRenderType;
import com.linkedin.datahub.graphql.generated.RecommendationRequestContext;
import com.linkedin.datahub.graphql.generated.SearchParams;
import com.linkedin.datahub.graphql.resolvers.EntityTypeMapper;
import com.linkedin.datahub.graphql.types.common.mappers.UrnToEntityMapper;
import com.linkedin.metadata.query.filter.Criterion;
import com.linkedin.metadata.query.filter.CriterionArray;
import com.linkedin.metadata.recommendation.EntityRequestContext;
import com.linkedin.metadata.recommendation.RecommendationsService;
import com.linkedin.metadata.recommendation.SearchRequestContext;
import graphql.schema.DataFetcher;
import graphql.schema.DataFetchingEnvironment;
import java.net.URISyntaxException;
import java.util.Collections;
import java.util.List;
import java.util.Optional;
import java.util.concurrent.CompletableFuture;
import java.util.stream.Collectors;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;

import static com.linkedin.datahub.graphql.resolvers.ResolverUtils.bindArgument;


@Slf4j
@RequiredArgsConstructor
public class ListRecommendationsResolver implements DataFetcher<CompletableFuture<ListRecommendationsResult>> {

private static final ListRecommendationsResult EMPTY_RECOMMENDATIONS = new ListRecommendationsResult(Collections.emptyList());

private final RecommendationsService _recommendationsService;

@Override
public CompletableFuture<ListRecommendationsResult> get(DataFetchingEnvironment environment) {
final ListRecommendationsInput input =
bindArgument(environment.getArgument("input"), ListRecommendationsInput.class);

return CompletableFuture.supplyAsync(() -> {
try {
log.debug("Listing recommendations for input {}", input);
List<com.linkedin.metadata.recommendation.RecommendationModule> modules =
_recommendationsService.listRecommendations(Urn.createFromString(input.getUserUrn()),
mapRequestContext(input.getRequestContext()), input.getLimit());
return ListRecommendationsResult.builder()
.setModules(modules.stream()
.map(this::mapRecommendationModule)
.filter(Optional::isPresent)
.map(Optional::get)
.collect(Collectors.toList()))
.build();
} catch (Exception e) {
log.error("Failed to get recommendations for input {}", input, e);
return EMPTY_RECOMMENDATIONS;
}
});
}

private com.linkedin.metadata.recommendation.RecommendationRequestContext mapRequestContext(
RecommendationRequestContext requestContext) {
com.linkedin.metadata.recommendation.ScenarioType mappedScenarioType;
try {
mappedScenarioType =
com.linkedin.metadata.recommendation.ScenarioType.valueOf(requestContext.getScenario().toString());
} catch (IllegalArgumentException e) {
log.error("Failed to map scenario type: {}", requestContext.getScenario(), e);
throw e;
}
com.linkedin.metadata.recommendation.RecommendationRequestContext mappedRequestContext =
new com.linkedin.metadata.recommendation.RecommendationRequestContext().setScenario(mappedScenarioType);
if (requestContext.getSearchRequestContext() != null) {
SearchRequestContext searchRequestContext =
new SearchRequestContext().setQuery(requestContext.getSearchRequestContext().getQuery());
if (requestContext.getSearchRequestContext().getFilters() != null) {
searchRequestContext.setFilters(new CriterionArray(requestContext.getSearchRequestContext()
.getFilters()
.stream()
.map(facetField -> new Criterion().setField(facetField.getField()).setValue(facetField.getValue()))
.collect(Collectors.toList())));
}
mappedRequestContext.setSearchRequestContext(searchRequestContext);
}
if (requestContext.getEntityRequestContext() != null) {
Urn entityUrn;
try {
entityUrn = Urn.createFromString(requestContext.getEntityRequestContext().getUrn());
} catch (URISyntaxException e) {
log.error("Malformed URN while mapping recommendations request: {}",
requestContext.getEntityRequestContext().getUrn(), e);
throw new IllegalArgumentException(e);
}
EntityRequestContext entityRequestContext = new EntityRequestContext().setUrn(entityUrn)
.setType(EntityTypeMapper.getName(requestContext.getEntityRequestContext().getType()));
mappedRequestContext.setEntityRequestContext(entityRequestContext);
}
return mappedRequestContext;
}

private Optional<RecommendationModule> mapRecommendationModule(
com.linkedin.metadata.recommendation.RecommendationModule module) {
RecommendationModule mappedModule = new RecommendationModule();
mappedModule.setTitle(module.getTitle());
mappedModule.setModuleId(module.getModuleId());
try {
mappedModule.setRenderType(RecommendationRenderType.valueOf(module.getRenderType().toString()));
} catch (IllegalArgumentException e) {
log.error("Failed to map render type: {}", module.getRenderType(), e);
throw e;
}
mappedModule.setContent(
module.getContent().stream().map(this::mapRecommendationContent).collect(Collectors.toList()));
return Optional.of(mappedModule);
}

private RecommendationContent mapRecommendationContent(
com.linkedin.metadata.recommendation.RecommendationContent content) {
RecommendationContent mappedContent = new RecommendationContent();
mappedContent.setValue(content.getValue());
if (content.hasEntity()) {
mappedContent.setEntity(UrnToEntityMapper.map(content.getEntity()));
}
if (content.hasParams()) {
mappedContent.setParams(mapRecommendationParams(content.getParams()));
}
return mappedContent;
}

private RecommendationParams mapRecommendationParams(
com.linkedin.metadata.recommendation.RecommendationParams params) {
RecommendationParams mappedParams = new RecommendationParams();
if (params.hasSearchParams()) {
SearchParams searchParams = new SearchParams();
searchParams.setQuery(params.getSearchParams().getQuery());
if (!params.getSearchParams().getFilters().isEmpty()) {
searchParams.setFilters(params.getSearchParams()
.getFilters()
.stream()
.map(criterion -> Filter.builder().setField(criterion.getField()).setValue(criterion.getValue()).build())
.collect(Collectors.toList()));
}
mappedParams.setSearchParams(searchParams);
}

if (params.hasEntityProfileParams()) {
mappedParams.setEntityProfileParams(
EntityProfileParams.builder().setUrn(params.getEntityProfileParams().getUrn().toString()).build());
}

if (params.hasContentParams()) {
mappedParams.setContentParams(ContentParams.builder().setCount(params.getContentParams().getCount()).build());
}

return mappedParams;
}
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
package com.linkedin.datahub.graphql.resolvers.search;

import com.google.common.collect.ImmutableList;
import com.linkedin.datahub.graphql.exception.ValidationException;
import com.linkedin.datahub.graphql.generated.EntityType;
import com.linkedin.datahub.graphql.generated.SearchAcrossEntitiesInput;
import com.linkedin.datahub.graphql.generated.SearchResults;
Expand All @@ -18,7 +17,6 @@
import lombok.extern.slf4j.Slf4j;

import static com.linkedin.datahub.graphql.resolvers.ResolverUtils.bindArgument;
import static org.apache.commons.lang3.StringUtils.isBlank;


/**
Expand Down Expand Up @@ -49,10 +47,6 @@ public CompletableFuture<SearchResults> get(DataFetchingEnvironment environment)

// escape forward slash since it is a reserved character in Elasticsearch
final String sanitizedQuery = ResolverUtils.escapeForwardSlash(input.getQuery());
if (isBlank(sanitizedQuery)) {
log.error("'query' parameter cannot was null or empty");
throw new ValidationException("'query' parameter cannot be null or empty");
}

final int start = input.getStart() != null ? input.getStart() : DEFAULT_START;
final int count = input.getCount() != null ? input.getCount() : DEFAULT_COUNT;
Expand Down
Loading

0 comments on commit 2f03ad8

Please sign in to comment.