Repository: feast-dev/feast

Issue:
Datastore online request makes a call once for each entity
**Is your feature request related to a problem? Please describe.**
There are performance issues with Datastore when fetching features for multiple entities. An individual fetch request is made for each entity 

**Describe the solution you'd like**
I would like to have a more efficient access to the Datastore for multiple entities. The python api for datastore offers a method `get_multi()` which allows to fetch multiple keys in one request.

**Describe alternatives you've considered**
---

**Additional context**

I have done some basic tests using the `get_multi` vs get and it seems that a improvement can be made by using the function `get_multi`

Here is part of code  I used for testing:

```
import numpy as np
import timeit
from google.cloud import datastore

def multi_online_read(num_keys=5):

    client = datastore.Client(
                    project=project_name, namespace=None,
                )
    
    feast_project = "feature_repo"
    key = client.key("Project", feast_project, "Table", table, "Row",  row)
    keys = []
    response = []
    for i in range(num_keys):
        keys.append(key)

    response = client.get_multi(keys)

    return response

def online_read(num_keys=5):
    client = datastore.Client(
                    project=project_name, namespace=None,
                )
    
    feast_project = "feature_repo"
    key = client.key("Project", feast_project, "Table", table, "Row", row)
    
    response = []
    for i in range(num_keys):
        response.append(client.get(key))


    return response

if __name__ == "__main__":

    results = timeit.repeat(online_read, repeat=10, number=1)
    print(results)
    print(np.mean(results))

    results = timeit.repeat(multi_online_read, repeat=10, number=1)
    print(results)
    print(np.mean(results))
```







Discussion:
We'd love to see a contribution here if you have time :)

Resolving patch:
diff --git a/sdk/python/feast/infra/online_stores/datastore.py b/sdk/python/feast/infra/online_stores/datastore.py
--- a/sdk/python/feast/infra/online_stores/datastore.py
+++ b/sdk/python/feast/infra/online_stores/datastore.py
@@ -30,6 +30,7 @@
 try:
     from google.auth.exceptions import DefaultCredentialsError
     from google.cloud import datastore
+    from google.cloud.datastore.client import Key
 except ImportError as e:
     from feast.errors import FeastExtrasDependencyImportError, FeastProviderLoginError
 
@@ -233,22 +234,32 @@ def online_read(
 
         feast_project = config.project
 
+        keys: List[Key] = []
         result: List[Tuple[Optional[datetime], Optional[Dict[str, ValueProto]]]] = []
         for entity_key in entity_keys:
             document_id = compute_entity_id(entity_key)
             key = client.key(
                 "Project", feast_project, "Table", table.name, "Row", document_id
             )
-            value = client.get(key)
-            if value is not None:
+            keys.append(key)
+
+        values = client.get_multi(keys)
+
+        if values is not None:
+            keys_missing_from_response = set(keys) - set([v.key for v in values])
+            values = sorted(values, key=lambda v: keys.index(v.key))
+            for value in values:
                 res = {}
                 for feature_name, value_bin in value["values"].items():
                     val = ValueProto()
                     val.ParseFromString(value_bin)
                     res[feature_name] = val
                 result.append((value["event_ts"], res))
-            else:
-                result.append((None, None))
+            for missing_key_idx in sorted(
+                [keys.index(k) for k in keys_missing_from_response]
+            ):
+                result.insert(missing_key_idx, (None, None))
+
         return result
 
 


Test patch: