Skip to content

Persisting results of historical retrieval - #2197

Merged
feast-ci-bot merged 2 commits into
masterfrom
saved-dataset
Jan 26, 2022
Merged

feast-ci-bot merged 2 commits into
masterfrom
saved-dataset

Conversation

@pyalex

@pyalex pyalex commented Jan 6, 2022 •

Copy link
Copy Markdown
Collaborator

Signed-off-by: pyalex [email protected]

What this PR does / why we need it:

This PR introduces persisting results of historical retrieval. This feature is required by Data Quality Monitoring and described in the DQM RFC. The difference with RFC, though, is that this PR proposes to persist datasets in configured offline store (currently supported Redshift / BigQuery / File) instead of always writing it to cloud storage as parquet files.

Example of usage:

from feast.infra.offline_stores.bigquery_source import SavedDatasetBigQueryStorage

job = store.get_historical_features(...)

ds = store.create_saved_dataset(
   from_=job,
   name='my_persisted_ds',
   storage=SavedDatasetBigQueryStorage(table_ref='project.dataset.table_name'),
   tags={'key': 'value'}
)

ds = store.get_saved_dataset(name='my_persisted_ds')
ds.to_df()

get_saved_dataset(name) returns SavedDataset object which provides access to dataset metadata. Data can be retrieved by calling .to_df() or .to_arrow() on this object.

Which issue(s) this PR fixes:

Fixes #

Does this PR introduce a user-facing change?:

Two new methods in FeatureStore class
create_saved_dataset - create dataset from historical retrieval job; persist it in offline store and write metadata to the registry
get_saved_dataset - retrieve metadata from registry and data from offline store

@codecov-commenter

codecov-commenter commented Jan 6, 2022 •

Copy link
Copy Markdown

Codecov Report

Merging #2197 (b6a165e) into master (396f729) will decrease coverage by 25.17%.
The diff coverage is 45.88%.

Impacted file tree graph

@@             Coverage Diff             @@
##           master    #2197       +/-   ##
===========================================
- Coverage   85.24%   60.06%   -25.18%     
===========================================
  Files         107      108        +1     
  Lines        8605     8943      +338     
===========================================
- Hits         7335     5372     -1963     
- Misses       1270     3571     +2301     
Flag Coverage Δ
integrationtests ?
unittests 60.06% <45.88%> (-0.37%) ⬇️

Flags with carried forward coverage won't be shown. Click here to find out more.

Impacted Files Coverage Δ
sdk/python/feast/registry.py 67.60% <16.66%> (-15.91%) ⬇️
...fline_store/test_universal_historical_retrieval.py 23.71% <23.72%> (-76.29%) ⬇️
sdk/python/feast/feature_store.py 70.00% <26.31%> (-19.67%) ⬇️
sdk/python/feast/infra/offline_stores/redshift.py 37.95% <32.35%> (-50.62%) ⬇️
sdk/python/feast/infra/offline_stores/bigquery.py 31.88% <35.29%> (-50.82%) ⬇️
...ation/feature_repos/universal/data_sources/file.py 49.33% <44.44%> (-17.34%) ⬇️
sdk/python/feast/saved_dataset.py 48.97% <48.97%> (ø)
sdk/python/feast/infra/passthrough_provider.py 84.50% <50.00%> (-15.50%) ⬇️
...ts/integration/feature_repos/repo_configuration.py 45.71% <50.00%> (-48.58%) ⬇️
...n/feature_repos/universal/data_sources/redshift.py 52.94% <50.00%> (-47.06%) ⬇️
... and 69 more

Continue to review full report at Codecov.

Legend - Click here to learn more
Δ = absolute <relative> (impact), ø = not affected, ? = missing data
Powered by Codecov. Last update 396f729...b6a165e. Read the comment docs.

