Skip to content

feat(idx): consult the hash index from vector-needle find / in / dict at, admit STR, carry across concat - #520

Merged
singaraiona merged 2 commits into
devfrom
feat/idx-vector-find
Sep 13, 2026
Merged

singaraiona merged 2 commits into
devfrom
feat/idx-vector-find

Conversation

@singaraiona

Copy link
Copy Markdown
Collaborator

Closes #519.

A keyed lookup against a large vector rebuilt a hash over the whole vector on every call, and nothing attachable changed that. Three changes, matching the issue's three asks, plus the dict lookup it mentions.

What changes

1. find / in with a vector of needles consult the attached hash index. ray_index_find_vec probes a fresh hash index once per needle: integer family by value, SYM through the column's domain (same or file domain), STR by the needle's bytes. Scalar find and the dict scalar lookup go through ray_index_find_atom, which extends the old integer-only scalar path to SYM and STR. (at dict keys) with a typed key vector resolves every position with one find instead of a scan per probe.

Without an index, find and the hashset fallback of in now hash whichever side is smaller and scan the other once (early exit when every distinct needle has been seen). That is the orientation trick from the issue's workaround, made the default.

2. RAY_STR is admitted to the hash attach ('grouped, 'unique, .idx.hash). The key word is a 64-bit hash of the bytes; every word hit in the builder and in the probes is confirmed against the payload of the group's first row, so a word collision becomes two groups rather than one merged group. The int64-keyed consults (eq/in rowsel, ray_index_hash_group, ray_index_find_row) decline a STR column instead of reading an out-of-range key as "provably absent". A STR hash persists through a splayed save verbatim (byte hashes and row ids are domain-free) in the column's single index slot, replacing the automatic string dictionary for that column.

3. concat carries the index when every appended key is new. ray_index_carry_append copies the CSR tables onto the concat result and adds each appended row as a single-row group; unique and the within-group order symbol carry over. A repeated key, a null, a float column or a SYM domain change leave the result a plain vector as before, so an insert loop with repeating keys never pays a hidden O(n) rebuild. Applies to (concat v rows), (concat v atom) and therefore (alter 'v concat ...).

Numbers (release, 362k keys, 100 needles, same box)

case before after
STR find, 'grouped 22 ms 1 ms (15 ms with no index at all)
STR dict at, 100 probes 139 ms 1 ms
I64 / SYM find, 'grouped 8 / 9 ms 0 ms
concat +100 rows onto indexed 362k I64 index dropped 4 ms, kept (re-attach: 7 ms; STR 9 vs 26 ms)

One clarification for the reporter: 'unique alone attaches a marker with no table, so the issue's "I64 with 'unique" cells never had an index to consult. 'grouped (optionally with 'unique on top) attaches the hash these paths use. Docs updated accordingly.

Tests

test/rfl/ops/idx_find_in.rfl: drop-differential for I64, SYM (same and file domain), STR; duplicates, misses, null needles; the small-side orientation; dict lookups; concat carry including bucket-table growth, repeat / null / STR / SYM cases; splayed round trips of STR and carried indexes. Two existing expectations updated: .idx.hash on STR now succeeds; concat of new keys keeps the index. Full suite: 3784/3784, no UBSan output.

Not in this PR

  • (== strcol "x") in a where still scans a STR hash (the filter decode is int-keyed).
  • A 'unique-only marker on a STR column does not persist (numeric ones do).

https://claude.ai/code/session_017fCcYRuzNagw6nmLqZ9zbU

… at, admit STR to it, carry it across concat

Closes #519.  A keyed lookup against a large vector rebuilt a hash over the
whole vector on every call, and nothing attachable changed that:

  * find / in with a vector of needles hashed one operand per call even when
    the haystack carried a hash index; the index was only consulted for a
    scalar integer needle.
  * .attr.set 'unique / 'grouped refused RAY_STR, so a content-keyed column
    (a digest) could not carry an index at all.
  * concat answered a plain vector, so an append-only keyed vector lost its
    index on every append and re-attaching was a full build.
  * (at dict keys) ran one linear scan of the keys per probe.

idxop: ray_index_find_atom / ray_index_find_vec probe a fresh hash index per
needle (integer family by value, SYM through the column's domain, STR by the
needle's bytes).  ray_index_attach_hash admits RAY_STR: the key word is a
64-bit hash of the bytes and every word hit in the builder and the probes is
confirmed against the payload of the group's first row, so a word collision
is two groups, not one.  The int64-keyed consults (eq/in rowsel, hash_group,
find_row) decline a STR column instead of reading an out-of-range key as
"provably absent".  ray_index_carry_append copies the CSR tables onto a
concat result and adds each appended row as a new group when every appended
key is new (markers and the within-group order symbol carry); a repeated
key, a null, a float column or a domain change leave the result unindexed as
before, so an insert loop with repeating keys never pays a hidden rebuild.
vec_all_distinct and attr_set_unique take SYM and STR too.

collection: find and in try the index first; without one, when there are
fewer needles than rows they hash the needles and scan the haystack once
(early exit once every distinct needle is seen) instead of hashing the big
side — the workaround the issue described, now the default orientation.
(at dict typed-keys) resolves all positions with one find.  dict.c consults
the index for a scalar key.  builtins: concat (vec+vec, vec+atom) carries.
store: a STR hash persists through a splayed save verbatim (byte hashes and
row ids are domain-free) in the column's single index slot; ray_col_save no
longer stamps the inline-index marker on a STR column, which would have made
the later append refuse the file.

Measured, release, 362k keys, 100 needles: STR find 22 -> 1 ms with
'grouped (15 ms with no index at all); dict at 100 probes 139 -> 1 ms;
I64 / SYM find 8 / 9 -> 0 ms; concat of 100 rows onto an indexed 362k I64
vector 4 ms with the index kept, where the re-attach cost 7 ms (STR: 9 vs
26 ms).  Note 'unique alone is a marker with no table; 'grouped attaches
the hash the lookups consult.

Tests: test/rfl/ops/idx_find_in.rfl (drop-differential for I64 / SYM same
and file domain / STR, duplicates, misses, null needles, the small-side
orientation, dict lookups, concat carry incl. table growth and the repeat /
null / STR / SYM cases, splayed round trips).  Two existing expectations
updated: .idx.hash on STR now succeeds; concat of new keys keeps the index.

Claude-Session: https://claude.ai/code/session_017fCcYRuzNagw6nmLqZ9zbU
@singaraiona
singaraiona merged commit 991702e into dev Sep 13, 2026
9 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

1 participant