kevy4.0

Secondary indexes (IDX.* / idx_*)

kevy can maintain declarative secondary indexes over a prefix domain of the keyspace: every hash key under a prefix is a "row", one declared hash field is the indexed value. Indexes are maintained synchronously with every write (derived-by-construction — the index can never drift from the data, and IDX.VERIFY makes that falsifiable), and queried with cursor pagination, two-index composition, and optional field hydration.

IDX.CREATE idx_age ON PREFIX user: FIELD age TYPE i64 KIND range
HSET user:42 age 31 name "……"
IDX.QUERY idx_age RANGE 18 30 LIMIT 100 FIELDS name

Declaring

IDX.CREATE <name> ON PREFIX <p> FIELD <f> TYPE i64|f64|str KIND range|unique [MAXMEM <bytes>]

Querying

Uniqueness is a fence, not a lock

A unique index does not block writes — enforcing global uniqueness at write time would serialize cross-shard writes. Instead: duplicates are counted (duplicates in VERIFY/LIST) and visible as multi-hit EQ reads. If you need hard uniqueness, pin the domain to one shard with a {hashtag} prefix in cluster mode, or check-then-write under MULTI/WATCH.

Embedded

Same engine, typed API: idx_create / idx_drop / idx_query / idx_count / idx_stats / idx_list (values as IndexValue, cursors as IndexCursor). No FIELDS hydration — you're in-process; read fields with hget. idx_create builds synchronously and returns when the index serves.

Consistency + cost model

Aggregate kind (KIND agg) — write-time GROUP BY

IDX.CREATE ord_amt ON PREFIX ord: FIELD amount TYPE i64 KIND agg GROUPBY status
IDX.QUERY ord_amt GROUP paid                      → [count, sum, min, max, avg]
IDX.QUERY ord_amt GROUPS BY sum LIMIT 100         → ranked [group, count, sum, min, max]

The engine answer to SELECT g, COUNT(*), SUM(v) … GROUP BY g: aggregates are maintained IN THE WRITE PATH (a declared access path — never a query-time row scan). min/max stay exact under deletion via a per-group value multiset; sums accumulate in f64 (documented precision bound); a row whose value fails coercion or whose group field is missing is excluded and counted (VERIFY). Cross-shard merging is exact: counts/sums add, extremes take. GROUPS ranks by count/sum/max descending or min ascending, LIMIT ≤ 1000.

No HAVING, no aggregate expressions, no approximate sketches — the query-language slope. Filter GROUPS results in the app.

Embedded: idx_create_agg(name, prefix, field, ty, group_by) / idx_group(name, g) / idx_groups(name, by, limit).

Memory ≈ groups × (gkey+64) + distinct_values × 18 + rows × (key+10) (constants calibrated against measured RSS); gated vs real RSS by bench/agggate.sh along with GROUP p99 < 1ms @ 1M×10k groups, GROUPS top-100 < 5ms, and write tax < 10%.