@pyalex pyalex changed the title [WIP] Persisting results of historical retrieval Persisting results of historical retrieval Jan 10, 2022
@pyalex
pyalex requested a review from tsotnet January 10, 2022 17:59
Comment thread protos/feast/core/Registry.proto Resolved
Comment thread protos/feast/core/SavedDataset.proto Resolved
Comment thread sdk/python/feast/feature_store.py Outdated
full_feature_names: A boolean that provides the option to add the feature view prefixes to the feature names,
changing them from the format "feature" to "feature_view__feature" (e.g., "daily_transactions" changes to
"customer_fv__daily_transactions"). By default, this value is set to False.
save_as: An optional configuration that enables persisting retrieved dataset in offline store.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Isn't this API overlapping with RetrievalJob.to_bigquery() etc? I think it's a bit confusing to me to have two places to tell Feast to save a dataset.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, you can call job.to_bigquery() or job.to_redshift() and this is what saved dataset implementation does under the hood. The difference is that you will need to add additional DataSource in order to read those saved tables back to Feast. And this is what this PR does automatically via store.get_saved_dataset

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it also is slightly unclear to me what the exact contract of RetrievalJob is. RetrievalJob otherwise is a deferred fetch that can be triggered on e.g. to_df() or to_arrow().

The save_as parameter seems to be ambiguous as to when it persists the data. (the implementation is it happens immediately). If we want the timing to be consistent, then it becomes very similar to the to_bigquery way of doing things.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  1. How it works: you're right that the original query will be triggered eagerly and the dataset will be persisted right away after calling get_historical_features. The caller will receive a substitution of RetrievalJob that will wrap a much simpler query: select * from persisted_table and process (contract) of materializing this query (job) remains the same (will be triggered on .to_df() or .to_arrow()).
  2. Why not postpone persisting until the user will call to_df(): current implementation doesn't cache query results, it means that if I'll call .to_df() 3 times - 3 jobs to BigQuery or Redshift would be created. In case we'll put persisting on that stage - we'll have to overwrite the destination table over and over, which I don't think is good contract. Ideally, we should write once and fail if the table already exists and not risk deleting data created by the user and not Feast.

@adchia adchia self-assigned this Jan 12, 2022
Comment thread protos/feast/core/SavedDataset.proto Resolved
Comment thread protos/feast/core/SavedDataset.proto Resolved
Comment thread sdk/python/feast/feature_store.py Outdated
full_feature_names: A boolean that provides the option to add the feature view prefixes to the feature names,
changing them from the format "feature" to "feature_view__feature" (e.g., "daily_transactions" changes to
"customer_fv__daily_transactions"). By default, this value is set to False.
save_as: An optional configuration that enables persisting retrieved dataset in offline store.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it also is slightly unclear to me what the exact contract of RetrievalJob is. RetrievalJob otherwise is a deferred fetch that can be triggered on e.g. to_df() or to_arrow().

The save_as parameter seems to be ambiguous as to when it persists the data. (the implementation is it happens immediately). If we want the timing to be consistent, then it becomes very similar to the to_bigquery way of doing things.

@MattDelac

Copy link
Copy Markdown
Collaborator

Thanks @pyalex for putting that together !

get_saved_dataset(name) returns RetrievalJob (similarly to get_historical_features), which can be materialized by calling .to_df() or .to_arrow().

What is weird in this behaviour is that store.get_historical_retrieval() will sometimes return a RetrievalJob object and sometimes will make modifications inplace.

Should we have SavedDatasetOptions() supported at the historical retrieval methods instead (like to_df() ) ? What would be the pros/cons of doing so in your opinion ?

Comment thread protos/feast/core/SavedDataset.proto Resolved
Comment thread protos/feast/core/SavedDataset.proto Resolved
@adchia adchia assigned MattDelac and unassigned achals Jan 20, 2022
@adchia

adchia commented Jan 20, 2022

Copy link
Copy Markdown
Collaborator

Thanks @pyalex for putting that together !

get_saved_dataset(name) returns RetrievalJob (similarly to get_historical_features), which can be materialized by calling .to_df() or .to_arrow().

