From 054a25cf62bbc5a4ff8f58c4e12b6602211e379f Mon Sep 17 00:00:00 2001 From: Willem Pienaar Date: Tue, 19 May 2020 17:14:50 +0800 Subject: [PATCH 01/26] Release v0.5 --- CHANGELOG.md | 75 ++++++++++++++++++++++++++++++++++++++++++++++++++++ pom.xml | 2 +- 2 files changed, 76 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5161174e48b..5339b8c5f57 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,80 @@ # Changelog +## [v0.5.0](https://github.com/feast-dev/feast/tree/v0.5.0) (2020-05-19) + +[Full Changelog](https://github.com/feast-dev/feast/compare/v0.4.7...v0.5.0) + +**Breaking changes:** + +- Add .proto to packages of Protobuf generated Java classes [\#700](https://github.com/feast-dev/feast/pull/700) ([woop](https://github.com/woop)) +- Add support for feature set updates and remove versions [\#676](https://github.com/feast-dev/feast/pull/676) ([zhilingc](https://github.com/zhilingc)) +- Feast configuration files refactored [\#611](https://github.com/feast-dev/feast/pull/611) ([woop](https://github.com/woop)) + +See [Feast 0.5 Release Issue](https://github.com/feast-dev/feast/issues/527) for more details. + +**Implemented enhancements:** + +- Add general storage API and refactor existing store implementations [\#567](https://github.com/feast-dev/feast/pull/567) ([zhilingc](https://github.com/zhilingc)) +- Add support for feature set updates and remove versions [\#676](https://github.com/feast-dev/feast/pull/676) ([zhilingc](https://github.com/zhilingc)) +- Add unique ingestion id for all batch ingestions [\#656](https://github.com/feast-dev/feast/pull/656) ([zhilingc](https://github.com/zhilingc)) +- Add storage interfaces [\#529](https://github.com/feast-dev/feast/pull/529) ([zhilingc](https://github.com/zhilingc)) +- Add BigQuery storage implementation [\#546](https://github.com/feast-dev/feast/pull/546) ([zhilingc](https://github.com/zhilingc)) +- Add Redis storage implementation [\#547](https://github.com/feast-dev/feast/pull/547) ([zhilingc](https://github.com/zhilingc)) +- Add Support for Redis Cluster [\#502](https://github.com/feast-dev/feast/pull/502) ([lavkesh](https://github.com/lavkesh)) +- Add Ingestion Job management API for Feast Core [\#548](https://github.com/feast-dev/feast/pull/548) ([mrzzy](https://github.com/mrzzy)) +- Add feature and feature set labels for metadata [\#536](https://github.com/feast-dev/feast/pull/536) ([imjuanleonard](https://github.com/imjuanleonard)) +- Update Python SDK so FeatureSet can import Schema from Tensorflow metadata [\#450](https://github.com/feast-dev/feast/pull/450) ([davidheryanto](https://github.com/davidheryanto)) + +**Fixed bugs:** + +- Add feature set status JOB\_STARTING to denote feature sets waiting for job to get to RUNNING state [\#714](https://github.com/feast-dev/feast/pull/714) ([zhilingc](https://github.com/zhilingc)) +- Remove feature set status check for job update requirement [\#708](https://github.com/feast-dev/feast/pull/708) ([khorshuheng](https://github.com/khorshuheng)) +- Fix Feast Core docker image [\#703](https://github.com/feast-dev/feast/pull/703) ([khorshuheng](https://github.com/khorshuheng)) +- Include server port config on the generated application.yml [\#696](https://github.com/feast-dev/feast/pull/696) ([khorshuheng](https://github.com/khorshuheng)) +- Fix typo in all types parquet yml file \(e2e test\) [\#683](https://github.com/feast-dev/feast/pull/683) ([khorshuheng](https://github.com/khorshuheng)) +- Add grpc health probe implementation to core [\#680](https://github.com/feast-dev/feast/pull/680) ([zhilingc](https://github.com/zhilingc)) +- Ensure that generated python code are considered as module [\#679](https://github.com/feast-dev/feast/pull/679) ([khorshuheng](https://github.com/khorshuheng)) +- Fix DataflowJobManager to update existing job instance instead of creating new one [\#678](https://github.com/feast-dev/feast/pull/678) ([zhilingc](https://github.com/zhilingc)) +- Fix config validation for feast.jobs.metrics.host [\#662](https://github.com/feast-dev/feast/pull/662) ([davidheryanto](https://github.com/davidheryanto)) +- Docker compose bug fix [\#661](https://github.com/feast-dev/feast/pull/661) ([woop](https://github.com/woop)) +- Swap join columns [\#647](https://github.com/feast-dev/feast/pull/647) ([zhilingc](https://github.com/zhilingc)) +- Fix Feast Serving not registering its store in Feast Core [\#641](https://github.com/feast-dev/feast/pull/641) ([mrzzy](https://github.com/mrzzy)) +- Kafka producer should raise an exception when it fails to connect to broker [\#636](https://github.com/feast-dev/feast/pull/636) ([junhui096](https://github.com/junhui096)) + +**Merged pull requests:** + +- Change organization from gojek to feast-dev [\#712](https://github.com/feast-dev/feast/pull/712) ([woop](https://github.com/woop)) +- Extract feature set update tests so CI doesn't run it [\#709](https://github.com/feast-dev/feast/pull/709) ([zhilingc](https://github.com/zhilingc)) +- Ensure that batch retrieval tests clean up after themselves [\#704](https://github.com/feast-dev/feast/pull/704) ([zhilingc](https://github.com/zhilingc)) +- Apply default project to rows without project during ingestion [\#701](https://github.com/feast-dev/feast/pull/701) ([zhilingc](https://github.com/zhilingc)) +- Update tests to correct compute region [\#699](https://github.com/feast-dev/feast/pull/699) ([terryyylim](https://github.com/terryyylim)) +- Make Projects optional & Update Feature References [\#693](https://github.com/feast-dev/feast/pull/693) ([mrzzy](https://github.com/mrzzy)) +- Add Java code coverage reporting [\#686](https://github.com/feast-dev/feast/pull/686) ([ches](https://github.com/ches)) +- Update e2e tests to allow non-SNAPSHOT testing [\#672](https://github.com/feast-dev/feast/pull/672) ([woop](https://github.com/woop)) +- Move TFDV stats to higher-numbered protobuf fields [\#669](https://github.com/feast-dev/feast/pull/669) ([ches](https://github.com/ches)) +- Clean up Docker Compose and add test [\#668](https://github.com/feast-dev/feast/pull/668) ([woop](https://github.com/woop)) +- Enable Prow e2e tests by default [\#666](https://github.com/feast-dev/feast/pull/666) ([woop](https://github.com/woop)) +- Add label checking to Prow [\#665](https://github.com/feast-dev/feast/pull/665) ([woop](https://github.com/woop)) +- Upgrade Github Checkout action to v2 [\#660](https://github.com/feast-dev/feast/pull/660) ([khorshuheng](https://github.com/khorshuheng)) +- Fix Redis cluster e2e [\#659](https://github.com/feast-dev/feast/pull/659) ([terryyylim](https://github.com/terryyylim)) +- Split Field model into distinct Feature and Entity objects [\#655](https://github.com/feast-dev/feast/pull/655) ([zhilingc](https://github.com/zhilingc)) +- Use Runner enum type instead of string for Job model [\#651](https://github.com/feast-dev/feast/pull/651) ([ches](https://github.com/ches)) +- JobUpdateTask cleanups [\#650](https://github.com/feast-dev/feast/pull/650) ([ches](https://github.com/ches)) +- Update approvers list [\#648](https://github.com/feast-dev/feast/pull/648) ([khorshuheng](https://github.com/khorshuheng)) +- Update end-to-end test config [\#645](https://github.com/feast-dev/feast/pull/645) ([zhilingc](https://github.com/zhilingc)) +- Fix bigquery config for serving store [\#644](https://github.com/feast-dev/feast/pull/644) ([zhilingc](https://github.com/zhilingc)) +- Fix Dataflow translator bug [\#643](https://github.com/feast-dev/feast/pull/643) ([zhilingc](https://github.com/zhilingc)) +- Fix subscription config and doctests [\#634](https://github.com/feast-dev/feast/pull/634) ([woop](https://github.com/woop)) +- Correct links to why-feast and concepts doc site [\#627](https://github.com/feast-dev/feast/pull/627) ([anderseriksson](https://github.com/anderseriksson)) +- Make error on retrieval of nonexistent feature humanly readable [\#625](https://github.com/feast-dev/feast/pull/625) ([mrzzy](https://github.com/mrzzy)) +- Generate golang code for non-serving protos [\#618](https://github.com/feast-dev/feast/pull/618) ([zhilingc](https://github.com/zhilingc)) +- Regenerate golang code, fix proto comparisons [\#616](https://github.com/feast-dev/feast/pull/616) ([zhilingc](https://github.com/zhilingc)) +- Fix doc building [\#603](https://github.com/feast-dev/feast/pull/603) ([woop](https://github.com/woop)) +- Pin Jupyter Notebook version [\#597](https://github.com/feast-dev/feast/pull/597) ([imjuanleonard](https://github.com/imjuanleonard)) +- Create project if not exists on applyFeatureSet [\#596](https://github.com/feast-dev/feast/pull/596) ([Joostrothweiler](https://github.com/Joostrothweiler)) +- Apply a fixed window before writing row metrics [\#590](https://github.com/feast-dev/feast/pull/590) ([davidheryanto](https://github.com/davidheryanto)) +- Allow tests to run on non-master branches [\#588](https://github.com/feast-dev/feast/pull/588) ([woop](https://github.com/woop)) + ## [v0.4.7](https://github.com/feast-dev/feast/tree/v0.4.7) (2020-03-17) [Full Changelog](https://github.com/feast-dev/feast/compare/v0.4.6...v0.4.7) diff --git a/pom.xml b/pom.xml index 19154b9cf13..fe849fcf8f2 100644 --- a/pom.xml +++ b/pom.xml @@ -39,7 +39,7 @@ - 0.5-SNAPSHOT + 0.5 https://github.com/feast-dev/feast UTF-8 From a102eb0a23824aa26121427c1402adc913ec46e9 Mon Sep 17 00:00:00 2001 From: Willem Pienaar <6728866+woop@users.noreply.github.com> Date: Thu, 21 May 2020 23:05:23 +0800 Subject: [PATCH 02/26] Backport Python SDK bug fixes (#724) Co-authored-by: zhilingc --- sdk/python/feast/cli.py | 2 +- sdk/python/feast/client.py | 8 +------- 2 files changed, 2 insertions(+), 8 deletions(-) diff --git a/sdk/python/feast/cli.py b/sdk/python/feast/cli.py index 87f12301f86..36d1d489e41 100644 --- a/sdk/python/feast/cli.py +++ b/sdk/python/feast/cli.py @@ -128,7 +128,7 @@ def feature_set_list(): feast_client = Client() # type: Client table = [] - for fs in feast_client.list_feature_sets(): + for fs in feast_client.list_feature_sets(project="*", name="*"): table.append([fs.name, repr(fs)]) from tabulate import tabulate diff --git a/sdk/python/feast/client.py b/sdk/python/feast/client.py index 50e3a65bad2..5e4baf9d598 100644 --- a/sdk/python/feast/client.py +++ b/sdk/python/feast/client.py @@ -385,13 +385,7 @@ def _apply_feature_set(self, feature_set: FeatureSet): feature_set.is_valid() feature_set_proto = feature_set.to_proto() if len(feature_set_proto.spec.project) == 0: - if self.project is None: - raise ValueError( - f"No project found in feature set {feature_set.name}. " - f"Please set the project within the feature set or within " - f"your Feast Client." - ) - else: + if self.project is not None: feature_set_proto.spec.project = self.project # Convert the feature set to a request and send to Feast Core From de0a614da981d6c53ec31684de12683990c160b6 Mon Sep 17 00:00:00 2001 From: Ches Martin Date: Sat, 23 May 2020 20:41:03 +0700 Subject: [PATCH 03/26] Restore Feast Java SDK and Ingestion compatibility with Java 8 runtimes (#739) Backports #722 to v0.5. --- core/pom.xml | 8 +++++ datatypes/java/pom.xml | 9 ----- ingestion/pom.xml | 34 ++++++++++++++----- pom.xml | 3 +- .../java/com/gojek/feast/RequestUtilTest.java | 3 +- serving/pom.xml | 8 +++++ .../redis/writer/RedisCustomIO.java | 6 ++-- .../retriever/RedisOnlineRetrieverTest.java | 8 ++--- .../writer/RedisClusterCustomIO.java | 6 ++-- .../RedisClusterOnlineRetrieverTest.java | 10 +++--- 10 files changed, 63 insertions(+), 32 deletions(-) diff --git a/core/pom.xml b/core/pom.xml index 2f616d5ed41..c4679df5f59 100644 --- a/core/pom.xml +++ b/core/pom.xml @@ -32,6 +32,14 @@ + + org.apache.maven.plugins + maven-compiler-plugin + + 11 + + + org.jacoco jacoco-maven-plugin diff --git a/datatypes/java/pom.xml b/datatypes/java/pom.xml index a127853258e..5810a6db96a 100644 --- a/datatypes/java/pom.xml +++ b/datatypes/java/pom.xml @@ -37,15 +37,6 @@ - - org.apache.maven.plugins - maven-compiler-plugin - - - 8 - - - org.apache.maven.plugins maven-dependency-plugin diff --git a/ingestion/pom.xml b/ingestion/pom.xml index a62d4202ee0..a004c6d9df6 100644 --- a/ingestion/pom.xml +++ b/ingestion/pom.xml @@ -31,15 +31,6 @@ - - org.apache.maven.plugins - maven-compiler-plugin - - - 8 - - - org.apache.maven.plugins maven-shade-plugin @@ -84,6 +75,31 @@ + + org.apache.maven.plugins + maven-enforcer-plugin + + + + enforce-bytecode-version + + enforce + + + + + 1.8 + + + + + + + org.jacoco jacoco-maven-plugin diff --git a/pom.xml b/pom.xml index fe849fcf8f2..0f1b5c94822 100644 --- a/pom.xml +++ b/pom.xml @@ -411,7 +411,8 @@ org.apache.maven.plugins maven-compiler-plugin - 11 + + 8 diff --git a/sdk/java/src/test/java/com/gojek/feast/RequestUtilTest.java b/sdk/java/src/test/java/com/gojek/feast/RequestUtilTest.java index f10d82b8726..60643115558 100644 --- a/sdk/java/src/test/java/com/gojek/feast/RequestUtilTest.java +++ b/sdk/java/src/test/java/com/gojek/feast/RequestUtilTest.java @@ -19,6 +19,7 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertThrows; +import com.google.common.collect.ImmutableList; import com.google.protobuf.TextFormat; import feast.proto.serving.ServingAPIProto.FeatureReference; import java.util.Arrays; @@ -82,7 +83,7 @@ void renderFeatureRef_ShouldReturnFeatureRefString( } private static Stream provideInvalidFeatureRefs() { - return Stream.of(Arguments.of(List.of("project/feature", ""))); + return Stream.of(Arguments.of(ImmutableList.of("project/feature", ""))); } @ParameterizedTest diff --git a/serving/pom.xml b/serving/pom.xml index 3754777da07..ce4ee6f49c3 100644 --- a/serving/pom.xml +++ b/serving/pom.xml @@ -40,6 +40,14 @@ + + org.apache.maven.plugins + maven-compiler-plugin + + 11 + + + org.jacoco jacoco-maven-plugin diff --git a/storage/connectors/redis/src/main/java/feast/storage/connectors/redis/writer/RedisCustomIO.java b/storage/connectors/redis/src/main/java/feast/storage/connectors/redis/writer/RedisCustomIO.java index 3fe9148eed5..eeac5b3d964 100644 --- a/storage/connectors/redis/src/main/java/feast/storage/connectors/redis/writer/RedisCustomIO.java +++ b/storage/connectors/redis/src/main/java/feast/storage/connectors/redis/writer/RedisCustomIO.java @@ -54,8 +54,10 @@ public class RedisCustomIO { private static final int DEFAULT_BATCH_SIZE = 1000; private static final int DEFAULT_TIMEOUT = 2000; - private static TupleTag successfulInsertsTag = new TupleTag<>("successfulInserts") {}; - private static TupleTag failedInsertsTupleTag = new TupleTag<>("failedInserts") {}; + private static TupleTag successfulInsertsTag = + new TupleTag("successfulInserts") {}; + private static TupleTag failedInsertsTupleTag = + new TupleTag("failedInserts") {}; private static final Logger log = LoggerFactory.getLogger(RedisCustomIO.class); diff --git a/storage/connectors/redis/src/test/java/feast/storage/connectors/redis/retriever/RedisOnlineRetrieverTest.java b/storage/connectors/redis/src/test/java/feast/storage/connectors/redis/retriever/RedisOnlineRetrieverTest.java index 4ab2ac9e130..adacacb941e 100644 --- a/storage/connectors/redis/src/test/java/feast/storage/connectors/redis/retriever/RedisOnlineRetrieverTest.java +++ b/storage/connectors/redis/src/test/java/feast/storage/connectors/redis/retriever/RedisOnlineRetrieverTest.java @@ -133,7 +133,7 @@ public void shouldReturnResponseWithValuesIfKeysPresent() { when(syncCommands.mget(redisKeyList)).thenReturn(featureRowBytes); List> expected = - List.of( + ImmutableList.of( Lists.newArrayList( FeatureRow.newBuilder() .setEventTimestamp(Timestamp.newBuilder().setSeconds(100)) @@ -153,7 +153,7 @@ public void shouldReturnResponseWithValuesIfKeysPresent() { .build())); List> actual = - redisOnlineRetriever.getOnlineFeatures(entityRows, List.of(featureSetRequest)); + redisOnlineRetriever.getOnlineFeatures(entityRows, ImmutableList.of(featureSetRequest)); assertThat(actual, equalTo(expected)); } @@ -201,7 +201,7 @@ public void shouldReturnResponseWithUnsetValuesIfKeysNotPresent() { when(syncCommands.mget(redisKeyList)).thenReturn(featureRowBytes); List> expected = - List.of( + ImmutableList.of( Lists.newArrayList( FeatureRow.newBuilder() .setEventTimestamp(Timestamp.newBuilder().setSeconds(100)) @@ -220,7 +220,7 @@ public void shouldReturnResponseWithUnsetValuesIfKeysNotPresent() { .build())); List> actual = - redisOnlineRetriever.getOnlineFeatures(entityRows, List.of(featureSetRequest)); + redisOnlineRetriever.getOnlineFeatures(entityRows, ImmutableList.of(featureSetRequest)); assertThat(actual, equalTo(expected)); } diff --git a/storage/connectors/rediscluster/src/main/java/feast/storage/connectors/rediscluster/writer/RedisClusterCustomIO.java b/storage/connectors/rediscluster/src/main/java/feast/storage/connectors/rediscluster/writer/RedisClusterCustomIO.java index 8984dc877cd..712012a2fb7 100644 --- a/storage/connectors/rediscluster/src/main/java/feast/storage/connectors/rediscluster/writer/RedisClusterCustomIO.java +++ b/storage/connectors/rediscluster/src/main/java/feast/storage/connectors/rediscluster/writer/RedisClusterCustomIO.java @@ -54,8 +54,10 @@ public class RedisClusterCustomIO { private static final int DEFAULT_BATCH_SIZE = 1000; private static final int DEFAULT_TIMEOUT = 2000; - private static TupleTag successfulInsertsTag = new TupleTag<>("successfulInserts") {}; - private static TupleTag failedInsertsTupleTag = new TupleTag<>("failedInserts") {}; + private static TupleTag successfulInsertsTag = + new TupleTag("successfulInserts") {}; + private static TupleTag failedInsertsTupleTag = + new TupleTag("failedInserts") {}; private static final Logger log = LoggerFactory.getLogger(RedisClusterCustomIO.class); diff --git a/storage/connectors/rediscluster/src/test/java/feast/storage/connectors/rediscluster/retriever/RedisClusterOnlineRetrieverTest.java b/storage/connectors/rediscluster/src/test/java/feast/storage/connectors/rediscluster/retriever/RedisClusterOnlineRetrieverTest.java index 16ab7962d60..deed53e1ae8 100644 --- a/storage/connectors/rediscluster/src/test/java/feast/storage/connectors/rediscluster/retriever/RedisClusterOnlineRetrieverTest.java +++ b/storage/connectors/rediscluster/src/test/java/feast/storage/connectors/rediscluster/retriever/RedisClusterOnlineRetrieverTest.java @@ -133,7 +133,7 @@ public void shouldReturnResponseWithValuesIfKeysPresent() { when(syncCommands.mget(redisKeyList)).thenReturn(featureRowBytes); List> expected = - List.of( + ImmutableList.of( Lists.newArrayList( FeatureRow.newBuilder() .setEventTimestamp(Timestamp.newBuilder().setSeconds(100)) @@ -153,7 +153,8 @@ public void shouldReturnResponseWithValuesIfKeysPresent() { .build())); List> actual = - redisClusterOnlineRetriever.getOnlineFeatures(entityRows, List.of(featureSetRequest)); + redisClusterOnlineRetriever.getOnlineFeatures( + entityRows, ImmutableList.of(featureSetRequest)); assertThat(actual, equalTo(expected)); } @@ -201,7 +202,7 @@ public void shouldReturnResponseWithUnsetValuesIfKeysNotPresent() { when(syncCommands.mget(redisKeyList)).thenReturn(featureRowBytes); List> expected = - List.of( + ImmutableList.of( Lists.newArrayList( FeatureRow.newBuilder() .setEventTimestamp(Timestamp.newBuilder().setSeconds(100)) @@ -220,7 +221,8 @@ public void shouldReturnResponseWithUnsetValuesIfKeysNotPresent() { .build())); List> actual = - redisClusterOnlineRetriever.getOnlineFeatures(entityRows, List.of(featureSetRequest)); + redisClusterOnlineRetriever.getOnlineFeatures( + entityRows, ImmutableList.of(featureSetRequest)); assertThat(actual, equalTo(expected)); } From 3be900d10e935372e89477cf32e191d380c67b73 Mon Sep 17 00:00:00 2001 From: Tan Yee Jian Date: Fri, 22 May 2020 00:05:00 +0800 Subject: [PATCH 04/26] Increase Docker Compose test timeout (#715) --- tests/e2e/basic-ingest-redis-serving.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/e2e/basic-ingest-redis-serving.py b/tests/e2e/basic-ingest-redis-serving.py index 311f1282477..619b8ca6ce2 100644 --- a/tests/e2e/basic-ingest-redis-serving.py +++ b/tests/e2e/basic-ingest-redis-serving.py @@ -129,7 +129,7 @@ def test_basic_ingest_success(client, cust_trans_df, driver_df): time.sleep(5) -@pytest.mark.timeout(45) +@pytest.mark.timeout(60) @pytest.mark.run(order=12) def test_basic_retrieve_online_success(client, cust_trans_df): # Poll serving for feature values until the correct values are returned From e1568aaa239880216234fee0bc247de6e57336aa Mon Sep 17 00:00:00 2001 From: Willem Pienaar Date: Sun, 24 May 2020 09:53:41 +0800 Subject: [PATCH 05/26] Increase Docker Compose test timeout for retrieval --- tests/e2e/basic-ingest-redis-serving.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/e2e/basic-ingest-redis-serving.py b/tests/e2e/basic-ingest-redis-serving.py index 619b8ca6ce2..4209ce1c220 100644 --- a/tests/e2e/basic-ingest-redis-serving.py +++ b/tests/e2e/basic-ingest-redis-serving.py @@ -129,7 +129,7 @@ def test_basic_ingest_success(client, cust_trans_df, driver_df): time.sleep(5) -@pytest.mark.timeout(60) +@pytest.mark.timeout(90) @pytest.mark.run(order=12) def test_basic_retrieve_online_success(client, cust_trans_df): # Poll serving for feature values until the correct values are returned @@ -171,7 +171,7 @@ def test_basic_retrieve_online_success(client, cust_trans_df): break -@pytest.mark.timeout(45) +@pytest.mark.timeout(90) @pytest.mark.run(order=13) def test_basic_retrieve_online_multiple_featureset(client, cust_trans_df, driver_df): # Poll serving for feature values until the correct values are returned @@ -359,7 +359,7 @@ def test_all_types_ingest_success(client, all_types_dataframe): client.ingest(all_types_fs, all_types_dataframe) -@pytest.mark.timeout(45) +@pytest.mark.timeout(90) @pytest.mark.run(order=22) def test_all_types_retrieve_online_success(client, all_types_dataframe): # Poll serving for feature values until the correct values are returned @@ -484,7 +484,7 @@ def test_large_volume_ingest_success(client, large_volume_dataframe): client.ingest(cust_trans_fs, large_volume_dataframe) -@pytest.mark.timeout(45) +@pytest.mark.timeout(90) @pytest.mark.run(order=32) def test_large_volume_retrieve_online_success(client, large_volume_dataframe): # Poll serving for feature values until the correct values are returned From 7029e8ee618b485c732145f84229d8957b077db1 Mon Sep 17 00:00:00 2001 From: Willem Pienaar Date: Sun, 24 May 2020 02:27:10 +0000 Subject: [PATCH 06/26] GitBook: [v0.5-branch] 13 pages and 5 assets modified --- docs/.gitbook/assets/architecture.png | Bin 0 -> 24680 bytes .../assets/basic-architecture-diagram (2).svg | 1 + .../assets/basic-architecture-diagram (3).svg | 1 + .../feast-docs-overview-diagram-2 (2).svg | 1 + .../feast-docs-overview-diagram-2 (3).svg | 1 + docs/README.md | 2 +- docs/concepts/concepts.md | 2 +- docs/contributing/contributing.md | 21 ++++---- docs/contributing/release-process.md | 45 ++++++++---------- docs/installation/gke.md | 2 +- docs/installation/overview.md | 2 - 11 files changed, 39 insertions(+), 39 deletions(-) create mode 100644 docs/.gitbook/assets/architecture.png create mode 100644 docs/.gitbook/assets/basic-architecture-diagram (2).svg create mode 100644 docs/.gitbook/assets/basic-architecture-diagram (3).svg create mode 100644 docs/.gitbook/assets/feast-docs-overview-diagram-2 (2).svg create mode 100644 docs/.gitbook/assets/feast-docs-overview-diagram-2 (3).svg diff --git a/docs/.gitbook/assets/architecture.png b/docs/.gitbook/assets/architecture.png new file mode 100644 index 0000000000000000000000000000000000000000..bc655b60f327b7d7d508885666064e0c713522b5 GIT binary patch literal 24680 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 U{S>BsDu7N?9j;cUYWeKH07gwlNdN!< literal 0 HcmV?d00001 diff --git a/docs/.gitbook/assets/basic-architecture-diagram (2).svg b/docs/.gitbook/assets/basic-architecture-diagram (2).svg new file mode 100644 index 00000000000..b707f490461 --- /dev/null +++ b/docs/.gitbook/assets/basic-architecture-diagram (2).svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/docs/.gitbook/assets/basic-architecture-diagram (3).svg b/docs/.gitbook/assets/basic-architecture-diagram (3).svg new file mode 100644 index 00000000000..b707f490461 --- /dev/null +++ b/docs/.gitbook/assets/basic-architecture-diagram (3).svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/docs/.gitbook/assets/feast-docs-overview-diagram-2 (2).svg b/docs/.gitbook/assets/feast-docs-overview-diagram-2 (2).svg new file mode 100644 index 00000000000..7f30963ec78 --- /dev/null +++ b/docs/.gitbook/assets/feast-docs-overview-diagram-2 (2).svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/docs/.gitbook/assets/feast-docs-overview-diagram-2 (3).svg b/docs/.gitbook/assets/feast-docs-overview-diagram-2 (3).svg new file mode 100644 index 00000000000..7f30963ec78 --- /dev/null +++ b/docs/.gitbook/assets/feast-docs-overview-diagram-2 (3).svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/docs/README.md b/docs/README.md index 9593e9f26e9..63887a31489 100644 --- a/docs/README.md +++ b/docs/README.md @@ -11,7 +11,7 @@ Feast aims to: * Provide consistent and point-in-time correct access to feature data. * Enable discovery, documentation, and insights into your features. -![](.gitbook/assets/feast-docs-overview-diagram-2.svg) +![](.gitbook/assets/feast-docs-overview-diagram-2%20%283%29.svg) **TL;DR:** Feast decouples feature engineering from feature usage. Features that are added to Feast become available immediately for training and serving. Models can retrieve the same features used in training from a low latency online store in production. diff --git a/docs/concepts/concepts.md b/docs/concepts/concepts.md index e54bd488d6a..89743376ff2 100644 --- a/docs/concepts/concepts.md +++ b/docs/concepts/concepts.md @@ -2,7 +2,7 @@ ## Architecture -![Logical diagram of a typical Feast deployment](../.gitbook/assets/basic-architecture-diagram%20%282%29.svg) +![Logical diagram of a typical Feast deployment](https://github.com/feast-dev/feast/tree/e1568aaa239880216234fee0bc247de6e57336aa/docs/.gitbook/assets/basic-architecture-diagram%20%282%29.svg) The core components of a Feast deployment are diff --git a/docs/contributing/contributing.md b/docs/contributing/contributing.md index 805721e3d22..dcb9b3ad0e7 100644 --- a/docs/contributing/contributing.md +++ b/docs/contributing/contributing.md @@ -6,7 +6,7 @@ We use [RFCs](https://en.wikipedia.org/wiki/Request_for_Comments) and [GitHub is Please communicate your ideas through a GitHub issue or through our Slack Channel before starting development. -Please [submit a PR ](https://github.com/feast-dev/feast/pulls)to the master branch of the Feast repository once you are ready to submit your contribution. Code submission to Feast \(including submission from project maintainers\) require review and approval from maintainers or code owners. +Please [submit a PR ](https://github.com/feast-dev/feast/pulls)to the master branch of the Feast repository once you are ready to submit your contribution. Code submission to Feast \(including submission from project maintainers\) require review and approval from maintainers or code owners. PRs that are submitted by the general public need to be identified as `ok-to-test`. Once enabled, [Prow](https://github.com/kubernetes/test-infra/tree/master/prow) will run a range of tests to verify the submission, after which community members will help to review the pull request. @@ -31,7 +31,7 @@ The main components of Feast are: ### 2.**2 Requirements** -#### 2.**2.1 Development environment** +#### 2.**2.1 Development environment** The following software is required for Feast development @@ -132,7 +132,7 @@ The following section is a quick walk-through to test whether your local Feast d can be accessed with credentials user `postgres` and password `password`. Different database configurations can be supplied here \(`/core/src/main/resources/application.yml`\) -* Redis is running locally and accessible from `localhost:6379` +* Redis is running locally and accessible from `localhost:6379` * \(optional\) The local environment has been authentication with Google Cloud Platform and has full access to BigQuery. This is only necessary for BigQuery testing/development. #### 2.4.2 Clone Feast @@ -170,7 +170,7 @@ Rpc succeeded with OK status Feast Serving is configured through the `$FEAST_HOME_DIR/serving/src/main/resources/application.yml`. Each Serving deployment must be configured with a store. The default store is Redis \(used for online serving\). -The configuration for this default store is located in a separate `.yml` file. The default location is `$FEAST_HOME_DIR/serving/sample_redis_config.yml`: +The configuration for this default store is located in a separate `.yml` file. The default location is `$FEAST_HOME_DIR/serving/sample_redis_config.yml`: ```text name: serving @@ -184,7 +184,7 @@ subscriptions: version: "*" ``` -Once Feast Serving is started, it will register its store with Feast Core \(by name\) and start to subscribe to a feature sets based on its subscription. +Once Feast Serving is started, it will register its store with Feast Core \(by name\) and start to subscribe to a feature sets based on its subscription. Start Feast Serving GRPC server on localhost:6566 with store name `serving` @@ -231,9 +231,9 @@ store { Rpc succeeded with OK status ``` -In order to use BigQuery as a historical store, it is necessary to start Feast Serving with a different store type. +In order to use BigQuery as a historical store, it is necessary to start Feast Serving with a different store type. -Copy `$FEAST_HOME_DIR/serving/sample_redis_config.yml` to the following location `$FEAST_HOME_DIR/serving/my_bigquery_config.yml` and update the configuration as below: +Copy `$FEAST_HOME_DIR/serving/sample_redis_config.yml` to the following location `$FEAST_HOME_DIR/serving/my_bigquery_config.yml` and update the configuration as below: ```text name: bigquery @@ -247,7 +247,7 @@ subscriptions: project: "*" ``` -Then inside `serving/src/main/resources/application.yml` modify the following key `feast.store.config-path` to point to the new store configuration. +Then inside `serving/src/main/resources/application.yml` modify the following key `feast.store.config-path` to point to the new store configuration. After making these changes, restart Feast Serving: @@ -299,7 +299,7 @@ grpc_cli call localhost:6565 CreateProject ' When a feature set is successfully registered, Feast Core will start an **ingestion** job that listens for new features in the feature set. {% hint style="info" %} -Note that Feast currently only supports source of type `KAFKA`, so you must have access to a running Kafka broker to register a FeatureSet successfully. It is possible to omit the `source` from a Feature Set, but Feast Core will still use Kafka behind the scenes, it is simply abstracted away from the user. +Note that Feast currently only supports source of type `KAFKA`, so you must have access to a running Kafka broker to register a FeatureSet successfully. It is possible to omit the `source` from a Feature Set, but Feast Core will still use Kafka behind the scenes, it is simply abstracted away from the user. {% endhint %} Create a new FeatureSet in Feast by sending a request to Feast Core: @@ -459,7 +459,7 @@ field_values { #### 2.4.8 Summary -If you have made it to this point successfully you should have a functioning Feast deployment, at the very least using the Apache Beam DirectRunner for ingestion jobs and Redis for online serving. +If you have made it to this point successfully you should have a functioning Feast deployment, at the very least using the Apache Beam DirectRunner for ingestion jobs and Redis for online serving. It is important to note that most of the functionality demonstrated above is already available in a more abstracted form in the Python SDK \(Feast management, data ingestion, feature retrieval\) and the Java/Go SDKs \(feature retrieval\). However, it is useful to understand these internals from a development standpoint. @@ -525,3 +525,4 @@ And to format: ```text $ make format-python ``` + diff --git a/docs/contributing/release-process.md b/docs/contributing/release-process.md index 72f91203c0d..1ced18ea7bc 100644 --- a/docs/contributing/release-process.md +++ b/docs/contributing/release-process.md @@ -1,10 +1,10 @@ -# Releasing Feast +# Release Process ## Versioning policy and branch workflow Feast uses [semantic versioning](https://semver.org/). As such, while it is still pre-1.0 breaking changes will happen in minor versions. -Contributors are encouraged to understand our branch workflow described below, for choosing where to branch when making a change (and thus the merge base for a pull request). +Contributors are encouraged to understand our branch workflow described below, for choosing where to branch when making a change \(and thus the merge base for a pull request\). * Major and minor releases are cut from the `master` branch. * Each major and minor release has a long-lived maintenance branch, for example `v0.3-branch`. This is called a "release branch". @@ -19,28 +19,23 @@ In general, unless you're committing code that only applies to a particular rele For Feast maintainers, these are the concrete steps for making a new release. 1. For a major or minor release, create and check out the release branch for the new stream, e.g. `v0.6-branch`. For a patch version, check out the stream's release branch. -1. Update the [CHANGELOG.md]. See the [Creating a change log](#creating-a-change-log) guide. -1. In the root `pom.xml`, remove `-SNAPSHOT` from the `` property, and commit. -1. Push. For a new release branch, open a PR against master. -1. When CI passes, merge. (Remember _not_ to delete the new release branch). -1. Tag the merge commit with the release version, using a `v` prefix. Push the tag. -1. Bump to the next working version and append `-SNAPSHOT` in `pom.xml`. -1. Commit the POM and open a PR. -1. Create a [GitHub release](https://github.com/feast-dev/feast/releases) which includes a summary of important changes as well as any artifacts associated with the release. Make sure to include the same change log as added in [CHANGELOG.md]. Use `Feast vX.Y.Z` as the title. -1. Create one final PR to the master branch and also update its [CHANGELOG.md]. +2. Update the [CHANGELOG.md](https://github.com/feast-dev/feast/blob/master/CHANGELOG.md). See the [Creating a change log](release-process.md#creating-a-change-log) guide. +3. In the root `pom.xml`, remove `-SNAPSHOT` from the `` property, and commit. +4. Push. For a new release branch, open a PR against master. +5. When CI passes, merge. \(Remember _not_ to delete the new release branch\). +6. Tag the merge commit with the release version, using a `v` prefix. Push the tag. +7. Bump to the next working version and append `-SNAPSHOT` in `pom.xml`. +8. Commit the POM and open a PR. +9. Create a [GitHub release](https://github.com/feast-dev/feast/releases) which includes a summary of important changes as well as any artifacts associated with the release. Make sure to include the same change log as added in [CHANGELOG.md](https://github.com/feast-dev/feast/blob/master/CHANGELOG.md). Use `Feast vX.Y.Z` as the title. +10. Create one final PR to the master branch and also update its [CHANGELOG.md](https://github.com/feast-dev/feast/blob/master/CHANGELOG.md). When a tag that matches a Semantic Version string is pushed, CI will automatically build and push the relevant artifacts to their repositories or package managers \(docker images, Python wheels, etc\). JVM artifacts are promoted from Sonatype OSSRH to Maven Central, but it sometimes takes some time for them to be available. -[CHANGELOG.md]: https://github.com/feast-dev/feast/blob/master/CHANGELOG.md - ### Creating a change log -We use an [open source change log generator](https://hub.docker.com/r/ferrarimarco/github-changelog-generator/) to generate change logs. The process still requires a little bit of manual effort. -1. Create a GitHub token as [per these instructions ](https://github.com/github-changelog-generator/github-changelog-generator#github-token). The token is used as an input argument (`-t`) to the changelog generator. -2. The change log generator configuration below will look for unreleased changes on a specific branch. The branch will be `master` for a major/minor release, or a release branch (`v0.4-branch`) for a patch release. You will need to set the branch using the `--release-branch` argument. -3. You should also set the `--future-release` argument. This is the version you are releasing. The version can still be changed at a later date. -4. Update the arguments below and run the command to generate the change log to the console. -``` +We use an [open source change log generator](https://hub.docker.com/r/ferrarimarco/github-changelog-generator/) to generate change logs. The process still requires a little bit of manual effort. 1. Create a GitHub token as [per these instructions ](https://github.com/github-changelog-generator/github-changelog-generator#github-token). The token is used as an input argument \(`-t`\) to the changelog generator. 2. The change log generator configuration below will look for unreleased changes on a specific branch. The branch will be `master` for a major/minor release, or a release branch \(`v0.4-branch`\) for a patch release. You will need to set the branch using the `--release-branch` argument. 3. You should also set the `--future-release` argument. This is the version you are releasing. The version can still be changed at a later date. 4. Update the arguments below and run the command to generate the change log to the console. + +```text docker run -it --rm ferrarimarco/github-changelog-generator \ --user feast-dev \ --project feast \ @@ -55,8 +50,10 @@ docker run -it --rm ferrarimarco/github-changelog-generator \ --max-issues 1 \ -o ``` -5. Review each change log item. - - Make sure that sentences are grammatically correct and well formatted (although we will try to enforce this at the PR review stage). - - Make sure that each item is categorized correctly. You will see the following categories: `Breaking changes`, `Implemented enhancements`, `Fixed bugs`, and `Merged pull requests`. Any unlabeled PRs will be found in `Merged pull requests`. It's important to make sure that any `breaking changes`, `enhancements`, or `bug fixes` are pulled up out of `merged pull requests` into the correct category. Housekeeping, tech debt clearing, infra changes, or refactoring do not count as `enhancements`. Only enhancements a user benefits from should be listed in that category. - - Make sure that the "Full Changelog" link is actually comparing the correct tags (normally your released version against the previously version). - - Make sure that release notes and breaking changes are present. + +1. Review each change log item. + * Make sure that sentences are grammatically correct and well formatted \(although we will try to enforce this at the PR review stage\). + * Make sure that each item is categorized correctly. You will see the following categories: `Breaking changes`, `Implemented enhancements`, `Fixed bugs`, and `Merged pull requests`. Any unlabeled PRs will be found in `Merged pull requests`. It's important to make sure that any `breaking changes`, `enhancements`, or `bug fixes` are pulled up out of `merged pull requests` into the correct category. Housekeeping, tech debt clearing, infra changes, or refactoring do not count as `enhancements`. Only enhancements a user benefits from should be listed in that category. + * Make sure that the "Full Changelog" link is actually comparing the correct tags \(normally your released version against the previously version\). + * Make sure that release notes and breaking changes are present. + diff --git a/docs/installation/gke.md b/docs/installation/gke.md index f2ccfacbf8b..c2becc91cd2 100644 --- a/docs/installation/gke.md +++ b/docs/installation/gke.md @@ -156,7 +156,7 @@ Install the Feast Helm chart: helm install --name feast -f my-feast-values.yaml . ``` -Ensure that the system comes online. This will take a few minutes. +Ensure that the system comes online. This will take a few minutes. ```bash kubectl get pods diff --git a/docs/installation/overview.md b/docs/installation/overview.md index e0cd44e962a..e63adeda497 100644 --- a/docs/installation/overview.md +++ b/docs/installation/overview.md @@ -10,5 +10,3 @@ This installation guide will demonstrate three ways of installing Feast: * Recommended way to install Feast for production use. * The guide has dependencies on BigQuery, and Google Cloud Storage. - - From f8ad6c12a5b67cb2018a3f8e1fdbe922c30dab57 Mon Sep 17 00:00:00 2001 From: Willem Pienaar Date: Sun, 24 May 2020 14:38:15 +0800 Subject: [PATCH 07/26] Backport master documentation to Feast v0.5-branch --- docs/README.md | 4 +- docs/SUMMARY.md | 51 +- docs/assets/feast_logo.png | Bin 0 -> 81755 bytes docs/contributing/adding-a-new-store-1.md | 87 +++ docs/contributing/adding-a-new-store.md | 87 +++ docs/contributing/contributing.md | 519 +----------------- docs/contributing/style-guide.md | 22 +- docs/getting-help.md | 36 ++ docs/getting-started/README.md | 14 + .../connecting-to-feast-1/README.md | 21 + .../connecting-to-feast.md | 38 ++ .../connecting-to-feast-1/python-sdk.md | 20 + .../getting-started/deploying-feast/README.md | 16 + .../deploying-feast/docker-compose.md | 112 ++++ .../deploying-feast/kubernetes.md | 211 +++++++ docs/reference/api.md | 10 + docs/reference/api/README.md | 10 + docs/reference/configuration-reference.md | 157 ++++++ docs/roadmap.md | 31 ++ docs/user-guide/architecture.md | 41 ++ docs/user-guide/data-ingestion.md | 126 +++++ docs/user-guide/entities.md | 43 ++ docs/user-guide/feature-retrieval.md | 116 ++++ docs/user-guide/feature-sets.md | 54 ++ docs/user-guide/features.md | 38 ++ docs/user-guide/overview.md | 58 ++ docs/user-guide/sources.md | 32 ++ docs/user-guide/stores.md | 37 ++ docs/why-feast.md | 32 ++ 29 files changed, 1481 insertions(+), 542 deletions(-) create mode 100644 docs/assets/feast_logo.png create mode 100644 docs/contributing/adding-a-new-store-1.md create mode 100644 docs/contributing/adding-a-new-store.md create mode 100644 docs/getting-help.md create mode 100644 docs/getting-started/README.md create mode 100644 docs/getting-started/connecting-to-feast-1/README.md create mode 100644 docs/getting-started/connecting-to-feast-1/connecting-to-feast.md create mode 100644 docs/getting-started/connecting-to-feast-1/python-sdk.md create mode 100644 docs/getting-started/deploying-feast/README.md create mode 100644 docs/getting-started/deploying-feast/docker-compose.md create mode 100644 docs/getting-started/deploying-feast/kubernetes.md create mode 100644 docs/reference/api.md create mode 100644 docs/reference/api/README.md create mode 100644 docs/reference/configuration-reference.md create mode 100644 docs/roadmap.md create mode 100644 docs/user-guide/architecture.md create mode 100644 docs/user-guide/data-ingestion.md create mode 100644 docs/user-guide/entities.md create mode 100644 docs/user-guide/feature-retrieval.md create mode 100644 docs/user-guide/feature-sets.md create mode 100644 docs/user-guide/features.md create mode 100644 docs/user-guide/overview.md create mode 100644 docs/user-guide/sources.md create mode 100644 docs/user-guide/stores.md create mode 100644 docs/why-feast.md diff --git a/docs/README.md b/docs/README.md index 63887a31489..d19b54960f8 100644 --- a/docs/README.md +++ b/docs/README.md @@ -1,4 +1,4 @@ -# What is Feast? +# Introduction Feast \(**Fea**ture **St**ore\) is a tool for managing and serving machine learning features. @@ -13,7 +13,7 @@ Feast aims to: ![](.gitbook/assets/feast-docs-overview-diagram-2%20%283%29.svg) -**TL;DR:** Feast decouples feature engineering from feature usage. Features that are added to Feast become available immediately for training and serving. Models can retrieve the same features used in training from a low latency online store in production. +Feast decouples feature engineering from feature usage. Features that are added to Feast become available immediately for training and serving. Models can retrieve the same features used in training from a low latency online store in production. This means that new ML projects start with a process of feature selection from a catalog instead of having to do feature engineering from scratch. diff --git a/docs/SUMMARY.md b/docs/SUMMARY.md index 345b49fe0d1..f6139c90212 100644 --- a/docs/SUMMARY.md +++ b/docs/SUMMARY.md @@ -1,27 +1,33 @@ # Table of contents -* [What is Feast?](README.md) - -## Introduction - -* [Why Feast?](introduction/why-feast.md) -* [Getting Help](introduction/getting-help.md) -* [Roadmap](introduction/roadmap.md) +* [Introduction](README.md) +* [Why Feast?](why-feast.md) +* [Getting Started](getting-started/README.md) + * [Deploying Feast](getting-started/deploying-feast/README.md) + * [Docker Compose](getting-started/deploying-feast/docker-compose.md) + * [Kubernetes \(GKE\)](getting-started/deploying-feast/kubernetes.md) + * [Connecting to Feast](getting-started/connecting-to-feast-1/README.md) + * [Python SDK](getting-started/connecting-to-feast-1/python-sdk.md) + * [Feast CLI](getting-started/connecting-to-feast-1/connecting-to-feast.md) +* [Roadmap](roadmap.md) * [Changelog](https://github.com/feast-dev/feast/blob/master/CHANGELOG.md) +* [Community](getting-help.md) -## Concepts - -* [Concepts](concepts/concepts.md) - -## Installation +## User Guide -* [Overview](installation/overview.md) -* [Docker Compose](installation/docker-compose.md) -* [Google Kubernetes Engine \(GKE\)](installation/gke.md) +* [Concepts](user-guide/overview.md) +* [Architecture](user-guide/architecture.md) +* [Feature Sets](user-guide/feature-sets.md) +* [Entities](user-guide/entities.md) +* [Features](user-guide/features.md) +* [Sources](user-guide/sources.md) +* [Data ingestion](user-guide/data-ingestion.md) +* [Stores](user-guide/stores.md) +* [Feature retrieval](user-guide/feature-retrieval.md) ## Tutorials -* [Basic](https://github.com/feast-dev/feast/blob/master/examples/basic/basic.ipynb) +* [Basic Tutorial](https://github.com/feast-dev/feast/blob/master/examples/basic/basic.ipynb) * [Churn Prediction \(XGBoost\)](https://github.com/feast-dev/feast/blob/master/examples/feast-xgboost-churn-prediction-tutorial/Telecom%20Customer%20Churn%20Prediction%20%28with%20Feast%20and%20XGBoost%29.ipynb) ## Administration @@ -30,11 +36,13 @@ ## Reference -* [Python SDK](https://api.docs.feast.dev/python/) -* [Go SDK](https://godoc.org/github.com/feast-dev/feast/sdk/go) -* [gRPC Types](https://api.docs.feast.dev/grpc/feast.types.pb.html) -* [Core gRPC API](https://api.docs.feast.dev/grpc/feast.core.pb.html) -* [Serving gRPC API](https://api.docs.feast.dev/grpc/feast.serving.pb.html) +* [Configuration Reference](reference/configuration-reference.md) +* [API](reference/api/README.md) + * [Core gRPC API](https://api.docs.feast.dev/grpc/feast.core.pb.html) + * [Serving gRPC API](https://api.docs.feast.dev/grpc/feast.serving.pb.html) + * [gRPC Types](https://api.docs.feast.dev/grpc/feast.types.pb.html) + * [Go SDK](https://godoc.org/github.com/feast-dev/feast/sdk/go) + * [Python SDK](https://api.docs.feast.dev/python/) ## Contributing @@ -42,4 +50,5 @@ * [Development Guide](contributing/development-guide.md) * [Style Guide](contributing/style-guide.md) * [Release Process](contributing/release-process.md) +* [Adding a New Store](contributing/adding-a-new-store.md) diff --git a/docs/assets/feast_logo.png b/docs/assets/feast_logo.png new file mode 100644 index 0000000000000000000000000000000000000000..cdd97770ed8cbc5b5bc67330ff43869f1de7193e GIT binary patch literal 81755 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 literal 0 HcmV?d00001 diff --git a/docs/contributing/adding-a-new-store-1.md b/docs/contributing/adding-a-new-store-1.md new file mode 100644 index 00000000000..56c06b9b4b6 --- /dev/null +++ b/docs/contributing/adding-a-new-store-1.md @@ -0,0 +1,87 @@ +# Adding a New Store + +The following guide will explain the process of adding a new store through the introduction of a storage connector. + +## 1. Storage API + +Feast has an external module where storage interfaces are defined: [Storage API](https://github.com/gojek/feast/tree/master/storage/api/src/main/java/feast/storage/api) + +Feast interacts with a store at three points . + +1. **During initialization:** Store configuration is loaded into memory by Feast Serving and synchronized with Feast Core +2. **During ingestion of feature data.** [writer interfaces](https://github.com/gojek/feast/tree/master/storage/api/src/main/java/feast/storage/api/writer) are used by the Apache Beam ingestion jobs in order to populate stores \(historical or online\). +3. **During retrieval of feature data:** [Retrieval interfaces](https://github.com/gojek/feast/tree/master/storage/api/src/main/java/feast/storage/api/retriever) are used by Feast Serving in order to read data from stores in order to create training datasets or to serve online data. + +All three of these components should be implemented in order to have a complete storage connector. + +## 2. Adding a Storage Connector + +### 2.1 Initialization and configuration + +Stores are configured in Feast Serving. Feast Serving publishes its store configuration to Feast Core, after which Feast Core can start ingestion/population jobs to populate it. + +Store configuration is always in the form of a map<String, String>. The keys and configuration for stores are defined in [protos](https://github.com/gojek/feast/blob/master/protos/feast/core/Store.proto). This must be added in order to define a new store + +Then the store must be configured to be loaded through Feast Serving. The above configuration is loaded through [FeastProperties.java](https://github.com/gojek/feast/blob/a1937c374a4e39b7a75d828e7b7c3b87a64d9d6e/serving/src/main/java/feast/serving/config/FeastProperties.java#L175). + +Once configuration is loaded, the store will then be instantiated. + +* Feast Core: The [StoreUtil.java](https://github.com/gojek/feast/blob/master/ingestion/src/main/java/feast/ingestion/utils/StoreUtil.java#L85) instantiates new stores for the purposes of feature ingestion. +* Feast Serving: The [ServingServiceConfig](https://github.com/gojek/feast/blob/a1937c374a4e39b7a75d828e7b7c3b87a64d9d6e/serving/src/main/java/feast/serving/config/ServingServiceConfig.java#L56) instantiates new stores for the purposes of retrieval + +{% hint style="info" %} +In the future we plan to provide a plugin interface for adding stores. +{% endhint %} + +### 2.2 Feature Ingestion \(Writer\) + +Feast creates and manages ingestion/population jobs that stream in data from upstream data sources. Currently Feast only supports Kafka as a data source, meaning these jobs are all long running. Batch ingestion \(from users\) results in data being pushed to Kafka topics after which they are picked up by these "population" jobs and written to stores. + +In order for ingestion to succeed, the destination store must be writable. This means that Feast must be able to create the appropriate tables/schemas in the store and also write data from the population job into the store. + +Currently Feast Core starts and manages these population jobs that ingest data into stores \(although we are planning to move this responsibility to the serving layer\). Feast Core starts an [Apache Beam](https://beam.apache.org/) job which synchronously runs migrations on the destination store and subsequently starts consuming [FeatureRows](https://github.com/gojek/feast/blob/master/protos/feast/types/FeatureRow.proto) from Kafka and writing it into stores using a [writer](https://github.com/gojek/feast/tree/master/storage/api/src/main/java/feast/storage/api/writer). + +Below is a "happy path" of a batch ingestion process which includes a blocking step at the Python SDK. + +![](https://user-images.githubusercontent.com/6728866/74807906-91e73c00-5324-11ea-8ba5-2b43c7c5282b.png) + + + +The complete ingestion flow is executed by a [FeatureSink](https://github.com/gojek/feast/blob/master/storage/api/src/main/java/feast/storage/api/writer/FeatureSink.java). Two methods should be implemented + +* [prepareWrite\(\)](https://github.com/gojek/feast/blob/a1937c374a4e39b7a75d828e7b7c3b87a64d9d6e/storage/api/src/main/java/feast/storage/api/writer/FeatureSink.java#L45): Sets up storage backend for writing/ingestion. This method will be called once during pipeline initialisation. Typically this is used to apply schemas. +* [writer\(\)](https://github.com/gojek/feast/blob/a1937c374a4e39b7a75d828e7b7c3b87a64d9d6e/storage/api/src/main/java/feast/storage/api/writer/FeatureSink.java#L53): Retrieves an Apache Beam PTransform that is used to write data to this store. + +### 2.2 Feature Serving \(Retriever\) + +Feast Serving can serve both historical/batch features and online features. Depending on the store that is being added, you should implement either a historical/batch store or an online storage. + +#### 2.2.1 Historical Serving + +The historical serving interface is defined through the [HistoricalRetriever](https://github.com/gojek/feast/blob/master/storage/api/src/main/java/feast/storage/api/retriever/HistoricalRetriever.java) interface. Historical retrieval is an asynchronous process. The client submits a request for a dataset to be produced, and polls until it is ready. + +![High-level flow for batch retrieval](https://user-images.githubusercontent.com/6728866/74797157-702a8c80-5305-11ea-8901-bf6f4eb075f9.png) + +The current implementation of batch retrieval starts and ends with a file \(dataset\) in a Google Cloud Storage bucket. The user ingests an entity dataset. This dataset is loaded into a store \(BigQuery0, joined to features in a point-in-time correct way, then exported again to the bucket. + +Additionally, we have also implemented a [batch retrieval method ](https://github.com/gojek/feast/blob/a1937c374a4e39b7a75d828e7b7c3b87a64d9d6e/sdk/python/feast/client.py#L509)in the Python SDK. Depending on the means through which this new store will export data, this client may have to change. At the very least it would change if Google Cloud Storage isn't used as the staging bucket. + +The means through which you implement the export/import of data into the store will depend on your store. + +#### 2.2.2 Online Serving + +In the case of online serving it is necessary to implement an [OnlineRetriever](https://github.com/gojek/feast/blob/master/storage/api/src/main/java/feast/storage/api/retriever/OnlineRetriever.java). This online retriever will read rows directly and synchronously from an online database. The exact encoding strategy you use to store your data in the store would be defined in the FeatureSink. The OnlineRetriever is expected to read and decode those rows. + +## 3. Storage Connectors Examples + +Feast currently provides support for the following storage types + +Historical storage + +* [BigQuery](https://github.com/gojek/feast/tree/master/storage/connectors/bigquery) + +Online storage + +* [Redis](https://github.com/gojek/feast/tree/master/storage/connectors/redis) +* [Redis Cluster](https://github.com/gojek/feast/tree/master/storage/connectors/rediscluster) + diff --git a/docs/contributing/adding-a-new-store.md b/docs/contributing/adding-a-new-store.md new file mode 100644 index 00000000000..56c06b9b4b6 --- /dev/null +++ b/docs/contributing/adding-a-new-store.md @@ -0,0 +1,87 @@ +# Adding a New Store + +The following guide will explain the process of adding a new store through the introduction of a storage connector. + +## 1. Storage API + +Feast has an external module where storage interfaces are defined: [Storage API](https://github.com/gojek/feast/tree/master/storage/api/src/main/java/feast/storage/api) + +Feast interacts with a store at three points . + +1. **During initialization:** Store configuration is loaded into memory by Feast Serving and synchronized with Feast Core +2. **During ingestion of feature data.** [writer interfaces](https://github.com/gojek/feast/tree/master/storage/api/src/main/java/feast/storage/api/writer) are used by the Apache Beam ingestion jobs in order to populate stores \(historical or online\). +3. **During retrieval of feature data:** [Retrieval interfaces](https://github.com/gojek/feast/tree/master/storage/api/src/main/java/feast/storage/api/retriever) are used by Feast Serving in order to read data from stores in order to create training datasets or to serve online data. + +All three of these components should be implemented in order to have a complete storage connector. + +## 2. Adding a Storage Connector + +### 2.1 Initialization and configuration + +Stores are configured in Feast Serving. Feast Serving publishes its store configuration to Feast Core, after which Feast Core can start ingestion/population jobs to populate it. + +Store configuration is always in the form of a map<String, String>. The keys and configuration for stores are defined in [protos](https://github.com/gojek/feast/blob/master/protos/feast/core/Store.proto). This must be added in order to define a new store + +Then the store must be configured to be loaded through Feast Serving. The above configuration is loaded through [FeastProperties.java](https://github.com/gojek/feast/blob/a1937c374a4e39b7a75d828e7b7c3b87a64d9d6e/serving/src/main/java/feast/serving/config/FeastProperties.java#L175). + +Once configuration is loaded, the store will then be instantiated. + +* Feast Core: The [StoreUtil.java](https://github.com/gojek/feast/blob/master/ingestion/src/main/java/feast/ingestion/utils/StoreUtil.java#L85) instantiates new stores for the purposes of feature ingestion. +* Feast Serving: The [ServingServiceConfig](https://github.com/gojek/feast/blob/a1937c374a4e39b7a75d828e7b7c3b87a64d9d6e/serving/src/main/java/feast/serving/config/ServingServiceConfig.java#L56) instantiates new stores for the purposes of retrieval + +{% hint style="info" %} +In the future we plan to provide a plugin interface for adding stores. +{% endhint %} + +### 2.2 Feature Ingestion \(Writer\) + +Feast creates and manages ingestion/population jobs that stream in data from upstream data sources. Currently Feast only supports Kafka as a data source, meaning these jobs are all long running. Batch ingestion \(from users\) results in data being pushed to Kafka topics after which they are picked up by these "population" jobs and written to stores. + +In order for ingestion to succeed, the destination store must be writable. This means that Feast must be able to create the appropriate tables/schemas in the store and also write data from the population job into the store. + +Currently Feast Core starts and manages these population jobs that ingest data into stores \(although we are planning to move this responsibility to the serving layer\). Feast Core starts an [Apache Beam](https://beam.apache.org/) job which synchronously runs migrations on the destination store and subsequently starts consuming [FeatureRows](https://github.com/gojek/feast/blob/master/protos/feast/types/FeatureRow.proto) from Kafka and writing it into stores using a [writer](https://github.com/gojek/feast/tree/master/storage/api/src/main/java/feast/storage/api/writer). + +Below is a "happy path" of a batch ingestion process which includes a blocking step at the Python SDK. + +![](https://user-images.githubusercontent.com/6728866/74807906-91e73c00-5324-11ea-8ba5-2b43c7c5282b.png) + + + +The complete ingestion flow is executed by a [FeatureSink](https://github.com/gojek/feast/blob/master/storage/api/src/main/java/feast/storage/api/writer/FeatureSink.java). Two methods should be implemented + +* [prepareWrite\(\)](https://github.com/gojek/feast/blob/a1937c374a4e39b7a75d828e7b7c3b87a64d9d6e/storage/api/src/main/java/feast/storage/api/writer/FeatureSink.java#L45): Sets up storage backend for writing/ingestion. This method will be called once during pipeline initialisation. Typically this is used to apply schemas. +* [writer\(\)](https://github.com/gojek/feast/blob/a1937c374a4e39b7a75d828e7b7c3b87a64d9d6e/storage/api/src/main/java/feast/storage/api/writer/FeatureSink.java#L53): Retrieves an Apache Beam PTransform that is used to write data to this store. + +### 2.2 Feature Serving \(Retriever\) + +Feast Serving can serve both historical/batch features and online features. Depending on the store that is being added, you should implement either a historical/batch store or an online storage. + +#### 2.2.1 Historical Serving + +The historical serving interface is defined through the [HistoricalRetriever](https://github.com/gojek/feast/blob/master/storage/api/src/main/java/feast/storage/api/retriever/HistoricalRetriever.java) interface. Historical retrieval is an asynchronous process. The client submits a request for a dataset to be produced, and polls until it is ready. + +![High-level flow for batch retrieval](https://user-images.githubusercontent.com/6728866/74797157-702a8c80-5305-11ea-8901-bf6f4eb075f9.png) + +The current implementation of batch retrieval starts and ends with a file \(dataset\) in a Google Cloud Storage bucket. The user ingests an entity dataset. This dataset is loaded into a store \(BigQuery0, joined to features in a point-in-time correct way, then exported again to the bucket. + +Additionally, we have also implemented a [batch retrieval method ](https://github.com/gojek/feast/blob/a1937c374a4e39b7a75d828e7b7c3b87a64d9d6e/sdk/python/feast/client.py#L509)in the Python SDK. Depending on the means through which this new store will export data, this client may have to change. At the very least it would change if Google Cloud Storage isn't used as the staging bucket. + +The means through which you implement the export/import of data into the store will depend on your store. + +#### 2.2.2 Online Serving + +In the case of online serving it is necessary to implement an [OnlineRetriever](https://github.com/gojek/feast/blob/master/storage/api/src/main/java/feast/storage/api/retriever/OnlineRetriever.java). This online retriever will read rows directly and synchronously from an online database. The exact encoding strategy you use to store your data in the store would be defined in the FeatureSink. The OnlineRetriever is expected to read and decode those rows. + +## 3. Storage Connectors Examples + +Feast currently provides support for the following storage types + +Historical storage + +* [BigQuery](https://github.com/gojek/feast/tree/master/storage/connectors/bigquery) + +Online storage + +* [Redis](https://github.com/gojek/feast/tree/master/storage/connectors/redis) +* [Redis Cluster](https://github.com/gojek/feast/tree/master/storage/connectors/rediscluster) + diff --git a/docs/contributing/contributing.md b/docs/contributing/contributing.md index dcb9b3ad0e7..71d53070465 100644 --- a/docs/contributing/contributing.md +++ b/docs/contributing/contributing.md @@ -1,12 +1,10 @@ # Contribution Process -## 1. Contribution process +We use [RFCs](https://en.wikipedia.org/wiki/Request_for_Comments) and [GitHub issues](https://github.com/gojek/feast/issues) to communicate development ideas. The simplest way to contribute to Feast is to leave comments in our [RFCs](https://drive.google.com/drive/u/0/folders/1Lj1nIeRB868oZvKTPLYqAvKQ4O0BksjY) in the [Feast Google Drive](https://drive.google.com/drive/u/0/folders/0AAe8j7ZK3sxSUk9PVA) or our GitHub issues. You will need to join our [Google Group](../getting-help.md) in order to get access. -We use [RFCs](https://en.wikipedia.org/wiki/Request_for_Comments) and [GitHub issues](https://github.com/feast-dev/feast/issues) to communicate development ideas. The simplest way to contribute to Feast is to leave comments in our [RFCs](https://drive.google.com/drive/u/0/folders/1Lj1nIeRB868oZvKTPLYqAvKQ4O0BksjY) in the [Feast Google Drive](https://drive.google.com/drive/u/0/folders/0AAe8j7ZK3sxSUk9PVA) or our GitHub issues. +We follow a process of [lazy consensus](http://community.apache.org/committers/lazyConsensus.html). If you believe you know what the project needs then just start development. If you are unsure about which direction to take with development then please communicate your ideas through a GitHub issue or through our [Slack Channel](../getting-help.md) before starting development. -Please communicate your ideas through a GitHub issue or through our Slack Channel before starting development. - -Please [submit a PR ](https://github.com/feast-dev/feast/pulls)to the master branch of the Feast repository once you are ready to submit your contribution. Code submission to Feast \(including submission from project maintainers\) require review and approval from maintainers or code owners. +Please [submit a PR ](https://github.com/gojek/feast/pulls)to the master branch of the Feast repository once you are ready to submit your contribution. Code submission to Feast \(including submission from project maintainers\) require review and approval from maintainers or code owners. PRs that are submitted by the general public need to be identified as `ok-to-test`. Once enabled, [Prow](https://github.com/kubernetes/test-infra/tree/master/prow) will run a range of tests to verify the submission, after which community members will help to review the pull request. @@ -14,515 +12,4 @@ PRs that are submitted by the general public need to be identified as `ok-to-tes Please sign the [Google CLA](https://cla.developers.google.com/) in order to have your code merged into the Feast repository. {% endhint %} -## 2. Development guide - -### 2.1 Overview - -The following guide will help you quickly run Feast in your local machine. - -The main components of Feast are: - -* **Feast Core:** Handles feature registration, starts and manages ingestion jobs and ensures that Feast internal metadata is consistent. -* **Feast Ingestion Jobs:** Subscribes to streams of FeatureRows and writes these as feature - - values to registered databases \(online, historical\) that can be read by Feast Serving. - -* **Feast Serving:** Service that handles requests for features values, either online or batch. - -### 2.**2 Requirements** - -#### 2.**2.1 Development environment** - -The following software is required for Feast development - -* Java SE Development Kit 11 -* Python version 3.6 \(or above\) and pip -* [Maven ](https://maven.apache.org/install.html)version 3.6.x - -Additionally, [grpc\_cli](https://github.com/grpc/grpc/blob/master/doc/command_line_tool.md) is useful for debugging and quick testing of gRPC endpoints. - -#### 2.**2.2 Services** - -The following components/services are required to develop Feast: - -* **Feast Core:** Requires PostgreSQL \(version 11 and above\) to store state, and requires a Kafka \(tested on version 2.x\) setup to allow for ingestion of FeatureRows. -* **Feast Serving:** Requires Redis \(tested on version 5.x\). - -These services should be running before starting development. The following snippet will start the services using Docker. - -```bash -# Start Postgres -docker run --name postgres --rm -it -d --net host -e POSTGRES_DB=postgres -e POSTGRES_USER=postgres \ --e POSTGRES_PASSWORD=password postgres:12-alpine - -# Start Redis -docker run --name redis --rm -it --net host -d redis:5-alpine - -# Start Zookeeper (needed by Kafka) -docker run --rm \ - --net=host \ - --name=zookeeper \ - --env=ZOOKEEPER_CLIENT_PORT=2181 \ - --detach confluentinc/cp-zookeeper:5.2.1 - -# Start Kafka -docker run --rm \ - --net=host \ - --name=kafka \ - --env=KAFKA_ZOOKEEPER_CONNECT=localhost:2181 \ - --env=KAFKA_ADVERTISED_LISTENERS=PLAINTEXT://localhost:9092 \ - --env=KAFKA_OFFSETS_TOPIC_REPLICATION_FACTOR=1 \ - --detach confluentinc/cp-kafka:5.2.1 -``` - -### 2.3 Testing and development - -#### 2.3.1 Running unit tests - -```text -$ mvn test -``` - -#### 2.3.2 Running integration tests - -_Note: integration suite isn't yet separated from unit._ - -```text -$ mvn verify -``` - -#### 2.3.3 Running components locally - -The `core` and `serving` modules are Spring Boot applications. These may be run as usual for [the Spring Boot Maven plugin](https://docs.spring.io/spring-boot/docs/current/maven-plugin/index.html): - -```text -$ mvn --projects core spring-boot:run - -# Or for short: -$ mvn -pl core spring-boot:run -``` - -Note that you should execute `mvn` from the Feast repository root directory, as there are intermodule dependencies that Maven will not resolve if you `cd` to subdirectories to run. - -#### 2.3.4 Running from IntelliJ - -Compiling and running tests in IntelliJ should work as usual. - -Running the Spring Boot apps may work out of the box in IDEA Ultimate, which has built-in support for Spring Boot projects, but the Community Edition needs a bit of help: - -The Spring Boot Maven plugin automatically puts dependencies with `provided` scope on the runtime classpath when using `spring-boot:run`, such as its embedded Tomcat server. The "Play" buttons in the gutter or right-click menu of a `main()` method [do not do this](https://stackoverflow.com/questions/30237768/run-spring-boots-main-using-ide). - -A solution to this is: - -1. Open `View > Tool Windows > Maven` -2. Drill down to e.g. `Feast Core > Plugins > spring-boot:run`, right-click and `Create 'feast-core [spring-boot'…` -3. In the dialog that pops up, check the `Resolve Workspace artifacts` box -4. Recommended: add `-Dspring-boot.run.fork=false` to the `Command line` field to get Debug working too -5. Click `OK`. You should now be able to select this run configuration for the Play button in the main toolbar, keyboard shortcuts, etc. - -It is recommend to have IntelliJ delegate building to Maven, if this is not enabled out of the box when you import the project, for greater assurance that build behavior is consistent with CI / production builds. This is set in Preferences at `Build, Execution, Deployment > Build Tools > Maven > Runner > Delegate IDE build/run actions to Maven`. - -### 2.**4** Validating your setup - -The following section is a quick walk-through to test whether your local Feast deployment is functional for development purposes. - -**2.4.1 Assumptions** - -* PostgreSQL is running in `localhost:5432` and has a database called `postgres` which - - can be accessed with credentials user `postgres` and password `password`. Different database configurations can be supplied here \(`/core/src/main/resources/application.yml`\) - -* Redis is running locally and accessible from `localhost:6379` -* \(optional\) The local environment has been authentication with Google Cloud Platform and has full access to BigQuery. This is only necessary for BigQuery testing/development. - -#### 2.4.2 Clone Feast - -```bash -git clone https://github.com/feast-dev/feast.git && cd feast && \ -export FEAST_HOME_DIR=$(pwd) -``` - -#### 2.4.3 Starting Feast Core - -To run Feast Core locally using Maven: - -```bash -# Feast Core can be configured from the following .yml file -# $FEAST_HOME_DIR/core/src/main/resources/application.yml -mvn --projects core spring-boot:run -``` - -Test whether Feast Core is running - -```text -grpc_cli call localhost:6565 ListStores '' -``` - -The output should list **no** stores since no Feast Serving has registered its stores to Feast Core: - -```text -connecting to localhost:6565 - -Rpc succeeded with OK status -``` - -#### 2.4.4 Starting Feast Serving - -Feast Serving is configured through the `$FEAST_HOME_DIR/serving/src/main/resources/application.yml`. Each Serving deployment must be configured with a store. The default store is Redis \(used for online serving\). - -The configuration for this default store is located in a separate `.yml` file. The default location is `$FEAST_HOME_DIR/serving/sample_redis_config.yml`: - -```text -name: serving -type: REDIS -redis_config: - host: localhost - port: 6379 -subscriptions: - - name: "*" - project: "*" - version: "*" -``` - -Once Feast Serving is started, it will register its store with Feast Core \(by name\) and start to subscribe to a feature sets based on its subscription. - -Start Feast Serving GRPC server on localhost:6566 with store name `serving` - -```text -mvn --projects serving spring-boot:run -``` - -Test connectivity to Feast Serving - -```text -grpc_cli call localhost:6566 GetFeastServingInfo '' -``` - -```text -connecting to localhost:6566 -version: "0.4.2-SNAPSHOT" -type: FEAST_SERVING_TYPE_ONLINE - -Rpc succeeded with OK status -``` - -Test Feast Core to see whether it is aware of the Feast Serving deployment - -```text -grpc_cli call localhost:6565 ListStores '' -``` - -```text -connecting to localhost:6565 -store { - name: "serving" - type: REDIS - subscriptions { - name: "*" - version: "*" - project: "*" - } - redis_config { - host: "localhost" - port: 6379 - } -} - -Rpc succeeded with OK status -``` - -In order to use BigQuery as a historical store, it is necessary to start Feast Serving with a different store type. - -Copy `$FEAST_HOME_DIR/serving/sample_redis_config.yml` to the following location `$FEAST_HOME_DIR/serving/my_bigquery_config.yml` and update the configuration as below: - -```text -name: bigquery -type: BIGQUERY -bigquery_config: - project_id: YOUR_GCP_PROJECT_ID - dataset_id: YOUR_GCP_DATASET -subscriptions: - - name: "*" - version: "*" - project: "*" -``` - -Then inside `serving/src/main/resources/application.yml` modify the following key `feast.store.config-path` to point to the new store configuration. - -After making these changes, restart Feast Serving: - -```text -mvn --projects serving spring-boot:run -``` - -You should see two stores registered: - -```text -store { - name: "serving" - type: REDIS - subscriptions { - name: "*" - version: "*" - project: "*" - } - redis_config { - host: "localhost" - port: 6379 - } -} -store { - name: "bigquery" - type: BIGQUERY - subscriptions { - name: "*" - version: "*" - project: "*" - } - bigquery_config { - project_id: "my_project" - dataset_id: "my_bq_dataset" - } -} -``` - -#### 2.4.5 Registering a FeatureSet - -Before registering a new FeatureSet, a project is required. - -```text -grpc_cli call localhost:6565 CreateProject ' - name: "your_project_name" -' -``` - -When a feature set is successfully registered, Feast Core will start an **ingestion** job that listens for new features in the feature set. - -{% hint style="info" %} -Note that Feast currently only supports source of type `KAFKA`, so you must have access to a running Kafka broker to register a FeatureSet successfully. It is possible to omit the `source` from a Feature Set, but Feast Core will still use Kafka behind the scenes, it is simply abstracted away from the user. -{% endhint %} - -Create a new FeatureSet in Feast by sending a request to Feast Core: - -```text -# Example of registering a new driver feature set -# Note the source value, it assumes that you have access to a Kafka broker -# running on localhost:9092 - -grpc_cli call localhost:6565 ApplyFeatureSet ' -feature_set { - spec { - project: "your_project_name" - name: "driver" - version: 1 - - entities { - name: "driver_id" - value_type: INT64 - } - - features { - name: "city" - value_type: STRING - } - - source { - type: KAFKA - kafka_source_config { - bootstrap_servers: "localhost:9092" - topic: "your-kafka-topic" - } - } - } -} -' -``` - -Verify that the FeatureSet has been registered correctly. - -```text -# To check that the FeatureSet has been registered correctly. -# You should also see logs from Feast Core of the ingestion job being started -grpc_cli call localhost:6565 GetFeatureSet ' - project: "your_project_name" - name: "driver" -' -``` - -Or alternatively, list all feature sets - -```text -grpc_cli call localhost:6565 ListFeatureSets ' - filter { - project: "your_project_name" - feature_set_name: "driver" - feature_set_version: "1" - } -' -``` - -#### 2.4.6 Ingestion and Population of Feature Values - -```text -# Produce FeatureRow messages to Kafka so it will be ingested by Feast -# and written to the registered stores. -# Make sure the value here is the topic assigned to the feature set -# ... producer.send("feast-driver-features" ...) -# -# Install Python SDK to help writing FeatureRow messages to Kafka -cd $FEAST_HOMEDIR/sdk/python -pip3 install -e . -pip3 install pendulum - -# Produce FeatureRow messages to Kafka so it will be ingested by Feast -# and written to the corresponding store. -# Make sure the value here is the topic assigned to the feature set -# ... producer.send("feast-test_feature_set-features" ...) -python3 - < + +This guide will install Feast into a Kubernetes cluster on GCP. It assumes that all of your services will run within a single Kubernetes cluster. Once Feast is installed you will be able to: + +* Define and register features. +* Load feature data from both batch and streaming sources. +* Retrieve features for model training. +* Retrieve features for online serving. + +{% hint style="info" %} +This guide requires [Google Cloud Platform](https://cloud.google.com/) for installation. + +* [BigQuery](https://cloud.google.com/bigquery/) is used for storing historical features. +* [Google Cloud Storage](https://cloud.google.com/storage/) is used for intermediate data storage. +{% endhint %} + +## 0. Requirements + +1. [Google Cloud SDK ](https://cloud.google.com/sdk/install)installed, authenticated, and configured to the project you will use. +2. [Kubectl](https://kubernetes.io/docs/tasks/tools/install-kubectl/) installed. +3. [Helm](https://helm.sh/3) \(2.16.0 or greater\) installed on your local machine with Tiller installed in your cluster. Helm 3 has not been tested yet. + +## 1. Set up GCP + +First define the environmental variables that we will use throughout this installation. Please customize these to reflect your environment. + +```bash +export FEAST_GCP_PROJECT_ID=my-gcp-project +export FEAST_GCP_REGION=us-central1 +export FEAST_GCP_ZONE=us-central1-a +export FEAST_BIGQUERY_DATASET_ID=feast +export FEAST_GCS_BUCKET=${FEAST_GCP_PROJECT_ID}_feast_bucket +export FEAST_GKE_CLUSTER_NAME=feast +export FEAST_SERVICE_ACCOUNT_NAME=feast-sa +``` + +Create a Google Cloud Storage bucket for Feast to stage batch data exports: + +```bash +gsutil mb gs://${FEAST_GCS_BUCKET} +``` + +Create the service account that Feast will run as: + +```bash +gcloud iam service-accounts create ${FEAST_SERVICE_ACCOUNT_NAME} + +gcloud projects add-iam-policy-binding ${FEAST_GCP_PROJECT_ID} \ + --member serviceAccount:${FEAST_SERVICE_ACCOUNT_NAME}@${FEAST_GCP_PROJECT_ID}.iam.gserviceaccount.com \ + --role roles/editor + +gcloud iam service-accounts keys create key.json --iam-account \ +${FEAST_SERVICE_ACCOUNT_NAME}@${FEAST_GCP_PROJECT_ID}.iam.gserviceaccount.com +``` + +## 2. Set up a Kubernetes \(GKE\) cluster + +{% hint style="warning" %} +Provisioning a GKE cluster can expose your services publicly. This guide does not cover securing access to the cluster. +{% endhint %} + +Create a GKE cluster: + +```bash +gcloud container clusters create ${FEAST_GKE_CLUSTER_NAME} \ + --machine-type n1-standard-4 +``` + +Create a secret in the GKE cluster based on your local key `key.json`: + +```bash +kubectl create secret generic feast-gcp-service-account --from-file=key.json +``` + +For this guide we will use `NodePort` for exposing Feast services. In order to do so, we must find an External IP of at least one GKE node. This should be a public IP. + +```bash +export FEAST_IP=$(kubectl describe nodes | grep ExternalIP | awk '{print $2}' | head -n 1) +export FEAST_CORE_URL=${FEAST_IP}:32090 +export FEAST_ONLINE_SERVING_URL=${FEAST_IP}:32091 +export FEAST_BATCH_SERVING_URL=${FEAST_IP}:32092 +``` + +Add firewall rules to open up ports on your Google Cloud Platform project: + +```bash +gcloud compute firewall-rules create feast-core-port --allow tcp:32090 +gcloud compute firewall-rules create feast-online-port --allow tcp:32091 +gcloud compute firewall-rules create feast-batch-port --allow tcp:32092 +gcloud compute firewall-rules create feast-redis-port --allow tcp:32101 +gcloud compute firewall-rules create feast-kafka-ports --allow tcp:31090-31095 +``` + +## 3. Set up Helm + +Run the following command to provide Tiller with authorization to install Feast: + +```bash +kubectl apply -f - < + + + Option + Description + Default + + + + + FEAST_CONFIG + + Location of Feast configuration file + /.feast/config + + + + CONFIG_FEAST_ENV_VAR_PREFIX + + +

