Building Gamatria: a search engine for Hebrew gematria
How a millennia-old numerical tradition became a modern search problem — corpus pipeline, FTS5 relevance, and one codebase that deploys three ways.
Gematria assigns numeric values to Hebrew letters, so every word has a number — and words that share a number are traditionally read as connected. There are many ways to do the counting: the standard method, ordinal positions, reduced digits, letter-name expansions, ciphers like Atbash. Practitioners flip between books of tables to cross-reference them. That felt like a search problem wearing a scholarly costume, so we built the search engine: type any Hebrew word, get its value under 17 classical methods, and see every word in the corpus that shares a value with it — plus its anagrams, reversals, and cipher pairs.
Gamatria is a self-initiated studio project, built end to end with the same process we use for client work: discovery, a fixed scope, weekly increments, and a public launch.
- Methods
- 17
- Corpus words
- 55,638
- Concordance rows
- 305k
- Engine tests
- 64
The corpus is the product
A gematria engine is only as good as its word list. Gamatria's corpus is the entire Hebrew Bible plus the Mishnah and a curated vocabulary — 55,638 unique words, 23,206 verses, and a 305,495-row concordance linking every word to every verse it appears in.
Getting there meant a pipeline of fetch and import scripts: download the full Tanakh from Sefaria — all 39 book files as Sefaria serves them, cached locally per book — pull Open Scriptures morphology for lemmas and glosses, and layer in rabbinic-Hebrew glosses from Jastrow's dictionary for high-frequency Mishnah words. Every import script is idempotent and re-runnable, so the whole corpus can be rebuilt from scratch — or patched incrementally — whenever the engine changes.
The subtle work lives in the tokenizer, and there is exactly one of them, shared by the build scripts and the runtime. Hebrew text arrives full of things that break naive matching: vowel points and cantillation marks to strip, parenthesized qere readings to drop, the maqaf hyphen that must become a separator (deleting it would silently merge adjacent words), and final letter forms to normalize. When a cipher transform lands on a word ending in a non-final form, the lookup retries with the final-form variant. None of this is glamorous; all of it decides whether search results are right.
Precompute what you search
The 17 calculation methods live in one engine module, and every method value is precomputed into its own indexed database column. Searching “which words equal 358 under the standard method” — or constraining several methods at once in the equation solver — is an index lookup, not a computation. The engine is pinned by a 64-test suite that includes the famous fixtures — Torah = 611, the divine name = 26 — while the classical pair that makes 358 interesting in the first place ships as curated seed data.
Text search is SQLite FTS5 with an external-content table kept in sync by triggers. The war story: relevance ranking originally applied a custom ORDER BYthat forced SQLite to materialize the full result set — broad queries took 15 seconds. The fix is a two-stage query: an inner subquery keeps SQLite's optimized rank path and limits to 200 candidates, then an outer re-rank applies a log-frequency boost so common words beat rare gloss-only matches. Same top results, in milliseconds.
One codebase, three deployments
The corpus is a 58 MB SQLite file that ships in the repo as a 22 MB gzipped snapshot. Build scripts use synchronous better-sqlite3; the app runtime talks async libSQL. That split buys deployment freedom with one env var's worth of configuration:
- Self-host: the prebuild step decompresses the snapshot and the app reads the local file. No database setup at all.
- Docker / Fly.io:a two-stage Dockerfile compiles the native SQLite addon and bakes the corpus into the image — scale-to-zero on a 512 MB machine, no volume required.
- Vercel / serverless: a load script seeds a Turso database from the same file, and the runtime points at it via
TURSO_DATABASE_URL.
Bilingual by construction
The interface is fully bilingual — Hebrew and English — and that means the entire layout mirrors between RTL and LTR. Language and dark mode are both read from cookies on the server and rendered into the initial HTML, so there is no flash of the wrong direction or the wrong theme. Hebrew display text is set large in a serif with vowel points intact; shared links get per-word OpenGraph images generated with a bundled Hebrew font, so a word's values render as a card wherever the link lands.
Beyond word lookup, the corpus enables the tools practitioners actually reach for: the full 22×22 matrix of the 231 Gates with real-word gates highlighted, an equation solver over any combination of methods, a letter encyclopedia, a root concordance with Strong's numbers, and the 72 Names derived at runtime from the three Exodus verses — computed from the stored text and spot-checked against the canonical list, not hardcoded.
What we'd tell a client
- If a query can be precomputed, precompute it — indexes are cheaper than cleverness at request time.
- Keep exactly one normalizer for your domain's messy text, and make every pipeline and runtime path go through it.
- Decide early which parts of the stack are build-time and which are runtime; the freedom to deploy anywhere falls out of that split.
- Ranking bugs feel like performance bugs. Measure the query plan before optimizing the hardware.
The mystical layer is the fun part, but the product is a boring, well-indexed database with one very careful tokenizer in front of it — and that's exactly why it's fast.