What is weird in this behaviour is that store.get_historical_retrieval() will sometimes return a RetrievalJob object and sometimes will make modifications inplace.

Should we have SavedDatasetOptions() supported at the historical retrieval methods instead (like to_df() ) ? What would be the pros/cons of doing so in your opinion ?

I think as per our discussion, we might want to have get_historical_retrieval always do some in place modification so it becomes more consistent. Though there are certainly concerns around how long this dataset should persist. My guess would be that we by default store data in temporary tables unless users explicitly specify a SavedDataset.

@pyalex

pyalex commented Jan 20, 2022

Copy link
Copy Markdown
Collaborator Author

Hi @MattDelac, thanks for your comment!

The main reason not to defer dataset persisting until RetrievalJob is used - is that besides storing dataframe we also create metadata object (see SavedDataset proto) and this object is being written to the registry. I don't think it's a good idea to modify the registry from RetrievalJob. That's why this PR proposes eager execution:

  1. Original query is being triggered synchronously during get_historical_features (only if save_as parameter is provided)
  2. Dataframe is stored to DWH
  3. SavedDataset object is written to the registry

Does it make sense?

@adchia

adchia commented Jan 21, 2022

Copy link
Copy Markdown
Collaborator

@mavysavydav fyi, we'll move forward with not adding any breaking changes while we test DQM. We can discuss if there are more elegant solutions to handle to_sql better later on!

@pyalex

pyalex commented Jan 21, 2022

Copy link
Copy Markdown
Collaborator Author

@MattDelac can we get your blessing to move forward with this PR?
At the end of the day - this API (I mean with save_as parameter) is experimental and will most probably change. Also, the important thing that I want to stress once more is that the previous behavior (with save_as parameter not passed) remains unchanged. So if you won't use this functionality - you won't feel a difference.

@MattDelac

Copy link
Copy Markdown
Collaborator

At the end of the day - this API (I mean with save_as parameter) is experimental and will most probably change. Also, the important thing that I want to stress once more is that the previous behavior (with save_as parameter not passed) remains unchanged. So if you won't use this functionality - you won't feel a difference

Sounds good then. I am fine with experimenting to gain context and experience about what should be built 👍
I will try to review the PR. Can you make sure that if save_as is used, we show am ExperimentalWarning to our users ? We need to make it clear that this is not stable and that is can change anytime

@pyalex

pyalex commented Jan 22, 2022

Copy link
Copy Markdown
Collaborator Author