Default prefix to Feast environmental variable options.

+

Does not apply to FEAST_CONFIG +

+ + FEAST_ + + + + PROJECT + + Default Feast project to use + default + + + + CORE_URL + + URL used to connect to Feast Core + localhost:6565 + + + + CORE_ENABLE_SSL + + Enables TLS/SSL on connections to Feast Core + False + + + + CORE_AUTH_ENABLED + + Enable user authentication when connecting to a Feast Core instance + False + + + + CORE_AUTH_TOKEN + + Provide a static JWT token to authenticate with Feast Core + Null + + + + CORE_SERVER_SSL_CERT + + Path to certificate(s) used by Feast Client to authenticate TLS connection + to Feast Core (not to authenticate you as a client). + Null + + + + SERVING_URL + + URL used to connect to Feast Serving + localhost:6566 + + + + SERVING_ENABLE_SSL + + Enables TLS/SSL on connections to Feast Serving + False + + + + SERVING_SERVER_SSL_CERT + + Path to certificate(s) used by Feast Client to authenticate TLS connection + to Feast Serving (not to authenticate you as a client). + None + + + + GRPC_CONNECTION_TIMEOUT_DEFAULT + + Default gRPC connection timeout to both Feast Serving and Feast Core (in + seconds) + 3 + + + + GRPC_CONNECTION_TIMEOUT_APPLY + + Default gRPC connection timeout when sending an ApplyFeatureSet command + to Feast Core (in seconds) + 600 + + + + BATCH_FEATURE_REQUEST_WAIT_S + + Time to wait for batch feature requests before timing out. + 600 + + + +### Usage + +#### Configuration File + +Feast Configuration File \(`~/.feast/config`\) + +```text +[general] +project = default +core_url = localhost:6565 +``` + +#### Environmental Variables + +```bash +FEAST_CORE_URL=my_feast:6565 FEAST_PROJECT=default feast projects list +``` + +#### Feast SDK + +```python +client = Client( + core_url="localhost:6565", + project="default" +) +``` + diff --git a/docs/roadmap.md b/docs/roadmap.md new file mode 100644 index 00000000000..a047cf41256 --- /dev/null +++ b/docs/roadmap.md @@ -0,0 +1,31 @@ +# Roadmap + +## Feast 0.5 \(Technical Release\) + +[Discussion](https://github.com/gojek/feast/issues/527) + +### New functionality + +1. Streaming statistics and validation \(M1 from [Feature Validation RFC](https://docs.google.com/document/d/1TPmd7r4mniL9Y-V_glZaWNo5LMXLshEAUpYsohojZ-8/edit)\) +2. Support for Redis Clusters \([\#478](https://github.com/gojek/feast/issues/478), [\#502](https://github.com/gojek/feast/issues/502)\) +3. Add feature and feature set labels, i.e. key/value registry metadata \([\#463](https://github.com/gojek/feast/issues/463)\) +4. Job management API \([\#302](https://github.com/gojek/feast/issues/302)\) + +### Technical debt, refactoring, or housekeeping + +1. Clean up and document all configuration options \([\#525](https://github.com/gojek/feast/issues/525)\) +2. Externalize storage interfaces \([\#402](https://github.com/gojek/feast/issues/402)\) +3. Reduce memory usage in Redis \([\#515](https://github.com/gojek/feast/issues/515)\) +4. Support for handling out of order ingestion \([\#273](https://github.com/gojek/feast/issues/273)\) +5. Remove feature versions and enable automatic data migration \([\#386](https://github.com/gojek/feast/issues/386)\) \([\#462](https://github.com/gojek/feast/issues/462)\) +6. Tracking of batch ingestion by with dataset\_id/job\_id \([\#461](https://github.com/gojek/feast/issues/461)\) +7. Write Beam metrics after ingestion to store \(not prior\) \([\#489](https://github.com/gojek/feast/issues/489)\) + +## Feast 0.6 \(Feature Release\) + +### New functionality + +1. User authentication & authorization \([\#504](https://github.com/gojek/feast/issues/504)\) +2. Batch statistics and validation \(M2 from [Feature Validation RFC](https://docs.google.com/document/d/1TPmd7r4mniL9Y-V_glZaWNo5LMXLshEAUpYsohojZ-8/edit)\) +3. Online feature/entity status metadata \([\#658](https://github.com/gojek/feast/pull/658)\) + diff --git a/docs/user-guide/architecture.md b/docs/user-guide/architecture.md new file mode 100644 index 00000000000..5de6fe66eba --- /dev/null +++ b/docs/user-guide/architecture.md @@ -0,0 +1,41 @@ +# Architecture + +![Feast high-level flow](../.gitbook/assets/blank-diagram-4.svg) + +### **Feast Core** + +Feast Core is the central management service of a Feast deployment. It's role is to: + +* Allows users to create [entities](entities.md) and [features](features.md) through the creation and management of [feature sets](feature-sets.md). +* Starts and manages [ingestion jobs](data-ingestion.md). These jobs populate [stores](stores.md) from [sources](sources.md) based on the feature sets that are defined and the subscription\(s\) that a [store](stores.md) has. + +{% hint style="info" %} +Job management may move out of Feast Core to Feast Serving in the future. +{% endhint %} + +### **Feast Ingestion** + +Before a user ingests data into Feast, they should register one or more feature sets. These [feature sets](feature-sets.md) tell Feast where to find their data, how to ingest it, and also describe the characteristics of the data for validation purposes. Once a feature set is registered, Feast will start an Apache Beam job in order to populate a store with data from a source. + +In order for stores to be populated with data, users must publish the data to a [source](sources.md). Currently Feast only supports Apache Kafka as a source. Feast users \(or pipelines\) ingest batch data through the [Feast SDK](../getting-started/connecting-to-feast-1/connecting-to-feast.md) using its `ingest()` method. The SDK publishes the data straight to Kafka. + +Streaming systems can also ingest data into Feast. This is done by publishing to the correct Kafka topic in the expected format. Feast expects data to be in [FeatureRow.proto](https://api.docs.feast.dev/grpc/feast.types.pb.html#FeatureRow) format. The topic and brokers can be found on the feature set schema using the [Python SDK](../getting-started/connecting-to-feast-1/python-sdk.md). + +### **Stores** + +Stores are nothing more than databases used to store feature data. Feast loads data into stores through an ingestion process, after which the data can be served through the Feast Serving API. Stores are documented in the following section. + +{% page-ref page="stores.md" %} + +### **Feast Serving** + + `Feast Serving` is the data access layer through which end users and production systems retrieve feature data. Each `Serving` instance is backed by a [store](stores.md). + +Since Feast supports multiple store types \(online, historical\) it is common to have two instances of Feast Serving deployed, one for online serving and one for historical serving. However, Feast allows for any number of `Feast Serving` deployments, meaning it is possible to deploy a `Feast Serving` deployment per production system, with its own stores and population jobs. + +`Serving` deployments can subscribe to a subset of feature data. Meaning they do not have to consume all features known to a `Feast Core` deployment. + +Feature retrieval \(and feature references\) are documented in more detail in subsequent sections + +{% page-ref page="feature-retrieval.md" %} + diff --git a/docs/user-guide/data-ingestion.md b/docs/user-guide/data-ingestion.md new file mode 100644 index 00000000000..464a51bbfd4 --- /dev/null +++ b/docs/user-guide/data-ingestion.md @@ -0,0 +1,126 @@ +# Data ingestion + +Users don't necessarily have to provide their external data sources in order to load data into Feast. Feast also allows users to exclude the source when registering a feature set. The following is a valid feature set specification. + +```python +feature_set = FeatureSet( + name="stream_feature", + entities=[Entity("entity_id", ValueType.INT64)], + features=[Feature("feature_value1", ValueType.STRING)], +) +``` + +If a user does not provide a source of data then they are required to publish data to Feast. This process is called ingestion. + +### Ingesting data + +The following example demonstrates how data ingestion works. For a full tutorial please see the [Telco Customer Churn Prediction Notebook](https://github.com/gojek/feast/blob/master/examples/feast-xgboost-churn-prediction-tutorial/Telecom%20Customer%20Churn%20Prediction%20%28with%20Feast%20and%20XGBoost%29.ipynb). + +1. Connect to Feast Core and load in a Pandas DataFrame. + +```python +from feast import FeatureSet, Client, Entity +import pandas as pd + +# Connect to Feast core +client = Client(core_url="feast-core.example.com") + +# Load in customer data +df = pd.read_csv("customer.csv") +``` + +1. Create, infer, and register a feature set from the DataFrame. This is a once off step that is required to initially register a feature set with Feast + +```python +# Create an empty feature set +customer_churn_fs = FeatureSet("customer_churn") + +# Infer the schema of the feature set from the Pandas DataFrame +customer_churn_fs.infer_fields_from_df( + df, + entities=[Entity(name='customer_id', + dtype=ValueType.STRING)] + ) + +# Register the feature set with Feast +client.apply(customer_churn_fs) +``` + +1. We can also test that the feature set was correctly registered with Feast by retrieving it again and printing it out + +```text +customer_churn_fs = client.get_feature_set('customer_churn') +print(client.get_feature_set('customer_churn')) +``` + +```yaml +{ + "spec": { + "name": "customer_churn", + "entities": [ + { + "name": "customer_id", + "valueType": "STRING" + } + ], + "features": [ + { + "name": "churn", + "valueType": "INT64" + }, + { + "name": "contract_month_to_month", + "valueType": "INT64" + }, + { + "name": "streamingmovies", + "valueType": "INT64" + }, + { + "name": "paperlessbilling", + "valueType": "INT64" + }, + { + "name": "contract_two_year", + "valueType": "INT64" + }, + { + "name": "partner", + "valueType": "INT64" + } + ], + "maxAge": "0s", + "source": { + "type": "KAFKA", + "kafkaSourceConfig": { + "bootstrapServers": "10.202.250.99:31190", + "topic": "feast" + } + }, + "project": "default" + }, + "meta": { + "createdTimestamp": "2020-03-15T07:47:52Z", + "status": "STATUS_READY" + } +} +``` + +Once we are happy that the schema is correct, we can start to ingest the DataFrame into Feast. + +```text +client.ingest(customer_churn_fs, telcom) +``` + +```text +100%|██████████| 7032/7032 [00:02<00:00, 2771.19rows/s] +Ingestion complete! + +Ingestion statistics: +Success: 7032/7032A rows ingested +``` + +{% hint style="warning" %} +Feast ingestion maintains the order of data that is ingested. This means that data that is written later will replace those that are written prior in stores. This is important to note when ingesting data that will end in a production system. +{% endhint %} + diff --git a/docs/user-guide/entities.md b/docs/user-guide/entities.md new file mode 100644 index 00000000000..f2383e440cb --- /dev/null +++ b/docs/user-guide/entities.md @@ -0,0 +1,43 @@ +# Entities + +An entity is any domain object that can be modeled and that information can be stored about. Entities are usually recognizable concepts, either concrete or abstract, such as persons, places, things, or events which have relevance to the modeled system. + +More formally, an entity is an instance of an entity type. An entity type is the class of entities where entities are the instances. + +* Examples of entity types in the context of ride-hailing and food delivery: `customer`, `order`, `driver`, `restaurant`, `dish`, `area`. +* A specific driver, for example a driver with ID `D011234` would be an entity of the entity type `driver` + +An entity is the object on which features are observed. For example we could have a feature `total_trips_24h` on the driver `D01123` with a feature value of `11`. + +In the context of Feast, entities are important because they are used as keys when looking up feature values. Entities are also used when joining feature values between different feature sets in order to build one large data set to train a model, or to serve a model. + +Example entity properties + +{% code title="customer\_id.yaml" %} +```yaml +# Entity name +name: customer_id + +# Entity value type +value_type: INT64 + +``` +{% endcode %} + +Entities can be created through the [Feast SDK](../getting-started/connecting-to-feast-1/connecting-to-feast.md) as follows + +```python +from feast import Entity, ValueType, FeatureSet + +# Create a customer entity +customer = Entity("customer_id", ValueType.INT64) + +# Create a feature set with only a single entity +customer_feature_set = FeatureSet("customer_fs", entities=[customer]) + +# Register the feature set with Feast +client.apply(customer_feature_set) +``` + +Please see the [EntitySpec](https://api.docs.feast.dev/grpc/feast.core.pb.html#EntitySpec) for the entity specification API. + diff --git a/docs/user-guide/feature-retrieval.md b/docs/user-guide/feature-retrieval.md new file mode 100644 index 00000000000..e49db24fe99 --- /dev/null +++ b/docs/user-guide/feature-retrieval.md @@ -0,0 +1,116 @@ +# Feature retrieval + +## 1. Overview + +Feature retrieval \(or serving\) is the process of retrieving either historical features or online features from Feast, for the purposes of training or serving a model. + +Feast attempts to unify the process of retrieving features in both the historical and online case. It does this through the creation of feature references. One of the major advantages of using Feast is that you have a single semantic reference to a feature. These feature references can then be stored alongside your model and loaded into a serving layer where it can be used for online feature retrieval. + +## 2. Feature references + +In Feast, each feature can be uniquely addressed through a feature reference. A feature reference is composed of the following components + +* Feature Set +* Feature + +These components can be used to create a string based feature reference as follows + +`:` + +Feast will attempt to infer both the `feature-set` name if it is not provided, but a feature reference must provide a `feature` name. + +```python +# Feature references +features = [ + 'partner', + 'daily_transactions', + 'customer_feature_set:dependents', + 'customer_feature_set:has_phone_service', + ] + +target = 'churn' +``` + +Feature references only apply to a single `project`. Features cannot be retrieved across projects in a single request. + +## 3. Historical feature retrieval + +Historical feature retrieval can be done through either the Feast SDK or directly through the Feast Serving gRPC API. Below is an example of historical retrieval from the [Churn Prediction Notebook](https://github.com/gojek/feast/blob/master/examples/feast-xgboost-churn-prediction-tutorial/Telecom%20Customer%20Churn%20Prediction%20%28with%20Feast%20and%20XGBoost%29.ipynb). + +```python +# Add the target variable to our feature list +features = self._features + [self._target] + +# Retrieve training dataset from Feast. The "entity_df" is a dataframe that contains +# timestamps and entity keys. In this case, it is a dataframe with two columns. +# One timestamp column, and one customer id column +dataset = client.get_batch_features( + feature_refs=features, + entity_rows=entity_df + ) + +# Materialize the dataset object to a Pandas DataFrame. +# Alternatively it is possible to use a file reference if the data is too large +df = dataset.to_dataframe() +``` + +In the above example, Feast does a point in time correct query from a single feature set. For each timestamp and entity key combination that is provided by `entity_df`, Feast determines the values of all the features in the `features` list at that respective point in time and then joins features values to that specific entity value and timestamp, and repeats this process for all timestamps. + +This is called a point in time correct join. + +Feast allows users to retrieve features from any feature sets and join them together in a single response dataset. The only requirement is that the user provides the correct entities in order to look up the features. + +### **Point-in-time-correct Join** + +Below is another example of how a point-in-time-correct join works. We have two dataframes. The first is the `entity dataframe` that contains timestamps, entities, and labels. The user would like to have driver features joined onto this `entity dataframe` from the `driver dataframe` to produce an `output dataframe` that contains both labels and features. They would then like to train their model on this output + +![Input 1: Entity DataFrame](https://lh3.googleusercontent.com/ecS5sqj3FHLFSm06XF11NmTQSru-bQ4Az3Kuko_vg5YlBxXjHadlsGwmo7d7wUx4fA1ssdZvxrESDKfkGWjj3HNJg_jIqXY0avz2JzCcEOXLBLmtXNEY8k2u3f4QusHdDWdqRARQHYE) + +![Input 2: Driver DataFrame](https://lh3.googleusercontent.com/LRtCOzmcfhLWzpyndbRKZSVPanLLzfULoHx2YxY6N3i1gQd2Eh6MS1igahOe8ydA7zQulIFJEaQ0IXFXOsdkKRobOC6ThSOnT4hACbCl1jeM4O2JDVC_kvw8lwTCezVUD3d6ZUYj31Q) + +Typically the `input 1` DataFrame would be provided by the user, and the `input 2` DataFrame would already be ingested into Feast. To join these two, the user would call Feast as follows: + +```python +# Feature references +features = [ + 'conv_rate', + 'acc_rate', + 'avg_daily_trips', + 'trip_completed' + ] + + +dataset = client.get_batch_features( + feature_refs=features, # this is a list of feature references + entity_rows=entity_df # This is the entity dataframe above + ) + +# This prints out the dataframe below +print(dataset.to_dataframe()) +``` + +![Output: Joined DataFrame](https://lh5.googleusercontent.com/Gm-4Ru68KyIQ2tQtaVTDFngqO7pMtlMP1YAQO-bqln6_Mo2XAPdbij6w5ACnHAmQ053XUPu6G-c2aYRVJxPqPTMN_BcH6PY0-E1kCwXQAdW1CcQo5tc0g5ilcuVAtqsHcJB1R5mBdLo) + +Feast is able to intelligently join feature data with different timestamps to a single basis table in a point-in-time-correct way. This allows users to join daily batch data with high-frequency event data transparently. They simply need to know the feature names. + +Point-in-time-correct joins also prevents the occurrence of feature leakage by trying to accurate the state of the world at a single point in time, instead of just joining features based on the nearest timestamps. + +## Online feature retrieval + +Online feature retrieval works in much the same way as batch retrieval, with one important distinction: Online stores only maintain the current state of features. No historical data is served. + +```python +features = [ + 'conv_rate', + 'acc_rate', + 'avg_daily_trips', + ] + +data = client.get_online_features( + feature_refs=features, # Contains only feature references + entity_rows=entity_rows, # Contains only entities (driver ids) + ) +``` + +Online serving with Feast is built to be very low latency. Feast Serving provides a [gRPC API](https://api.docs.feast.dev/grpc/feast.serving.pb.html) that is backed by [Redis](https://redis.io/). We also provide support for [Python](https://api.docs.feast.dev/python/), [Go](https://godoc.org/github.com/gojek/feast/sdk/go), and Java clients. + diff --git a/docs/user-guide/feature-sets.md b/docs/user-guide/feature-sets.md new file mode 100644 index 00000000000..48ad81a9e58 --- /dev/null +++ b/docs/user-guide/feature-sets.md @@ -0,0 +1,54 @@ +# Feature Sets + +Feature sets are both a schema and a means of identifying data sources for features. + +Data typically comes in the form of flat files, dataframes, tables in a database, or events on a stream. Thus the data occurs with multiple columns/fields in multiple rows/events. + +Feature sets are a way for defining the unique properties of these data sources, how Feast should interpret them, and how Feast should source them. Feature sets allow for groups of fields in these data sources to be [ingested](data-ingestion.md) and [stored](stores.md) together. Feature sets allow for efficient storage and logical namespacing of data within [stores](stores.md). + +{% hint style="info" %} +Feature sets are a grouping of feature sets based on how they are loaded into Feast. They ensure that data is efficiently stored during ingestion. Feature sets are not a grouping of features for retrieval of features. During retrieval it is possible to retrieve feature values from any number of feature sets. +{% endhint %} + +### Customer Transactions Example + +Below is an example of a basic `customer transactions` feature set that has been exported to YAML: + +{% tabs %} +{% tab title="customer\_transactions\_feature\_set.yaml" %} +```yaml +name: customer_transactions +entities: +- name: customer_id + valueType: INT64 +features: +- name: daily_transactions + valueType: FLOAT +- name: total_transactions + valueType: FLOAT +``` +{% endtab %} +{% endtabs %} + +The dataframe below \(`customer_data.csv`\) contains the features and entities of the above feature set + +| datetime | customer\_id | daily\_transactions | total\_tra**nsactions** | +| :--- | :--- | :--- | :--- | +| 2019-01-01 01:00:00 | 20001 | 5.0 | 14.0 | +| 2019-01-01 01:00:00 | 20002 | 2.6 | 43.0 | +| 2019-01-01 01:00:00 | 20003 | 4.1 | 154.0 | +| 2019-01-01 01:00:00 | 20004 | 3.4 | 74.0 | + +In order to ingest feature data into Feast for this specific feature set: + +```python +# Load dataframe +customer_df = pd.read_csv("customer_data.csv") + +# Create feature set from YAML (using YAML is optional) +cust_trans_fs = FeatureSet.from_yaml("customer_transactions_feature_set.yaml") + +# Load feature data into Feast for this specific feature set +client.ingest(cust_trans_fs, customer_data) +``` + diff --git a/docs/user-guide/features.md b/docs/user-guide/features.md new file mode 100644 index 00000000000..224ca798a54 --- /dev/null +++ b/docs/user-guide/features.md @@ -0,0 +1,38 @@ +# Features + +A feature is an individual measurable property or characteristic of a phenomenon being observed. Features are the most important concepts within a feature store. Feature data is used both as input to models during training and when models are served in production. + +In the context of Feast, features are values that are associated with either one or more entities over time. In Feast, these values are either primitives or lists of primitives. Each feature can also have additional information attached to it. + +The following is a YAML representation of a feature specification. This specification would form part of a larger specification within a [feature set](feature-sets.md). + +{% code title="total\_trips\_feature.yaml" %} +```yaml +# Entity name +name: total_trips_24h + +# Entity value type +value_type: INT64 +``` +{% endcode %} + + Features can be created through the[ Feast SDK](../getting-started/connecting-to-feast-1/connecting-to-feast.md) as follows + +```python +from feast import Entity, Feature, ValueType, FeatureSet + +# Create a driver entity +driver = Entity("driver_id", ValueType.INT64) + +# Create a total trips 24h feature +total_trips_24h = Feature("total_trips_24h", ValueType.INT64) + +# Create a feature set with a single entity and a single feature +driver_fs = FeatureSet("customer_fs", entities=[driver], features=[total_trips_24h]) + +# Register the feature set with Feast +client.apply(driver_fs) +``` + +Please see the [FeatureSpec](https://api.docs.feast.dev/grpc/feast.core.pb.html#FeatureSpec) for the complete feature specification API. + diff --git a/docs/user-guide/overview.md b/docs/user-guide/overview.md new file mode 100644 index 00000000000..0d45825368b --- /dev/null +++ b/docs/user-guide/overview.md @@ -0,0 +1,58 @@ +# Concepts + +## Using Feast + +Feast acts as the interface between ML models and data. Feast enables your team to + +1. Create feature specifications to manage features and load in data that should be managed +2. Retrieve historical features for training models +3. Retrieve online features for serving models + +{% hint style="info" %} +Feast currently does not apply feature transformations to data. +{% endhint %} + +### 1. Creating and managing features + +Feature creators model the data within their organization into Feast through the creation of [feature sets](feature-sets.md). + +Feature sets are specifications that contain both schema and data source information. They allow Feast to know how to interpret your data, and optionally where to find it. Feature sets allow users to define domain [entities](entities.md) along with the [features](features.md) that are available on these entities. Feature sets also allow users to define schemas that describe the properties of the data, which in turn can be used for validation purposes. + +Once a feature set has been registered, Feast will create the relevant schemas to store feature data within it's feature [stores](stores.md). These stores are then automatically populated by by jobs that ingest data from data [sources](sources.md), making it possible for Feast to provide access to features for training and serving. It is also possible for users to [ingest](data-ingestion.md) data into Feast instead of using an external source. + +Read more about [feature sets](feature-sets.md). + +### 2. Retrieving historical features during training + +Both online and historical retrieval are executed through an API call to `Feast Serving` using [feature references](feature-retrieval.md). In the case of historical serving it is necessary to provide Feast with the entities and timestamps that feature data will be joined to. Feast eagerly produces a point-in-time correct dataset based on the features that have been requested. These features can come from any number of feature sets. + +Stores supported: [BigQuery](https://cloud.google.com/bigquery) + +### 3. Retrieving online features during serving + +Feast also allows users to call `Feast Serving` for online feature data. Feast only stores the latest values during online serving for each feature, as opposed to historical serving where all historical values are stored. Online serving allows for very low latency requests to feature data at very high throughput. + +Stores supported: [Redis](https://redis.io/), [Redis Cluster](https://redis.io/topics/cluster-tutorial) + +## Concept Hierarchy + +![](../.gitbook/assets/image%20%283%29.png) + +Feast resources are arranged in the above hierarchy, with projects grouping one or more [feature sets](feature-sets.md), which in turn groups multiple [features](features.md) or [entities](entities.md). + +The logical grouping of these resources are important for namespacing as well as retrieval. During retrieval time it is necessary to reference individual features through feature references. These references uniquely identify a feature or entity within a Feast deployment. + +## Concepts + +[Entities](entities.md) are objects in an organization that model a specific construct. Examples of these include customers, transactions, and drivers. + +[Features](features.md) are measurable properties that are observed on entities. Features are used as inputs to models. + +[Feature Sets](feature-sets.md) are schemas that define logical groupings of entities, features, data sources, and other related metadata. + +[Stores](stores.md) are databases that maintain feature data that gets served to models during training or inference. + +[Sources](sources.md) are either internal or external data sources where feature data can be found. + +[Ingestion](data-ingestion.md) is the process of loading data into Feast. + diff --git a/docs/user-guide/sources.md b/docs/user-guide/sources.md new file mode 100644 index 00000000000..35fcb41a11b --- /dev/null +++ b/docs/user-guide/sources.md @@ -0,0 +1,32 @@ +# Sources + +A `source` is a data source that can be used to find feature data. Users define sources as part of [feature sets](feature-sets.md). Once a feature set is registered with a source, Feast will automatically start to populate its stores with data from this source. + +{% hint style="info" %} +Feast only supports [Kafka](https://kafka.apache.org/) as a source currently. +{% endhint %} + +An example of a user provided source can be seen in the following code snippet + +```python +feature_set = FeatureSet( + name="stream_feature", + entities=[ + Entity("entity_id", ValueType.INT64) + ], + features=[ + Feature("feature_value1", ValueType.STRING) + ], + source=KafkaSource( + brokers="mybroker:9092", + topic="my_feature_topic" + ) +) +``` + +Once this feature set is registered, Feast will start an ingestion job that retrieves data from this source and starts to populate all [stores](stores.md) that subscribe to it. + +In most cases a feature set \(and by extension its source\) will be used to populate both an online store and a historical store. This allows users to both train and serve their model with the same feature data. + +Feast will ensure that the source complies with the schema of the feature set. The event data has to be [Protobuf](https://developers.google.com/protocol-buffers) encoded and must contain the necessary [FeatureRow](https://api.docs.feast.dev/grpc/feast.types.pb.html#FeatureRow) structure. + diff --git a/docs/user-guide/stores.md b/docs/user-guide/stores.md new file mode 100644 index 00000000000..c15cbb3db02 --- /dev/null +++ b/docs/user-guide/stores.md @@ -0,0 +1,37 @@ +# Stores + +In Feast, a store describes a database that is populated with feature data in order to be served to models. + +Feast supports two classes of stores + +* Historical stores +* Online stores + +In order to populate these stores, Feast Core creates a long running ingestion job that streams in data from all feature sources to all stores that subscribe to those feature sets. + +![](../.gitbook/assets/image%20%282%29.png) + +## Historical Stores + +Historical stores maintain a complete history of feature data for the feature sets they are subscribed to. + +Feast currently only supports [Google BigQuery](https://cloud.google.com/bigquery) as a feature store, but we have [developed a storage API ](https://github.com/gojek/feast/issues/482)that makes adding a new store possible. + +Each historical store models its data differently, but in the case of a relational store \(like BigQuery\), each feature set maps directly to a table. Each feature and entity within a feature set maps directly to a column within a table. + +Data from historical stores can be used to train a model. In order to retrieve data from a historical store it is necessary to connect to a Feast Serving deployment and request historical features. Please see feature retrieval for more details. + +{% hint style="danger" %} +Data is persisted in historical stores like BigQuery in log format. Repeated ingestions will duplicate the data is persisted in the store. Feast will automatically deduplicate data during retrieval, but it doesn't currently remove data from the stores themselves. +{% endhint %} + +## Online Stores + +Online stores maintain only the latest values for a specific feature. Feast currently supports Redis as an online store. Online stores are meant for very high throughput writes from ingestion jobs and very low latency access to features during online serving. + +Please continue to the [feature retrieval](feature-retrieval.md) section for more details on retrieving data from online storage. + +## Subscriptions + +Stores are populated by ingestion jobs \(Apache Beam\) that retrieve feature data from sources based on subscriptions. These subscriptions are typically defined by the administrators of the Feast deployment. In most cases a store would simply subscribe to all features, but in some cases it may subscribe to a subset in order to improve performance or efficiency. + diff --git a/docs/why-feast.md b/docs/why-feast.md new file mode 100644 index 00000000000..75048ace2cc --- /dev/null +++ b/docs/why-feast.md @@ -0,0 +1,32 @@ +# Why Feast? + +## Lack of feature reuse + +**Problem:** The process of engineering features is one of the most time consuming activities in building an end-to-end ML system. Despite this, many teams continue to redevelop the same features from scratch for every new project. Often these features never leaving the notebooks or pipelines they are built in. + +**Solution:** A centralized feature store allows organizations to build up a foundation of features that can be reused across projects. Teams are then able to utilize features developed by other teams, and as more features are added to the store it becomes easier and cheaper to build models. + +## Serving features is hard + +**Problem:** Serving up to date features at scale is hard. Raw data can come from a variety of sources, from data lakes, to even streams, to data warehouse, to simply flat files. Data scientists need the ability to produce massive datasets of features from this data in order to train their models offline. These models then need access to real-time feature data at low latency and high throughput when they are served in production. + +**Solution:** Feast is built to be able to ingest data from a variety of sources, supporting both streaming and batch sources. Once data is loaded into Feast as features, they become available through both a batch serving API as well as an real-time \(online serving\) API. These APIs allows data scientists and ML engineers to easily retrieve feature data for their development, training, or in production. Feast also comes with a Java, Go, and Python SDK to make this experience easy. + +## **Models need point-in-time correctness** + +**Problem:** Most data sources are not built with ML use cases in mind and by extension don't provide point-in-time correct lookups of feature data. One of the reasons why features are often re-engineered is because ML practitioners need to ensure that their models are trained on a dataset that accurately models the state of the world when the model runs in production. + +**Solution:** Feast allows end users to create point-in-time correct datasets across multiple entities. Feast ensures that there is no data leakage, that cross feature set joins are valid, and that models are not fed expired data. + +## Definitions of features vary + +**Problem:** Teams define features differently and there is no easy access to the documentation of a feature. + +**Solution:** Feast becomes the single source of truth for all feature data for all models within an organization. Teams are able to capture documentation, metadata and metrics about features. This allows teams to communicate clearly about features, test features data, and determine if a feature is useful for a particular model. + +## **Inconsistency between training and serving** + +**Problem:** Training requires access to historical data, whereas models that serve predictions need the latest values. Inconsistencies arise when data is siloed into many independent systems requiring separate tooling. Often teams are using Python for creating batch features off line, but these features are redeveloped with different libraries and languages when moving to serving or streaming systems. + +**Solution:** Feast provides consistency by managing and unifying the ingestion of data from batch and streaming sources into both the feature warehouse and feature serving stores. Feast becomes the bridge between your model and your data, both for training and serving. This ensures that there is a consistency in the feature data that your model receives. + From a5c5d69bab7a729c107c40ec7cef001bf439461f Mon Sep 17 00:00:00 2001 From: Willem Pienaar Date: Sun, 24 May 2020 10:55:04 +0000 Subject: [PATCH 08/26] GitBook: [v0.5-branch] 29 pages and 4 assets modified --- docs/.gitbook/assets/blank-diagram-4 (1).svg | 1 + .../feast-docs-overview-diagram-2 (3) (1).svg | 1 + docs/.gitbook/assets/image (2) (1).png | Bin 0 -> 149255 bytes docs/.gitbook/assets/image (3) (1).png | Bin 0 -> 17434 bytes docs/README.md | 2 +- docs/SUMMARY.md | 1 + docs/contributing/adding-a-new-store.md | 6 +- docs/contributing/contributing.md | 1 - docs/getting-started/README.md | 2 +- .../connecting-to-feast-1/README.md | 6 + .../connecting-to-feast.md | 2 - docs/reference/configuration-reference.md | 181 +++++++----------- docs/user-guide/architecture.md | 14 +- docs/user-guide/data-ingestion.md | 2 +- docs/user-guide/entities.md | 3 +- docs/user-guide/feature-sets.md | 4 +- docs/user-guide/features.md | 4 +- docs/user-guide/overview.md | 10 +- docs/user-guide/sources.md | 2 +- docs/user-guide/stores.md | 4 +- 20 files changed, 100 insertions(+), 146 deletions(-) create mode 100644 docs/.gitbook/assets/blank-diagram-4 (1).svg create mode 100644 docs/.gitbook/assets/feast-docs-overview-diagram-2 (3) (1).svg create mode 100644 docs/.gitbook/assets/image (2) (1).png create mode 100644 docs/.gitbook/assets/image (3) (1).png diff --git a/docs/.gitbook/assets/blank-diagram-4 (1).svg b/docs/.gitbook/assets/blank-diagram-4 (1).svg new file mode 100644 index 00000000000..fb5e0659e55 --- /dev/null +++ b/docs/.gitbook/assets/blank-diagram-4 (1).svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/docs/.gitbook/assets/feast-docs-overview-diagram-2 (3) (1).svg b/docs/.gitbook/assets/feast-docs-overview-diagram-2 (3) (1).svg new file mode 100644 index 00000000000..7f30963ec78 --- /dev/null +++ b/docs/.gitbook/assets/feast-docs-overview-diagram-2 (3) (1).svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/docs/.gitbook/assets/image (2) (1).png b/docs/.gitbook/assets/image (2) (1).png new file mode 100644 index 0000000000000000000000000000000000000000..d3b359a5988f3b0d75afe44b0ae2cd7d9ef0aab1 GIT binary patch literal 149255 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 literal 0 HcmV?d00001 diff --git a/docs/.gitbook/assets/image (3) (1).png b/docs/.gitbook/assets/image (3) (1).png new file mode 100644 index 0000000000000000000000000000000000000000..2442410112fc9413bf4969dddf296cb06f201d3a GIT binary patch literal 17434 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 literal 0 HcmV?d00001 diff --git a/docs/README.md b/docs/README.md index d19b54960f8..1200b7bbaaa 100644 --- a/docs/README.md +++ b/docs/README.md @@ -11,7 +11,7 @@ Feast aims to: * Provide consistent and point-in-time correct access to feature data. * Enable discovery, documentation, and insights into your features. -![](.gitbook/assets/feast-docs-overview-diagram-2%20%283%29.svg) +![](.gitbook/assets/feast-docs-overview-diagram-2%20%283%29%20%281%29.svg) Feast decouples feature engineering from feature usage. Features that are added to Feast become available immediately for training and serving. Models can retrieve the same features used in training from a low latency online store in production. diff --git a/docs/SUMMARY.md b/docs/SUMMARY.md index f6139c90212..aaea553e95a 100644 --- a/docs/SUMMARY.md +++ b/docs/SUMMARY.md @@ -42,6 +42,7 @@ * [Serving gRPC API](https://api.docs.feast.dev/grpc/feast.serving.pb.html) * [gRPC Types](https://api.docs.feast.dev/grpc/feast.types.pb.html) * [Go SDK](https://godoc.org/github.com/feast-dev/feast/sdk/go) + * [Java SDK](https://javadoc.io/doc/dev.feast/feast-sdk) * [Python SDK](https://api.docs.feast.dev/python/) ## Contributing diff --git a/docs/contributing/adding-a-new-store.md b/docs/contributing/adding-a-new-store.md index 56c06b9b4b6..05583d221cb 100644 --- a/docs/contributing/adding-a-new-store.md +++ b/docs/contributing/adding-a-new-store.md @@ -22,7 +22,7 @@ Stores are configured in Feast Serving. Feast Serving publishes its store config Store configuration is always in the form of a map<String, String>. The keys and configuration for stores are defined in [protos](https://github.com/gojek/feast/blob/master/protos/feast/core/Store.proto). This must be added in order to define a new store -Then the store must be configured to be loaded through Feast Serving. The above configuration is loaded through [FeastProperties.java](https://github.com/gojek/feast/blob/a1937c374a4e39b7a75d828e7b7c3b87a64d9d6e/serving/src/main/java/feast/serving/config/FeastProperties.java#L175). +Then the store must be configured to be loaded through Feast Serving. The above configuration is loaded through [FeastProperties.java](https://github.com/gojek/feast/blob/a1937c374a4e39b7a75d828e7b7c3b87a64d9d6e/serving/src/main/java/feast/serving/config/FeastProperties.java#L175). Once configuration is loaded, the store will then be instantiated. @@ -45,8 +45,6 @@ Below is a "happy path" of a batch ingestion process which includes a blocking s ![](https://user-images.githubusercontent.com/6728866/74807906-91e73c00-5324-11ea-8ba5-2b43c7c5282b.png) - - The complete ingestion flow is executed by a [FeatureSink](https://github.com/gojek/feast/blob/master/storage/api/src/main/java/feast/storage/api/writer/FeatureSink.java). Two methods should be implemented * [prepareWrite\(\)](https://github.com/gojek/feast/blob/a1937c374a4e39b7a75d828e7b7c3b87a64d9d6e/storage/api/src/main/java/feast/storage/api/writer/FeatureSink.java#L45): Sets up storage backend for writing/ingestion. This method will be called once during pipeline initialisation. Typically this is used to apply schemas. @@ -66,7 +64,7 @@ The current implementation of batch retrieval starts and ends with a file \(data Additionally, we have also implemented a [batch retrieval method ](https://github.com/gojek/feast/blob/a1937c374a4e39b7a75d828e7b7c3b87a64d9d6e/sdk/python/feast/client.py#L509)in the Python SDK. Depending on the means through which this new store will export data, this client may have to change. At the very least it would change if Google Cloud Storage isn't used as the staging bucket. -The means through which you implement the export/import of data into the store will depend on your store. +The means through which you implement the export/import of data into the store will depend on your store. #### 2.2.2 Online Serving diff --git a/docs/contributing/contributing.md b/docs/contributing/contributing.md index 71d53070465..2be097d6ffd 100644 --- a/docs/contributing/contributing.md +++ b/docs/contributing/contributing.md @@ -12,4 +12,3 @@ PRs that are submitted by the general public need to be identified as `ok-to-tes Please sign the [Google CLA](https://cla.developers.google.com/) in order to have your code merged into the Feast repository. {% endhint %} - diff --git a/docs/getting-started/README.md b/docs/getting-started/README.md index 9945ab197d9..7b902d830fb 100644 --- a/docs/getting-started/README.md +++ b/docs/getting-started/README.md @@ -10,5 +10,5 @@ If you would like to deploy a new installation of Feast, then click on `Deployin {% page-ref page="deploying-feast/" %} -If you are connected to a running Feast deployment, then have a look at our [example tutorials](https://github.com/gojek/feast/tree/master/examples). +If you are connected to a running Feast deployment, then have a look at our [example tutorials](https://github.com/gojek/feast/tree/master/examples). diff --git a/docs/getting-started/connecting-to-feast-1/README.md b/docs/getting-started/connecting-to-feast-1/README.md index 7cc7e5d8c5b..fd8fe8e4982 100644 --- a/docs/getting-started/connecting-to-feast-1/README.md +++ b/docs/getting-started/connecting-to-feast-1/README.md @@ -17,5 +17,11 @@ {% page-ref page="connecting-to-feast.md" %} +## Online Serving Clients +Feast provides three ways to retrieve data in production for online serving + +* [Python SDK](https://api.docs.feast.dev/python/) \(as above\) +* [Go Client library](https://godoc.org/github.com/feast-dev/feast/sdk/go) +* [Java Client library](https://javadoc.io/doc/dev.feast/feast-sdk) diff --git a/docs/getting-started/connecting-to-feast-1/connecting-to-feast.md b/docs/getting-started/connecting-to-feast-1/connecting-to-feast.md index a2cafda0db2..9cba57f2566 100644 --- a/docs/getting-started/connecting-to-feast-1/connecting-to-feast.md +++ b/docs/getting-started/connecting-to-feast-1/connecting-to-feast.md @@ -34,5 +34,3 @@ Commands: version Displays version and connectivity information ``` - - diff --git a/docs/reference/configuration-reference.md b/docs/reference/configuration-reference.md index cefd478ef9d..4d5e3377c89 100644 --- a/docs/reference/configuration-reference.md +++ b/docs/reference/configuration-reference.md @@ -10,127 +10,78 @@ Configuration options for both the Feast CLI and Feast Python SDK can be defined The following list is a subset of all configuration properties. Please see [this link](https://github.com/feast-dev/feast/blob/master/sdk/python/feast/constants.py) for the complete list of configuration options +| Option | Description | Default | +| :--- | :--- | :--- | + + +| `FEAST_CONFIG` | Location of Feast configuration file | `/.feast/config` | +| :--- | :--- | :--- | + + - - - - - - - - - - - - - - + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + - - - - - - - - - - - -
OptionDescriptionDefault
FEAST_CONFIG - Location of Feast configuration file/.feast/config -
CONFIG_FEAST_ENV_VAR_PREFIX - + CONFIG_FEAST_ENV_VAR_PREFIX +

