-
Notifications
You must be signed in to change notification settings - Fork 605
[docs] Add Real-Time User Profile quickstart tutorial #2669
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
pbanakar
wants to merge
13
commits into
apache:main
Choose a base branch
from
pbanakar:docs-newQuickstart
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
13 commits
Select commit
Hold shift + click to select a range
759336d
Add Real-Time User Profile quickstart tutorial
pbanakar d00658a
Added Diagram
pbanakar fa6cb63
enhanced guide by adding the environment setup
pbanakar b7678b2
enhanced guide by adding the environment setup
pbanakar cca5dac
verified environment setup
pbanakar ec9c5cd
improved
pbanakar 04beb0e
Fix quickstart:by temp repo
pbanakar 45588e1
[docs] added Real-Time User Profile quickstart
pbanakar 3699445
[docs] added Real-Time User Profile quickstart
pbanakar 89fa8f2
[docs] remove temporary blog link
pbanakar d8a0782
updated to 0.9.1 version
pbanakar c150293
fixed broken link
pbanakar cd53eea
updated
pbanakar File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,290 @@ | ||
| --- | ||
| title: Real-Time User Profile | ||
| sidebar_position: 4 | ||
| --- | ||
|
|
||
| # Real-Time User Profile | ||
|
|
||
| This tutorial demonstrates how to build a real-time user profiling system using three core Apache Fluss features: the **Auto-Increment Column**, the **Aggregation Merge Engine**, and the built-in **RoaringBitmap SQL functions**. You will learn how to automatically map high-cardinality email identifiers to compact integer UIDs, accumulate click metrics, and count unique visitors — all directly in the storage layer, keeping the Flink job entirely stateless. | ||
|
|
||
| ## How the System Works | ||
|
|
||
| ### Core Concepts | ||
|
|
||
| - **Identity Mapping**: Incoming email strings are automatically mapped to compact `INT` UIDs using Fluss's auto-increment column — no manual ID management required. | ||
| - **Storage-Level Aggregation**: Click counts are summed and unique visitor bitmaps are OR-ed directly inside the Fluss TabletServers via the Aggregation Merge Engine. | ||
| - **Built-in Bitmap Functions**: `rb_build_agg` and `rb_cardinality` are registered natively in FlussCatalog — no external JAR or `CREATE TEMPORARY FUNCTION` required. | ||
|
|
||
| ### Data Flow | ||
|
|
||
| 1. **Ingestion**: Raw click events arrive with an email address and a click count. | ||
| 2. **Mapping**: A Flink lookup join against `user_dict` resolves the email to a UID. If the email is new, the `insert-if-not-exists` hint instructs Fluss to generate a new UID automatically. | ||
| 3. **Aggregation**: The resolved UID is written to `user_profiles`. The Aggregation Merge Engine sums `total_clicks` and OR-s the `unique_visitors` bitmap at the storage layer — no windowing or Flink state required. | ||
|
|
||
| ## Prerequisites | ||
|
|
||
| Before proceeding, ensure that [Docker](https://docs.docker.com/engine/install/) and the [Docker Compose plugin](https://docs.docker.com/compose/install/linux/) are installed on your machine. | ||
|
|
||
| ## Environment Setup | ||
|
|
||
| 1. Create a working directory and navigate into it. | ||
| ```shell | ||
| mkdir fluss-user-profile | ||
| cd fluss-user-profile | ||
| ``` | ||
|
|
||
| 2. Create a `docker-compose.yml` file with the following content: | ||
| ```yaml | ||
| services: | ||
| coordinator-server: | ||
| image: apache/fluss:$FLUSS_DOCKER_VERSION$ | ||
| command: coordinatorServer | ||
| depends_on: | ||
| - zookeeper | ||
| environment: | ||
| - | | ||
| FLUSS_PROPERTIES= | ||
| zookeeper.address: zookeeper:2181 | ||
| bind.listeners: FLUSS://coordinator-server:9123 | ||
| remote.data.dir: /tmp/fluss/remote | ||
| volumes: | ||
| - fluss-remote-data:/tmp/fluss/remote | ||
| tablet-server: | ||
| image: apache/fluss:$FLUSS_DOCKER_VERSION$ | ||
| command: tabletServer | ||
| depends_on: | ||
| - coordinator-server | ||
| environment: | ||
| - | | ||
| FLUSS_PROPERTIES= | ||
| zookeeper.address: zookeeper:2181 | ||
| bind.listeners: FLUSS://tablet-server:9123 | ||
| data.dir: /tmp/fluss/data | ||
| remote.data.dir: /tmp/fluss/remote | ||
| volumes: | ||
| - fluss-remote-data:/tmp/fluss/remote | ||
| zookeeper: | ||
| restart: always | ||
| image: zookeeper:3.9.2 | ||
| jobmanager: | ||
| image: apache/fluss-quickstart-flink:$FLUSS_QUICKSTART_FLINK_DOCKER_VERSION$ | ||
| ports: | ||
| - "8083:8081" | ||
| command: jobmanager | ||
| environment: | ||
| - | | ||
| FLINK_PROPERTIES= | ||
| jobmanager.rpc.address: jobmanager | ||
| rest.address: jobmanager | ||
| rest.port: 8081 | ||
| volumes: | ||
| - fluss-remote-data:/tmp/fluss/remote | ||
| taskmanager: | ||
| image: apache/fluss-quickstart-flink:$FLUSS_QUICKSTART_FLINK_DOCKER_VERSION$ | ||
| depends_on: | ||
| - jobmanager | ||
| command: taskmanager | ||
| environment: | ||
| - | | ||
| FLINK_PROPERTIES= | ||
| jobmanager.rpc.address: jobmanager | ||
| taskmanager.numberOfTaskSlots: 2 | ||
| volumes: | ||
| - fluss-remote-data:/tmp/fluss/remote | ||
| sql-client: | ||
| image: apache/fluss-quickstart-flink:$FLUSS_QUICKSTART_FLINK_DOCKER_VERSION$ | ||
| command: ["/opt/sql-client/sql-client"] | ||
| depends_on: | ||
| - jobmanager | ||
| environment: | ||
| - | | ||
| FLINK_PROPERTIES= | ||
| jobmanager.rpc.address: jobmanager | ||
| rest.address: jobmanager | ||
| rest.port: 8081 | ||
| volumes: | ||
| - fluss-remote-data:/tmp/fluss/remote | ||
|
|
||
| volumes: | ||
| fluss-remote-data: | ||
| ``` | ||
|
|
||
| 3. Start all services. | ||
| ```shell | ||
| docker compose up -d | ||
| ``` | ||
|
|
||
| 4. Confirm all containers are running. | ||
| ```shell | ||
| docker compose ps | ||
| ``` | ||
| You should see `coordinator-server`, `tablet-server`, `zookeeper`, `jobmanager`, `taskmanager`, and `sql-client` all in the `running` state. | ||
|
|
||
| :::note | ||
| All the following commands involving `docker compose` should be executed in the working directory that contains the `docker-compose.yml` file. | ||
| ::: | ||
|
|
||
| ## Enter the SQL Client | ||
|
|
||
| Use the following command to enter the Flink SQL Client: | ||
|
|
||
| ```shell | ||
| docker compose run --entrypoint bash sql-client -c " | ||
| \${FLINK_HOME}/bin/sql-client.sh \ | ||
| -Drest.address=jobmanager \ | ||
| -Drest.port=8081 \ | ||
| -i /opt/sql-client/sql/sql-client.sql | ||
| " | ||
| ``` | ||
|
|
||
| ## Step 1: Create the Fluss Catalog | ||
|
|
||
| Run these statements one by one in the SQL Client. | ||
|
|
||
| :::tip | ||
| Run SQL statements one by one to avoid errors. | ||
| ::: | ||
|
|
||
| ```sql | ||
| CREATE CATALOG fluss_catalog WITH ( | ||
| 'type' = 'fluss', | ||
| 'bootstrap.servers' = 'coordinator-server:9123' | ||
| ); | ||
| ``` | ||
|
|
||
| ```sql | ||
| USE CATALOG fluss_catalog; | ||
| ``` | ||
|
|
||
| :::note | ||
| Once you switch to the Fluss catalog, all RoaringBitmap SQL functions (`rb_build_agg`, `rb_cardinality`, `rb_or_agg`, and others) are available immediately — no `CREATE TEMPORARY FUNCTION` statement is needed. | ||
| ::: | ||
|
|
||
| ## Step 2: Create the User Dictionary Table | ||
|
|
||
| Create the `user_dict` table to map email addresses to integer UIDs. The `auto-increment.fields` property instructs Fluss to automatically assign a unique `INT` UID for every new email it receives. | ||
|
|
||
| ```sql | ||
| CREATE TABLE user_dict ( | ||
| email STRING, | ||
| uid INT, | ||
| PRIMARY KEY (email) NOT ENFORCED | ||
| ) WITH ( | ||
| 'auto-increment.fields' = 'uid' | ||
| ); | ||
| ``` | ||
|
|
||
| ## Step 3: Create the Aggregated Profile Table | ||
|
|
||
| Create the `user_profiles` table using the **Aggregation Merge Engine**. Each user's UID is the primary key. `total_clicks` is summed and `unique_visitors` accumulates a [RoaringBitmap](https://roaringbitmap.org/) of all UIDs seen — both computed directly at the storage layer. | ||
|
|
||
| ```sql | ||
| CREATE TABLE user_profiles ( | ||
| uid INT, | ||
| total_clicks BIGINT, | ||
| unique_visitors BYTES, | ||
| PRIMARY KEY (uid) NOT ENFORCED | ||
| ) WITH ( | ||
| 'table.merge-engine' = 'aggregation', | ||
| 'fields.total_clicks.agg' = 'sum', | ||
| 'fields.unique_visitors.agg' = 'rbm32' | ||
| ); | ||
| ``` | ||
|
|
||
| ## Step 4: Ingest and Process Data | ||
|
|
||
| Create a temporary source table to simulate raw click events using the Faker connector. | ||
|
|
||
| :::note | ||
| Java Faker's `numberBetween(min, max)` treats `max` as exclusive. The expression below produces click counts of 1–10. | ||
| ::: | ||
|
|
||
| ```sql | ||
| CREATE TEMPORARY TABLE raw_events ( | ||
| email STRING, | ||
| click_count INT, | ||
| proctime AS PROCTIME() | ||
| ) WITH ( | ||
| 'connector' = 'faker', | ||
| 'rows-per-second' = '1', | ||
| 'fields.email.expression' = '#{internet.emailAddress}', | ||
| 'fields.click_count.expression' = '#{number.numberBetween ''1'',''11''}' | ||
| ); | ||
| ``` | ||
|
|
||
| Now run the pipeline. The `lookup.insert-if-not-exists` hint ensures that if an email is not found in `user_dict`, Fluss generates a new `uid` automatically. `rb_build_agg(d.uid)` builds a one-element RoaringBitmap from each UID — the Aggregation Merge Engine OR-s it into the stored bitmap, giving an exact unique visitor count per user over time. | ||
|
|
||
| ```sql | ||
| INSERT INTO user_profiles | ||
| SELECT | ||
| d.uid, | ||
| CAST(e.click_count AS BIGINT), | ||
| rb_build_agg(d.uid) | ||
| FROM raw_events AS e | ||
| JOIN user_dict /*+ OPTIONS('lookup.insert-if-not-exists' = 'true') */ | ||
| FOR SYSTEM_TIME AS OF e.proctime AS d | ||
| ON e.email = d.email | ||
| GROUP BY d.uid, e.click_count; | ||
| ``` | ||
|
|
||
| ## Step 5: Verify Results | ||
|
|
||
| Open a **second terminal**, navigate to the working directory, and launch another SQL Client session to query results while the pipeline runs. | ||
|
|
||
| ```shell | ||
| docker compose run --entrypoint bash sql-client -c " | ||
| \${FLINK_HOME}/bin/sql-client.sh \ | ||
| -Drest.address=jobmanager \ | ||
| -Drest.port=8081 | ||
| " | ||
| ``` | ||
|
|
||
| Set up the catalog: | ||
|
|
||
| ```sql | ||
| CREATE CATALOG fluss_catalog WITH ( | ||
| 'type' = 'fluss', | ||
| 'bootstrap.servers' = 'coordinator-server:9123' | ||
| ); | ||
| USE CATALOG fluss_catalog; | ||
| SET 'sql-client.execution.result-mode' = 'tableau'; | ||
| ``` | ||
|
|
||
| Query the aggregated profile table. `rb_cardinality` converts the stored bitmap into a human-readable unique visitor count: | ||
|
|
||
| ```sql | ||
| SELECT | ||
| uid, | ||
| total_clicks, | ||
| rb_cardinality(unique_visitors) AS unique_visitor_count | ||
| FROM user_profiles; | ||
| ``` | ||
|
|
||
| You should see rows appearing for each new user, with `total_clicks` and `unique_visitor_count` growing in real time. | ||
|
|
||
| To verify the email-to-UID dictionary mapping: | ||
|
|
||
| ```sql | ||
| SELECT * FROM user_dict LIMIT 10; | ||
| ``` | ||
|
|
||
| Each email should have a unique compact `INT` uid automatically assigned by Fluss. | ||
|
|
||
| ## Clean Up | ||
|
|
||
| Exit the SQL Client by typing `exit;`, then stop all services. | ||
|
|
||
| ```shell | ||
| docker compose down -v | ||
| ``` | ||
|
|
||
| ## Architectural Benefits | ||
|
|
||
| - **Stateless Flink Jobs:** Offloading identity mapping, click aggregation, and bitmap union to Fluss makes the Flink job lightweight, with fast checkpoints and minimal recovery time. | ||
| - **Compact Storage:** Using auto-incremented `INT` UIDs instead of raw email strings reduces memory and storage footprint significantly. | ||
| - **Exact Unique Counting:** RoaringBitmap provides exact distinct counts — no approximations like HyperLogLog. | ||
| - **Exactly-Once Accuracy:** The Undo Recovery mechanism in the Fluss Flink connector ensures replayed data during failovers does not result in double-counting. | ||
|
|
||
| ## What's Next? | ||
|
|
||
| For the full reference of all RoaringBitmap SQL functions available in FlussCatalog (`rb_or_agg`, `rb_and`, `rb_contains`, `rb_to_array`, and more), see the [SQL Functions](../../engine-flink/sql-functions/) documentation. | ||
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The uid column from user_dict is never referenced in the SELECT. The lookup join exists solely to trigger insert-if-not-exists, but the generated ID plays no role in the aggregation. This makes the pipeline feel contrived — a reader would expect the dictionary-mapped uid to be the primary key of user_profiles, not an unrelated profile_group_id.