Skip to content
26 changes: 26 additions & 0 deletions core/src/main/java/feast/core/grpc/CoreServiceImpl.java
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,32 @@ public void listFeatureSets(
}
}

/** Retrieve a list of features */
@Override
public void listFeatures(
ListFeaturesRequest request, StreamObserver<ListFeaturesResponse> responseObserver) {
try {
ListFeaturesResponse response = specService.listFeatures(request.getFilter());
responseObserver.onNext(response);
responseObserver.onCompleted();
} catch (IllegalArgumentException e) {
log.error("Illegal arguments provided to ListFeatures method: ", e);
responseObserver.onError(
Status.INVALID_ARGUMENT
.withDescription(e.getMessage())
.withCause(e)
.asRuntimeException());
} catch (RetrievalException e) {
log.error("Unable to fetch entities requested in ListFeatures method: ", e);
responseObserver.onError(
Status.NOT_FOUND.withDescription(e.getMessage()).withCause(e).asRuntimeException());
} catch (Exception e) {
log.error("Exception has occurred in ListFeatures method: ", e);
responseObserver.onError(
Status.INTERNAL.withDescription(e.getMessage()).withCause(e).asRuntimeException());
}
}

@Override
public void getFeatureStatistics(
GetFeatureStatisticsRequest request,
Expand Down
17 changes: 17 additions & 0 deletions core/src/main/java/feast/core/model/Feature.java
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,23 @@ private Feature(String name, ValueType.Enum type) {
this.setType(type.toString());
}

/**
* Return a boolean to facilitate streaming elements on the basis of given predicate.
*
* @param labelsFilter contain labels that should be attached to Feature
* @return boolean True if Feature contains all labels in the labelsFilter
*/
public boolean hasAllLabels(Map<String, String> labelsFilter) {
Map<String, String> featureLabelsMap = this.getLabels();
for (String key : labelsFilter.keySet()) {
if (!featureLabelsMap.containsKey(key)
|| !featureLabelsMap.get(key).equals(labelsFilter.get(key))) {
return false;
}
}
return true;
}

public static Feature fromProto(FeatureSpec featureSpec) {
Feature feature = new Feature(featureSpec.getName(), featureSpec.getValueType());
feature.labels = TypeConversion.convertMapToJsonString(featureSpec.getLabelsMap());
Expand Down
81 changes: 81 additions & 0 deletions core/src/main/java/feast/core/model/FeatureSet.java
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
import feast.core.util.TypeConversion;
import feast.proto.core.FeatureSetProto;
import feast.proto.core.FeatureSetProto.*;
import feast.proto.serving.ServingAPIProto.FeatureReference;
import java.util.*;
import java.util.stream.Collectors;
import javax.persistence.*;
Expand Down Expand Up @@ -128,6 +129,86 @@ private String getProjectName() {
}
}

/**
* Return a boolean to facilitate streaming elements on the basis of given predicate.
*
* @param entitiesFilter contain entities that should be attached to the FeatureSet
* @return boolean True if FeatureSet contains all entities in the entitiesFilter
*/
public boolean hasAllEntities(List<String> entitiesFilter) {
List<String> allEntitiesName =
this.getEntities().stream().map(entity -> entity.getName()).collect(Collectors.toList());
return allEntitiesName.equals(entitiesFilter);
}

/**
* Returns a map of Feature references and Features if FeatureSet's Feature contains all labels in
* the labelsFilter
*
* @param labelsFilter contain labels that should be attached to FeatureSet's features
* @return Map of Feature references and Features
*/
public Map<String, Feature> getFeaturesByRef(Map<String, String> labelsFilter) {
Map<String, Feature> validFeaturesMap = new HashMap<>();
List<Feature> validFeatures;
if (labelsFilter.size() > 0) {
validFeatures = filterFeaturesByAllLabels(this.getFeatures(), labelsFilter);
for (Feature feature : validFeatures) {
FeatureReference featureRef =
FeatureReference.newBuilder()
.setProject(this.getProjectName())
.setFeatureSet(this.getName())
.setName(feature.getName())
.build();
validFeaturesMap.put(renderFeatureRef(featureRef), feature);
}
return validFeaturesMap;
}
for (Feature feature : this.getFeatures()) {
FeatureReference featureRef =
FeatureReference.newBuilder()
.setProject(this.getProjectName())
.setFeatureSet(this.getName())
.setName(feature.getName())
.build();
validFeaturesMap.put(renderFeatureRef(featureRef), feature);
}
return validFeaturesMap;
}

/**
* Returns a list of Features if FeatureSet's Feature contains all labels in labelsFilter
*
* @param labelsFilter contain labels that should be attached to FeatureSet's features
* @return List of Features
*/
public static List<Feature> filterFeaturesByAllLabels(
Set<Feature> features, Map<String, String> labelsFilter) {
List<Feature> validFeatures =
features.stream()
.filter(feature -> feature.hasAllLabels(labelsFilter))
.collect(Collectors.toList());

return validFeatures;
}

/**
* Render a feature reference as string.
*
* @param featureReference to render as string
* @return string representation of feature reference.
*/
public static String renderFeatureRef(FeatureReference featureReference) {
String refStr =
featureReference.getProject()
+ "/"
+ featureReference.getFeatureSet()
+ ":"
+ featureReference.getName();

return refStr;
}

/**
* Return a boolean to facilitate streaming elements on the basis of given predicate.
*
Expand Down
62 changes: 62 additions & 0 deletions core/src/main/java/feast/core/service/SpecService.java
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,8 @@
import feast.proto.core.CoreServiceProto.GetFeatureSetResponse;
import feast.proto.core.CoreServiceProto.ListFeatureSetsRequest;
import feast.proto.core.CoreServiceProto.ListFeatureSetsResponse;
import feast.proto.core.CoreServiceProto.ListFeaturesRequest;
import feast.proto.core.CoreServiceProto.ListFeaturesResponse;
import feast.proto.core.CoreServiceProto.ListStoresRequest;
import feast.proto.core.CoreServiceProto.ListStoresResponse;
import feast.proto.core.CoreServiceProto.ListStoresResponse.Builder;
Expand Down Expand Up @@ -215,6 +217,65 @@ public ListFeatureSetsResponse listFeatureSets(ListFeatureSetsRequest.Filter fil
return response.build();
}

/**
* Return a map of feature references and features matching the project, labels and entities
* provided in the filter. All fields are required.
*
* <p>Project name must be explicitly provided or if the project name is omitted, the default
* project would be used. A combination of asterisks/wildcards and text is not allowed.
*
* <p>The entities in the filter accepts a list. All matching features will be returned. Regex is
* not supported. If no entities are provided, features will not be filtered by entities.
*
* <p>The labels in the filter accepts a map. All matching features will be returned. Regex is not
* supported. If no labels are provided, features will not be filtered by labels.
*
* @param filter filter containing the desired project name, entities and labels
* @return ListEntitiesResponse with map of feature references and features found matching the
* filter
*/
public ListFeaturesResponse listFeatures(ListFeaturesRequest.Filter filter) {
try {
String project = filter.getProject();
List<String> entities = filter.getEntitiesList();
Map<String, String> labels = filter.getLabelsMap();

checkValidCharactersAllowAsterisk(project, "projectName");

// Autofill default project if project not specified
if (project.isEmpty()) {
project = Project.DEFAULT_NAME;
}

// Currently defaults to all FeatureSets
List<FeatureSet> featureSets =
featureSetRepository.findAllByNameLikeAndProject_NameOrderByNameAsc("%", project);

ListFeaturesResponse.Builder response = ListFeaturesResponse.newBuilder();
if (entities.size() > 0) {
featureSets =
featureSets.stream()
.filter(featureSet -> featureSet.hasAllEntities(entities))
.collect(Collectors.toList());
}

Map<String, Feature> featuresMap;
for (FeatureSet featureSet : featureSets) {
featuresMap = featureSet.getFeaturesByRef(labels);
for (Map.Entry<String, Feature> entry : featuresMap.entrySet()) {
response.putFeatures(entry.getKey(), entry.getValue().toProto());
}
}

return response.build();
} catch (InvalidProtocolBufferException e) {
throw io.grpc.Status.NOT_FOUND
.withDescription("Unable to retrieve features")
.withCause(e)
.asRuntimeException();
}
}

/**
* Get stores matching the store name provided in the filter. If the store name is not provided,
* the method will return all stores currently registered to Feast.
Expand Down Expand Up @@ -242,6 +303,7 @@ public ListStoresResponse listStores(ListStoresRequest.Filter filter) {
String.format("Store with name '%s' not found", name)));
return ListStoresResponse.newBuilder().addStore(store.toProto()).build();
} catch (InvalidProtocolBufferException e) {

throw io.grpc.Status.NOT_FOUND
.withDescription("Unable to retrieve stores")
.withCause(e)
Expand Down
Loading