Default prefix to Feast environmental variable options.

Does not apply to FEAST_CONFIG

- -
FEAST_ -
PROJECT - Default Feast project to usedefault -
CORE_URL - URL used to connect to Feast Corelocalhost:6565 -
CORE_ENABLE_SSL - Enables TLS/SSL on connections to Feast CoreFalse -
CORE_AUTH_ENABLED - Enable user authentication when connecting to a Feast Core instanceFalse -
CORE_AUTH_TOKEN - Provide a static JWT token to authenticate with Feast CoreNull -
CORE_SERVER_SSL_CERT - Path to certificate(s) used by Feast Client to authenticate TLS connection - to Feast Core (not to authenticate you as a client).Null -
SERVING_URL - URL used to connect to Feast Servinglocalhost:6566 -
SERVING_ENABLE_SSL - Enables TLS/SSL on connections to Feast ServingFalse -
SERVING_SERVER_SSL_CERT - Path to certificate(s) used by Feast Client to authenticate TLS connection - to Feast Serving (not to authenticate you as a client).None -
GRPC_CONNECTION_TIMEOUT_DEFAULT - Default gRPC connection timeout to both Feast Serving and Feast Core (in - seconds)3 - FEAST_ +
GRPC_CONNECTION_TIMEOUT_APPLY - Default gRPC connection timeout when sending an ApplyFeatureSet command - to Feast Core (in seconds)600 -
BATCH_FEATURE_REQUEST_WAIT_S - Time to wait for batch feature requests before timing out.600 -
### Usage + + +| `PROJECT` | Default Feast project to use | `default` | +| :--- | :--- | :--- | + + +| `CORE_URL` | URL used to connect to Feast Core | `localhost:6565` | +| :--- | :--- | :--- | + + +| `CORE_ENABLE_SSL` | Enables TLS/SSL on connections to Feast Core | `False` | +| :--- | :--- | :--- | + + +| `CORE_AUTH_ENABLED` | Enable user authentication when connecting to a Feast Core instance | `False` | +| :--- | :--- | :--- | + + +| `CORE_AUTH_TOKEN` | Provide a static `JWT` token to authenticate with Feast Core | `Null` | +| :--- | :--- | :--- | + + +| `CORE_SERVER_SSL_CERT` | Path to certificate\(s\) used by Feast Client to authenticate TLS connection to Feast Core \(not to authenticate you as a client\). | `Null` | +| :--- | :--- | :--- | + + +| `SERVING_URL` | URL used to connect to Feast Serving | `localhost:6566` | +| :--- | :--- | :--- | + + +| `SERVING_ENABLE_SSL` | Enables TLS/SSL on connections to Feast Serving | `False` | +| :--- | :--- | :--- | + + +| `SERVING_SERVER_SSL_CERT` | Path to certificate\(s\) used by Feast Client to authenticate TLS connection to Feast Serving \(not to authenticate you as a client\). | `None` | +| :--- | :--- | :--- | + + +| `GRPC_CONNECTION_TIMEOUT_DEFAULT` | Default gRPC connection timeout to both Feast Serving and Feast Core \(in seconds\) | `3` | +| :--- | :--- | :--- | + + +| `GRPC_CONNECTION_TIMEOUT_APPLY` | Default gRPC connection timeout when sending an ApplyFeatureSet command to Feast Core \(in seconds\) | `600` | +| :--- | :--- | :--- | + + +| `BATCH_FEATURE_REQUEST_WAIT_S` | Time to wait for batch feature requests before timing out. | `600` | +| :--- | :--- | :--- | + -#### Configuration File +### Configuration File Feast Configuration File \(`~/.feast/config`\) @@ -140,13 +91,13 @@ project = default core_url = localhost:6565 ``` -#### Environmental Variables +### Environmental Variables ```bash FEAST_CORE_URL=my_feast:6565 FEAST_PROJECT=default feast projects list ``` -#### Feast SDK +### Feast SDK ```python client = Client( diff --git a/docs/user-guide/architecture.md b/docs/user-guide/architecture.md index 5de6fe66eba..39b6d0a1f97 100644 --- a/docs/user-guide/architecture.md +++ b/docs/user-guide/architecture.md @@ -1,8 +1,8 @@ # Architecture -![Feast high-level flow](../.gitbook/assets/blank-diagram-4.svg) +![Feast high-level flow](../.gitbook/assets/blank-diagram-4%20%281%29.svg) -### **Feast Core** +## **Feast Core** Feast Core is the central management service of a Feast deployment. It's role is to: @@ -13,7 +13,7 @@ Feast Core is the central management service of a Feast deployment. It's role is Job management may move out of Feast Core to Feast Serving in the future. {% endhint %} -### **Feast Ingestion** +## **Feast Ingestion** Before a user ingests data into Feast, they should register one or more feature sets. These [feature sets](feature-sets.md) tell Feast where to find their data, how to ingest it, and also describe the characteristics of the data for validation purposes. Once a feature set is registered, Feast will start an Apache Beam job in order to populate a store with data from a source. @@ -21,17 +21,17 @@ In order for stores to be populated with data, users must publish the data to a Streaming systems can also ingest data into Feast. This is done by publishing to the correct Kafka topic in the expected format. Feast expects data to be in [FeatureRow.proto](https://api.docs.feast.dev/grpc/feast.types.pb.html#FeatureRow) format. The topic and brokers can be found on the feature set schema using the [Python SDK](../getting-started/connecting-to-feast-1/python-sdk.md). -### **Stores** +## **Stores** Stores are nothing more than databases used to store feature data. Feast loads data into stores through an ingestion process, after which the data can be served through the Feast Serving API. Stores are documented in the following section. {% page-ref page="stores.md" %} -### **Feast Serving** +## **Feast Serving** - `Feast Serving` is the data access layer through which end users and production systems retrieve feature data. Each `Serving` instance is backed by a [store](stores.md). +`Feast Serving` is the data access layer through which end users and production systems retrieve feature data. Each `Serving` instance is backed by a [store](stores.md). -Since Feast supports multiple store types \(online, historical\) it is common to have two instances of Feast Serving deployed, one for online serving and one for historical serving. However, Feast allows for any number of `Feast Serving` deployments, meaning it is possible to deploy a `Feast Serving` deployment per production system, with its own stores and population jobs. +Since Feast supports multiple store types \(online, historical\) it is common to have two instances of Feast Serving deployed, one for online serving and one for historical serving. However, Feast allows for any number of `Feast Serving` deployments, meaning it is possible to deploy a `Feast Serving` deployment per production system, with its own stores and population jobs. `Serving` deployments can subscribe to a subset of feature data. Meaning they do not have to consume all features known to a `Feast Core` deployment. diff --git a/docs/user-guide/data-ingestion.md b/docs/user-guide/data-ingestion.md index 464a51bbfd4..b9c4dce2819 100644 --- a/docs/user-guide/data-ingestion.md +++ b/docs/user-guide/data-ingestion.md @@ -12,7 +12,7 @@ feature_set = FeatureSet( If a user does not provide a source of data then they are required to publish data to Feast. This process is called ingestion. -### Ingesting data +## Ingesting data The following example demonstrates how data ingestion works. For a full tutorial please see the [Telco Customer Churn Prediction Notebook](https://github.com/gojek/feast/blob/master/examples/feast-xgboost-churn-prediction-tutorial/Telecom%20Customer%20Churn%20Prediction%20%28with%20Feast%20and%20XGBoost%29.ipynb). diff --git a/docs/user-guide/entities.md b/docs/user-guide/entities.md index f2383e440cb..07355798983 100644 --- a/docs/user-guide/entities.md +++ b/docs/user-guide/entities.md @@ -11,7 +11,7 @@ An entity is the object on which features are observed. For example we could hav In the context of Feast, entities are important because they are used as keys when looking up feature values. Entities are also used when joining feature values between different feature sets in order to build one large data set to train a model, or to serve a model. -Example entity properties +Example entity properties {% code title="customer\_id.yaml" %} ```yaml @@ -20,7 +20,6 @@ name: customer_id # Entity value type value_type: INT64 - ``` {% endcode %} diff --git a/docs/user-guide/feature-sets.md b/docs/user-guide/feature-sets.md index 48ad81a9e58..3353c5ade64 100644 --- a/docs/user-guide/feature-sets.md +++ b/docs/user-guide/feature-sets.md @@ -2,7 +2,7 @@ Feature sets are both a schema and a means of identifying data sources for features. -Data typically comes in the form of flat files, dataframes, tables in a database, or events on a stream. Thus the data occurs with multiple columns/fields in multiple rows/events. +Data typically comes in the form of flat files, dataframes, tables in a database, or events on a stream. Thus the data occurs with multiple columns/fields in multiple rows/events. Feature sets are a way for defining the unique properties of these data sources, how Feast should interpret them, and how Feast should source them. Feature sets allow for groups of fields in these data sources to be [ingested](data-ingestion.md) and [stored](stores.md) together. Feature sets allow for efficient storage and logical namespacing of data within [stores](stores.md). @@ -10,7 +10,7 @@ Feature sets are a way for defining the unique properties of these data sources, Feature sets are a grouping of feature sets based on how they are loaded into Feast. They ensure that data is efficiently stored during ingestion. Feature sets are not a grouping of features for retrieval of features. During retrieval it is possible to retrieve feature values from any number of feature sets. {% endhint %} -### Customer Transactions Example +## Customer Transactions Example Below is an example of a basic `customer transactions` feature set that has been exported to YAML: diff --git a/docs/user-guide/features.md b/docs/user-guide/features.md index 224ca798a54..61bd6cd28bb 100644 --- a/docs/user-guide/features.md +++ b/docs/user-guide/features.md @@ -2,7 +2,7 @@ A feature is an individual measurable property or characteristic of a phenomenon being observed. Features are the most important concepts within a feature store. Feature data is used both as input to models during training and when models are served in production. -In the context of Feast, features are values that are associated with either one or more entities over time. In Feast, these values are either primitives or lists of primitives. Each feature can also have additional information attached to it. +In the context of Feast, features are values that are associated with either one or more entities over time. In Feast, these values are either primitives or lists of primitives. Each feature can also have additional information attached to it. The following is a YAML representation of a feature specification. This specification would form part of a larger specification within a [feature set](feature-sets.md). @@ -16,7 +16,7 @@ value_type: INT64 ``` {% endcode %} - Features can be created through the[ Feast SDK](../getting-started/connecting-to-feast-1/connecting-to-feast.md) as follows +Features can be created through the[ Feast SDK](../getting-started/connecting-to-feast-1/connecting-to-feast.md) as follows ```python from feast import Entity, Feature, ValueType, FeatureSet diff --git a/docs/user-guide/overview.md b/docs/user-guide/overview.md index 0d45825368b..5078010a294 100644 --- a/docs/user-guide/overview.md +++ b/docs/user-guide/overview.md @@ -2,7 +2,7 @@ ## Using Feast -Feast acts as the interface between ML models and data. Feast enables your team to +Feast acts as the interface between ML models and data. Feast enables your team to 1. Create feature specifications to manage features and load in data that should be managed 2. Retrieve historical features for training models @@ -14,7 +14,7 @@ Feast currently does not apply feature transformations to data. ### 1. Creating and managing features -Feature creators model the data within their organization into Feast through the creation of [feature sets](feature-sets.md). +Feature creators model the data within their organization into Feast through the creation of [feature sets](feature-sets.md). Feature sets are specifications that contain both schema and data source information. They allow Feast to know how to interpret your data, and optionally where to find it. Feature sets allow users to define domain [entities](entities.md) along with the [features](features.md) that are available on these entities. Feature sets also allow users to define schemas that describe the properties of the data, which in turn can be used for validation purposes. @@ -36,7 +36,7 @@ Stores supported: [Redis](https://redis.io/), [Redis Cluster](https://redis.io/t ## Concept Hierarchy -![](../.gitbook/assets/image%20%283%29.png) +![](../.gitbook/assets/image%20%283%29%20%281%29.png) Feast resources are arranged in the above hierarchy, with projects grouping one or more [feature sets](feature-sets.md), which in turn groups multiple [features](features.md) or [entities](entities.md). @@ -44,11 +44,11 @@ The logical grouping of these resources are important for namespacing as well as ## Concepts -[Entities](entities.md) are objects in an organization that model a specific construct. Examples of these include customers, transactions, and drivers. +[Entities](entities.md) are objects in an organization that model a specific construct. Examples of these include customers, transactions, and drivers. [Features](features.md) are measurable properties that are observed on entities. Features are used as inputs to models. -[Feature Sets](feature-sets.md) are schemas that define logical groupings of entities, features, data sources, and other related metadata. +[Feature Sets](feature-sets.md) are schemas that define logical groupings of entities, features, data sources, and other related metadata. [Stores](stores.md) are databases that maintain feature data that gets served to models during training or inference. diff --git a/docs/user-guide/sources.md b/docs/user-guide/sources.md index 35fcb41a11b..64234f78cf8 100644 --- a/docs/user-guide/sources.md +++ b/docs/user-guide/sources.md @@ -24,7 +24,7 @@ feature_set = FeatureSet( ) ``` -Once this feature set is registered, Feast will start an ingestion job that retrieves data from this source and starts to populate all [stores](stores.md) that subscribe to it. +Once this feature set is registered, Feast will start an ingestion job that retrieves data from this source and starts to populate all [stores](stores.md) that subscribe to it. In most cases a feature set \(and by extension its source\) will be used to populate both an online store and a historical store. This allows users to both train and serve their model with the same feature data. diff --git a/docs/user-guide/stores.md b/docs/user-guide/stores.md index c15cbb3db02..e742eb3dfc2 100644 --- a/docs/user-guide/stores.md +++ b/docs/user-guide/stores.md @@ -9,11 +9,11 @@ Feast supports two classes of stores In order to populate these stores, Feast Core creates a long running ingestion job that streams in data from all feature sources to all stores that subscribe to those feature sets. -![](../.gitbook/assets/image%20%282%29.png) +![](../.gitbook/assets/image%20%282%29%20%281%29.png) ## Historical Stores -Historical stores maintain a complete history of feature data for the feature sets they are subscribed to. +Historical stores maintain a complete history of feature data for the feature sets they are subscribed to. Feast currently only supports [Google BigQuery](https://cloud.google.com/bigquery) as a feature store, but we have [developed a storage API ](https://github.com/gojek/feast/issues/482)that makes adding a new store possible. From 4920cd7aabf4c3165a5af86bcd829e08be40dbfc Mon Sep 17 00:00:00 2001 From: Willem Pienaar Date: Sun, 24 May 2020 10:56:54 +0000 Subject: [PATCH 09/26] GitBook: [v0.5-branch] 28 pages modified --- docs/reference/api/README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/reference/api/README.md b/docs/reference/api/README.md index c30736dcc2d..86a0576e15f 100644 --- a/docs/reference/api/README.md +++ b/docs/reference/api/README.md @@ -5,6 +5,7 @@ The following API docs are available * [Feast Core gRPC API](https://api.docs.feast.dev/grpc/feast.core.pb.html): This is the gRPC API used by Feast Core. Feast Core has a dual function of schema registry and job manager. This API contains RPCs for creating and managing feature sets, stores, projects, and jobs. * [Feast Serving gRPC API](https://api.docs.feast.dev/grpc/feast.serving.pb.html): This is the gRPC API used by Feast Serving. It contains RPCs used for the retrieval of online feature data or historical feature data. * [Feast gRPC Types](https://api.docs.feast.dev/grpc/feast.types.pb.html): These are the gRPC types used by both Feast Core, Feast Serving, and the Go, Java, and Python clients. -* [Go Client SDK](https://godoc.org/github.com/feast-dev/feast/sdk/go): This SDK is used for the retrieval of online features from Feast. +* [Go Client SDK](https://godoc.org/github.com/feast-dev/feast/sdk/go): The Go library used for the retrieval of online features from Feast. +* [Java Client SDK](https://javadoc.io/doc/dev.feast/feast-sdk): The Java library used for the retrieval of online features from Feast. * [Python SDK](https://api.docs.feast.dev/python/): This is the complete reference to the Feast Python SDK. The SDK is used to manage feature sets, features, jobs, projects, and entities. It can also be used to retrieve training datasets or online features from Feast Serving. From cd490b11234e7bdb7b836bf821c403464e5520f6 Mon Sep 17 00:00:00 2001 From: Willem Pienaar Date: Tue, 26 May 2020 11:05:37 +0000 Subject: [PATCH 10/26] GitBook: [v0.5-branch] one page modified --- docs/user-guide/features.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/user-guide/features.md b/docs/user-guide/features.md index 61bd6cd28bb..40ca35f44e0 100644 --- a/docs/user-guide/features.md +++ b/docs/user-guide/features.md @@ -28,7 +28,7 @@ driver = Entity("driver_id", ValueType.INT64) total_trips_24h = Feature("total_trips_24h", ValueType.INT64) # Create a feature set with a single entity and a single feature -driver_fs = FeatureSet("customer_fs", entities=[driver], features=[total_trips_24h]) +driver_fs = FeatureSet("driver_fs", entities=[driver], features=[total_trips_24h]) # Register the feature set with Feast client.apply(driver_fs) From 9f8b271ec54ca6388812962a2c967e42c138bfd7 Mon Sep 17 00:00:00 2001 From: Willem Pienaar Date: Wed, 27 May 2020 01:19:49 +0000 Subject: [PATCH 11/26] GitBook: [v0.5-branch] 28 pages modified --- docs/administration/troubleshooting.md | 2 +- docs/contributing/development-guide.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/administration/troubleshooting.md b/docs/administration/troubleshooting.md index 7924a33e733..e293945acce 100644 --- a/docs/administration/troubleshooting.md +++ b/docs/administration/troubleshooting.md @@ -1,6 +1,6 @@ # Troubleshooting -If at any point in time you cannot resolve a problem, please see the [Getting Help](https://github.com/feast-dev/feast/tree/75f3b783e5a7c5e0217a3020422548fb0d0ce0bf/docs/getting-help.md) section for reaching out to the Feast community. +If at any point in time you cannot resolve a problem, please see the [Community](../getting-help.md) section for reaching out to the Feast community. ## How can I verify that all services are operational? diff --git a/docs/contributing/development-guide.md b/docs/contributing/development-guide.md index 81de60202b5..677504e987d 100644 --- a/docs/contributing/development-guide.md +++ b/docs/contributing/development-guide.md @@ -77,7 +77,7 @@ $ mvn verify ### 3.3 Running components locally -The `core` and `serving` modules are Spring Boot applications. These may be run as usual for [the Spring Boot Maven plugin](https://docs.spring.io/spring-boot/docs/current/maven-plugin/index.html): +The `core` and `serving` modules are Spring Boot applications. These may be run as usual for the Spring Boot Maven plugin: ```text $ mvn --projects core spring-boot:run From 939b078b17745621c070bf70da2756897f14c92c Mon Sep 17 00:00:00 2001 From: Khor Shu Heng <32997938+khorshuheng@users.noreply.github.com> Date: Thu, 28 May 2020 22:58:58 +0800 Subject: [PATCH 12/26] Remove usage of parallel stream for feature value map generation (#751) Co-authored-by: Khor Shu Heng --- .../main/java/feast/serving/service/OnlineServingService.java | 1 - 1 file changed, 1 deletion(-) diff --git a/serving/src/main/java/feast/serving/service/OnlineServingService.java b/serving/src/main/java/feast/serving/service/OnlineServingService.java index d5c615059d1..7339a19c1a4 100644 --- a/serving/src/main/java/feast/serving/service/OnlineServingService.java +++ b/serving/src/main/java/feast/serving/service/OnlineServingService.java @@ -98,7 +98,6 @@ public GetOnlineFeaturesResponse getOnlineFeatures(GetOnlineFeaturesRequest requ if (isStale(featureSetRequest, entityRow, featureRow)) { featureSetRequest .getFeatureReferences() - .parallelStream() .forEach( ref -> { populateStaleKeyCountMetrics(project, ref); From d229058ef5859ae776dd5d7b657ff634387d6001 Mon Sep 17 00:00:00 2001 From: Zhu Zhanyan Date: Fri, 29 May 2020 04:32:55 +0000 Subject: [PATCH 13/26] GitBook: [v0.5-branch] 28 pages modified --- docs/contributing/development-guide.md | 28 +++++----------- docs/reference/configuration-reference.md | 4 ++- docs/user-guide/feature-retrieval.md | 12 +++++++ docs/user-guide/feature-sets.md | 39 ++++++++++++++++++++++- docs/user-guide/features.md | 4 +-- 5 files changed, 63 insertions(+), 24 deletions(-) diff --git a/docs/contributing/development-guide.md b/docs/contributing/development-guide.md index 677504e987d..ba78e488a15 100644 --- a/docs/contributing/development-guide.md +++ b/docs/contributing/development-guide.md @@ -32,7 +32,7 @@ The following components/services are required to develop Feast: * **Feast Core:** Requires PostgreSQL \(version 11 and above\) to store state, and requires a Kafka \(tested on version 2.x\) setup to allow for ingestion of FeatureRows. * **Feast Serving:** Requires Redis \(tested on version 5.x\). -These services should be running before starting development. The following snippet will start the services using Docker. For the +These services should be running before starting development. The following snippet will start the services using Docker: ```bash # Start Postgres @@ -269,20 +269,6 @@ store { ### 4.5 Registering a FeatureSet -Before registering a new FeatureSet, a project is required. - -```text -grpc_cli call localhost:6565 CreateProject ' - name: "your_project_name" -' -``` - -When a feature set is successfully registered, Feast Core will start an **ingestion** job that listens for new features in the feature set. - -{% hint style="info" %} -Note that Feast currently only supports source of type `KAFKA`, so you must have access to a running Kafka broker to register a FeatureSet successfully. It is possible to omit the `source` from a Feature Set, but Feast Core will still use Kafka behind the scenes, it is simply abstracted away from the user. -{% endhint %} - Create a new FeatureSet in Feast by sending a request to Feast Core: ```text @@ -293,7 +279,6 @@ Create a new FeatureSet in Feast by sending a request to Feast Core: grpc_cli call localhost:6565 ApplyFeatureSet ' feature_set { spec { - project: "your_project_name" name: "driver" version: 1 @@ -325,7 +310,6 @@ Verify that the FeatureSet has been registered correctly. # To check that the FeatureSet has been registered correctly. # You should also see logs from Feast Core of the ingestion job being started grpc_cli call localhost:6565 GetFeatureSet ' - project: "your_project_name" name: "driver" ' ``` @@ -335,13 +319,18 @@ Or alternatively, list all feature sets ```text grpc_cli call localhost:6565 ListFeatureSets ' filter { - project: "your_project_name" feature_set_name: "driver" feature_set_version: "1" } ' ``` +When a feature set is successfully registered, Feast Core will start an **ingestion** job that listens for new features in the feature set. + +{% hint style="info" %} +Note that Feast currently only supports source of type `KAFKA`, so you must have access to a running Kafka broker to register a FeatureSet successfully. It is possible to omit the `source` from a Feature Set, but Feast Core will still use Kafka behind the scenes, it is simply abstracted away from the user. +{% endhint %} + ### 4.6 Ingestion and Population of Feature Values ```text @@ -403,7 +392,6 @@ Ensure that Feast Serving returns results for the feature value for the specific ```text grpc_cli call localhost:6566 GetOnlineFeatures ' features { - project: "your_project_name" name: "city" version: 1 max_age { @@ -430,7 +418,7 @@ field_values { } } fields { - key: "your_project_name/city:1" + key: "city" value { string_val: "JAKARTA" } diff --git a/docs/reference/configuration-reference.md b/docs/reference/configuration-reference.md index 4d5e3377c89..3a8ed05ca65 100644 --- a/docs/reference/configuration-reference.md +++ b/docs/reference/configuration-reference.md @@ -33,7 +33,9 @@ The following list is a subset of all configuration properties. Please see [this -| `PROJECT` | Default Feast project to use | `default` | + + +| `PROJECT` | Default Feast project to use | `default` | | :--- | :--- | :--- | diff --git a/docs/user-guide/feature-retrieval.md b/docs/user-guide/feature-retrieval.md index e49db24fe99..bbe57ec2b86 100644 --- a/docs/user-guide/feature-retrieval.md +++ b/docs/user-guide/feature-retrieval.md @@ -31,6 +31,10 @@ features = [ target = 'churn' ``` +{% hint style="info" %} +Where the Features from different Feature Sets use the same name, the Feature Set name `feature-set` is required to disambiguate which feature is specified. +{% endhint %} + Feature references only apply to a single `project`. Features cannot be retrieved across projects in a single request. ## 3. Historical feature retrieval @@ -54,6 +58,10 @@ dataset = client.get_batch_features( df = dataset.to_dataframe() ``` +{% hint style="info" %} +When no project is specified when retrieving features with `get_batch_features()`, Feast infers that the features specified belong to the `default` project. To retrieve from another project, specify the `default` parameter when retrieving features. +{% endhint %} + In the above example, Feast does a point in time correct query from a single feature set. For each timestamp and entity key combination that is provided by `entity_df`, Feast determines the values of all the features in the `features` list at that respective point in time and then joins features values to that specific entity value and timestamp, and repeats this process for all timestamps. This is called a point in time correct join. @@ -112,5 +120,9 @@ data = client.get_online_features( ) ``` +{% hint style="info" %} +When no project is specified when retrieving features with `get_online_feature()`, Feast infers that the features specified belong to the `default` project. To retrieve from another project, specify the `project` parameter when retrieving features. +{% endhint %} + Online serving with Feast is built to be very low latency. Feast Serving provides a [gRPC API](https://api.docs.feast.dev/grpc/feast.serving.pb.html) that is backed by [Redis](https://redis.io/). We also provide support for [Python](https://api.docs.feast.dev/python/), [Go](https://godoc.org/github.com/gojek/feast/sdk/go), and Java clients. diff --git a/docs/user-guide/feature-sets.md b/docs/user-guide/feature-sets.md index 3353c5ade64..399e5ac6829 100644 --- a/docs/user-guide/feature-sets.md +++ b/docs/user-guide/feature-sets.md @@ -12,7 +12,7 @@ Feature sets are a grouping of feature sets based on how they are loaded into Fe ## Customer Transactions Example -Below is an example of a basic `customer transactions` feature set that has been exported to YAML: +Below is an example specification of a basic `customer transactions` feature set that has been exported to YAML: {% tabs %} {% tab title="customer\_transactions\_feature\_set.yaml" %} @@ -48,7 +48,44 @@ customer_df = pd.read_csv("customer_data.csv") # Create feature set from YAML (using YAML is optional) cust_trans_fs = FeatureSet.from_yaml("customer_transactions_feature_set.yaml") +# Apply new feature set +client.apply(cust_trans_fs) + # Load feature data into Feast for this specific feature set client.ingest(cust_trans_fs, customer_data) ``` +{% hint style="info" %} +When applying a Feature Set without specifying a project in its specification, Feast creates/updates the Feature Set in the `default` project. To create a Feature Set in another project, specify the project of choice in the Feature Set specification's project field. +{% endhint %} + +### **Making changes to Feature Sets** + +In order to facilitate the need for feature set definitions to change over time, a limited set of changes can be made to existing feature sets. + +To apply changes to a feature set: + +```python +# With existing feature set +cust_trans_fs = FeatureSet.from_yaml("customer_transactions_feature_set.yaml") + +# Add new feature, avg_basket_size +cust_trans_fs.add(Feature(name="avg_basket_size", dtype=ValueType.INT32)) + +# Apply changed feature set +client.apply(cust_trans_fs) +``` + +Permitted changes include: + +* Adding new features +* Deleting existing features \(note that features are tombstoned and remain on record, rather than removed completely; as a result, new features will not be able to take the names of these deleted features\) +* Changing features' TFX schemas +* Changing the feature set's source and max age + +Note that the following are **not** allowed: + +* Changes to project or name of the feature set. +* Changes to entities. +* Changes to names and types of existing features. + diff --git a/docs/user-guide/features.md b/docs/user-guide/features.md index 40ca35f44e0..9b51d9f6299 100644 --- a/docs/user-guide/features.md +++ b/docs/user-guide/features.md @@ -8,10 +8,10 @@ The following is a YAML representation of a feature specification. This specific {% code title="total\_trips\_feature.yaml" %} ```yaml -# Entity name +# Feature name name: total_trips_24h -# Entity value type +# Feature value type value_type: INT64 ``` {% endcode %} From be2e3bc5ac62294557f20d7928184dbab08cb1fc Mon Sep 17 00:00:00 2001 From: Willem Pienaar Date: Thu, 4 Jun 2020 01:45:13 +0000 Subject: [PATCH 14/26] GitBook: [v0.5-branch] one page modified --- docs/roadmap.md | 27 +++++++++++++++++++-------- 1 file changed, 19 insertions(+), 8 deletions(-) diff --git a/docs/roadmap.md b/docs/roadmap.md index a047cf41256..09d564d45bd 100644 --- a/docs/roadmap.md +++ b/docs/roadmap.md @@ -1,5 +1,24 @@ # Roadmap +## Feast 0.6 \(Feature Release\) + +[Discussion](https://github.com/feast-dev/feast/issues/767) + +[GitHub Milestone](https://github.com/feast-dev/feast/milestone/3) + +### New functionality + +1. Batch statistics and validation [\#612](https://github.com/feast-dev/feast/pull/612) +2. Authentication and authorization [\#554](https://github.com/feast-dev/feast/pull/554) +3. Online feature and entity status metadata [\#658](https://github.com/feast-dev/feast/pull/658) +4. Improved searching and filtering of features and entities +5. Python support for labels [\#663](https://github.com/feast-dev/feast/issues/663) + +### Technical debt, refactoring, or housekeeping + +1. Improved job life cycle management [\#761](https://github.com/feast-dev/feast/issues/761) +2. Compute and write metrics for rows prior to store writes [\#763](https://github.com/feast-dev/feast/pull/763) + ## Feast 0.5 \(Technical Release\) [Discussion](https://github.com/gojek/feast/issues/527) @@ -21,11 +40,3 @@ 6. Tracking of batch ingestion by with dataset\_id/job\_id \([\#461](https://github.com/gojek/feast/issues/461)\) 7. Write Beam metrics after ingestion to store \(not prior\) \([\#489](https://github.com/gojek/feast/issues/489)\) -## Feast 0.6 \(Feature Release\) - -### New functionality - -1. User authentication & authorization \([\#504](https://github.com/gojek/feast/issues/504)\) -2. Batch statistics and validation \(M2 from [Feature Validation RFC](https://docs.google.com/document/d/1TPmd7r4mniL9Y-V_glZaWNo5LMXLshEAUpYsohojZ-8/edit)\) -3. Online feature/entity status metadata \([\#658](https://github.com/gojek/feast/pull/658)\) - From dc0f04a3ac95d6d4f6d20a8783a0eaa4b3af526c Mon Sep 17 00:00:00 2001 From: Ches Martin Date: Fri, 22 May 2020 21:49:55 +0700 Subject: [PATCH 15/26] Remove unused Hibernate dep from Serving (#721) --- core/pom.xml | 2 -- pom.xml | 6 ++++++ serving/pom.xml | 7 ------- 3 files changed, 6 insertions(+), 9 deletions(-) diff --git a/core/pom.xml b/core/pom.xml index c4679df5f59..f28a1fa28ba 100644 --- a/core/pom.xml +++ b/core/pom.xml @@ -157,11 +157,9 @@ v1b3-rev266-1.25.0 - org.hibernate hibernate-core - 5.3.6.Final diff --git a/pom.xml b/pom.xml index 0f1b5c94822..3211d585294 100644 --- a/pom.xml +++ b/pom.xml @@ -54,6 +54,7 @@ 0.8.0 1.9.10 1.3 + 5.3.6.Final 2.3.0 2.28.2 @@ -249,6 +250,11 @@ kafka-clients ${kafka.version} + + org.hibernate + hibernate-core + ${hibernate.version} + net.bytebuddy byte-buddy diff --git a/serving/pom.xml b/serving/pom.xml index ce4ee6f49c3..c7a30166f6b 100644 --- a/serving/pom.xml +++ b/serving/pom.xml @@ -267,13 +267,6 @@ test - - - org.hibernate - hibernate-core - 5.4.5.Final - - com.fasterxml.jackson.dataformat From 2504c918dcc3701d79c32384db0bb039b0f2e20c Mon Sep 17 00:00:00 2001 From: Oleksii Moskalenko Date: Thu, 28 May 2020 15:46:58 +0300 Subject: [PATCH 16/26] redis-cluster is being merged into redis (#752) styling handle null case styling --- docs/coverage/java/pom.xml | 6 - ingestion/pom.xml | 6 - .../java/feast/ingestion/utils/StoreUtil.java | 3 +- serving/pom.xml | 6 - .../serving/config/ServingServiceConfig.java | 2 +- storage/connectors/pom.xml | 1 - storage/connectors/redis/pom.xml | 7 + .../RedisClusterOnlineRetriever.java | 2 +- .../writer/RedisClusterIngestionClient.java | 2 +- .../redis/writer/RedisCustomIO.java | 28 +- .../redis/writer/RedisFeatureSink.java | 49 ++- .../RedisClusterOnlineRetrieverTest.java | 2 +- .../writer/RedisClusterFeatureSinkTest.java | 10 +- storage/connectors/rediscluster/pom.xml | 81 ----- .../retriever/FeatureRowDecoder.java | 82 ----- .../writer/RedisClusterCustomIO.java | 296 ------------------ .../writer/RedisClusterFeatureSink.java | 75 ----- .../writer/RedisIngestionClient.java | 49 --- 18 files changed, 70 insertions(+), 637 deletions(-) rename storage/connectors/{rediscluster/src/main/java/feast/storage/connectors/rediscluster => redis/src/main/java/feast/storage/connectors/redis}/retriever/RedisClusterOnlineRetriever.java (99%) rename storage/connectors/{rediscluster/src/main/java/feast/storage/connectors/rediscluster => redis/src/main/java/feast/storage/connectors/redis}/writer/RedisClusterIngestionClient.java (98%) rename storage/connectors/{rediscluster/src/test/java/feast/storage/connectors/rediscluster => redis/src/test/java/feast/storage/connectors/redis}/retriever/RedisClusterOnlineRetrieverTest.java (99%) rename storage/connectors/{rediscluster/src/test/java/feast/storage/connectors/rediscluster => redis/src/test/java/feast/storage/connectors/redis}/writer/RedisClusterFeatureSinkTest.java (98%) delete mode 100644 storage/connectors/rediscluster/pom.xml delete mode 100644 storage/connectors/rediscluster/src/main/java/feast/storage/connectors/rediscluster/retriever/FeatureRowDecoder.java delete mode 100644 storage/connectors/rediscluster/src/main/java/feast/storage/connectors/rediscluster/writer/RedisClusterCustomIO.java delete mode 100644 storage/connectors/rediscluster/src/main/java/feast/storage/connectors/rediscluster/writer/RedisClusterFeatureSink.java delete mode 100644 storage/connectors/rediscluster/src/main/java/feast/storage/connectors/rediscluster/writer/RedisIngestionClient.java diff --git a/docs/coverage/java/pom.xml b/docs/coverage/java/pom.xml index 05ffaf374e0..9666b7092c7 100644 --- a/docs/coverage/java/pom.xml +++ b/docs/coverage/java/pom.xml @@ -59,12 +59,6 @@ ${project.version} - - dev.feast - feast-storage-connector-redis-cluster - ${project.version} - - dev.feast feast-ingestion diff --git a/ingestion/pom.xml b/ingestion/pom.xml index a004c6d9df6..36d5cd6269b 100644 --- a/ingestion/pom.xml +++ b/ingestion/pom.xml @@ -126,12 +126,6 @@ ${project.version} - - dev.feast - feast-storage-connector-redis-cluster - ${project.version} - - dev.feast feast-storage-connector-bigquery diff --git a/ingestion/src/main/java/feast/ingestion/utils/StoreUtil.java b/ingestion/src/main/java/feast/ingestion/utils/StoreUtil.java index 54323869cf8..4efec994f7e 100644 --- a/ingestion/src/main/java/feast/ingestion/utils/StoreUtil.java +++ b/ingestion/src/main/java/feast/ingestion/utils/StoreUtil.java @@ -26,7 +26,6 @@ import feast.storage.api.writer.FeatureSink; import feast.storage.connectors.bigquery.writer.BigQueryFeatureSink; import feast.storage.connectors.redis.writer.RedisFeatureSink; -import feast.storage.connectors.rediscluster.writer.RedisClusterFeatureSink; import java.util.HashMap; import java.util.Map; import org.slf4j.Logger; @@ -84,7 +83,7 @@ public static FeatureSink getFeatureSink( StoreType storeType = store.getType(); switch (storeType) { case REDIS_CLUSTER: - return RedisClusterFeatureSink.fromConfig(store.getRedisClusterConfig(), featureSetSpecs); + return RedisFeatureSink.fromConfig(store.getRedisClusterConfig(), featureSetSpecs); case REDIS: return RedisFeatureSink.fromConfig(store.getRedisConfig(), featureSetSpecs); case BIGQUERY: diff --git a/serving/pom.xml b/serving/pom.xml index c7a30166f6b..8304899b515 100644 --- a/serving/pom.xml +++ b/serving/pom.xml @@ -110,12 +110,6 @@ ${project.version} - - dev.feast - feast-storage-connector-redis-cluster - ${project.version} - - dev.feast feast-storage-connector-bigquery diff --git a/serving/src/main/java/feast/serving/config/ServingServiceConfig.java b/serving/src/main/java/feast/serving/config/ServingServiceConfig.java index 46ffdb2fcc8..41a92e4bc64 100644 --- a/serving/src/main/java/feast/serving/config/ServingServiceConfig.java +++ b/serving/src/main/java/feast/serving/config/ServingServiceConfig.java @@ -28,8 +28,8 @@ import feast.storage.api.retriever.HistoricalRetriever; import feast.storage.api.retriever.OnlineRetriever; import feast.storage.connectors.bigquery.retriever.BigQueryHistoricalRetriever; +import feast.storage.connectors.redis.retriever.RedisClusterOnlineRetriever; import feast.storage.connectors.redis.retriever.RedisOnlineRetriever; -import feast.storage.connectors.rediscluster.retriever.RedisClusterOnlineRetriever; import io.opentracing.Tracer; import java.util.Map; import org.slf4j.Logger; diff --git a/storage/connectors/pom.xml b/storage/connectors/pom.xml index 280b0d40bf1..6cd949acfdf 100644 --- a/storage/connectors/pom.xml +++ b/storage/connectors/pom.xml @@ -16,7 +16,6 @@ redis - rediscluster bigquery diff --git a/storage/connectors/redis/pom.xml b/storage/connectors/redis/pom.xml index 6c50895bd20..2a19c0e8287 100644 --- a/storage/connectors/redis/pom.xml +++ b/storage/connectors/redis/pom.xml @@ -70,6 +70,13 @@ test + + net.ishiis.redis + redis-unit + 1.0.3 + test + + junit diff --git a/storage/connectors/rediscluster/src/main/java/feast/storage/connectors/rediscluster/retriever/RedisClusterOnlineRetriever.java b/storage/connectors/redis/src/main/java/feast/storage/connectors/redis/retriever/RedisClusterOnlineRetriever.java similarity index 99% rename from storage/connectors/rediscluster/src/main/java/feast/storage/connectors/rediscluster/retriever/RedisClusterOnlineRetriever.java rename to storage/connectors/redis/src/main/java/feast/storage/connectors/redis/retriever/RedisClusterOnlineRetriever.java index f80de0fc8af..18619252c27 100644 --- a/storage/connectors/rediscluster/src/main/java/feast/storage/connectors/rediscluster/retriever/RedisClusterOnlineRetriever.java +++ b/storage/connectors/redis/src/main/java/feast/storage/connectors/redis/retriever/RedisClusterOnlineRetriever.java @@ -14,7 +14,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package feast.storage.connectors.rediscluster.retriever; +package feast.storage.connectors.redis.retriever; import com.google.protobuf.AbstractMessageLite; import com.google.protobuf.InvalidProtocolBufferException; diff --git a/storage/connectors/rediscluster/src/main/java/feast/storage/connectors/rediscluster/writer/RedisClusterIngestionClient.java b/storage/connectors/redis/src/main/java/feast/storage/connectors/redis/writer/RedisClusterIngestionClient.java similarity index 98% rename from storage/connectors/rediscluster/src/main/java/feast/storage/connectors/rediscluster/writer/RedisClusterIngestionClient.java rename to storage/connectors/redis/src/main/java/feast/storage/connectors/redis/writer/RedisClusterIngestionClient.java index 5bd70d08b43..389db4be3ad 100644 --- a/storage/connectors/rediscluster/src/main/java/feast/storage/connectors/rediscluster/writer/RedisClusterIngestionClient.java +++ b/storage/connectors/redis/src/main/java/feast/storage/connectors/redis/writer/RedisClusterIngestionClient.java @@ -14,7 +14,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package feast.storage.connectors.rediscluster.writer; +package feast.storage.connectors.redis.writer; import com.google.common.collect.Lists; import feast.proto.core.StoreProto; diff --git a/storage/connectors/redis/src/main/java/feast/storage/connectors/redis/writer/RedisCustomIO.java b/storage/connectors/redis/src/main/java/feast/storage/connectors/redis/writer/RedisCustomIO.java index eeac5b3d964..b7fd68755c1 100644 --- a/storage/connectors/redis/src/main/java/feast/storage/connectors/redis/writer/RedisCustomIO.java +++ b/storage/connectors/redis/src/main/java/feast/storage/connectors/redis/writer/RedisCustomIO.java @@ -19,7 +19,7 @@ import feast.proto.core.FeatureSetProto.EntitySpec; import feast.proto.core.FeatureSetProto.FeatureSetSpec; import feast.proto.core.FeatureSetProto.FeatureSpec; -import feast.proto.core.StoreProto.Store.RedisConfig; +import feast.proto.core.StoreProto.Store.*; import feast.proto.storage.RedisProto.RedisKey; import feast.proto.storage.RedisProto.RedisKey.Builder; import feast.proto.types.FeatureRowProto.FeatureRow; @@ -28,7 +28,7 @@ import feast.storage.api.writer.FailedElement; import feast.storage.api.writer.WriteResult; import feast.storage.common.retry.Retriable; -import io.lettuce.core.RedisConnectionException; +import io.lettuce.core.RedisException; import java.io.IOException; import java.util.ArrayList; import java.util.HashMap; @@ -63,21 +63,22 @@ public class RedisCustomIO { private RedisCustomIO() {} - public static Write write(RedisConfig redisConfig, Map featureSetSpecs) { - return new Write(redisConfig, featureSetSpecs); + public static Write write( + RedisIngestionClient redisIngestionClient, Map featureSetSpecs) { + return new Write(redisIngestionClient, featureSetSpecs); } /** ServingStoreWrite data to a Redis server. */ public static class Write extends PTransform, WriteResult> { private Map featureSetSpecs; - private RedisConfig redisConfig; + private RedisIngestionClient redisIngestionClient; private int batchSize; private int timeout; - public Write(RedisConfig redisConfig, Map featureSetSpecs) { - - this.redisConfig = redisConfig; + public Write( + RedisIngestionClient redisIngestionClient, Map featureSetSpecs) { + this.redisIngestionClient = redisIngestionClient; this.featureSetSpecs = featureSetSpecs; } @@ -95,7 +96,7 @@ public Write withTimeout(int timeout) { public WriteResult expand(PCollection input) { PCollectionTuple redisWrite = input.apply( - ParDo.of(new WriteDoFn(redisConfig, featureSetSpecs)) + ParDo.of(new WriteDoFn(redisIngestionClient, featureSetSpecs)) .withOutputTags(successfulInsertsTag, TupleTagList.of(failedInsertsTupleTag))); return WriteResult.in( input.getPipeline(), @@ -111,9 +112,10 @@ public static class WriteDoFn extends DoFn { private int timeout = DEFAULT_TIMEOUT; private RedisIngestionClient redisIngestionClient; - WriteDoFn(RedisConfig config, Map featureSetSpecs) { + WriteDoFn( + RedisIngestionClient redisIngestionClient, Map featureSetSpecs) { - this.redisIngestionClient = new RedisStandaloneIngestionClient(config); + this.redisIngestionClient = redisIngestionClient; this.featureSetSpecs = featureSetSpecs; } @@ -140,7 +142,7 @@ public void setup() { public void startBundle() { try { redisIngestionClient.connect(); - } catch (RedisConnectionException e) { + } catch (RedisException e) { log.error("Connection to redis cannot be established ", e); } featureRows.clear(); @@ -165,7 +167,7 @@ public void execute() throws ExecutionException, InterruptedException { @Override public Boolean isExceptionRetriable(Exception e) { - return e instanceof RedisConnectionException; + return e instanceof RedisException; } @Override diff --git a/storage/connectors/redis/src/main/java/feast/storage/connectors/redis/writer/RedisFeatureSink.java b/storage/connectors/redis/src/main/java/feast/storage/connectors/redis/writer/RedisFeatureSink.java index 171f17daac2..3ddaef7779e 100644 --- a/storage/connectors/redis/src/main/java/feast/storage/connectors/redis/writer/RedisFeatureSink.java +++ b/storage/connectors/redis/src/main/java/feast/storage/connectors/redis/writer/RedisFeatureSink.java @@ -20,6 +20,7 @@ import feast.proto.core.FeatureSetProto.FeatureSet; import feast.proto.core.FeatureSetProto.FeatureSetSpec; import feast.proto.core.StoreProto; +import feast.proto.core.StoreProto.Store.RedisClusterConfig; import feast.proto.core.StoreProto.Store.RedisConfig; import feast.proto.types.FeatureRowProto.FeatureRow; import feast.storage.api.writer.FeatureSink; @@ -28,6 +29,7 @@ import io.lettuce.core.RedisConnectionException; import io.lettuce.core.RedisURI; import java.util.Map; +import javax.annotation.Nullable; import org.apache.beam.sdk.transforms.PTransform; import org.apache.beam.sdk.values.PCollection; @@ -46,8 +48,17 @@ public static FeatureSink fromConfig( return builder().setFeatureSetSpecs(featureSetSpecs).setRedisConfig(redisConfig).build(); } + public static FeatureSink fromConfig( + RedisClusterConfig redisConfig, Map featureSetSpecs) { + return builder().setFeatureSetSpecs(featureSetSpecs).setRedisClusterConfig(redisConfig).build(); + } + + @Nullable public abstract RedisConfig getRedisConfig(); + @Nullable + public abstract RedisClusterConfig getRedisClusterConfig(); + public abstract Map getFeatureSetSpecs(); public abstract Builder toBuilder(); @@ -60,6 +71,8 @@ public static Builder builder() { public abstract static class Builder { public abstract Builder setRedisConfig(RedisConfig redisConfig); + public abstract Builder setRedisClusterConfig(RedisClusterConfig redisConfig); + public abstract Builder setFeatureSetSpecs(Map featureSetSpecs); public abstract RedisFeatureSink build(); @@ -67,22 +80,36 @@ public abstract static class Builder { @Override public void prepareWrite(FeatureSet featureSet) { - - RedisClient redisClient = - RedisClient.create(RedisURI.create(getRedisConfig().getHost(), getRedisConfig().getPort())); - try { - redisClient.connect(); - } catch (RedisConnectionException e) { + if (getRedisConfig() != null) { + RedisClient redisClient = + RedisClient.create( + RedisURI.create(getRedisConfig().getHost(), getRedisConfig().getPort())); + try { + redisClient.connect(); + } catch (RedisConnectionException e) { + throw new RuntimeException( + String.format( + "Failed to connect to Redis at host: '%s' port: '%d'. Please check that your Redis is running and accessible from Feast.", + getRedisConfig().getHost(), getRedisConfig().getPort())); + } + redisClient.shutdown(); + } else if (getRedisClusterConfig() == null) { throw new RuntimeException( - String.format( - "Failed to connect to Redis at host: '%s' port: '%d'. Please check that your Redis is running and accessible from Feast.", - getRedisConfig().getHost(), getRedisConfig().getPort())); + "At least one RedisConfig or RedisClusterConfig must be provided to Redis Sink"); } - redisClient.shutdown(); } @Override public PTransform, WriteResult> writer() { - return new RedisCustomIO.Write(getRedisConfig(), getFeatureSetSpecs()); + if (getRedisClusterConfig() != null) { + return new RedisCustomIO.Write( + new RedisClusterIngestionClient(getRedisClusterConfig()), getFeatureSetSpecs()); + } else if (getRedisConfig() != null) { + return new RedisCustomIO.Write( + new RedisStandaloneIngestionClient(getRedisConfig()), getFeatureSetSpecs()); + } else { + throw new RuntimeException( + "At least one RedisConfig or RedisClusterConfig must be provided to Redis Sink"); + } } } diff --git a/storage/connectors/rediscluster/src/test/java/feast/storage/connectors/rediscluster/retriever/RedisClusterOnlineRetrieverTest.java b/storage/connectors/redis/src/test/java/feast/storage/connectors/redis/retriever/RedisClusterOnlineRetrieverTest.java similarity index 99% rename from storage/connectors/rediscluster/src/test/java/feast/storage/connectors/rediscluster/retriever/RedisClusterOnlineRetrieverTest.java rename to storage/connectors/redis/src/test/java/feast/storage/connectors/redis/retriever/RedisClusterOnlineRetrieverTest.java index deed53e1ae8..45842cef6ee 100644 --- a/storage/connectors/rediscluster/src/test/java/feast/storage/connectors/rediscluster/retriever/RedisClusterOnlineRetrieverTest.java +++ b/storage/connectors/redis/src/test/java/feast/storage/connectors/redis/retriever/RedisClusterOnlineRetrieverTest.java @@ -14,7 +14,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package feast.storage.connectors.rediscluster.retriever; +package feast.storage.connectors.redis.retriever; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.equalTo; diff --git a/storage/connectors/rediscluster/src/test/java/feast/storage/connectors/rediscluster/writer/RedisClusterFeatureSinkTest.java b/storage/connectors/redis/src/test/java/feast/storage/connectors/redis/writer/RedisClusterFeatureSinkTest.java similarity index 98% rename from storage/connectors/rediscluster/src/test/java/feast/storage/connectors/rediscluster/writer/RedisClusterFeatureSinkTest.java rename to storage/connectors/redis/src/test/java/feast/storage/connectors/redis/writer/RedisClusterFeatureSinkTest.java index 8c5afd3d679..7bdc9bba2bb 100644 --- a/storage/connectors/rediscluster/src/test/java/feast/storage/connectors/rediscluster/writer/RedisClusterFeatureSinkTest.java +++ b/storage/connectors/redis/src/test/java/feast/storage/connectors/redis/writer/RedisClusterFeatureSinkTest.java @@ -14,7 +14,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package feast.storage.connectors.rediscluster.writer; +package feast.storage.connectors.redis.writer; import static feast.storage.common.testing.TestUtil.field; import static org.hamcrest.CoreMatchers.equalTo; @@ -67,7 +67,7 @@ public class RedisClusterFeatureSinkTest { private RedisClusterClient redisClusterClient; private RedisClusterCommands redisClusterCommands; - private RedisClusterFeatureSink redisClusterFeatureSink; + private RedisFeatureSink redisClusterFeatureSink; @Before public void setUp() throws IOException { @@ -123,7 +123,7 @@ public void setUp() throws IOException { .build(); redisClusterFeatureSink = - RedisClusterFeatureSink.builder() + RedisFeatureSink.builder() .setFeatureSetSpecs(specMap) .setRedisClusterConfig(redisClusterConfig) .build(); @@ -141,8 +141,8 @@ static boolean deleteDirectory(File directoryToBeDeleted) { @After public void teardown() { - redisClusterClient.shutdown(); redisCluster.stop(); + redisClusterClient.shutdown(); deleteDirectory(new File(String.valueOf(Paths.get(System.getProperty("user.dir"), ".redis")))); } @@ -259,7 +259,7 @@ public void shouldProduceFailedElementIfRetryExceeded() { .build(); Map specMap = ImmutableMap.of("myproject/fs", spec1); redisClusterFeatureSink = - RedisClusterFeatureSink.builder() + RedisFeatureSink.builder() .setFeatureSetSpecs(specMap) .setRedisClusterConfig(redisClusterConfig) .build(); diff --git a/storage/connectors/rediscluster/pom.xml b/storage/connectors/rediscluster/pom.xml deleted file mode 100644 index 5c3cb6e42d3..00000000000 --- a/storage/connectors/rediscluster/pom.xml +++ /dev/null @@ -1,81 +0,0 @@ - - - - dev.feast - feast-storage-connectors - ${revision} - - - 4.0.0 - feast-storage-connector-redis-cluster - - Feast Storage Connector for Redis Cluster - - - - io.lettuce - lettuce-core - - - - org.apache.commons - commons-lang3 - 3.9 - - - - com.google.auto.value - auto-value-annotations - 1.6.6 - - - - com.google.auto.value - auto-value - 1.6.6 - provided - - - - org.mockito - mockito-core - 2.23.0 - test - - - - org.apache.beam - beam-runners-direct-java - ${org.apache.beam.version} - test - - - - org.hamcrest - hamcrest-core - test - - - - org.hamcrest - hamcrest-library - test - - - - net.ishiis.redis - redis-unit - 1.0.3 - test - - - - junit - junit - 4.12 - test - - - - diff --git a/storage/connectors/rediscluster/src/main/java/feast/storage/connectors/rediscluster/retriever/FeatureRowDecoder.java b/storage/connectors/rediscluster/src/main/java/feast/storage/connectors/rediscluster/retriever/FeatureRowDecoder.java deleted file mode 100644 index d398752aa47..00000000000 --- a/storage/connectors/rediscluster/src/main/java/feast/storage/connectors/rediscluster/retriever/FeatureRowDecoder.java +++ /dev/null @@ -1,82 +0,0 @@ -/* - * SPDX-License-Identifier: Apache-2.0 - * Copyright 2018-2020 The Feast Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package feast.storage.connectors.rediscluster.retriever; - -import feast.proto.core.FeatureSetProto.FeatureSetSpec; -import feast.proto.core.FeatureSetProto.FeatureSpec; -import feast.proto.types.FeatureRowProto.FeatureRow; -import feast.proto.types.FieldProto.Field; -import java.util.Comparator; -import java.util.List; -import java.util.stream.Collectors; -import java.util.stream.IntStream; - -public class FeatureRowDecoder { - - private final String featureSetRef; - private final FeatureSetSpec spec; - - public FeatureRowDecoder(String featureSetRef, FeatureSetSpec spec) { - this.featureSetRef = featureSetRef; - this.spec = spec; - } - - /** - * Validates if an encoded feature row can be decoded without exception. - * - * @param featureRow Feature row - * @return boolean - */ - public Boolean isEncodingValid(FeatureRow featureRow) { - return featureRow.getFieldsList().size() == spec.getFeaturesList().size(); - } - - /** - * Decoding feature row by repopulating the field names based on the corresponding feature set - * spec. - * - * @param encodedFeatureRow Feature row - * @return boolean - */ - public FeatureRow decode(FeatureRow encodedFeatureRow) { - final List fieldsWithoutName = encodedFeatureRow.getFieldsList(); - - List featureNames = - spec.getFeaturesList().stream() - .sorted(Comparator.comparing(FeatureSpec::getName)) - .map(FeatureSpec::getName) - .collect(Collectors.toList()); - List fields = - IntStream.range(0, featureNames.size()) - .mapToObj( - featureNameIndex -> { - String featureName = featureNames.get(featureNameIndex); - return fieldsWithoutName - .get(featureNameIndex) - .toBuilder() - .setName(featureName) - .build(); - }) - .collect(Collectors.toList()); - return encodedFeatureRow - .toBuilder() - .clearFields() - .setFeatureSet(featureSetRef) - .addAllFields(fields) - .build(); - } -} diff --git a/storage/connectors/rediscluster/src/main/java/feast/storage/connectors/rediscluster/writer/RedisClusterCustomIO.java b/storage/connectors/rediscluster/src/main/java/feast/storage/connectors/rediscluster/writer/RedisClusterCustomIO.java deleted file mode 100644 index 712012a2fb7..00000000000 --- a/storage/connectors/rediscluster/src/main/java/feast/storage/connectors/rediscluster/writer/RedisClusterCustomIO.java +++ /dev/null @@ -1,296 +0,0 @@ -/* - * SPDX-License-Identifier: Apache-2.0 - * Copyright 2018-2019 The Feast Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package feast.storage.connectors.rediscluster.writer; - -import feast.proto.core.FeatureSetProto.EntitySpec; -import feast.proto.core.FeatureSetProto.FeatureSetSpec; -import feast.proto.core.FeatureSetProto.FeatureSpec; -import feast.proto.core.StoreProto.Store.RedisClusterConfig; -import feast.proto.storage.RedisProto.RedisKey; -import feast.proto.storage.RedisProto.RedisKey.Builder; -import feast.proto.types.FeatureRowProto.FeatureRow; -import feast.proto.types.FieldProto.Field; -import feast.proto.types.ValueProto; -import feast.storage.api.writer.FailedElement; -import feast.storage.api.writer.WriteResult; -import feast.storage.common.retry.Retriable; -import io.lettuce.core.RedisException; -import java.io.IOException; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.concurrent.ExecutionException; -import java.util.stream.Collectors; -import org.apache.beam.sdk.transforms.DoFn; -import org.apache.beam.sdk.transforms.PTransform; -import org.apache.beam.sdk.transforms.ParDo; -import org.apache.beam.sdk.transforms.windowing.GlobalWindow; -import org.apache.beam.sdk.values.PCollection; -import org.apache.beam.sdk.values.PCollectionTuple; -import org.apache.beam.sdk.values.TupleTag; -import org.apache.beam.sdk.values.TupleTagList; -import org.apache.commons.lang3.exception.ExceptionUtils; -import org.joda.time.Instant; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -public class RedisClusterCustomIO { - - private static final int DEFAULT_BATCH_SIZE = 1000; - private static final int DEFAULT_TIMEOUT = 2000; - - private static TupleTag successfulInsertsTag = - new TupleTag("successfulInserts") {}; - private static TupleTag failedInsertsTupleTag = - new TupleTag("failedInserts") {}; - - private static final Logger log = LoggerFactory.getLogger(RedisClusterCustomIO.class); - - private RedisClusterCustomIO() {} - - public static Write write( - RedisClusterConfig redisClusterConfig, Map featureSetSpecs) { - return new Write(redisClusterConfig, featureSetSpecs); - } - - /** ServingStoreWrite data to a Redis server. */ - public static class Write extends PTransform, WriteResult> { - - private Map featureSetSpecs; - private RedisClusterConfig redisClusterConfig; - private int batchSize; - private int timeout; - - public Write( - RedisClusterConfig redisClusterConfig, Map featureSetSpecs) { - - this.redisClusterConfig = redisClusterConfig; - this.featureSetSpecs = featureSetSpecs; - } - - public Write withBatchSize(int batchSize) { - this.batchSize = batchSize; - return this; - } - - public Write withTimeout(int timeout) { - this.timeout = timeout; - return this; - } - - @Override - public WriteResult expand(PCollection input) { - PCollectionTuple redisWrite = - input.apply( - ParDo.of(new WriteDoFn(redisClusterConfig, featureSetSpecs)) - .withOutputTags(successfulInsertsTag, TupleTagList.of(failedInsertsTupleTag))); - return WriteResult.in( - input.getPipeline(), - redisWrite.get(successfulInsertsTag), - redisWrite.get(failedInsertsTupleTag)); - } - - public static class WriteDoFn extends DoFn { - - private final List featureRows = new ArrayList<>(); - private Map featureSetSpecs; - private int batchSize = DEFAULT_BATCH_SIZE; - private int timeout = DEFAULT_TIMEOUT; - private RedisIngestionClient redisIngestionClient; - - WriteDoFn(RedisClusterConfig config, Map featureSetSpecs) { - - this.redisIngestionClient = new RedisClusterIngestionClient(config); - this.featureSetSpecs = featureSetSpecs; - } - - public WriteDoFn withBatchSize(int batchSize) { - if (batchSize > 0) { - this.batchSize = batchSize; - } - return this; - } - - public WriteDoFn withTimeout(int timeout) { - if (timeout > 0) { - this.timeout = timeout; - } - return this; - } - - @Setup - public void setup() { - this.redisIngestionClient.setup(); - } - - @StartBundle - public void startBundle() { - try { - redisIngestionClient.connect(); - } catch (RedisException e) { - log.error("Connection to redis cannot be established ", e); - } - featureRows.clear(); - } - - private void executeBatch() throws Exception { - this.redisIngestionClient - .getBackOffExecutor() - .execute( - new Retriable() { - @Override - public void execute() throws ExecutionException, InterruptedException { - if (!redisIngestionClient.isConnected()) { - redisIngestionClient.connect(); - } - featureRows.forEach( - row -> { - redisIngestionClient.set(getKey(row), getValue(row)); - }); - redisIngestionClient.sync(); - } - - @Override - public Boolean isExceptionRetriable(Exception e) { - return e instanceof RedisException; - } - - @Override - public void cleanUpAfterFailure() {} - }); - } - - private FailedElement toFailedElement( - FeatureRow featureRow, Exception exception, String jobName) { - return FailedElement.newBuilder() - .setJobName(jobName) - .setTransformName("RedisClusterCustomIO") - .setPayload(featureRow.toString()) - .setErrorMessage(exception.getMessage()) - .setStackTrace(ExceptionUtils.getStackTrace(exception)) - .build(); - } - - private byte[] getKey(FeatureRow featureRow) { - FeatureSetSpec featureSetSpec = featureSetSpecs.get(featureRow.getFeatureSet()); - List entityNames = - featureSetSpec.getEntitiesList().stream() - .map(EntitySpec::getName) - .sorted() - .collect(Collectors.toList()); - - Map entityFields = new HashMap<>(); - Builder redisKeyBuilder = RedisKey.newBuilder().setFeatureSet(featureRow.getFeatureSet()); - for (Field field : featureRow.getFieldsList()) { - if (entityNames.contains(field.getName())) { - entityFields.putIfAbsent( - field.getName(), - Field.newBuilder().setName(field.getName()).setValue(field.getValue()).build()); - } - } - for (String entityName : entityNames) { - redisKeyBuilder.addEntities(entityFields.get(entityName)); - } - return redisKeyBuilder.build().toByteArray(); - } - - private byte[] getValue(FeatureRow featureRow) { - FeatureSetSpec spec = featureSetSpecs.get(featureRow.getFeatureSet()); - - List featureNames = - spec.getFeaturesList().stream().map(FeatureSpec::getName).collect(Collectors.toList()); - Map fieldValueOnlyMap = - featureRow.getFieldsList().stream() - .filter(field -> featureNames.contains(field.getName())) - .distinct() - .collect( - Collectors.toMap( - Field::getName, - field -> Field.newBuilder().setValue(field.getValue()).build())); - - List values = - featureNames.stream() - .sorted() - .map( - featureName -> - fieldValueOnlyMap.getOrDefault( - featureName, - Field.newBuilder() - .setValue(ValueProto.Value.getDefaultInstance()) - .build())) - .collect(Collectors.toList()); - - return FeatureRow.newBuilder() - .setEventTimestamp(featureRow.getEventTimestamp()) - .addAllFields(values) - .build() - .toByteArray(); - } - - @ProcessElement - public void processElement(ProcessContext context) { - FeatureRow featureRow = context.element(); - featureRows.add(featureRow); - if (featureRows.size() >= batchSize) { - try { - executeBatch(); - featureRows.forEach(row -> context.output(successfulInsertsTag, row)); - featureRows.clear(); - } catch (Exception e) { - featureRows.forEach( - failedMutation -> { - FailedElement failedElement = - toFailedElement(failedMutation, e, context.getPipelineOptions().getJobName()); - context.output(failedInsertsTupleTag, failedElement); - }); - featureRows.clear(); - } - } - } - - @FinishBundle - public void finishBundle(FinishBundleContext context) - throws IOException, InterruptedException { - if (featureRows.size() > 0) { - try { - executeBatch(); - featureRows.forEach( - row -> - context.output( - successfulInsertsTag, row, Instant.now(), GlobalWindow.INSTANCE)); - featureRows.clear(); - } catch (Exception e) { - featureRows.forEach( - failedMutation -> { - FailedElement failedElement = - toFailedElement(failedMutation, e, context.getPipelineOptions().getJobName()); - context.output( - failedInsertsTupleTag, failedElement, Instant.now(), GlobalWindow.INSTANCE); - }); - featureRows.clear(); - } - } - } - - @Teardown - public void teardown() { - redisIngestionClient.shutdown(); - } - } - } -} diff --git a/storage/connectors/rediscluster/src/main/java/feast/storage/connectors/rediscluster/writer/RedisClusterFeatureSink.java b/storage/connectors/rediscluster/src/main/java/feast/storage/connectors/rediscluster/writer/RedisClusterFeatureSink.java deleted file mode 100644 index 160df6553cc..00000000000 --- a/storage/connectors/rediscluster/src/main/java/feast/storage/connectors/rediscluster/writer/RedisClusterFeatureSink.java +++ /dev/null @@ -1,75 +0,0 @@ -/* - * SPDX-License-Identifier: Apache-2.0 - * Copyright 2018-2020 The Feast Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package feast.storage.connectors.rediscluster.writer; - -import com.google.auto.value.AutoValue; -import feast.proto.core.FeatureSetProto; -import feast.proto.core.StoreProto.Store.RedisClusterConfig; -import feast.proto.types.FeatureRowProto; -import feast.storage.api.writer.FeatureSink; -import feast.storage.api.writer.WriteResult; -import java.util.Map; -import org.apache.beam.sdk.transforms.PTransform; -import org.apache.beam.sdk.values.PCollection; - -@AutoValue -public abstract class RedisClusterFeatureSink implements FeatureSink { - - /** - * Initialize a {@link RedisClusterFeatureSink.Builder} from a {@link RedisClusterConfig}. - * - * @param redisClusterConfig {@link RedisClusterConfig} - * @param featureSetSpecs - * @return {@link RedisClusterFeatureSink.Builder} - */ - public static FeatureSink fromConfig( - RedisClusterConfig redisClusterConfig, - Map featureSetSpecs) { - return builder() - .setFeatureSetSpecs(featureSetSpecs) - .setRedisClusterConfig(redisClusterConfig) - .build(); - } - - public abstract RedisClusterConfig getRedisClusterConfig(); - - public abstract Map getFeatureSetSpecs(); - - public abstract Builder toBuilder(); - - public static Builder builder() { - return new AutoValue_RedisClusterFeatureSink.Builder(); - } - - @AutoValue.Builder - public abstract static class Builder { - public abstract Builder setRedisClusterConfig(RedisClusterConfig redisClusterConfig); - - public abstract Builder setFeatureSetSpecs( - Map featureSetSpecs); - - public abstract RedisClusterFeatureSink build(); - } - - @Override - public void prepareWrite(FeatureSetProto.FeatureSet featureSet) {} - - @Override - public PTransform, WriteResult> writer() { - return new RedisClusterCustomIO.Write(getRedisClusterConfig(), getFeatureSetSpecs()); - } -} diff --git a/storage/connectors/rediscluster/src/main/java/feast/storage/connectors/rediscluster/writer/RedisIngestionClient.java b/storage/connectors/rediscluster/src/main/java/feast/storage/connectors/rediscluster/writer/RedisIngestionClient.java deleted file mode 100644 index 5a0b54e6970..00000000000 --- a/storage/connectors/rediscluster/src/main/java/feast/storage/connectors/rediscluster/writer/RedisIngestionClient.java +++ /dev/null @@ -1,49 +0,0 @@ -/* - * SPDX-License-Identifier: Apache-2.0 - * Copyright 2018-2020 The Feast Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package feast.storage.connectors.rediscluster.writer; - -import feast.storage.common.retry.BackOffExecutor; -import java.io.Serializable; - -public interface RedisIngestionClient extends Serializable { - - void setup(); - - BackOffExecutor getBackOffExecutor(); - - void shutdown(); - - void connect(); - - boolean isConnected(); - - void sync(); - - void pexpire(byte[] key, Long expiryMillis); - - void append(byte[] key, byte[] value); - - void set(byte[] key, byte[] value); - - void lpush(byte[] key, byte[] value); - - void rpush(byte[] key, byte[] value); - - void sadd(byte[] key, byte[] value); - - void zadd(byte[] key, Long score, byte[] value); -} From 8bac2404c5e218604b71352eb342e78a5424cf61 Mon Sep 17 00:00:00 2001 From: Chen Zhiling Date: Fri, 29 May 2020 13:37:58 +0800 Subject: [PATCH 17/26] Recompile golang protos (#755) --- go.mod | 4 +-- go.sum | 6 ++++ sdk/go/protos/feast/core/CoreService.pb.go | 2 +- sdk/go/protos/feast/core/FeatureSet.pb.go | 36 ++++++++++--------- .../feast/core/FeatureSetReference.pb.go | 2 +- sdk/go/protos/feast/core/IngestionJob.pb.go | 2 +- sdk/go/protos/feast/core/Runner.pb.go | 2 +- sdk/go/protos/feast/core/Source.pb.go | 2 +- sdk/go/protos/feast/core/Store.pb.go | 2 +- .../protos/feast/serving/ServingService.pb.go | 2 +- sdk/go/protos/feast/storage/Redis.pb.go | 2 +- sdk/go/protos/feast/types/FeatureRow.pb.go | 2 +- .../feast/types/FeatureRowExtended.pb.go | 2 +- sdk/go/protos/feast/types/Field.pb.go | 2 +- sdk/go/protos/feast/types/Value.pb.go | 2 +- .../tensorflow_metadata/proto/v0/path.pb.go | 2 +- .../tensorflow_metadata/proto/v0/schema.pb.go | 2 +- 17 files changed, 42 insertions(+), 32 deletions(-) diff --git a/go.mod b/go.mod index 9a4456616b7..1be2396f6e6 100644 --- a/go.mod +++ b/go.mod @@ -25,9 +25,9 @@ require ( golang.org/x/lint v0.0.0-20200302205851-738671d3881b // indirect golang.org/x/net v0.0.0-20200513185701-a91f0712d120 golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9 // indirect - golang.org/x/tools v0.0.0-20200515220128-d3bf790afa53 // indirect - google.golang.org/genproto v0.0.0-20200515170657-fc4c6c6a6587 // indirect + golang.org/x/tools v0.0.0-20200528185414-6be401e3f76e // indirect google.golang.org/grpc v1.29.1 + google.golang.org/protobuf v1.24.0 // indirect gopkg.in/russross/blackfriday.v2 v2.0.0 // indirect gopkg.in/yaml.v2 v2.2.4 istio.io/gogo-genproto v0.0.0-20191212213402-78a529a42cd8 // indirect diff --git a/go.sum b/go.sum index 7608c511c25..1b2aa8bf63b 100644 --- a/go.sum +++ b/go.sum @@ -462,6 +462,8 @@ golang.org/x/tools v0.0.0-20200504022951-6b6965ac5dd1 h1:C8rdnd6KieI73Z2Av0sS0t4 golang.org/x/tools v0.0.0-20200504022951-6b6965ac5dd1/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20200515220128-d3bf790afa53 h1:vmsb6v0zUdmUlXfwKaYrHPPRCV0lHq/IwNIf0ASGjyQ= golang.org/x/tools v0.0.0-20200515220128-d3bf790afa53/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200528185414-6be401e3f76e h1:jTL1CJ2kmavapMVdBKy6oVrhBHByRCMfykS45+lEFQk= +golang.org/x/tools v0.0.0-20200528185414-6be401e3f76e/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= @@ -487,6 +489,7 @@ google.golang.org/genproto v0.0.0-20200319113533-08878b785e9c h1:5aI3/f/3eCZps9x google.golang.org/genproto v0.0.0-20200319113533-08878b785e9c/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= google.golang.org/genproto v0.0.0-20200515170657-fc4c6c6a6587 h1:1Ym+vvUpq1ZHvxzn34gENJX8U4aKO+vhy2P/2+Xl6qQ= google.golang.org/genproto v0.0.0-20200515170657-fc4c6c6a6587/go.mod h1:YsZOwe1myG/8QRHRsmBRE1LrgQY60beZKjly0O1fX9U= +google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= google.golang.org/grpc v1.13.0/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= google.golang.org/grpc v1.19.1 h1:TrBcJ1yqAl1G++wO39nD/qtgpsW9/1+QGrluyMGEYgM= @@ -512,6 +515,9 @@ google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzi google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= google.golang.org/protobuf v1.23.0 h1:4MY060fB1DLGMB/7MBTLnwQUY6+F09GEiz6SsrNqyzM= google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.24.0 h1:UhZDfRO8JRQru4/+LlLE0BRKGF8L+PICnvYZmx/fEGA= +google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4= gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= diff --git a/sdk/go/protos/feast/core/CoreService.pb.go b/sdk/go/protos/feast/core/CoreService.pb.go index 75f988ed528..4f22ae4c373 100644 --- a/sdk/go/protos/feast/core/CoreService.pb.go +++ b/sdk/go/protos/feast/core/CoreService.pb.go @@ -16,7 +16,7 @@ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: -// protoc-gen-go v1.23.0 +// protoc-gen-go v1.24.0 // protoc v3.10.0 // source: feast/core/CoreService.proto diff --git a/sdk/go/protos/feast/core/FeatureSet.pb.go b/sdk/go/protos/feast/core/FeatureSet.pb.go index 3c6441f2ac9..acd111614d2 100644 --- a/sdk/go/protos/feast/core/FeatureSet.pb.go +++ b/sdk/go/protos/feast/core/FeatureSet.pb.go @@ -16,7 +16,7 @@ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: -// protoc-gen-go v1.23.0 +// protoc-gen-go v1.24.0 // protoc v3.10.0 // source: feast/core/FeatureSet.proto @@ -48,9 +48,10 @@ const _ = proto.ProtoPackageIsVersion4 type FeatureSetStatus int32 const ( - FeatureSetStatus_STATUS_INVALID FeatureSetStatus = 0 - FeatureSetStatus_STATUS_PENDING FeatureSetStatus = 1 - FeatureSetStatus_STATUS_READY FeatureSetStatus = 2 + FeatureSetStatus_STATUS_INVALID FeatureSetStatus = 0 + FeatureSetStatus_STATUS_PENDING FeatureSetStatus = 1 + FeatureSetStatus_STATUS_JOB_STARTING FeatureSetStatus = 3 + FeatureSetStatus_STATUS_READY FeatureSetStatus = 2 ) // Enum value maps for FeatureSetStatus. @@ -58,12 +59,14 @@ var ( FeatureSetStatus_name = map[int32]string{ 0: "STATUS_INVALID", 1: "STATUS_PENDING", + 3: "STATUS_JOB_STARTING", 2: "STATUS_READY", } FeatureSetStatus_value = map[string]int32{ - "STATUS_INVALID": 0, - "STATUS_PENDING": 1, - "STATUS_READY": 2, + "STATUS_INVALID": 0, + "STATUS_PENDING": 1, + "STATUS_JOB_STARTING": 3, + "STATUS_READY": 2, } ) @@ -873,18 +876,19 @@ var file_feast_core_FeatureSet_proto_rawDesc = []byte{ 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x1c, 0x2e, 0x66, 0x65, 0x61, 0x73, 0x74, 0x2e, 0x63, 0x6f, 0x72, 0x65, 0x2e, 0x46, 0x65, 0x61, 0x74, 0x75, 0x72, 0x65, 0x53, 0x65, 0x74, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x52, 0x06, 0x73, 0x74, 0x61, - 0x74, 0x75, 0x73, 0x2a, 0x4c, 0x0a, 0x10, 0x46, 0x65, 0x61, 0x74, 0x75, 0x72, 0x65, 0x53, 0x65, + 0x74, 0x75, 0x73, 0x2a, 0x65, 0x0a, 0x10, 0x46, 0x65, 0x61, 0x74, 0x75, 0x72, 0x65, 0x53, 0x65, 0x74, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x12, 0x0a, 0x0e, 0x53, 0x54, 0x41, 0x54, 0x55, 0x53, 0x5f, 0x49, 0x4e, 0x56, 0x41, 0x4c, 0x49, 0x44, 0x10, 0x00, 0x12, 0x12, 0x0a, 0x0e, 0x53, 0x54, 0x41, 0x54, 0x55, 0x53, 0x5f, 0x50, 0x45, 0x4e, 0x44, 0x49, 0x4e, 0x47, 0x10, 0x01, 0x12, - 0x10, 0x0a, 0x0c, 0x53, 0x54, 0x41, 0x54, 0x55, 0x53, 0x5f, 0x52, 0x45, 0x41, 0x44, 0x59, 0x10, - 0x02, 0x42, 0x58, 0x0a, 0x10, 0x66, 0x65, 0x61, 0x73, 0x74, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, - 0x2e, 0x63, 0x6f, 0x72, 0x65, 0x42, 0x0f, 0x46, 0x65, 0x61, 0x74, 0x75, 0x72, 0x65, 0x53, 0x65, - 0x74, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x5a, 0x33, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, - 0x6f, 0x6d, 0x2f, 0x66, 0x65, 0x61, 0x73, 0x74, 0x2d, 0x64, 0x65, 0x76, 0x2f, 0x66, 0x65, 0x61, - 0x73, 0x74, 0x2f, 0x73, 0x64, 0x6b, 0x2f, 0x67, 0x6f, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x73, - 0x2f, 0x66, 0x65, 0x61, 0x73, 0x74, 0x2f, 0x63, 0x6f, 0x72, 0x65, 0x62, 0x06, 0x70, 0x72, 0x6f, - 0x74, 0x6f, 0x33, + 0x17, 0x0a, 0x13, 0x53, 0x54, 0x41, 0x54, 0x55, 0x53, 0x5f, 0x4a, 0x4f, 0x42, 0x5f, 0x53, 0x54, + 0x41, 0x52, 0x54, 0x49, 0x4e, 0x47, 0x10, 0x03, 0x12, 0x10, 0x0a, 0x0c, 0x53, 0x54, 0x41, 0x54, + 0x55, 0x53, 0x5f, 0x52, 0x45, 0x41, 0x44, 0x59, 0x10, 0x02, 0x42, 0x58, 0x0a, 0x10, 0x66, 0x65, + 0x61, 0x73, 0x74, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x63, 0x6f, 0x72, 0x65, 0x42, 0x0f, + 0x46, 0x65, 0x61, 0x74, 0x75, 0x72, 0x65, 0x53, 0x65, 0x74, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x5a, + 0x33, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x66, 0x65, 0x61, 0x73, + 0x74, 0x2d, 0x64, 0x65, 0x76, 0x2f, 0x66, 0x65, 0x61, 0x73, 0x74, 0x2f, 0x73, 0x64, 0x6b, 0x2f, + 0x67, 0x6f, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x73, 0x2f, 0x66, 0x65, 0x61, 0x73, 0x74, 0x2f, + 0x63, 0x6f, 0x72, 0x65, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, } var ( diff --git a/sdk/go/protos/feast/core/FeatureSetReference.pb.go b/sdk/go/protos/feast/core/FeatureSetReference.pb.go index 3da39ad922a..dfa47a25a7c 100644 --- a/sdk/go/protos/feast/core/FeatureSetReference.pb.go +++ b/sdk/go/protos/feast/core/FeatureSetReference.pb.go @@ -16,7 +16,7 @@ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: -// protoc-gen-go v1.23.0 +// protoc-gen-go v1.24.0 // protoc v3.10.0 // source: feast/core/FeatureSetReference.proto diff --git a/sdk/go/protos/feast/core/IngestionJob.pb.go b/sdk/go/protos/feast/core/IngestionJob.pb.go index bf30f25168f..a54b23c4498 100644 --- a/sdk/go/protos/feast/core/IngestionJob.pb.go +++ b/sdk/go/protos/feast/core/IngestionJob.pb.go @@ -16,7 +16,7 @@ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: -// protoc-gen-go v1.23.0 +// protoc-gen-go v1.24.0 // protoc v3.10.0 // source: feast/core/IngestionJob.proto diff --git a/sdk/go/protos/feast/core/Runner.pb.go b/sdk/go/protos/feast/core/Runner.pb.go index 54e03714d2a..b1255e52ba6 100644 --- a/sdk/go/protos/feast/core/Runner.pb.go +++ b/sdk/go/protos/feast/core/Runner.pb.go @@ -16,7 +16,7 @@ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: -// protoc-gen-go v1.23.0 +// protoc-gen-go v1.24.0 // protoc v3.10.0 // source: feast/core/Runner.proto diff --git a/sdk/go/protos/feast/core/Source.pb.go b/sdk/go/protos/feast/core/Source.pb.go index b29dc5af96c..2aa0c3f12e6 100644 --- a/sdk/go/protos/feast/core/Source.pb.go +++ b/sdk/go/protos/feast/core/Source.pb.go @@ -16,7 +16,7 @@ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: -// protoc-gen-go v1.23.0 +// protoc-gen-go v1.24.0 // protoc v3.10.0 // source: feast/core/Source.proto diff --git a/sdk/go/protos/feast/core/Store.pb.go b/sdk/go/protos/feast/core/Store.pb.go index 866dc71fe94..a36c6afc6ab 100644 --- a/sdk/go/protos/feast/core/Store.pb.go +++ b/sdk/go/protos/feast/core/Store.pb.go @@ -16,7 +16,7 @@ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: -// protoc-gen-go v1.23.0 +// protoc-gen-go v1.24.0 // protoc v3.10.0 // source: feast/core/Store.proto diff --git a/sdk/go/protos/feast/serving/ServingService.pb.go b/sdk/go/protos/feast/serving/ServingService.pb.go index 9e5a151b7ee..fc2748d5d84 100644 --- a/sdk/go/protos/feast/serving/ServingService.pb.go +++ b/sdk/go/protos/feast/serving/ServingService.pb.go @@ -15,7 +15,7 @@ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: -// protoc-gen-go v1.23.0 +// protoc-gen-go v1.24.0 // protoc v3.10.0 // source: feast/serving/ServingService.proto diff --git a/sdk/go/protos/feast/storage/Redis.pb.go b/sdk/go/protos/feast/storage/Redis.pb.go index cfc22a458e5..92c1040ca5a 100644 --- a/sdk/go/protos/feast/storage/Redis.pb.go +++ b/sdk/go/protos/feast/storage/Redis.pb.go @@ -15,7 +15,7 @@ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: -// protoc-gen-go v1.23.0 +// protoc-gen-go v1.24.0 // protoc v3.10.0 // source: feast/storage/Redis.proto diff --git a/sdk/go/protos/feast/types/FeatureRow.pb.go b/sdk/go/protos/feast/types/FeatureRow.pb.go index 3b42b44889e..4c094379eb1 100644 --- a/sdk/go/protos/feast/types/FeatureRow.pb.go +++ b/sdk/go/protos/feast/types/FeatureRow.pb.go @@ -15,7 +15,7 @@ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: -// protoc-gen-go v1.23.0 +// protoc-gen-go v1.24.0 // protoc v3.10.0 // source: feast/types/FeatureRow.proto diff --git a/sdk/go/protos/feast/types/FeatureRowExtended.pb.go b/sdk/go/protos/feast/types/FeatureRowExtended.pb.go index 1692a55dd4f..3cb15a21ce0 100644 --- a/sdk/go/protos/feast/types/FeatureRowExtended.pb.go +++ b/sdk/go/protos/feast/types/FeatureRowExtended.pb.go @@ -15,7 +15,7 @@ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: -// protoc-gen-go v1.23.0 +// protoc-gen-go v1.24.0 // protoc v3.10.0 // source: feast/types/FeatureRowExtended.proto diff --git a/sdk/go/protos/feast/types/Field.pb.go b/sdk/go/protos/feast/types/Field.pb.go index 901075e2620..08b35f66272 100644 --- a/sdk/go/protos/feast/types/Field.pb.go +++ b/sdk/go/protos/feast/types/Field.pb.go @@ -15,7 +15,7 @@ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: -// protoc-gen-go v1.23.0 +// protoc-gen-go v1.24.0 // protoc v3.10.0 // source: feast/types/Field.proto diff --git a/sdk/go/protos/feast/types/Value.pb.go b/sdk/go/protos/feast/types/Value.pb.go index 317768fd549..bb777a07661 100644 --- a/sdk/go/protos/feast/types/Value.pb.go +++ b/sdk/go/protos/feast/types/Value.pb.go @@ -15,7 +15,7 @@ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: -// protoc-gen-go v1.23.0 +// protoc-gen-go v1.24.0 // protoc v3.10.0 // source: feast/types/Value.proto diff --git a/sdk/go/protos/tensorflow_metadata/proto/v0/path.pb.go b/sdk/go/protos/tensorflow_metadata/proto/v0/path.pb.go index fe4cc3ee47a..f58247299d9 100644 --- a/sdk/go/protos/tensorflow_metadata/proto/v0/path.pb.go +++ b/sdk/go/protos/tensorflow_metadata/proto/v0/path.pb.go @@ -15,7 +15,7 @@ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: -// protoc-gen-go v1.23.0 +// protoc-gen-go v1.24.0 // protoc v3.10.0 // source: tensorflow_metadata/proto/v0/path.proto diff --git a/sdk/go/protos/tensorflow_metadata/proto/v0/schema.pb.go b/sdk/go/protos/tensorflow_metadata/proto/v0/schema.pb.go index 97117947b78..a04f5bba8ca 100644 --- a/sdk/go/protos/tensorflow_metadata/proto/v0/schema.pb.go +++ b/sdk/go/protos/tensorflow_metadata/proto/v0/schema.pb.go @@ -15,7 +15,7 @@ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: -// protoc-gen-go v1.23.0 +// protoc-gen-go v1.24.0 // protoc v3.10.0 // source: tensorflow_metadata/proto/v0/schema.proto From a4adc251af8bc643e4e5ea1d3d5c9693e094e1cd Mon Sep 17 00:00:00 2001 From: Zhu Zhan Yan Date: Fri, 29 May 2020 18:01:58 +0800 Subject: [PATCH 18/26] Fix Go and Java SDK Regressions (#729) * Fix nil error cause by adding items to a nil map in go SDK's client.GetOnlineFeatures() * Fix issue where go sdk client.GetOnlineFeatures() does not return entity values * Fixed issue where Row is unable to parse Value type due to proto package rename. * Fix wrong path of test_feature python SDK test. * Revert stub and channel private fields to final in FeastClient java SDK Co-authored-by: Zhu Zhanyan --- sdk/go/client.go | 8 ++++---- sdk/go/types.go | 4 ++-- sdk/java/src/main/java/com/gojek/feast/FeastClient.java | 7 ++++--- sdk/java/src/main/java/com/gojek/feast/Row.java | 2 +- sdk/python/{feast => tests}/test_feature.py | 2 +- 5 files changed, 12 insertions(+), 11 deletions(-) rename sdk/python/{feast => tests}/test_feature.py (92%) diff --git a/sdk/go/client.go b/sdk/go/client.go index 38c9e2fb7e5..ef8ec8b8cf9 100644 --- a/sdk/go/client.go +++ b/sdk/go/client.go @@ -52,7 +52,7 @@ func (fc *GrpcClient) GetOnlineFeatures(ctx context.Context, req *OnlineFeatures resp, err := fc.cli.GetOnlineFeatures(ctx, featuresRequest) // collect unqiue entity refs from entity rows - var entityRefs map[string]struct{} + entityRefs := make(map[string]struct{}) for _, entityRows := range req.Entities { for ref, _ := range entityRows { entityRefs[ref] = struct{}{} @@ -61,7 +61,7 @@ func (fc *GrpcClient) GetOnlineFeatures(ctx context.Context, req *OnlineFeatures // strip projects from to projects for _, fieldValue := range resp.GetFieldValues() { - var stripFields map[string]*types.Value + stripFields := make(map[string]*types.Value) for refStr, value := range fieldValue.Fields { _, isEntity := entityRefs[refStr] if !isEntity { // is feature ref @@ -69,9 +69,9 @@ func (fc *GrpcClient) GetOnlineFeatures(ctx context.Context, req *OnlineFeatures if err != nil { return nil, err } - stripRefStr := toFeatureRefStr(featureRef) - stripFields[stripRefStr] = value + refStr = toFeatureRefStr(featureRef) } + stripFields[refStr] = value } fieldValue.Fields = stripFields } diff --git a/sdk/go/types.go b/sdk/go/types.go index 7fe400e45bb..dd6553163bd 100644 --- a/sdk/go/types.go +++ b/sdk/go/types.go @@ -21,12 +21,12 @@ func (r Row) equalTo(other Row) bool { return true } -// StrVal is a int64 type feast value +// StrVal is a string type feast value func StrVal(val string) *types.Value { return &types.Value{Val: &types.Value_StringVal{StringVal: val}} } -// Int32Val is a int64 type feast value +// Int32Val is a int32 type feast value func Int32Val(val int32) *types.Value { return &types.Value{Val: &types.Value_Int32Val{Int32Val: val}} } diff --git a/sdk/java/src/main/java/com/gojek/feast/FeastClient.java b/sdk/java/src/main/java/com/gojek/feast/FeastClient.java index c6e7edd0764..a81fdab21a2 100644 --- a/sdk/java/src/main/java/com/gojek/feast/FeastClient.java +++ b/sdk/java/src/main/java/com/gojek/feast/FeastClient.java @@ -23,6 +23,7 @@ import feast.proto.serving.ServingAPIProto.GetOnlineFeaturesRequest.EntityRow; import feast.proto.serving.ServingAPIProto.GetOnlineFeaturesResponse; import feast.proto.serving.ServingServiceGrpc; +import feast.proto.serving.ServingServiceGrpc.ServingServiceBlockingStub; import feast.proto.types.ValueProto.Value; import io.grpc.ManagedChannel; import io.grpc.ManagedChannelBuilder; @@ -40,7 +41,7 @@ public class FeastClient implements AutoCloseable { private static final int CHANNEL_SHUTDOWN_TIMEOUT_SEC = 5; private final ManagedChannel channel; - private final ServingServiceGrpc.ServingServiceBlockingStub stub; + private final ServingServiceBlockingStub stub; /** * Create a client to access Feast @@ -161,9 +162,9 @@ public List getOnlineFeatures( .collect(Collectors.toList()); } - private FeastClient(ManagedChannel channel) { + protected FeastClient(ManagedChannel channel) { this.channel = channel; - stub = ServingServiceGrpc.newBlockingStub(channel); + this.stub = ServingServiceGrpc.newBlockingStub(channel); } public void close() throws Exception { diff --git a/sdk/java/src/main/java/com/gojek/feast/Row.java b/sdk/java/src/main/java/com/gojek/feast/Row.java index 2ac2581cac9..4a3035f8f19 100644 --- a/sdk/java/src/main/java/com/gojek/feast/Row.java +++ b/sdk/java/src/main/java/com/gojek/feast/Row.java @@ -76,7 +76,7 @@ public Row set(String fieldName, Object value) { fields.put( fieldName, Value.newBuilder().setBytesVal(ByteString.copyFrom((byte[]) value)).build()); break; - case "feast.types.ValueProto.Value": + case "feast.proto.types.ValueProto.Value": fields.put(fieldName, (Value) value); break; default: diff --git a/sdk/python/feast/test_feature.py b/sdk/python/tests/test_feature.py similarity index 92% rename from sdk/python/feast/test_feature.py rename to sdk/python/tests/test_feature.py index a4f0ff58ff2..bc83683e0fe 100644 --- a/sdk/python/feast/test_feature.py +++ b/sdk/python/tests/test_feature.py @@ -17,7 +17,7 @@ class TestFeatureRef: def test_str_ref(self): - original_ref = FeatureRef(project="test", name="test") + original_ref = FeatureRef(feature_set="test", name="test") ref_str = repr(original_ref) parsed_ref = FeatureRef.from_str(ref_str) assert original_ref == parsed_ref From ab6d52fa295131085d2da500dd3838958e0c1752 Mon Sep 17 00:00:00 2001 From: Willem Pienaar Date: Fri, 22 May 2020 10:09:53 +0800 Subject: [PATCH 19/26] Remove Docker Compose test from PRs --- .github/workflows/docker_compose_tests.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/docker_compose_tests.yml b/.github/workflows/docker_compose_tests.yml index 28411b88802..301ba116675 100644 --- a/.github/workflows/docker_compose_tests.yml +++ b/.github/workflows/docker_compose_tests.yml @@ -1,6 +1,6 @@ name: docker compose tests -on: [push, pull_request] +on: [push] jobs: basic-redis-e2e-tests-docker-compose: From c8cc58b71e953919b2109cadd9975a8c32c2000c Mon Sep 17 00:00:00 2001 From: Willem Pienaar <6728866+woop@users.noreply.github.com> Date: Sat, 23 May 2020 21:35:22 +0800 Subject: [PATCH 20/26] Restrict Docker Compose tests to master branch --- .github/workflows/docker_compose_tests.yml | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/.github/workflows/docker_compose_tests.yml b/.github/workflows/docker_compose_tests.yml index 301ba116675..2984ebc37ba 100644 --- a/.github/workflows/docker_compose_tests.yml +++ b/.github/workflows/docker_compose_tests.yml @@ -1,6 +1,9 @@ name: docker compose tests -on: [push] +on: + push: + branches: + - master jobs: basic-redis-e2e-tests-docker-compose: @@ -9,4 +12,4 @@ jobs: steps: - uses: actions/checkout@v2 - name: test docker compose - run: ./infra/scripts/test-docker-compose.sh \ No newline at end of file + run: ./infra/scripts/test-docker-compose.sh From 21d62d6ef63f6f66575cd8fa603c153bdad2c4ff Mon Sep 17 00:00:00 2001 From: Terence Lim Date: Sat, 30 May 2020 09:45:58 +0800 Subject: [PATCH 21/26] Fix feature_set_describe cli (#731) Co-authored-by: Terence --- sdk/python/feast/cli.py | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/sdk/python/feast/cli.py b/sdk/python/feast/cli.py index 36d1d489e41..6d1df292764 100644 --- a/sdk/python/feast/cli.py +++ b/sdk/python/feast/cli.py @@ -156,12 +156,20 @@ def feature_set_create(filename): @feature_set.command("describe") @click.argument("name", type=click.STRING) -def feature_set_describe(name: str): +@click.option( + "--project", + "-p", + help="Project that feature set belongs to", + type=click.STRING, + default="default", +) +def feature_set_describe(name: str, project: str): """ Describe a feature set """ feast_client = Client() # type: Client - fs = feast_client.get_feature_set(name=name) + fs = feast_client.get_feature_set(name=name, project=project) + if not fs: print(f'Feature set with name "{name}" could not be found') return From 06eefd9def229b7f7c4854ba7c561732e393161a Mon Sep 17 00:00:00 2001 From: Willem Pienaar <6728866+woop@users.noreply.github.com> Date: Sat, 30 May 2020 13:50:14 +0800 Subject: [PATCH 22/26] Add support for version method in Feast SDK and Core (#759) * Add support for version method * Fix typo in e2e tests for version method --- .../java/feast/core/grpc/CoreServiceImpl.java | 17 +++++++++-- sdk/python/feast/client.py | 8 +++++- tests/e2e/basic-ingest-redis-serving.py | 28 ++++++++++++++----- 3 files changed, 43 insertions(+), 10 deletions(-) diff --git a/core/src/main/java/feast/core/grpc/CoreServiceImpl.java b/core/src/main/java/feast/core/grpc/CoreServiceImpl.java index 412d3d31198..db65328b821 100644 --- a/core/src/main/java/feast/core/grpc/CoreServiceImpl.java +++ b/core/src/main/java/feast/core/grpc/CoreServiceImpl.java @@ -18,6 +18,7 @@ import com.google.api.gax.rpc.InvalidArgumentException; import com.google.protobuf.InvalidProtocolBufferException; +import feast.core.config.FeastProperties; import feast.core.exception.RetrievalException; import feast.core.grpc.interceptors.MonitoringInterceptor; import feast.core.model.Project; @@ -64,6 +65,7 @@ @GRpcService(interceptors = {MonitoringInterceptor.class}) public class CoreServiceImpl extends CoreServiceImplBase { + private final FeastProperties feastProperties; private SpecService specService; private AccessManagementService accessManagementService; private JobService jobService; @@ -72,17 +74,28 @@ public class CoreServiceImpl extends CoreServiceImplBase { public CoreServiceImpl( SpecService specService, AccessManagementService accessManagementService, - JobService jobService) { + JobService jobService, + FeastProperties feastProperties) { this.specService = specService; this.accessManagementService = accessManagementService; this.jobService = jobService; + this.feastProperties = feastProperties; } @Override public void getFeastCoreVersion( GetFeastCoreVersionRequest request, StreamObserver responseObserver) { - super.getFeastCoreVersion(request, responseObserver); + try { + GetFeastCoreVersionResponse response = + GetFeastCoreVersionResponse.newBuilder().setVersion(feastProperties.getVersion()).build(); + responseObserver.onNext(response); + responseObserver.onCompleted(); + } catch (RetrievalException | StatusRuntimeException e) { + log.error("Could not determine Feast Core version: ", e); + responseObserver.onError( + Status.INTERNAL.withDescription(e.getMessage()).withCause(e).asRuntimeException()); + } } @Override diff --git a/sdk/python/feast/client.py b/sdk/python/feast/client.py index 5e4baf9d598..1c34213ad88 100644 --- a/sdk/python/feast/client.py +++ b/sdk/python/feast/client.py @@ -195,7 +195,13 @@ def version(self): """ Returns version information from Feast Core and Feast Serving """ - result = {} + import pkg_resources + + result = { + "sdk": {"version": pkg_resources.get_distribution("feast").version}, + "serving": "not configured", + "core": "not configured", + } if self.serving_url: self._connect_serving() diff --git a/tests/e2e/basic-ingest-redis-serving.py b/tests/e2e/basic-ingest-redis-serving.py index 4209ce1c220..da0967fd764 100644 --- a/tests/e2e/basic-ingest-redis-serving.py +++ b/tests/e2e/basic-ingest-redis-serving.py @@ -30,6 +30,7 @@ FLOAT_TOLERANCE = 0.00001 PROJECT_NAME = 'basic_' + uuid.uuid4().hex.upper()[0:6] + @pytest.fixture(scope='module') def core_url(pytestconfig): return pytestconfig.getoption("core_url") @@ -62,6 +63,7 @@ def client(core_url, serving_url, allow_dirty): return client + def basic_dataframe(entities, features, ingest_time, n_size): offset = random.randint(1000, 100000) # ensure a unique key space is used df_dict = { @@ -79,6 +81,7 @@ def basic_dataframe(entities, features, ingest_time, n_size): def ingest_time(): return datetime.utcnow() + @pytest.fixture(scope="module") def cust_trans_df(ingest_time): return basic_dataframe(entities=["customer_id"], @@ -86,6 +89,7 @@ def cust_trans_df(ingest_time): ingest_time=ingest_time, n_size=5) + @pytest.fixture(scope="module") def driver_df(ingest_time): return basic_dataframe(entities=["driver_id"], @@ -93,6 +97,13 @@ def driver_df(ingest_time): ingest_time=ingest_time, n_size=5) + +def test_version_returns_results(client): + version_info = client.version() + assert not version_info['core'] is 'not configured' + assert not version_info['serving'] is 'not configured' + + @pytest.mark.timeout(45) @pytest.mark.run(order=10) def test_basic_register_feature_set_success(client): @@ -117,6 +128,7 @@ def test_basic_register_feature_set_success(client): # reset client's project for other tests client.set_project() + @pytest.mark.timeout(300) @pytest.mark.run(order=11) def test_basic_ingest_success(client, cust_trans_df, driver_df): @@ -157,16 +169,16 @@ def test_basic_retrieve_online_success(client, cust_trans_df): returned_daily_transactions = float( response.field_values[0] - .fields["daily_transactions"] - .float_val + .fields["daily_transactions"] + .float_val ) sent_daily_transactions = float( cust_trans_df.iloc[0]["daily_transactions"]) if math.isclose( - sent_daily_transactions, - returned_daily_transactions, - abs_tol=FLOAT_TOLERANCE, + sent_daily_transactions, + returned_daily_transactions, + abs_tol=FLOAT_TOLERANCE, ): break @@ -206,8 +218,8 @@ def test_basic_retrieve_online_multiple_featureset(client, cust_trans_df, driver def check_response(ingest_df, response, feature_ref): returned_value = float( response.field_values[0] - .fields[feature_ref] - .float_val + .fields[feature_ref] + .float_val ) feature_ref_splits = feature_ref.split(":") if len(feature_ref_splits) == 1: @@ -223,9 +235,11 @@ def check_response(ingest_df, response, feature_ref): returned_value, abs_tol=FLOAT_TOLERANCE, ) + if all([check_response(df, response, ref) for ref, df in feature_ref_df_mapping]): break + @pytest.mark.timeout(300) @pytest.mark.run(order=19) def test_basic_ingest_jobs(client): From 097afddfb4503139ea655e853e9f32715a72940d Mon Sep 17 00:00:00 2001 From: Chen Zhiling Date: Mon, 1 Jun 2020 10:32:59 +0800 Subject: [PATCH 23/26] Refactor runner configuration, add labels to dataflow options (#718) * Refactor runner configuration, add labels to dataflow options * Remove util method * Add javadocs for RunnerConfig class * Apply spotless --- .../feast/core/config/FeastProperties.java | 2 +- .../java/feast/core/config/JobConfig.java | 24 +++++- .../core/job/dataflow/DataflowJobManager.java | 23 +++--- .../job/dataflow/DataflowRunnerConfig.java | 78 ++++++++----------- .../core/job/direct/DirectRunnerConfig.java | 36 +++++++++ .../job/direct/DirectRunnerJobManager.java | 15 ++-- .../feast/core/job/option/RunnerConfig.java | 75 ++++++++++++++++++ .../java/feast/core/util/TypeConversion.java | 16 ---- .../job/dataflow/DataflowJobManagerTest.java | 23 +++--- .../direct/DirectRunnerJobManagerTest.java | 9 +-- .../feast/core/util/TypeConversionTest.java | 12 --- protos/feast/core/Runner.proto | 3 + 12 files changed, 202 insertions(+), 114 deletions(-) create mode 100644 core/src/main/java/feast/core/job/direct/DirectRunnerConfig.java create mode 100644 core/src/main/java/feast/core/job/option/RunnerConfig.java diff --git a/core/src/main/java/feast/core/config/FeastProperties.java b/core/src/main/java/feast/core/config/FeastProperties.java index eb50728baf5..6dad2782429 100644 --- a/core/src/main/java/feast/core/config/FeastProperties.java +++ b/core/src/main/java/feast/core/config/FeastProperties.java @@ -103,7 +103,7 @@ public static class Runner { * Job runner configuration options. See the following for options * https://api.docs.feast.dev/grpc/feast.core.pb.html#Runner */ - Map options = new HashMap<>(); + Map options = new HashMap<>(); /** * Gets the job runner type as an enum. diff --git a/core/src/main/java/feast/core/config/JobConfig.java b/core/src/main/java/feast/core/config/JobConfig.java index 69636963bea..30023de0641 100644 --- a/core/src/main/java/feast/core/config/JobConfig.java +++ b/core/src/main/java/feast/core/config/JobConfig.java @@ -16,11 +16,16 @@ */ package feast.core.config; +import com.google.gson.Gson; +import com.google.protobuf.InvalidProtocolBufferException; +import com.google.protobuf.util.JsonFormat; import feast.core.config.FeastProperties.JobProperties; import feast.core.job.JobManager; import feast.core.job.dataflow.DataflowJobManager; import feast.core.job.direct.DirectJobRegistry; import feast.core.job.direct.DirectRunnerJobManager; +import feast.proto.core.RunnerProto.DataflowRunnerConfigOptions; +import feast.proto.core.RunnerProto.DirectRunnerConfigOptions; import java.util.Map; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; @@ -31,6 +36,7 @@ @Slf4j @Configuration public class JobConfig { + private final Gson gson = new Gson(); /** * Get a JobManager according to the runner type and Dataflow configuration. @@ -39,18 +45,28 @@ public class JobConfig { */ @Bean @Autowired - public JobManager getJobManager(FeastProperties feastProperties) { + public JobManager getJobManager(FeastProperties feastProperties) + throws InvalidProtocolBufferException { JobProperties jobProperties = feastProperties.getJobs(); FeastProperties.JobProperties.Runner runner = jobProperties.getActiveRunner(); - Map runnerConfigOptions = runner.getOptions(); + Map runnerConfigOptions = runner.getOptions(); + String configJson = gson.toJson(runnerConfigOptions); + FeastProperties.MetricsProperties metrics = jobProperties.getMetrics(); switch (runner.getType()) { case DATAFLOW: - return new DataflowJobManager(runnerConfigOptions, metrics); + DataflowRunnerConfigOptions.Builder dataflowRunnerConfigOptions = + DataflowRunnerConfigOptions.newBuilder(); + JsonFormat.parser().merge(configJson, dataflowRunnerConfigOptions); + return new DataflowJobManager(dataflowRunnerConfigOptions.build(), metrics); case DIRECT: - return new DirectRunnerJobManager(runnerConfigOptions, new DirectJobRegistry(), metrics); + DirectRunnerConfigOptions.Builder directRunnerConfigOptions = + DirectRunnerConfigOptions.newBuilder(); + JsonFormat.parser().merge(configJson, directRunnerConfigOptions); + return new DirectRunnerJobManager( + directRunnerConfigOptions.build(), new DirectJobRegistry(), metrics); default: throw new IllegalArgumentException("Unsupported runner: " + runner); } diff --git a/core/src/main/java/feast/core/job/dataflow/DataflowJobManager.java b/core/src/main/java/feast/core/job/dataflow/DataflowJobManager.java index 7b9df0abd52..2c3da255f5b 100644 --- a/core/src/main/java/feast/core/job/dataflow/DataflowJobManager.java +++ b/core/src/main/java/feast/core/job/dataflow/DataflowJobManager.java @@ -33,12 +33,12 @@ import feast.core.job.Runner; import feast.core.job.option.FeatureSetJsonByteConverter; import feast.core.model.*; -import feast.core.util.TypeConversion; import feast.ingestion.ImportJob; import feast.ingestion.options.BZip2Compressor; import feast.ingestion.options.ImportOptions; import feast.ingestion.options.OptionCompressor; import feast.proto.core.FeatureSetProto; +import feast.proto.core.RunnerProto.DataflowRunnerConfigOptions; import feast.proto.core.SourceProto; import feast.proto.core.StoreProto; import java.io.IOException; @@ -46,7 +46,6 @@ import java.util.ArrayList; import java.util.Collections; import java.util.List; -import java.util.Map; import lombok.extern.slf4j.Slf4j; import org.apache.beam.runners.dataflow.DataflowPipelineJob; import org.apache.beam.runners.dataflow.DataflowRunner; @@ -61,21 +60,20 @@ public class DataflowJobManager implements JobManager { private final String projectId; private final String location; private final Dataflow dataflow; - private final Map defaultOptions; + private final DataflowRunnerConfig defaultOptions; private final MetricsProperties metrics; public DataflowJobManager( - Map runnerConfigOptions, MetricsProperties metricsProperties) { + DataflowRunnerConfigOptions runnerConfigOptions, MetricsProperties metricsProperties) { this(runnerConfigOptions, metricsProperties, getGoogleCredential()); } public DataflowJobManager( - Map runnerConfigOptions, + DataflowRunnerConfigOptions runnerConfigOptions, MetricsProperties metricsProperties, Credential credential) { - DataflowRunnerConfig config = new DataflowRunnerConfig(runnerConfigOptions); - + defaultOptions = new DataflowRunnerConfig(runnerConfigOptions); Dataflow dataflow = null; try { dataflow = @@ -89,11 +87,10 @@ public DataflowJobManager( throw new IllegalStateException("Unable to initialize DataflowJobManager", e); } - this.defaultOptions = runnerConfigOptions; this.dataflow = dataflow; this.metrics = metricsProperties; - this.projectId = config.getProject(); - this.location = config.getRegion(); + this.projectId = defaultOptions.getProject(); + this.location = defaultOptions.getRegion(); } private static Credential getGoogleCredential() { @@ -270,9 +267,9 @@ private ImportOptions getPipelineOptions( List featureSets, StoreProto.Store sink, boolean update) - throws IOException { - String[] args = TypeConversion.convertMapToArgs(defaultOptions); - ImportOptions pipelineOptions = PipelineOptionsFactory.fromArgs(args).as(ImportOptions.class); + throws IOException, IllegalAccessException { + ImportOptions pipelineOptions = + PipelineOptionsFactory.fromArgs(defaultOptions.toArgs()).as(ImportOptions.class); OptionCompressor> featureSetJsonCompressor = new BZip2Compressor<>(new FeatureSetJsonByteConverter()); diff --git a/core/src/main/java/feast/core/job/dataflow/DataflowRunnerConfig.java b/core/src/main/java/feast/core/job/dataflow/DataflowRunnerConfig.java index 6fe93ca80cd..85628d2cd0e 100644 --- a/core/src/main/java/feast/core/job/dataflow/DataflowRunnerConfig.java +++ b/core/src/main/java/feast/core/job/dataflow/DataflowRunnerConfig.java @@ -16,9 +16,9 @@ */ package feast.core.job.dataflow; -import java.lang.reflect.Field; -import java.util.Map; -import java.util.Set; +import feast.core.job.option.RunnerConfig; +import feast.proto.core.RunnerProto.DataflowRunnerConfigOptions; +import java.util.*; import javax.validation.*; import javax.validation.constraints.NotBlank; import lombok.Getter; @@ -27,77 +27,65 @@ /** DataflowRunnerConfig contains configuration fields for the Dataflow job runner. */ @Getter @Setter -public class DataflowRunnerConfig { - - public DataflowRunnerConfig(Map runnerConfigOptions) { - - // Try to find all fields in DataflowRunnerConfig inside the runnerConfigOptions and map it into - // this object - for (Field field : DataflowRunnerConfig.class.getFields()) { - String fieldName = field.getName(); - try { - if (!runnerConfigOptions.containsKey(fieldName)) { - continue; - } - String value = runnerConfigOptions.get(fieldName); - - if (Boolean.class.equals(field.getType())) { - field.set(this, Boolean.valueOf(value)); - continue; - } - if (field.getType() == Integer.class) { - field.set(this, Integer.valueOf(value)); - continue; - } - field.set(this, value); - } catch (IllegalAccessException e) { - throw new RuntimeException( - String.format( - "Could not successfully convert DataflowRunnerConfig for key: %s", fieldName), - e); - } - } +public class DataflowRunnerConfig extends RunnerConfig { + + public DataflowRunnerConfig(DataflowRunnerConfigOptions runnerConfigOptions) { + this.project = runnerConfigOptions.getProject(); + this.region = runnerConfigOptions.getRegion(); + this.zone = runnerConfigOptions.getZone(); + this.serviceAccount = runnerConfigOptions.getServiceAccount(); + this.network = runnerConfigOptions.getNetwork(); + this.subnetwork = runnerConfigOptions.getSubnetwork(); + this.workerMachineType = runnerConfigOptions.getWorkerMachineType(); + this.autoscalingAlgorithm = runnerConfigOptions.getAutoscalingAlgorithm(); + this.usePublicIps = runnerConfigOptions.getUsePublicIps(); + this.tempLocation = runnerConfigOptions.getTempLocation(); + this.maxNumWorkers = runnerConfigOptions.getMaxNumWorkers(); + this.deadLetterTableSpec = runnerConfigOptions.getDeadLetterTableSpec(); + this.labels = runnerConfigOptions.getLabelsMap(); validate(); } /* Project id to use when launching jobs. */ - @NotBlank public String project; + @NotBlank String project; /* The Google Compute Engine region for creating Dataflow jobs. */ - @NotBlank public String region; + @NotBlank String region; /* GCP availability zone for operations. */ - @NotBlank public String zone; + @NotBlank String zone; /* Run the job as a specific service account, instead of the default GCE robot. */ - public String serviceAccount; + String serviceAccount; /* GCE network for launching workers. */ - @NotBlank public String network; + @NotBlank String network; /* GCE subnetwork for launching workers. */ - @NotBlank public String subnetwork; + @NotBlank String subnetwork; /* Machine type to create Dataflow worker VMs as. */ - public String workerMachineType; + String workerMachineType; /* The autoscaling algorithm to use for the workerpool. */ - public String autoscalingAlgorithm; + String autoscalingAlgorithm; /* Specifies whether worker pools should be started with public IP addresses. */ - public Boolean usePublicIps; + Boolean usePublicIps; /** * A pipeline level default location for storing temporary files. Support Google Cloud Storage * locations, e.g. gs://bucket/object */ - @NotBlank public String tempLocation; + @NotBlank String tempLocation; /* The maximum number of workers to use for the workerpool. */ - public Integer maxNumWorkers; + Integer maxNumWorkers; /* BigQuery table specification, e.g. PROJECT_ID:DATASET_ID.PROJECT_ID */ - public String deadLetterTableSpec; + String deadLetterTableSpec; + + Map labels; /** Validates Dataflow runner configuration options */ public void validate() { diff --git a/core/src/main/java/feast/core/job/direct/DirectRunnerConfig.java b/core/src/main/java/feast/core/job/direct/DirectRunnerConfig.java new file mode 100644 index 00000000000..ebd327f2f75 --- /dev/null +++ b/core/src/main/java/feast/core/job/direct/DirectRunnerConfig.java @@ -0,0 +1,36 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright 2018-2020 The Feast Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package feast.core.job.direct; + +import feast.core.job.option.RunnerConfig; +import feast.proto.core.RunnerProto.DirectRunnerConfigOptions; + +public class DirectRunnerConfig extends RunnerConfig { + /** + * Controls the amount of target parallelism the DirectRunner will use. Defaults to the greater of + * the number of available processors and 3. Must be a value greater than zero. + */ + Integer targetParallelism; + + /* BigQuery table specification, e.g. PROJECT_ID:DATASET_ID.PROJECT_ID */ + String deadletterTableSpec; + + public DirectRunnerConfig(DirectRunnerConfigOptions runnerConfigOptions) { + this.deadletterTableSpec = runnerConfigOptions.getDeadLetterTableSpec(); + this.targetParallelism = runnerConfigOptions.getTargetParallelism(); + } +} diff --git a/core/src/main/java/feast/core/job/direct/DirectRunnerJobManager.java b/core/src/main/java/feast/core/job/direct/DirectRunnerJobManager.java index 2e2b43047ea..715adbdd430 100644 --- a/core/src/main/java/feast/core/job/direct/DirectRunnerJobManager.java +++ b/core/src/main/java/feast/core/job/direct/DirectRunnerJobManager.java @@ -27,18 +27,17 @@ import feast.core.model.Job; import feast.core.model.JobStatus; import feast.core.model.Project; -import feast.core.util.TypeConversion; import feast.ingestion.ImportJob; import feast.ingestion.options.BZip2Compressor; import feast.ingestion.options.ImportOptions; import feast.ingestion.options.OptionCompressor; import feast.proto.core.FeatureSetProto; +import feast.proto.core.RunnerProto.DirectRunnerConfigOptions; import feast.proto.core.StoreProto; import java.io.IOException; import java.util.ArrayList; import java.util.Collections; import java.util.List; -import java.util.Map; import lombok.extern.slf4j.Slf4j; import org.apache.beam.runners.direct.DirectRunner; import org.apache.beam.sdk.PipelineResult; @@ -49,15 +48,15 @@ public class DirectRunnerJobManager implements JobManager { private final Runner RUNNER_TYPE = Runner.DIRECT; - protected Map defaultOptions; + private DirectRunnerConfig defaultOptions; private final DirectJobRegistry jobs; private MetricsProperties metrics; public DirectRunnerJobManager( - Map defaultOptions, + DirectRunnerConfigOptions directRunnerConfigOptions, DirectJobRegistry jobs, MetricsProperties metricsProperties) { - this.defaultOptions = defaultOptions; + this.defaultOptions = new DirectRunnerConfig(directRunnerConfigOptions); this.jobs = jobs; this.metrics = metricsProperties; } @@ -95,9 +94,9 @@ public Job startJob(Job job) { private ImportOptions getPipelineOptions( String jobName, List featureSets, StoreProto.Store sink) - throws IOException { - String[] args = TypeConversion.convertMapToArgs(defaultOptions); - ImportOptions pipelineOptions = PipelineOptionsFactory.fromArgs(args).as(ImportOptions.class); + throws IOException, IllegalAccessException { + ImportOptions pipelineOptions = + PipelineOptionsFactory.fromArgs(defaultOptions.toArgs()).as(ImportOptions.class); OptionCompressor> featureSetJsonCompressor = new BZip2Compressor<>(new FeatureSetJsonByteConverter()); diff --git a/core/src/main/java/feast/core/job/option/RunnerConfig.java b/core/src/main/java/feast/core/job/option/RunnerConfig.java new file mode 100644 index 00000000000..4b937074a31 --- /dev/null +++ b/core/src/main/java/feast/core/job/option/RunnerConfig.java @@ -0,0 +1,75 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright 2018-2020 The Feast Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package feast.core.job.option; + +import feast.core.util.TypeConversion; +import java.lang.reflect.Field; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +/** + * Value class containing the application-default configuration for a runner. When a job is started + * by core, all fields in the object will be converted into --key=value args to seed the beam + * pipeline options. + */ +public abstract class RunnerConfig { + + /** + * Converts the fields in this class to a list of --key=value args to be passed to a {@link + * org.apache.beam.sdk.options.PipelineOptionsFactory}. + * + *

