Real-time customer segmentation tutorial

Open In Colab

This tutorial turns a customer promotion rule into a real-time feature backed by Redis, verifies live updates, and serves the result through a prepared statement.

Complete the FeatureMesh homepage walkthrough first if source features, VARIANT(), or serving refreshes are new. The Colab notebook installs Redis locally and verifies it before creating the serving client.

The serving data

Each customer has a Redis hash with two precomputed values:

  • days_since_order measures recency.
  • lifetime_value_cents avoids floating-point currency comparisons.

The promotion rule targets customers whose last order was at least 30 days ago and whose lifetime value is at least $100.

Define the serving model

The model declares a CUSTOMERS entity and maps CUSTOMER_ID to a Redis hash key. EXTERNAL_REDIS() reads each field, and SHOW_PROMO combines the typed values into the business rule:

FeatureQL
CREATE OR REPLACE FEATURES IN FM.TUTORIALS.REALTIME_SEGMENTATION AS
SELECT
    CUSTOMERS := ENTITY(),
    CUSTOMER_ID := INPUT(BIGINT#CUSTOMERS),
    REDIS_SOURCE := SOURCE_REDIS(
        'redis://host.docker.internal:6380'
        WITH (timeout='500ms')
    ),
    REDIS_KEY := 'tutorial:segment:' || UNSAFE_CAST(CUSTOMER_ID AS VARCHAR),
    DAYS_SINCE_ORDER := CAST(
        EXTERNAL_REDIS(KEY REDIS_KEY FIELD 'days_since_order' FROM REDIS_SOURCE)
        AS BIGINT
    ),
    LIFETIME_VALUE_CENTS := CAST(
        EXTERNAL_REDIS(KEY REDIS_KEY FIELD 'lifetime_value_cents' FROM REDIS_SOURCE)
        AS BIGINT
    ),
    SHOW_PROMO := DAYS_SINCE_ORDER >= 30 AND LIFETIME_VALUE_CENTS >= 10000
;
Result
feature_name VARCHARstatus VARCHARmessage VARCHAR
FM.TUTORIALS.REALTIME_SEGMENTATION.CUSTOMERSCREATEDFeature created as not exists
FM.TUTORIALS.REALTIME_SEGMENTATION.CUSTOMER_IDCREATEDFeature created as not exists
FM.TUTORIALS.REALTIME_SEGMENTATION.REDIS_SOURCECREATEDFeature created as not exists
FM.TUTORIALS.REALTIME_SEGMENTATION.REDIS_KEYCREATEDFeature created as not exists
FM.TUTORIALS.REALTIME_SEGMENTATION.DAYS_SINCE_ORDERCREATEDFeature created as not exists
FM.TUTORIALS.REALTIME_SEGMENTATION.LIFETIME_VALUE_CENTSCREATEDFeature created as not exists
FM.TUTORIALS.REALTIME_SEGMENTATION.SHOW_PROMOCREATEDFeature created as not exists

Persisting the source updates the registry. A local serving process needs an explicit refresh before it can use the connection:

FeatureQL
REFRESH FEATURES FM.TUTORIALS.REALTIME_SEGMENTATION.REDIS_SOURCE;
Result
FEATURE VARCHARKIND VARCHARSTATUS VARCHARMESSAGE VARCHAR
FM.TUTORIALS.REALTIME_SEGMENTATION.REDIS_SOURCESOURCE_REDISREFRESHED(empty)

Which customers receive the promotion?

The first customer meets both conditions. Customer 2 is too recent and customer 3 has insufficient lifetime value:

FeatureQL
SELECT
    FM.TUTORIALS.REALTIME_SEGMENTATION.CUSTOMER_ID := BIND_VALUES(ARRAY[1, 2, 3]),
    FM.TUTORIALS.REALTIME_SEGMENTATION.DAYS_SINCE_ORDER,
    FM.TUTORIALS.REALTIME_SEGMENTATION.LIFETIME_VALUE_CENTS,
    FM.TUTORIALS.REALTIME_SEGMENTATION.SHOW_PROMO
;
Result
CUSTOMER_ID BIGINTDAYS_SINCE_ORDER BIGINTLIFETIME_VALUE_CENTS BIGINTSHOW_PROMO BOOLEAN
14515000TRUE
2108000FALSE
3605000FALSE

What happens when Redis changes?

Reverse ETL updates customer 2 to 40 days since the last order and $200 in lifetime value. The next serving query reads the new hash values immediately:

FeatureQL
SELECT
    FM.TUTORIALS.REALTIME_SEGMENTATION.CUSTOMER_ID := BIND_VALUE(2),
    FM.TUTORIALS.REALTIME_SEGMENTATION.SHOW_PROMO
;
Result
CUSTOMER_ID BIGINTSHOW_PROMO BOOLEAN
2TRUE

The source definition did not change, so no connection refresh was needed.

Compile the rule for serving

PREPARED_STATEMENT() compiles the promotion rule and declares CUSTOMER_ID as its input table:

FeatureQL
CREATE OR REPLACE FEATURE FM.TUTORIALS.REALTIME_SEGMENTATION.SHOW_PROMO_PS AS
PREPARED_STATEMENT(
    FM.TUTORIALS.REALTIME_SEGMENTATION.SHOW_PROMO
    USING FM.TUTORIALS.REALTIME_SEGMENTATION.CUSTOMER_ID
);
Result
feature_name VARCHARstatus VARCHARmessage VARCHAR
FM.TUTORIALS.REALTIME_SEGMENTATION.SHOW_PROMO_PSCREATEDFeature created as not exists

Refresh the prepared statement after persisting it:

FeatureQL
REFRESH FEATURES FM.TUTORIALS.REALTIME_SEGMENTATION.SHOW_PROMO_PS;
Result
FEATURE VARCHARKIND VARCHARSTATUS VARCHARMESSAGE VARCHAR
FM.TUTORIALS.REALTIME_SEGMENTATION.SHOW_PROMO_PSPREPARED_STATEMENTREFRESHED(empty)

Call the prepared feature with two request payloads. Each payload supplies the rows for the generated input table; the responses stay separate:

Prepared
FM.TUTORIALS.REALTIME_SEGMENTATION.SHOW_PROMO_PS
Call 1
{"input_table_1": [[1], [2], [3]]}
CUSTOMER_ID BIGINTSHOW_PROMO_PS BOOLEAN
1TRUE
2TRUE
3FALSE
Call 2
{"input_table_1": [[2]]}
CUSTOMER_ID BIGINTSHOW_PROMO_PS BOOLEAN
2TRUE

What's next