Real-time customer segmentation tutorial

Open In Colab

This tutorial builds a promotion decision from values in Redis, updates those values live, and compiles the rule for repeated low-latency calls.

By the end, you will have followed the complete serving lifecycle: seed data, define a source-backed feature, refresh the serving process, evaluate it, observe a live data change, and create a prepared statement.

Complete the FeatureMesh homepage walkthrough first if source features or serving refreshes are new. These examples require the serving backend, Redis, and a serving client. Run them from top to bottom because later steps reuse the persisted model and mutated Redis data.

Meet the three customers

Each customer is stored as a Redis hash with two precomputed values:

  • days_since_order measures recency.
  • lifetime_value_cents stores currency as an integer.

Seed the three hashes:

Redis
DEL tutorial:segment:1 tutorial:segment:2 tutorial:segment:3
HSET tutorial:segment:1 days_since_order 45 lifetime_value_cents 15000
HSET tutorial:segment:2 days_since_order 10 lifetime_value_cents 8000
HSET tutorial:segment:3 days_since_order 60 lifetime_value_cents 5000
Result

The starting values are:

  • Customer 1: 45 days, 15000 cents
  • Customer 2: 10 days, 8000 cents
  • Customer 3: 60 days, 5000 cents

Now reset the tutorial feature namespace so the model starts clean:

FeatureQL
DROP FEATURES IF EXISTS IN FM.TUTORIALS.REALTIME_SEGMENTATION UP TO LEVEL 9;
Result
feature_name VARCHARstatus VARCHARmessage VARCHAR

The promotion rule will return true when the last order was at least 30 days ago and lifetime value is at least $100 (10000 cents).

Define the serving model

The model follows the same path the serving query will execute:

  1. Bind a typed CUSTOMER_ID.
  2. Build the corresponding Redis key.
  3. Read the two hash fields with EXTERNAL_REDIS().
  4. Cast the external values to BIGINT.
  5. Evaluate the SHOW_PROMO rule.

FeatureQL
CREATE OR REPLACE FEATURES IN FM.TUTORIALS.REALTIME_SEGMENTATION AS
PERSIST
    CUSTOMERS := ENTITY(),
    CUSTOMER_ID := INPUT(BIGINT#CUSTOMERS),
    REDIS_SOURCE := SOURCE_REDIS(
        'redis://host.docker.internal:6380'
        WITH (timeout='500ms')
    ),
    REDIS_KEY := 'tutorial:segment:' || CAST(KEY_TO_SCALAR(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 this definition updates the registry, but the running serving process does not automatically install the new connection. Refresh the Redis source before evaluating the model:

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

This refresh is about the source definition—its URL, timeout, and connection—not the customer values stored behind it.

Evaluate the starting population

Bind all three customer IDs and return the source values beside the decision:

FeatureQL
RETURN
    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

Customer 1 passes both thresholds. Customer 2 is too recent and below $100; customer 3 is old enough but below $100.

Observe a live Redis update

Now simulate a reverse-ETL update for customer 2. Its Redis hash changes to 40 days since the last order and 20000 cents in lifetime value.

The next query reads the new values and reevaluates the existing feature:

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

Customer 2 now returns true. No refresh was needed because the source definition did not change; only the data behind the connection changed.

Compile the rule for serving

An ad-hoc serving query still needs to compile and plan the FeatureQL expression. PREPARED_STATEMENT() compiles SHOW_PROMO once and declares CUSTOMER_ID as its input:

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

Like a new source, a newly persisted prepared statement must be refreshed into the serving process:

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 it first with all three customers, then with customer 2 alone:

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

The first response returns true for customers 1 and 2, and false for 3. The second response contains only customer 2. Each payload becomes a separate input table and receives a separate result.

Full query vs prepared statement

The notebook can compare both call paths. The first loop sends the full FeatureQL query 100 times; the second reuses the prepared plan and changes only its input rows:

import time

_N = 100
_full_query = """
RETURN
    FM.TUTORIALS.REALTIME_SEGMENTATION.CUSTOMER_ID,
    FM.TUTORIALS.REALTIME_SEGMENTATION.SHOW_PROMO
FOR
    FM.TUTORIALS.REALTIME_SEGMENTATION.CUSTOMER_ID := BIND_VALUES(ARRAY[1, 2, 3])
;
"""
_prepared_id = "FM.TUTORIALS.REALTIME_SEGMENTATION.SHOW_PROMO_PS"
_prepared_inputs = {"input_table_1": [[1], [2], [3]]}

_t0 = time.perf_counter()
for _ in range(_N):
    _result = serving_client.query(_full_query)
    if _result.errors:
        raise RuntimeError(_result.errors)
_full_seconds = time.perf_counter() - _t0

_t0 = time.perf_counter()
for _ in range(_N):
    _result = serving_client.execute_prepared_statement(_prepared_id, _prepared_inputs)
    if _result.errors:
        raise RuntimeError(_result.errors)
_prepared_seconds = time.perf_counter() - _t0

print(
    f"Full FeatureQL query × {_N}: {_full_seconds:.3f}s "
    f"({_full_seconds / _N * 1000:.2f} ms/call)"
)
print(
    f"Prepared statement × {_N}: {_prepared_seconds:.3f}s "
    f"({_prepared_seconds / _N * 1000:.2f} ms/call)"
)
print(f"Speedup: {_full_seconds / _prepared_seconds:.1f}×")
python
Run this cell in the notebook to discover the result.
text

Treat the printed timings as a local observation, not a universal benchmark. Network latency, warm-up, and the serving environment all affect the ratio. The architectural difference is stable: the prepared path avoids recompiling the same feature graph on every request.

What's next

You have separated source installation from live source data, then exposed the same promotion rule through an ad-hoc query and a prepared statement.

Continue with Federated serving to follow one feature graph across PostgreSQL and Redis. Use the Redis source and prepared statement pages when you need complete syntax and payload options.