Ignores values that are proto-default (e.g. empty string, 0). + * + * @return Array of string args in the format --key=value. + * @throws IllegalAccessException + */ + public String[] toArgs() throws IllegalAccessException { + List args = new ArrayList<>(); + for (Field field : this.getClass().getFields()) { + if (field.get(this) == null) { + continue; + } + Class type = field.getType(); + if (Map.class.equals(type)) { + String jsonString = + TypeConversion.convertMapToJsonString((Map) field.get(this)); + args.add(String.format("--%s=%s", field.getName(), jsonString)); + continue; + } + + if (String.class.equals(type)) { + String val = (String) field.get(this); + if (!val.equals("")) { + args.add(String.format("--%s=%s", field.getName(), val)); + } + continue; + } + + if (Integer.class.equals(type)) { + Integer val = (Integer) field.get(this); + if (val != 0) { + args.add(String.format("--%s=%d", field.getName(), val)); + } + continue; + } + + args.add(String.format("--%s=%s", field.getName(), field.get(this))); + } + return args.toArray(String[]::new); + } +} diff --git a/core/src/main/java/feast/core/util/TypeConversion.java b/core/src/main/java/feast/core/util/TypeConversion.java index 6ee990fc1c5..e6b7ef33cbc 100644 --- a/core/src/main/java/feast/core/util/TypeConversion.java +++ b/core/src/main/java/feast/core/util/TypeConversion.java @@ -16,12 +16,10 @@ */ package feast.core.util; -import com.google.common.base.Strings; import com.google.gson.Gson; import com.google.gson.reflect.TypeToken; import java.lang.reflect.Type; import java.util.*; -import java.util.Map.Entry; public class TypeConversion { private static Gson gson = new Gson(); @@ -72,18 +70,4 @@ public static Map convertJsonStringToMap(String jsonString) { public static String convertMapToJsonString(Map map) { return gson.toJson(map); } - - /** - * Convert a map of key value pairs to a array of java arguments in format --key=value - * - * @param map - * @return array of string arguments - */ - public static String[] convertMapToArgs(Map map) { - List args = new ArrayList<>(); - for (Entry arg : map.entrySet()) { - args.add(Strings.lenientFormat("--%s=%s", arg.getKey(), arg.getValue())); - } - return args.toArray(new String[] {}); - } } diff --git a/core/src/test/java/feast/core/job/dataflow/DataflowJobManagerTest.java b/core/src/test/java/feast/core/job/dataflow/DataflowJobManagerTest.java index 632c9d22a25..ea9caa91ff6 100644 --- a/core/src/test/java/feast/core/job/dataflow/DataflowJobManagerTest.java +++ b/core/src/test/java/feast/core/job/dataflow/DataflowJobManagerTest.java @@ -40,6 +40,8 @@ import feast.proto.core.FeatureSetProto; import feast.proto.core.FeatureSetProto.FeatureSetMeta; import feast.proto.core.FeatureSetProto.FeatureSetSpec; +import feast.proto.core.RunnerProto.DataflowRunnerConfigOptions; +import feast.proto.core.RunnerProto.DataflowRunnerConfigOptions.Builder; import feast.proto.core.SourceProto; import feast.proto.core.SourceProto.KafkaSourceConfig; import feast.proto.core.SourceProto.SourceType; @@ -49,9 +51,7 @@ import feast.proto.core.StoreProto.Store.Subscription; import java.io.IOException; import java.util.Collections; -import java.util.HashMap; import java.util.List; -import java.util.Map; import org.apache.beam.runners.dataflow.DataflowPipelineJob; import org.apache.beam.runners.dataflow.DataflowRunner; import org.apache.beam.sdk.PipelineResult.State; @@ -70,19 +70,21 @@ public class DataflowJobManagerTest { @Mock private Dataflow dataflow; - private Map defaults; + private DataflowRunnerConfigOptions defaults; private DataflowJobManager dfJobManager; @Before public void setUp() { initMocks(this); - defaults = new HashMap<>(); - defaults.put("project", "project"); - defaults.put("region", "region"); - defaults.put("zone", "zone"); - defaults.put("tempLocation", "tempLocation"); - defaults.put("network", "network"); - defaults.put("subnetwork", "subnetwork"); + Builder optionsBuilder = DataflowRunnerConfigOptions.newBuilder(); + optionsBuilder.setProject("project"); + optionsBuilder.setRegion("region"); + optionsBuilder.setZone("zone"); + optionsBuilder.setTempLocation("tempLocation"); + optionsBuilder.setNetwork("network"); + optionsBuilder.setSubnetwork("subnetwork"); + optionsBuilder.putLabels("orchestrator", "feast"); + defaults = optionsBuilder.build(); MetricsProperties metricsProperties = new MetricsProperties(); metricsProperties.setEnabled(false); Credential credential = null; @@ -137,6 +139,7 @@ public void shouldStartJobWithCorrectPipelineOptions() throws IOException { expectedPipelineOptions.setRegion("region"); expectedPipelineOptions.setUpdate(false); expectedPipelineOptions.setAppName("DataflowJobManager"); + expectedPipelineOptions.setLabels(defaults.getLabelsMap()); expectedPipelineOptions.setJobName(jobName); expectedPipelineOptions.setStoreJson(Lists.newArrayList(printer.print(store))); diff --git a/core/src/test/java/feast/core/job/direct/DirectRunnerJobManagerTest.java b/core/src/test/java/feast/core/job/direct/DirectRunnerJobManagerTest.java index 42d3189a730..0128f5aa0b3 100644 --- a/core/src/test/java/feast/core/job/direct/DirectRunnerJobManagerTest.java +++ b/core/src/test/java/feast/core/job/direct/DirectRunnerJobManagerTest.java @@ -42,6 +42,7 @@ import feast.ingestion.options.OptionCompressor; import feast.proto.core.FeatureSetProto; import feast.proto.core.FeatureSetProto.FeatureSetSpec; +import feast.proto.core.RunnerProto.DirectRunnerConfigOptions; import feast.proto.core.SourceProto; import feast.proto.core.SourceProto.KafkaSourceConfig; import feast.proto.core.SourceProto.SourceType; @@ -51,9 +52,7 @@ import feast.proto.core.StoreProto.Store.Subscription; import java.io.IOException; import java.util.Collections; -import java.util.HashMap; import java.util.List; -import java.util.Map; import org.apache.beam.runners.direct.DirectRunner; import org.apache.beam.sdk.PipelineResult; import org.apache.beam.sdk.options.PipelineOptionsFactory; @@ -71,12 +70,12 @@ public class DirectRunnerJobManagerTest { @Mock private DirectJobRegistry directJobRegistry; private DirectRunnerJobManager drJobManager; - private Map defaults; + private DirectRunnerConfigOptions defaults; @Before public void setUp() { initMocks(this); - defaults = new HashMap<>(); + defaults = DirectRunnerConfigOptions.newBuilder().setTargetParallelism(1).build(); MetricsProperties metricsProperties = new MetricsProperties(); metricsProperties.setEnabled(false); @@ -123,7 +122,7 @@ public void shouldStartDirectJobAndRegisterPipelineResult() throws IOException { expectedPipelineOptions.setAppName("DirectRunnerJobManager"); expectedPipelineOptions.setRunner(DirectRunner.class); expectedPipelineOptions.setBlockOnRun(false); - expectedPipelineOptions.setProject(""); + expectedPipelineOptions.setTargetParallelism(1); expectedPipelineOptions.setStoreJson(Lists.newArrayList(printer.print(store))); expectedPipelineOptions.setProject(""); diff --git a/core/src/test/java/feast/core/util/TypeConversionTest.java b/core/src/test/java/feast/core/util/TypeConversionTest.java index 02f0a7cee45..c44bf50129e 100644 --- a/core/src/test/java/feast/core/util/TypeConversionTest.java +++ b/core/src/test/java/feast/core/util/TypeConversionTest.java @@ -74,16 +74,4 @@ public void convertMapToJsonStringShouldReturnEmptyJsonForAnEmptyMap() { Map input = new HashMap<>(); assertThat(TypeConversion.convertMapToJsonString(input), equalTo("{}")); } - - @Test - public void convertJsonStringToArgsShouldReturnCorrectListOfArgs() { - Map input = new HashMap<>(); - input.put("key", "value"); - input.put("key2", "value2"); - - String[] expected = new String[] {"--key=value", "--key2=value2"}; - String[] actual = TypeConversion.convertMapToArgs(input); - assertThat(actual.length, equalTo(expected.length)); - assertTrue(Arrays.asList(actual).containsAll(Arrays.asList(expected))); - } } diff --git a/protos/feast/core/Runner.proto b/protos/feast/core/Runner.proto index 544972286df..91c1e99485f 100644 --- a/protos/feast/core/Runner.proto +++ b/protos/feast/core/Runner.proto @@ -70,4 +70,7 @@ message DataflowRunnerConfigOptions { /* BigQuery table specification, e.g. PROJECT_ID:DATASET_ID.PROJECT_ID */ string deadLetterTableSpec = 12; + + /* Labels to apply to the dataflow job */ + map labels = 13; } \ No newline at end of file From b497a0b833d139ffc9a224085deafafacaeabb71 Mon Sep 17 00:00:00 2001 From: Terence Lim Date: Mon, 1 Jun 2020 16:11:59 +0800 Subject: [PATCH 24/26] Increase Jaeger Tracing coverage (#719) * Add jaegertracing logs to getOnlineFeatures * Apply spotless --- .../main/java/feast/serving/service/OnlineServingService.java | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/serving/src/main/java/feast/serving/service/OnlineServingService.java b/serving/src/main/java/feast/serving/service/OnlineServingService.java index 7339a19c1a4..bb73e34f51c 100644 --- a/serving/src/main/java/feast/serving/service/OnlineServingService.java +++ b/serving/src/main/java/feast/serving/service/OnlineServingService.java @@ -16,6 +16,7 @@ */ package feast.serving.service; +import com.google.common.collect.ImmutableMap; import com.google.common.collect.Maps; import com.google.protobuf.Duration; import feast.proto.serving.ServingAPIProto.*; @@ -75,6 +76,9 @@ public GetOnlineFeaturesResponse getOnlineFeatures(GetOnlineFeaturesRequest requ // feature set request. List> featureRows = retriever.getOnlineFeatures(entityRows, featureSetRequests); + if (scope != null) { + scope.span().log(ImmutableMap.of("event", "featureRows", "value", featureRows)); + } // For each feature set request, read the feature rows returned by the retriever, and // populate the featureValuesMap with the feature values corresponding to that entity row. From 8bb7f2786cfd91dbde1fd2cd3b4b4e6e789743ca Mon Sep 17 00:00:00 2001 From: Khor Shu Heng Date: Sun, 7 Jun 2020 11:40:06 +0800 Subject: [PATCH 25/26] Fix pipeline options toArgs() returning empty list #765 --- .../job/dataflow/DataflowRunnerConfig.java | 26 +++---- .../core/job/direct/DirectRunnerConfig.java | 4 +- .../feast/core/job/option/RunnerConfig.java | 4 +- .../dataflow/DataflowRunnerConfigTest.java | 68 +++++++++++++++++++ .../job/direct/DirectRunnerConfigTest.java | 44 ++++++++++++ 5 files changed, 129 insertions(+), 17 deletions(-) create mode 100644 core/src/test/java/feast/core/job/dataflow/DataflowRunnerConfigTest.java create mode 100644 core/src/test/java/feast/core/job/direct/DirectRunnerConfigTest.java diff --git a/core/src/main/java/feast/core/job/dataflow/DataflowRunnerConfig.java b/core/src/main/java/feast/core/job/dataflow/DataflowRunnerConfig.java index 85628d2cd0e..df0cb325388 100644 --- a/core/src/main/java/feast/core/job/dataflow/DataflowRunnerConfig.java +++ b/core/src/main/java/feast/core/job/dataflow/DataflowRunnerConfig.java @@ -47,45 +47,45 @@ public DataflowRunnerConfig(DataflowRunnerConfigOptions runnerConfigOptions) { } /* Project id to use when launching jobs. */ - @NotBlank String project; + @NotBlank public String project; /* The Google Compute Engine region for creating Dataflow jobs. */ - @NotBlank String region; + @NotBlank public String region; /* GCP availability zone for operations. */ - @NotBlank String zone; + @NotBlank public String zone; /* Run the job as a specific service account, instead of the default GCE robot. */ - String serviceAccount; + public String serviceAccount; /* GCE network for launching workers. */ - @NotBlank String network; + @NotBlank public String network; /* GCE subnetwork for launching workers. */ - @NotBlank String subnetwork; + @NotBlank public String subnetwork; /* Machine type to create Dataflow worker VMs as. */ - String workerMachineType; + public String workerMachineType; /* The autoscaling algorithm to use for the workerpool. */ - String autoscalingAlgorithm; + public String autoscalingAlgorithm; /* Specifies whether worker pools should be started with public IP addresses. */ - Boolean usePublicIps; + public Boolean usePublicIps; /** * A pipeline level default location for storing temporary files. Support Google Cloud Storage * locations, e.g. gs://bucket/object */ - @NotBlank String tempLocation; + @NotBlank public String tempLocation; /* The maximum number of workers to use for the workerpool. */ - Integer maxNumWorkers; + public Integer maxNumWorkers; /* BigQuery table specification, e.g. PROJECT_ID:DATASET_ID.PROJECT_ID */ - String deadLetterTableSpec; + public String deadLetterTableSpec; - Map labels; + public Map labels; /** Validates Dataflow runner configuration options */ public void validate() { diff --git a/core/src/main/java/feast/core/job/direct/DirectRunnerConfig.java b/core/src/main/java/feast/core/job/direct/DirectRunnerConfig.java index ebd327f2f75..c7459cb9d16 100644 --- a/core/src/main/java/feast/core/job/direct/DirectRunnerConfig.java +++ b/core/src/main/java/feast/core/job/direct/DirectRunnerConfig.java @@ -24,10 +24,10 @@ public class DirectRunnerConfig extends RunnerConfig { * Controls the amount of target parallelism the DirectRunner will use. Defaults to the greater of * the number of available processors and 3. Must be a value greater than zero. */ - Integer targetParallelism; + public Integer targetParallelism; /* BigQuery table specification, e.g. PROJECT_ID:DATASET_ID.PROJECT_ID */ - String deadletterTableSpec; + public String deadletterTableSpec; public DirectRunnerConfig(DirectRunnerConfigOptions runnerConfigOptions) { this.deadletterTableSpec = runnerConfigOptions.getDeadLetterTableSpec(); diff --git a/core/src/main/java/feast/core/job/option/RunnerConfig.java b/core/src/main/java/feast/core/job/option/RunnerConfig.java index 4b937074a31..44550f9608e 100644 --- a/core/src/main/java/feast/core/job/option/RunnerConfig.java +++ b/core/src/main/java/feast/core/job/option/RunnerConfig.java @@ -30,8 +30,8 @@ public abstract class RunnerConfig { /** - * Converts the fields in this class to a list of --key=value args to be passed to a {@link - * org.apache.beam.sdk.options.PipelineOptionsFactory}. + * Converts the public-access fields in this class to a list of --key=value args to be passed to a + * {@link org.apache.beam.sdk.options.PipelineOptionsFactory}. * *

Ignores values that are proto-default (e.g. empty string, 0). * diff --git a/core/src/test/java/feast/core/job/dataflow/DataflowRunnerConfigTest.java b/core/src/test/java/feast/core/job/dataflow/DataflowRunnerConfigTest.java new file mode 100644 index 00000000000..d5c69af6a72 --- /dev/null +++ b/core/src/test/java/feast/core/job/dataflow/DataflowRunnerConfigTest.java @@ -0,0 +1,68 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright 2018-2020 The Feast Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package feast.core.job.dataflow; + +import static org.hamcrest.CoreMatchers.equalTo; +import static org.hamcrest.Matchers.containsInAnyOrder; +import static org.junit.Assert.*; + +import com.google.common.collect.Lists; +import feast.proto.core.RunnerProto.DataflowRunnerConfigOptions; +import java.util.Arrays; +import java.util.List; +import org.junit.Test; + +public class DataflowRunnerConfigTest { + @Test + public void shouldConvertToPipelineArgs() throws IllegalAccessException { + DataflowRunnerConfigOptions opts = + DataflowRunnerConfigOptions.newBuilder() + .setProject("my-project") + .setRegion("asia-east1") + .setZone("asia-east1-a") + .setTempLocation("gs://bucket/tempLocation") + .setNetwork("default") + .setSubnetwork("regions/asia-east1/subnetworks/mysubnetwork") + .setMaxNumWorkers(1) + .setAutoscalingAlgorithm("THROUGHPUT_BASED") + .setUsePublicIps(false) + .setWorkerMachineType("n1-standard-1") + .setDeadLetterTableSpec("project_id:dataset_id.table_id") + .putLabels("key", "value") + .build(); + + DataflowRunnerConfig dataflowRunnerConfig = new DataflowRunnerConfig(opts); + List args = Lists.newArrayList(dataflowRunnerConfig.toArgs()); + String[] expectedArgs = + Arrays.asList( + "--project=my-project", + "--region=asia-east1", + "--zone=asia-east1-a", + "--tempLocation=gs://bucket/tempLocation", + "--network=default", + "--subnetwork=regions/asia-east1/subnetworks/mysubnetwork", + "--maxNumWorkers=1", + "--autoscalingAlgorithm=THROUGHPUT_BASED", + "--usePublicIps=false", + "--workerMachineType=n1-standard-1", + "--deadLetterTableSpec=project_id:dataset_id.table_id", + "--labels={\"key\":\"value\"}") + .toArray(String[]::new); + assertThat(args.size(), equalTo(expectedArgs.length)); + assertThat(args, containsInAnyOrder(expectedArgs)); + } +} diff --git a/core/src/test/java/feast/core/job/direct/DirectRunnerConfigTest.java b/core/src/test/java/feast/core/job/direct/DirectRunnerConfigTest.java new file mode 100644 index 00000000000..c4114dd2104 --- /dev/null +++ b/core/src/test/java/feast/core/job/direct/DirectRunnerConfigTest.java @@ -0,0 +1,44 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright 2018-2020 The Feast Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package feast.core.job.direct; + +import static org.hamcrest.Matchers.containsInAnyOrder; +import static org.hamcrest.Matchers.equalTo; +import static org.junit.Assert.*; + +import com.google.common.collect.Lists; +import feast.proto.core.RunnerProto.DirectRunnerConfigOptions; +import java.util.List; +import org.junit.Test; + +public class DirectRunnerConfigTest { + @Test + public void shouldConvertToPipelineArgs() throws IllegalAccessException { + DirectRunnerConfigOptions opts = + DirectRunnerConfigOptions.newBuilder() + .setTargetParallelism(1) + .setDeadLetterTableSpec("project_id:dataset_id.table_id") + .build(); + DirectRunnerConfig directRunnerConfig = new DirectRunnerConfig(opts); + List args = Lists.newArrayList(directRunnerConfig.toArgs()); + assertThat(args.size(), equalTo(2)); + assertThat( + args, + containsInAnyOrder( + "--targetParallelism=1", "--deadletterTableSpec=project_id:dataset_id.table_id")); + } +} From 3ced5f2f4a6f1cb09120858b35bd6dfcc41ef71d Mon Sep 17 00:00:00 2001 From: Khor Shu Heng <32997938+khorshuheng@users.noreply.github.com> Date: Sun, 7 Jun 2020 12:14:01 +0800 Subject: [PATCH 26/26] Release v0.5.1 (#781) Co-authored-by: Khor Shu Heng --- CHANGELOG.md | 22 +++++++++++++++++++ infra/charts/feast/Chart.yaml | 2 +- infra/charts/feast/README.md | 8 +++---- .../charts/feast/charts/feast-core/Chart.yaml | 2 +- .../charts/feast/charts/feast-core/README.md | 2 +- .../feast/charts/feast-serving/Chart.yaml | 2 +- .../feast/charts/feast-serving/README.md | 2 +- infra/charts/feast/requirements.lock | 6 ++--- infra/charts/feast/requirements.yaml | 6 ++--- pom.xml | 2 +- 10 files changed, 38 insertions(+), 16 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5339b8c5f57..77f666cf395 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,27 @@ # Changelog +## [0.5.1](https://github.com/feast-dev/feast/tree/0.5.1) (2020-06-06) + +[Full Changelog](https://github.com/feast-dev/feast/compare/v0.5.0...v0.5.1) + +**Implemented enhancements:** +- Add support for version method in Feast SDK and Core [\#759](https://github.com/feast-dev/feast/pull/759) ([woop](https://github.com/woop)) +- Refactor runner configuration, add labels to dataflow options [\#718](https://github.com/feast-dev/feast/pull/718) ([zhilingc](https://github.com/zhilingc)) + +**Fixed bugs:** +- Fix pipeline options toArgs\(\) returning empty list [\#765](https://github.com/feast-dev/feast/pull/765) ([zhilingc](https://github.com/zhilingc)) +- Fix project argument for feature set describe in CLI [\#731](https://github.com/feast-dev/feast/pull/731) ([terryyylim](https://github.com/terryyylim)) +- Fix Go and Java SDK Regressions [\#729](https://github.com/feast-dev/feast/pull/729) ([mrzzy](https://github.com/mrzzy)) +- Remove usage of parallel stream for feature value map generation [\#751](https://github.com/feast-dev/feast/pull/751) ([khorshuheng](https://github.com/khorshuheng)) +- Restore Feast Java SDK and Ingestion compatibility with Java 8 runtimes [\#722](https://github.com/feast-dev/feast/pull/722) ([ches](https://github.com/ches)) +- Python sdk bug fixes [\#723](https://github.com/feast-dev/feast/pull/723) ([zhilingc](https://github.com/zhilingc)) + +**Merged pull requests:** +- Increase Jaeger Tracing coverage [\#719](https://github.com/feast-dev/feast/pull/719) ([terryyylim](https://github.com/terryyylim)) +- Recompile golang protos to include new FeatureSetStatus [\#755](https://github.com/feast-dev/feast/pull/755) ([zhilingc](https://github.com/zhilingc)) +- Merge Redis cluster connector with Redis connector [\#752](https://github.com/feast-dev/feast/pull/752) ([pyalex](https://github.com/pyalex)) +- Remove unused Hibernate dep from Serving [\#721](https://github.com/feast-dev/feast/pull/721) ([ches](https://github.com/ches)) + ## [v0.5.0](https://github.com/feast-dev/feast/tree/v0.5.0) (2020-05-19) [Full Changelog](https://github.com/feast-dev/feast/compare/v0.4.7...v0.5.0) diff --git a/infra/charts/feast/Chart.yaml b/infra/charts/feast/Chart.yaml index 8ce82e4a01d..0a175ffc9d5 100644 --- a/infra/charts/feast/Chart.yaml +++ b/infra/charts/feast/Chart.yaml @@ -1,4 +1,4 @@ apiVersion: v1 description: Feature store for machine learning. name: feast -version: 0.5.0-alpha.1 +version: 0.5.1 diff --git a/infra/charts/feast/README.md b/infra/charts/feast/README.md index 3d868b2fd13..aa3a7a3c1f0 100644 --- a/infra/charts/feast/README.md +++ b/infra/charts/feast/README.md @@ -1,7 +1,7 @@ feast ===== -Feature store for machine learning. Current chart version is `0.5.0-alpha.1` +Feature store for machine learning. Current chart version is `0.5.1` ## TL;DR; @@ -32,9 +32,9 @@ This chart install Feast deployment on a Kubernetes cluster using the [Helm](htt | Repository | Name | Version | |------------|------|---------| -| | feast-core | 0.5.0-alpha.1 | -| | feast-serving | 0.5.0-alpha.1 | -| | feast-serving | 0.5.0-alpha.1 | +| | feast-core | 0.5.1 | +| | feast-serving | 0.5.1 | +| | feast-serving | 0.5.1 | | | prometheus-statsd-exporter | 0.1.2 | | https://kubernetes-charts-incubator.storage.googleapis.com/ | kafka | 0.20.8 | | https://kubernetes-charts.storage.googleapis.com/ | grafana | 5.0.5 | diff --git a/infra/charts/feast/charts/feast-core/Chart.yaml b/infra/charts/feast/charts/feast-core/Chart.yaml index 5b832943cfe..e5155a64e18 100644 --- a/infra/charts/feast/charts/feast-core/Chart.yaml +++ b/infra/charts/feast/charts/feast-core/Chart.yaml @@ -1,4 +1,4 @@ apiVersion: v1 description: Feast Core registers feature specifications and manage ingestion jobs. name: feast-core -version: 0.5.0-alpha.1 +version: 0.5.1 diff --git a/infra/charts/feast/charts/feast-core/README.md b/infra/charts/feast/charts/feast-core/README.md index 40d6bd91129..fc8358c808c 100644 --- a/infra/charts/feast/charts/feast-core/README.md +++ b/infra/charts/feast/charts/feast-core/README.md @@ -2,7 +2,7 @@ feast-core ========== Feast Core registers feature specifications and manage ingestion jobs. -Current chart version is `0.5.0-alpha.1` +Current chart version is `0.5.1` diff --git a/infra/charts/feast/charts/feast-serving/Chart.yaml b/infra/charts/feast/charts/feast-serving/Chart.yaml index 7c8e6131cf7..0b23af87ac3 100644 --- a/infra/charts/feast/charts/feast-serving/Chart.yaml +++ b/infra/charts/feast/charts/feast-serving/Chart.yaml @@ -1,4 +1,4 @@ apiVersion: v1 description: Feast Serving serves low-latency latest features and historical batch features. name: feast-serving -version: 0.5.0-alpha.1 +version: 0.5.1 diff --git a/infra/charts/feast/charts/feast-serving/README.md b/infra/charts/feast/charts/feast-serving/README.md index d30da2eb3a8..f278fadc006 100644 --- a/infra/charts/feast/charts/feast-serving/README.md +++ b/infra/charts/feast/charts/feast-serving/README.md @@ -2,7 +2,7 @@ feast-serving ============= Feast Serving serves low-latency latest features and historical batch features. -Current chart version is `0.5.0-alpha.1` +Current chart version is `0.5.1` diff --git a/infra/charts/feast/requirements.lock b/infra/charts/feast/requirements.lock index c6ff995bacc..6281cc1506f 100644 --- a/infra/charts/feast/requirements.lock +++ b/infra/charts/feast/requirements.lock @@ -1,13 +1,13 @@ dependencies: - name: feast-core repository: "" - version: 0.5.0-alpha.1 + version: 0.5.1 - name: feast-serving repository: "" - version: 0.5.0-alpha.1 + version: 0.5.1 - name: feast-serving repository: "" - version: 0.5.0-alpha.1 + version: 0.5.1 - name: postgresql repository: https://kubernetes-charts.storage.googleapis.com/ version: 8.6.1 diff --git a/infra/charts/feast/requirements.yaml b/infra/charts/feast/requirements.yaml index 7de78705752..b215103ed23 100644 --- a/infra/charts/feast/requirements.yaml +++ b/infra/charts/feast/requirements.yaml @@ -1,14 +1,14 @@ dependencies: - name: feast-core - version: 0.5.0-alpha.1 + version: 0.5.1 condition: feast-core.enabled - name: feast-serving alias: feast-online-serving - version: 0.5.0-alpha.1 + version: 0.5.1 condition: feast-online-serving.enabled - name: feast-serving alias: feast-batch-serving - version: 0.5.0-alpha.1 + version: 0.5.1 condition: feast-batch-serving.enabled - name: postgresql version: 8.6.1 diff --git a/pom.xml b/pom.xml index 3211d585294..c1990b8ce99 100644 --- a/pom.xml +++ b/pom.xml @@ -39,7 +39,7 @@ - 0.5 + 0.5.1 https://github.com/feast-dev/feast UTF-8