fs-filter¶
Segmented ingestion with FilterManager — standard, hybrid, or clone modes.
name: "Feature Store: Filter" description: "Generate code for segmented feature ingestion with a FilterManager — one segment at a time (standard), segments under a TimeManager, hybrid (each segment a (value, date) pair), or a cloned manager. Invoke when a process must run one category / region / partition at a time, when the source is too large for one pass, or for a daily backfill per category." argument-hint: "[filter-column][mode]" allowed-tools: "Read, Edit, Write, Bash, Glob, Grep"
Generate Python code for segmented feature ingestion with a FilterManager.
User context: $ARGUMENTS
Related skills¶
| Step | Skill | Why |
|---|---|---|
| Trigger | fs-analyze |
Decide which column to segment on (ideally a partitioning column of the source). |
| Sibling | fs-upload |
upload_features(..., filtermanager=fm) attaches the manager to the process. |
| Sibling | fs-rollout |
Segments under a TimeManager: roll_out loops over dates, each date over segments. |
| Inspect | fs-inspect |
follow_up_report() has one row per segment (APPLIED_FILTER). |
Demos: demos/notebooks/07 (FilterManager alone), 08 (with a TimeManager), 09 (hybrid).
The model¶
A FilterManager is a table of segments (one row each, numbered filter_id 1..n) and a one-row view showing
the current one. The process view reads the current segment through a scalar subquery on that view.
run(process_id) sees the manager attached to the process and loops over the segments: move the cursor,
ingest, next segment. Each segment gets its own follow-up row, so a failed segment is visible on its own and the
others still complete.
| Mode | Segment | When |
|---|---|---|
| Standard | one value of a column (a category, a region) | One slice at a time: smaller spool and unpivot per ingestion, per-segment trace. |
| With a TimeManager | the same segments, at each business date | A history computed segment by segment (roll_out over dates). |
| Hybrid | a (value, date) pair; the view exposes the column and BUSINESS_DATE |
Daily backfill per category; only the listed pairs are processed. |
| Clone | a copy of another manager's segments | Another process needs the same segments with its own cursor. |
The segments must split the entity. Each segment writes its own rows, so no two segments may produce the same
entity key. Segmenting by Category requires the category in the entity (['CustomerID', 'Category']), or entities
that each fall in one segment. With CustomerID alone, the segment processed last overwrites the values of a
customer active in several categories.
Ask for any missing values before generating code:
- Database of the feature store, data domain, source table.
- Filter column(s) — e.g. 'Category', or Category + a date column for a hybrid manager.
- Entity — must contain the filter column (or be split by it, see above).
- Mode — standard, with a TimeManager, hybrid, or clone.
Standard FilterManager¶
import tdfs4ds # reads the TDFS4DS_* settings of .env
tdfs4ds.reconnect_teradata(raise_error=True) # teradataml context from TDFS4DS_TD_HOST / _USERNAME / _PASSWORD
tdfs4ds.connect(database='<DATABASE>')
tdfs4ds.select_data_domain('<DATA_DOMAIN>')
import teradataml as tdml
from tdfs4ds.utils.filter_management import FilterManager
SOURCE = '<SOURCE_DATABASE>.<SOURCE_TABLE>'
# 1. The segments: one row per value. The view shows the current one.
fm = FilterManager(table_name='<FILTER_TABLE_NAME>', schema_name='<DATABASE>')
fm.load_filter(tdml.DataFrame.from_query(f"SELECT DISTINCT <FILTER_COLUMN> FROM {SOURCE}"))
print(fm.nb_filters, 'segments')
fm.update(filter_id=2); print(fm.display()) # move the cursor by hand to look at one segment
fm.update(filter_id=1)
# 2. The process view reads the current segment through a SCALAR subquery
# (never a join, never a hard-coded value). The filter column is in the entity.
sql = f"""
SELECT
A.<ENTITY_COLUMN>
, A.<FILTER_COLUMN>
, SUM(A.<VALUE_COLUMN>) AS <FEATURE_1>
, COUNT(*) AS <FEATURE_2>
FROM {SOURCE} A
WHERE A.<FILTER_COLUMN> = (SELECT <FILTER_COLUMN> FROM {fm.sql_view})
GROUP BY A.<ENTITY_COLUMN>, A.<FILTER_COLUMN>"""
pid = tdfs4ds.upload_features(
df=None, sql_query=sql, target_view_name='<DATABASE>.<VIEW_NAME>',
entity_id=['<ENTITY_COLUMN>', '<FILTER_COLUMN>'],
feature_names=['<FEATURE_1>', '<FEATURE_2>'],
filtermanager=fm, # attaches the manager: run() loops over its segments
run_now=False, return_process_id=True)
# 3. Run every segment; a failed segment raises once all have been attempted
tdfs4ds.run(pid, on_failure='raise')
# 4. One follow-up row per segment
fu = tdfs4ds.follow_up_report(process_id=pid).to_pandas().reset_index()
print(fu[['APPLIED_FILTER', 'STATUS', 'START_DATETIME']])
Check once that segment by segment equals all at once: build a dataset on the entity and compare it with one
unsegmented GROUP BY of the source (same row count, no differing value). Demo 07 does it: 8,858 rows on both sides,
0 differing values.
Segments under a TimeManager¶
from tdfs4ds.utils.time_management import TimeManager
tm = TimeManager(table_name='<TIME_TABLE_NAME>', schema_name='<DATABASE>') # see fs-rollout
sql = f"""
SELECT A.<FILTER_COLUMN>, SUM(A.<VALUE_COLUMN>) AS <FEATURE_1>
FROM {SOURCE} A
WHERE A.<DATE_COLUMN> <= (SELECT BUSINESS_DATE FROM {tm.sql_view})
AND A.<FILTER_COLUMN> = (SELECT <FILTER_COLUMN> FROM {fm.sql_view})
GROUP BY A.<FILTER_COLUMN>"""
# upload_features(..., filtermanager=fm, run_now=False, return_process_id=True) as above, then:
tdfs4ds.roll_out([pid], time_manager=tm) # each date, every segment: one follow-up row per (segment, date)
# with a domain clock (0.3.6.0): tdfs4ds.roll_out(data_domain='<DOMAIN>') - each date, every process, every segment
Hybrid FilterManager (each segment a (value, date) pair)¶
hf = FilterManager(table_name='<HYBRID_FILTER_TABLE>', schema_name='<DATABASE>')
hf.load_filter(
tdml.DataFrame.from_query(f"""
SELECT DISTINCT <FILTER_COLUMN>, <DATE_COLUMN> FROM {SOURCE}
WHERE <DATE_COLUMN> BETWEEN DATE '<FROM>' AND DATE '<TO>'"""),
time_column='<DATE_COLUMN>') # the view exposes <FILTER_COLUMN> and BUSINESS_DATE
sql = f"""
SELECT A.<FILTER_COLUMN>, A.<DATE_COLUMN>, SUM(A.<VALUE_COLUMN>) AS <FEATURE_1>
FROM {SOURCE} A
WHERE A.<FILTER_COLUMN> = (SELECT <FILTER_COLUMN> FROM {hf.sql_view})
AND A.<DATE_COLUMN> = (SELECT BUSINESS_DATE FROM {hf.sql_view})
GROUP BY A.<FILTER_COLUMN>, A.<DATE_COLUMN>"""
# entity_id=['<FILTER_COLUMN>', '<DATE_COLUMN>']: the pair is the entity, so each segment writes its own rows
Clone a FilterManager¶
fm_copy = FilterManager(table_name='<NEW_TABLE>', schema_name='<DATABASE>')
fm_copy.clone_filter(fm, if_exists='replace') # independent cursor: moving the copy does not move fm
Rules¶
- The segments must split the entity (see above). Put the filter column in
entity_idunless every entity lies in exactly one segment. - Scalar subqueries on the manager view, never a join, never a literal. Teradata reads the one-row view once per request and passes the value to the plan as a parameter.
- What the plan reads — measured, not assumed (
EXPLAIN, source partitioned by day × category, 50,000 rows): | Filter in the process view | Source read | |---|---| | category and day, each a scalar on a hybrid manager | a single partition | | the same filter written as a join to the manager view | all-rows scan | |date <= literal date| 1,267 partitions | |date <= (SELECT BUSINESS_DATE ...)(TimeManager, a range) | all-rows scan | |category = (SELECT ...)(one level only) | all-rows scan | A scalar eliminates partitions when it pins every partitioning level; on a small table one level or a range gets a full scan. Read the EXPLAIN step that accesses the source; do not count steps. - Segmenting pays most when the filter column is a partitioning column of the source.
- Always pass
filtermanager=fmtoupload_features; without itrun()computes only the segment the view shows. fm.update(filter_id=N)moves the cursor by hand (1-based), to inspect or test one segment.- Use
on_failure='raise'under a scheduler: every segment is attempted, thenFeatureIngestionErrorlists the failed ones.'stop'halts at the first failure; the default'continue'records it and goes on. - Hybrid managers expose the date as
BUSINESS_DATE; filter both columns, each with its own scalar subquery. - Vantage Cloud Lake + tdstone2 Apply only — OFS manager table + JOIN the filter in. A view scored by Apply runs
on the Analytical Compute Cluster (ACC): create the manager with
FilterManager(..., storage='OFS')and reference it through a JOIN (the ACC rejects scalar subqueries with Error 6881). This is the one exception to the scalar-subquery rule;roll_out/run()cannot drive an Apply model themselves.