Resolving Ambiguous Resolver Errors

In standard operation, Chalk assumes that each feature is un-ambiguously resolved by a single “in-scope” resolver. If this assumption is violated, Chalk’s planner will raise an “ambiguous resolver” error that looks like —

Chalk was unable to determine which resolvers to run for feature 'user.id'.
There were multiple candidates ....

This error typically indicates that there is a mistake in resolver definitions, but it may arise in other situations as well. This document describes common causes of ambiguous resolver errors and how to resolve them.


Multiple DataFrame-Returning Resolvers

Often, ambiguous resolver errors arise when there are multiple resolvers that return dataframes that include the same feature. This occurs most often when multiple offline resolvers return the “primary feature” for a given feature class.

For example:

@online
def get_users() -> DataFrame[User.id, User.name]:
    ...

@online
def get_user_registration_dates() -> DataFrame[User.id, user.registration_date]:
    ...

This tends to arise when some of the features for a particular entity are resolved from one data source, and other features for the same entity are resolved from another data source (i.e. a different SQL table).

This causes ambiguity because the query planner cannot determine which resolver returns “all” of the user rows. To resolve this ambiguity, you can define a single “primary” resolver that returns all the features for the entity in question by adding the total=True keyword argument —

from chalk import online

@online(total=True)
def get_users() -> DataFrame[User.id, User.name]:
  ...

or for SQL resolvers:

users_offline.chalk.sql
-- resolves: User
-- source: snowflake
-- type: offline
-- total: true
select
  id,
  name
from users

The total keyword argument indicates to the Chalk query planner that all primary keys for the entity are returned by this resolver. Other resolvers which return subsets of the entity’s features can still be defined, but they will be combined with the “total” resolver to ensure that all rows are returned via a left-outer-join operation.


Multiple Implementations of Resolvers

Sometimes, overlaps in resolver definitions arise when multiple implementations of the same feature are defined. Common scenarios include:

  • if an A/B test is being run
  • a new version of a feature is being rolled out
  • offline query workloads with multiple different source parquet/csv datasets
  • switching between a production or sandbox version of a service call

In these cases, you can resolve the ambiguity by adding tags to resolvers in order to break scoping ties —

@online(tags="api:mock")
def simulate_no_fraud(id: User.id) -> User.fraud_score:
    return 10

@online(tags="api:sandbox")
def sandbox_score(
    name: User.name,
    email: User.email,
) -> User.fraud_score:
    return sandbox.fraud_score(name, email).score

@online
def real_score(
    name: User.name,
    email: User.email,
) -> Features[User.fraud_score, User.fraud_tags]:
    r = prod.fraud_score(name, email).score
    return User(fraud_score=r.score, fraud_tags=r.tags)

In this example, the simulate_no_fraud and sandbox_score resolvers can be selected by specifying the appropriate tag when making a feature request. The real_score resolver will be used when no tags are specified.

For more information, see resolver tags.


Online and Offline Resolvers with Overlapping Features

Often, a single feature may have a natural “online” source (i.e. PostgreSQL, or a microservice API), and a natural “offline” source (i.e. Snowflake, or another bulk warehouse system). This case is handled automatically by Chalk’s planner, which will prioritize @online resolvers over @offline resolvers when both are available.