Skip to main content

AI/ML in Gnok Goes Production-Grade: Filtered ANN, Point-in-Time Features, and Schema-Aware Autocomplete

· 6 min read
Gnok Team

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:

StrategyWhenWhat it does
IndexOnlyNo predicate above the scanPure ANN: probe HNSW for top-k row ids → materialise from source
PostFilterNon-selective predicate (~10%+)Probe with overfetch (k × factor / sel), apply filter, take top-k
PreFilterVery 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.