@adchia adchia left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How does a user "delete" a SavedDataset? At least remove references to it from the registry? As well as what happens if the underlying dataset is removed? (which isn't too crazy to imagine e.g. if user generated data must be tied to a TTL for GDPR reasons)

timestamp_desc_string = " DESC, ".join(timestamps) + " DESC"
field_string = ", ".join(join_key_columns + feature_name_columns + timestamps)
field_string = ", ".join(
set(join_key_columns + feature_name_columns + timestamps)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is there a reason you made this a set? should probably call this out in the description of the PR too since it can cause a behavior change

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was trying to reuse pull_latest_from_table_or_query because it's already implemented in all offline stores including extensions. But those implementations also include deduplication, which shouldn't happen for retrieving a saved dataset.
Now I see that it probably wasn't a good idea. I will use a separate method.

block_until_done(client=self.client, bq_job=bq_job, timeout=timeout)
return bq_job

def persist(self, storage: SavedDatasetStorage) -> "BigQueryRetrievalJob":

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we want this to be a publically facing method? Seems confusing to a user why there'd be

  1. this persist method, which happens lazily.
  2. to_bigquery, which exists right next to this method and also saves.

I think the return type can be confusing too since .persist has indeed executed a query & persisted, but we return a RetrievalJob. I might as a user expect to need to call RetrievalJob#to_bigquery again, which actually will persist to a different location.

Two options I think could make sense:

  • Have this be an internal only method by name so people don't try to call it
  • Expose a separate way to instantiate a RetrievalJob (like RetrievalJob.from_saved_dataset which executes this wrapped query) so you can return it in get_historical_features

@pyalex pyalex Jan 25, 2022 •

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

persist doesn't happen lazily. It's generic equivalent of methods to_bigquery or to_redshift which exist only in specific implementation classes. predict is part of abstract API (RetrievalJob) and aimed to unify these methods. Under the hood, it's calling to_bigquery or to_redshift.

The feature that is absent in previous methods is that persist returns RetrievalJob, which supposedly retrieves from a persisted table.

Why it's public? As we discussed on call with Shopify - it could be useful if the user doesn't want get_historical_features to run query eagerly. In this case, user would not pass save_as parameter but call persist themselves:

job = fs.get_historical_feature(...)
persisted = job.persist(storage=SavedDatasetBigQueryStorage(...))
persisted.to_df()

Regarding cyclical calls, I think that we could deprecate specific methods like .to_bigquery() and then it should be less confusing.

Expose a separate way to instantiate a RetrievalJob (like RetrievalJob.from_saved_dataset which executes this wrapped query) so you can return it in get_historical_features

This option makes sense to me as well. Please let me know what you think is better @adchia

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

UPD. persist signature updated. Now it doesn't return RetrievalJob to avoid duplication of responsibilities. Read of saved dataset moved to Provider.read_saved_dataset, which uses OfflineSource.pull_all_from_table_or_query.

So now .persist() is just a universal replacement for .to_bigquery() and .to_redshift().

pyarrow.parquet.write_table(
self._to_arrow_internal(), where=path, filesystem=filesystem
)
else:

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

probably makes sense to check it's not some other file format like .csv which we don't support

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In this else block we treat the destination as a directory, so it could be named even as dir.csv -> dir.csv/ folder would be created.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added comment line


raise EntityNotFoundException(name, project)

def apply_saved_dataset(

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

not a blocking comment. More of an FYI

fyi @felixwang9817 since you are adding a new concept to the registry. Don't think feast plan could cover this since this isn't introduced at feast apply time.

Also doesn't really make sense to treat this as a real FCO since the schema is a snapshot of how feature services / feature views resolve at a point in time. Doubt we'd be able to do this properly unless we have some form of stricter versioning.

@MattDelac MattDelac left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My :2-cents:

entity_df=...,
save_as=SavedDatasetOptions(
name='my_training_dataset',
storage=SavedDatasetBigQueryStorage(table_ref='<gcp-project>.<gcp-dataset>.my_training_dataset'),

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Don t we have an issue if the data is store in multiple storages ? I though that all the data should be in the same offline store (eg: BigQuery) so that we can join any tables between them.

By allowing the "saved dataset" to be stored in another offline storage, it would mean that we will never be able to JOIN a saved dataset with a feature view. Any thoughts on that ?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You won't be able to pass here different storage other than the one you have configured as a global offline store. There will be an exception if your features are being pulled from BigQuery and you will try to store them into Redshift.
This is just a generic API to support typed configuration for all kinds of stores. I couldn't find a better way (at least in Python type system is not so strong). If you know how to do it better please let me know.

entity_df=...,
save_as=SavedDatasetOptions(
name='my_training_dataset',
storage=SavedDatasetBigQueryStorage(table_ref='<gcp-project>.<gcp-dataset>.my_training_dataset'),

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also I would believe that we should provide a "smart default" here with storage=... is inferred and will use the same one as the offline store specified in the config

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can infer the type of storage, but each type will still require its own parameters (eg, table_ref for BQ, path for File) and we need those parameters provided by user anyway. So those classes (SavedDatasetBigQueryStorage, SavedDatasetRedshiftStorage, etc) are useful at least for parameters type checking.


Saved dataset can be later retrieved using `get_saved_dataset` method:
```python
retrieval_job = store.get_saved_dataset('my_training_dataset')

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this when the saved dataset is computed ?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If by compute you mean materialized, then no. The saved dataset is computed eagerly on creation. For example, when you call get_historical_features with the save_as parameter saved dataset would be already materialized & stored to offline store & registry by the time this function returns.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For example, when you call get_historical_features with the save_as parameter saved dataset would be already materialized

That's where it could be confusing because fs.get_historical_features() does not materialize anything normally but it woudl do it when save_as is passed. Two completely different behaviour with the same method.


store = FeatureStore()

store.get_historical_features(

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I still want to think out-loud about the behaviour of this new API. I just want to understand the pros/cons of each approach.

I understand that you want to have the saved dataset computed only when explicitly asked by the user.

Let's assume that store.get_historical_features() should only return one type of object (aka RetrievalJobs).
What do you think about the following ?

store = FeatureStore()
offline_job: OfflineRetrievalJob = store.get_historical_features(
    features=["driver:avg_trip"],
    entity_df=...,
)

saved_dataset: SavedDatasetJob = store.create_saved_dataset(
    name='my_training_dataset',
    storage=SavedDatasetBigQueryStorage(table_ref='<gcp-project>.<gcp-dataset>.my_training_dataset'), # Should be inferred by default and use the offline storage of the retrieval job
    retrieval_job=offline_job, # Any RetrievalJob should have methods available to write the output in the desired SavedDatasetStorage
    tags={'author': 'oleksii'},
)

retrieval_job: OfflineRetrievalJob = store.get_saved_dataset('my_training_dataset')
retrieval_job.to_df()

WDYT ?

@pyalex pyalex Jan 25, 2022 •

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yep, this could work. But what are benefits of this approach?

  1. Do you want to reuse offline_job? But in that case, historical query can be run several times, which is additional costs:
offline_job = store.get_historical_features(...)
store.create_saved_dataset(offline_job)
offline_job.to_df()  # historical query (with point-in-time join) executed twice

Whereas in the current proposed version saved dataset plays the role of cache. Eg,

result = store.get_historical_features(..., save_as='...')
result.to_df()
result.to_df() # both times reads from persisted table, not running the original query
  1. Will create_saved_dataset run offline_job eagerly & what is the purpose of SavedDatasetJob?

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

But what are benefits of this approach?

Mostly consistency: store.get_historical_features() always return an OfflineRetrievalJob object

Do you want to reuse offline_job?

I don't want to have strong assumptions on that. What I know is that the BQ Retrieval job has a dry-run option. So I would not be surprised if some users use it multiple times.

query can be run several times, which is additional costs

That's on the user to decide if this additional cost is useful or not IMO

Whereas in the current proposed version saved dataset plays the role of cache

I get that and this is great 👍

what is the purpose of SavedDatasetJob?

Now I understand it better, I think that it's not needed. Here is my updated example

store = FeatureStore()
offline_job: OfflineRetrievalJob = store.get_historical_features(
    features=["driver:avg_trip"],
    entity_df=...,
)

retrieval_job: OfflineRetrievalJob = store.create_saved_dataset( # That operation executes the RetrievalJob
    name='my_training_dataset',
    storage=SavedDatasetBigQueryStorage(table_ref='<gcp-project>.<gcp-dataset>.my_training_dataset'),
    retrieval_job=offline_job,
    tags={'author': 'oleksii'},
)

retrieval_job.to_df()

As I said, I am thinking out-loud

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

API is updated.

return bq_job

def persist(self, storage: SavedDatasetStorage) -> "BigQueryRetrievalJob":
assert isinstance(storage, SavedDatasetBigQueryStorage)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is the BigQuery offline store can only persist the SavedDataset into BigQuery, why making it possible to pass any SavedDatasetStorage and not infer it ?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As described above, that's merely for type checking: good for verification on development and could be helpful when using Feast in some kind of IDE.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If I understand correctly, you suggesting just plain dict:

fs.get_historical_features(..., save_as={'storage': {'table_ref': '...'}})

vs proposed API

fs.get_historical_features(..., save_as=SavedDatasetOpts(storage=SavedDatasetBigQueryStorage(table_ref=...))

just to free you from remembering those dict keys.

@MattDelac MattDelac Jan 25, 2022 •

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If I understand correctly, you suggesting just plain dict

Or sending parameters right away. Something like

get_historical_features(..., save_as=SavedDataset(table_ref="...")) # Example 1
get_historical_features(..., save_as=SavedDataset(table_ref="...", table_ttl=timedelta(days=30))) # Example 2
get_historical_features(..., save_as=SavedDataset(file_path="...")) # Example 3

# Under the hood
@dataclass
class SavedDataset:
    table_ref: Optional[str] = None
    table_ttl: Optional[timedelta] = None
    file_path: Optional[str] = None

def get_historical_features(..., save_as: SavedDataset):
    SavedDatasetOptions = config.offline_store.get_saved_dataset_options(**asdict(save_as))

Unsure if that makes it more confusing or easier to use for our users TBH 🤔

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think subclasses is a better approach:

  1. Easier to extend (add new kind of storage)
  2. You can have mandatory & optional parameters, whereas with a single class they all are optional.

Plus I believe this approach (more strict types) is used in various places in Feast, so keeping things consistent is good.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think subclasses is a better approach

Sounds good to me 👍

@pyalex

pyalex commented Jan 25, 2022

Copy link
Copy Markdown
Collaborator Author

How does a user "delete" a SavedDataset? At least remove references to it from the registry? As well as what happens if the underlying dataset is removed? (which isn't too crazy to imagine e.g. if user generated data must be tied to a TTL for GDPR reasons)

As far as I know, only BigQuery supports TTL for tables, so we'll probably need to implement FeatureStore level method (delete_saved_dataset), which will include underlying storage cleanup. But that probably is not in scope of this PR.

@pyalex

pyalex commented Jan 26, 2022

Copy link
Copy Markdown
Collaborator Author

@adchia @MattDelac I have refactored some APIs. Please give it one more look.

  1. Saved dataset creation moved to a dedicated method, which expects retrieval job and calls .persist() on it (among other things). See docs or PR description for details.
  2. Both create_saved_dataset and get_saved_dataset return SavedDataset object instead of RetrievalJob (however the job is still inside). This is for better separation between these methods and for example get_historical_features, but also I need this for validation API (my next PR).
  3. .persist() not returning anything anymore. It simplifies the code in general and now its only purpose is to be unifying replacement for .to_bigquery() and .to_redshift(), etc.

@adchia adchia left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

/lgtm

@feast-ci-bot

Copy link
Copy Markdown
Collaborator

[APPROVALNOTIFIER] This PR is APPROVED

This pull-request has been approved by: adchia, pyalex

The full list of commands accepted by this bot can be found here.

The pull request process is described here

Details Needs approval from an approver in each of these files:

Approvers can indicate their approval by writing /approve in a comment
Approvers can cancel approval by writing /approve cancel in a comment

@feast-ci-bot
feast-ci-bot merged commit 6f1174a into master Jan 26, 2022
@MattDelac

Copy link
Copy Markdown
Collaborator

@adchia @MattDelac I have refactored some APIs. Please give it one more look.

  1. Saved dataset creation moved to a dedicated method, which expects retrieval job and calls .persist() on it (among other things). See docs or PR description for details.
  2. Both create_saved_dataset and get_saved_dataset return SavedDataset object instead of RetrievalJob (however the job is still inside). This is for better separation between these methods and for example get_historical_features, but also I need this for validation API (my next PR).
  3. .persist() not returning anything anymore. It simplifies the code in general and now its only purpose is to be unifying replacement for .to_bigquery() and .to_redshift(), etc.

This is awesome 🚀 🚀 🚀 🚀

Thanks for putting the extra effort here 🙏

@adchia
adchia deleted the saved-dataset branch February 3, 2022 17:12
@adchia adchia linked an issue Apr 21, 2022 that may be closed by this pull request
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Data quality monitoring

7 participants