AI/ML in Gnok Goes Production-Grade: Filtered ANN, Point-in-Time Features, and Schema-Aware Autocomplete
The AI/ML stack in Gnok has been usable for a while — register an
ONNX model, build an HNSW index, materialize a feature group, run
inference inline. What's new this month is that every track now
holds up at production scale: vector search with WHERE clauses
no longer falls back to brute-force, point-in-time training joins
read durably from Iceberg time-travel, and the SQL editor knows
which models you've actually registered.
This post walks through the three biggest changes — filtered ANN, offline-backed feature lookups, and the schema-aware ML autocomplete — with the SQL you can run today.
1. Filtered ANN: vector search with WHERE clauses, fast
Production vector queries are almost never "find me 10 things
similar to X". They're "find me 10 things similar to X that are
in stock and in my region". Until this release, that WHERE
clause forced the planner to brute-force the scan over the entire
table — defeating the purpose of having an HNSW index.
Now the planner picks among three strategies based on the predicate's selectivity:
| Strategy | When | What it does |
|---|---|---|
| IndexOnly | No predicate above the scan | Pure ANN: probe HNSW for top-k row ids → materialise from source |
| PostFilter | Non-selective predicate (~10%+) | Probe with overfetch (k × factor / sel), apply filter, take top-k |
| PreFilter | Very selective predicate (<1%) | Apply filter first → SIMD brute-force ANN over the candidate set |
Same query — the planner chooses transparently:
-- IndexOnly: no predicate. Pure ANN.
SELECT id, title
FROM documents
ORDER BY COSINE_DISTANCE(embedding, EMBED('refund policy'))
LIMIT 10;
-- PostFilter: predicate is ~30% selective. The planner emits
-- VectorIndexTopK with k=80 (overfetch=8x), Filter, then
-- LIMIT 10 — bounded at 32x to keep memory predictable.
SELECT id, title
FROM documents
WHERE region IN ('us-east', 'us-west')
ORDER BY COSINE_DISTANCE(embedding, EMBED('refund policy'))
LIMIT 10;
You can inspect the planner's choice with EXPLAIN:
EXPLAIN
SELECT id, title FROM documents
WHERE filed_at > '2026-01-01'
ORDER BY COSINE_DISTANCE(embedding, EMBED('contract dispute'))
LIMIT 50;
A VectorIndexTopK { k = 800, ... } under a Filter → Limit(50)
tells you PostFilter fired with 16× overfetch — the query is going
to be ~10× faster than the brute-force fallback while still
returning the right top-50.
Industries
- E-commerce. "Headphones similar to this, in stock and under $200." The price + stock predicate is moderately selective → PostFilter wins.
- Legal e-discovery. "Documents semantically similar to this contract, filed in this 90-day window, by these custodians." Custodian + time predicates are very selective → PreFilter gives the best latency.
- Streaming personalization. "Shows like Severance, available in my region." Region is non-selective in most catalogues → PostFilter with modest overfetch.
2. Offline-Backed Feature Store: point-in-time training without leaks
Online features (the current snapshot, low latency) and offline features (historical, for training) used to be two separate systems with two separate APIs and two opportunities to leak. Gnok's feature store now serves both from a single SQL surface, backed by Iceberg time-travel underneath.
The CREATE side
Add an OFFLINE TABLE clause to a feature group and every
materialization runs an INSERT OVERWRITE into the Iceberg table.
The latest snapshot stays in memory for online lookups; older
snapshots are reachable through Iceberg's snapshot history.
CREATE FEATURE GROUP user_transaction_features (
user_id BIGINT PRIMARY KEY
)
MATERIALIZED FROM (
SELECT user_id,
COUNT(*) AS tx_count,
AVG(amount) AS avg_amount,
MAX(created_at) AS last_tx_time
FROM transactions
GROUP BY user_id
)
REFRESH EVERY 300
OFFLINE TABLE warehouse.feature_store.user_transaction_features;
The lookup side
Two UDFs — same SQL surface, different semantics:
-- Online: live snapshot, O(1), microseconds
SELECT t.transaction_id,
FEATURE_LOOKUP('user_transaction_features',
t.user_id,
'avg_amount') AS avg_amt
FROM transactions t
WHERE t.created_at > now() - INTERVAL '5 minutes';
-- Point-in-time: rebuilds the snapshot in effect at as_of_ms.
-- First checks the in-memory snapshot ring; if the timestamp is
-- older than the ring retains, falls through to the offline
-- Iceberg table via FOR SYSTEM_TIME AS OF time-travel.
SELECT lbl.user_id, lbl.churned,
FEATURE_LOOKUP_AS_OF(
'user_transaction_features',
lbl.user_id,
'avg_amount',
CAST(EXTRACT(EPOCH FROM lbl.observed_at) * 1000 AS BIGINT)
) AS avg_at_label_time
FROM training_labels lbl;
For training joins that pull many features per entity, use a SQL JOIN against the offline table directly with explicit time-travel — the planner pushes down better than per-feature UDF calls:
-- Multi-feature join: every column reflects the snapshot in effect on Jan 1
SELECT lbl.user_id, lbl.churned,
fg.avg_amount, fg.tx_count, fg.last_tx_time
FROM training_labels lbl
LEFT JOIN warehouse.feature_store.user_transaction_features
FOR SYSTEM_TIME AS OF '2026-01-01T00:00:00Z' AS fg
ON lbl.user_id = fg.user_id;
Why this matters
The classic mistake in ML training is temporal leakage: a
training row with label "user churned on Feb 1" enriched with
features computed today (Apr 28) — the features know things they
shouldn't. The new FEATURE_LOOKUP_AS_OF surface makes the
correct semantic the easy one: pass the label's observation
timestamp, get the features as they existed at that point, no
manual snapshot management.
Industries
- Fintech / underwriting. A claim model joining 20 features for one applicant, all at the application's submission timestamp. No leakage; reproducible.
- Subscription churn modelling. Train on "what did this user look like two weeks before they churned" without time machine hacks.
- Insurance. Auditable point-in-time joins are a regulatory requirement, not a nice-to-have.
3. Schema-Aware ML Autocomplete
The SQL editor used to suggest ML_PREDICT and FEATURE_LOOKUP as
static keywords. Now, when your cursor sits inside the function's
string-literal arg, it suggests the actual model and feature-group
names registered in your catalog:
ML_PREDICT('fr|
↑ cursor here
suggestions:
fraud_detector model · onnx · v3
fraud_classifier model · pytorch · v1
fr_credit_scoring model · sklearn · v2
Studio looks up the models, vector indexes, and feature groups you can see, matches them by case-insensitive substring, and shows each suggestion's kind, name, and a one-line detail ("model · onnx · v3", "feature group · keys: user_id · 12 features").
The same autocomplete fires inside FEATURE_LOOKUP('|,
FEATURE_LOOKUP_AS_OF('|, EXPLAIN MODEL '|, and ALTER MODEL '| —
Studio detects which kind of object you're naming and only
shows that kind.
Why this matters
In a tenant with hundreds of models and feature groups, you stop tab-switching between the editor and the catalog browser to remember names. For onboarding new analysts, the editor itself becomes a discoverability surface.
What's next
Roadmap items that build on this foundation:
- In-traversal attribute filtering so HNSW prunes during the graph walk
- Multi-group point-in-time joins that fan out across feature groups in parallel
- Per-feature regression detection — catch models that go bad on a high-value slice while looking flat in aggregate
- Drag-and-drop feature group composer in Studio for citizen data scientists
If any of the industry scenarios above match yours, let us know — sequencing follows real demand.