Real-time customer segmentation tutorial
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_ordermeasures recency.lifetime_value_centsavoids 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:
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
;| feature_name VARCHAR | status VARCHAR | message VARCHAR |
|---|---|---|
| FM.TUTORIALS.REALTIME_SEGMENTATION.CUSTOMERS | CREATED | Feature created as not exists |
| FM.TUTORIALS.REALTIME_SEGMENTATION.CUSTOMER_ID | CREATED | Feature created as not exists |
| FM.TUTORIALS.REALTIME_SEGMENTATION.REDIS_SOURCE | CREATED | Feature created as not exists |
| FM.TUTORIALS.REALTIME_SEGMENTATION.REDIS_KEY | CREATED | Feature created as not exists |
| FM.TUTORIALS.REALTIME_SEGMENTATION.DAYS_SINCE_ORDER | CREATED | Feature created as not exists |
| FM.TUTORIALS.REALTIME_SEGMENTATION.LIFETIME_VALUE_CENTS | CREATED | Feature created as not exists |
| FM.TUTORIALS.REALTIME_SEGMENTATION.SHOW_PROMO | CREATED | Feature created as not exists |
Persisting the source updates the registry. A local serving process needs an explicit refresh before it can use the connection:
REFRESH FEATURES FM.TUTORIALS.REALTIME_SEGMENTATION.REDIS_SOURCE;| FEATURE VARCHAR | KIND VARCHAR | STATUS VARCHAR | MESSAGE VARCHAR |
|---|---|---|---|
| FM.TUTORIALS.REALTIME_SEGMENTATION.REDIS_SOURCE | SOURCE_REDIS | REFRESHED | (empty) |
Which customers receive the promotion?
The first customer meets both conditions. Customer 2 is too recent and customer 3 has insufficient lifetime value:
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
;| CUSTOMER_ID BIGINT | DAYS_SINCE_ORDER BIGINT | LIFETIME_VALUE_CENTS BIGINT | SHOW_PROMO BOOLEAN |
|---|---|---|---|
| 1 | 45 | 15000 | TRUE |
| 2 | 10 | 8000 | FALSE |
| 3 | 60 | 5000 | FALSE |
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:
SELECT
FM.TUTORIALS.REALTIME_SEGMENTATION.CUSTOMER_ID := BIND_VALUE(2),
FM.TUTORIALS.REALTIME_SEGMENTATION.SHOW_PROMO
;| CUSTOMER_ID BIGINT | SHOW_PROMO BOOLEAN |
|---|---|
| 2 | TRUE |
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:
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
);| feature_name VARCHAR | status VARCHAR | message VARCHAR |
|---|---|---|
| FM.TUTORIALS.REALTIME_SEGMENTATION.SHOW_PROMO_PS | CREATED | Feature created as not exists |
Refresh the prepared statement after persisting it:
REFRESH FEATURES FM.TUTORIALS.REALTIME_SEGMENTATION.SHOW_PROMO_PS;| FEATURE VARCHAR | KIND VARCHAR | STATUS VARCHAR | MESSAGE VARCHAR |
|---|---|---|---|
| FM.TUTORIALS.REALTIME_SEGMENTATION.SHOW_PROMO_PS | PREPARED_STATEMENT | REFRESHED | (empty) |
Call the prepared feature with two request payloads. Each payload supplies the rows for the generated input table; the responses stay separate:
FM.TUTORIALS.REALTIME_SEGMENTATION.SHOW_PROMO_PS{"input_table_1": [[1], [2], [3]]} | CUSTOMER_ID BIGINT | SHOW_PROMO_PS BOOLEAN |
|---|---|
| 1 | TRUE |
| 2 | TRUE |
| 3 | FALSE |
{"input_table_1": [[2]]} | CUSTOMER_ID BIGINT | SHOW_PROMO_PS BOOLEAN |
|---|---|
| 2 | TRUE |
What's next
- Redis data sources — key, hash, and table access patterns
- Prepared statements — input grouping and serving payloads
- Multi-source queries — combine Redis with relational data