Feature Group Matching Criteria

Overview

The mloda framework uses a sophisticated matching system to determine which feature group should handle a given feature. The modern approach supports both traditional string-based matching and configuration-based matching through the unified FeatureChainParser.

Matching Process

When a feature is requested, the system checks all available feature groups to find the one that should handle the feature. This is done through the match_feature_group_criteria method in each feature group, which now typically uses the unified parser approach.

Modern Unified Matching

The recommended approach uses FeatureChainParserMixin, whose default match_feature_group_criteria already does this. Override it only to add your own checks, and reach the parser through cls.match_parser_criteria, which reads PROPERTY_MAPPING (configuration-based matching) and PREFIX_PATTERN / SUFFIX_PATTERN (string-based matching) off the class:

1. Dual Approach Support

from mloda.provider import DefaultOptionKeys
from mloda.user import Options

@classmethod
def match_feature_group_criteria(cls, feature_name, options, data_access_collection=None):
    return cls.match_parser_criteria(feature_name, options)

Do not call FeatureChainParser.match_configuration_feature_chain_parser directly from a match hook: it raises on an option value the PROPERTY_MAPPING rejects. An exception out of a match hook is contained as a match hook near-miss for that candidate instead of taking the whole resolution down, but a contained crash is a worse reason than a rejection. match_parser_criteria turns that rejection into a non-match, and the reason still reaches the user in the "No feature groups found" error.

Containment covers plugin raises only: a framework-owned raise (a two-readers conflict, a forwarded value contradicting the feature name, a rejected effective-options build) still aborts the whole resolution, because it reports a misconfiguration you have to fix.

Filter matching contains the same way: a raise is a non-match for that probe, like a False return, and is recorded in GlobalFilter.dropped_filters as a match hook near-miss, as is a typed decline the matcher records; a framework-owned raise still aborts. Every entry names the gate that dropped the filter and that gate's reason. How filters reach your FeatureGroup tables the gates the two paths share and where filter policy differs.

The probe runs per feature, but a matched filter attaches to the whole FeatureSet, so a non-match for one feature does not suppress a filter a sibling matched. See Filter scope.

Every caller reads the return by truthiness: any falsy value is a non-match, any truthy value a match. Filter matching additionally reports a falsy value that is not False, and each distinct report is a WARNING once per setup.

The options view depends on the caller: feature resolution passes declared (pre-default) options, while filter matching runs after intake and passes the resolved feature's effective (post-default) options merged onto the filter feature's own. Matching logic that reads option values can see different values on the two paths. See Applying declared defaults.

2. PROPERTY_MAPPING Configuration

The PROPERTY_MAPPING defines how configuration-based features are validated:

from mloda.provider import PropertySpec

PROPERTY_MAPPING = {
    "aggregation_type": PropertySpec(
        "Aggregation to apply",
        allowed_values={
            "sum": "Sum aggregation",
            "avg": "Average aggregation",
            "max": "Maximum aggregation",
        },
        strict_validation=True,
    ),
    DefaultOptionKeys.in_features: PropertySpec(
        "Source feature for aggregation",
        strict_validation=False,
    ),
}

Every value in the mapping is a PropertySpec; accepted values go under allowed_values. A raw dict spec raises at class definition, and an unknown field is a constructor TypeError. See PROPERTY_MAPPING Configuration for the full model.

3. Validation Modes

Strict Validation

With strict_validation=True, parameter values must be in the value space:

# This will match
options = Options(context={"aggregation_type": "sum"})  # "sum" is in mapping

# This will fail validation
options = Options(context={"aggregation_type": "custom"})  # "custom" not in mapping

Flexible Validation

With strict_validation=False (the default), any value is accepted:

# Both will match
options = Options(context={"in_features": "sales"})      # Any value OK
options = Options(context={"in_features": "custom_feature"})  # Any value OK

Custom Validation Functions

For complex validation beyond simple value lists:

PROPERTY_MAPPING = {
    "window_size": PropertySpec(
        "Size of the time window",
        strict_validation=True,
        element_validator=lambda x: isinstance(x, int) and x > 0,
    ),
}

Legacy Default Matching Criteria

For feature groups not yet modernized, the default matching criteria still apply:

  1. Root Feature with Matching Input Data: The feature group is a root feature (has no dependencies) and its input data matches the feature.

  2. Class Name Match: The feature name exactly matches the feature group's class name. py feature_name == FeatureGroup.get_class_name()

  3. Prefix Match: The feature name starts with the feature group's class name as a prefix. py feature_name.startswith(FeatureGroup.prefix()) # Default prefix is "ClassName_"

  4. Explicitly Supported: The feature name is in the set of explicitly supported feature names. py feature_name in FeatureGroup.feature_names_supported()

An owned reader veto recorded during rule 1 (the user addressed the reader family by name and its declaration rejected the request, or its probe recorded a content decline and matched nothing) gates the name-based rules 2 to 4; see Data Access Patterns for the recording contract.

Matching Examples

Modern Feature Group (Aggregation)

from mloda.user import Feature, Options

# String-based matching
feature = Feature("sales__sum_aggr")  # Matches via pattern

# Configuration-based matching
feature = Feature(
    "placeholder",
    Options(context={
        "aggregation_type": "sum",
        "in_features": "sales"
    })
)  # Matches via PROPERTY_MAPPING validation

Parameter Classification Impact

The group/context parameter separation affects matching behavior:

# These create different Feature Group instances (different group parameters)
feature1 = Feature("placeholder", Options(
    group={"data_source": "production"},
    context={"aggregation_type": "sum", "in_features": "sales"}
))

feature2 = Feature("placeholder", Options(
    group={"data_source": "staging"},  # Different group parameter
    context={"aggregation_type": "sum", "in_features": "sales"}
))

# These create the same Feature Group instance (same group, different context)
feature3 = Feature("placeholder", Options(
    group={"data_source": "production"},
    context={"aggregation_type": "sum", "in_features": "sales"}
))

feature4 = Feature("placeholder", Options(
    group={"data_source": "production"},  # Same group parameter
    context={"aggregation_type": "avg", "in_features": "revenue"}  # Different context
))

Migration Path

When modernizing a feature group:

  1. Add PROPERTY_MAPPING with parameter definitions
  2. Update match_feature_group_criteria to use unified parser
  3. Classify parameters as group vs context appropriately
  4. Test both approaches work correctly
  5. Update documentation and examples