Skip to content

Add shape (character-class skeleton) index layer#5

Merged
platypii merged 1 commit into
masterfrom
shape-index-layer
Jul 9, 2026
Merged

Add shape (character-class skeleton) index layer#5
platypii merged 1 commit into
masterfrom
shape-index-layer

Conversation

@platypii

@platypii platypii commented Jul 9, 2026

Copy link
Copy Markdown
Contributor

Adds a second, coarser tokenization layer to the index so literal-free pattern regexes (phone numbers, SSNs, IPs, dates) can prune blocks by their digit/punctuation structure instead of falling back to a full scan.

How it works

  • Every letter collapses to L, every digit to D, and a small set of structural punctuation (@.-_/:) is kept verbatim; everything else is a run boundary. Shape n-grams (length 4) are taken over this tiny alphabet.
  • Shape tokens share the index's ngram column, prefixed with a control byte (\x01) so they can never collide with text n-grams.
  • Only shapes containing a non-letter are emitted; all-letter shapes are as non-selective as prose.
  • The query side projects the regex into the shape alphabet and reuses the existing extractRegexLiterals to find the mandatory shape runs, so there are no hand-coded assumptions about what a query "looks like".

Correctness

Shape tokens are only required when mandatory across the whole pattern (single projected branch) and only spent on a branch with no literal n-gram to prune on, so they never cause a false negative. The layer is gated on new kv metadata (hypgrep.shape_ngram_length, hypgrep.shape_chars), so an index built without it is never asked for shape tokens. i+u case folding and v-mode class syntax bail out conservatively.

Cost

About 2% index growth.

Tests

Adds test/shape.test.js covering shape extraction, projection through groups/classes/ranges, flag edge cases, and end-to-end brute-force equivalence (no false negatives).

Accelerates literal-free pattern regexes (phones, SSNs, IPs, dates) that
have no fixed n-gram to prune on. Text is indexed under a second, coarser
tokenizer: letters collapse to L, digits to D, structural punctuation is
kept, everything else is a boundary. Only shapes containing a non-letter
are emitted, so a short shape stays selective (a phone DDD-DDDD is rare
even though every character class is common).

Queries project the regex through the same coarse alphabet and reuse the
existing extractRegexLiterals, so there is no bespoke analyzer and no
hand-coded assumptions about what a query looks like: /\d{3}-\d{2}-\d{4}/
becomes D{3}-D{2}-D{4} and the trusted extractor yields DDD-DD-DDDD.

The index stays a superset candidate filter and the per-row scan still
delivers exact grep, so shape tokens (mandatory, hence never a false
negative) only change which blocks are fetched, never the results. On
25k WildChat rows this takes SSN/date/time/phone regexes from full scans
to 5-26% of blocks for ~2% more index. The layer is always on and not
configurable; its settings live in kv metadata (format, not config) so
queries tokenize identically and shape-less indexes are detected.
@platypii platypii merged commit 3f5fb77 into master Jul 9, 2026
6 checks passed
@platypii platypii deleted the shape-index-layer branch July 9, 2026 17:50
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant