Skip to content
All posts
8 min read

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.

Case studyData engineeringSearch

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
116,637
Concordance rows
305k
Interface languages
7

The corpus is the product

A gematria engine is only as good as its word list. Gamatria launched on the entire Hebrew Bible plus the Mishnah and a curated vocabulary; it now also draws on the Babylonian Talmud, the Zohar, three siddur rites, and a modern Hebrew dictionary — 116,637 unique words in all. The Tanakh layer carries 23,206 verses and a 305,495-row concordance linking every biblical 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 Strong's numbers, and STEPBible's tagged Leningrad text for pointing, transliteration and in-context meanings. Later sources follow the same rule: openly licensed editions only, and no machine vocalization. A Mishnah-only word gets an English gloss from Jastrow's dictionary only when exactly one Jastrow entry is headed by that word, letter for letter; otherwise its meaning stays blank. The imports run as one idempotent pipeline ending in a validation pass, 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 and normalizer are pinned by 53 tests that include the famous fixtures — Torah = 611, the divine name = 26 — inside a suite of more than 2,300 tests with a 100% coverage threshold. 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 a few hundred candidates, then an outer query re-ranks only those — first by how a word matched (its own meaning, then a meaning that starts with the query, then a transliteration), then headwords before inflected forms, then attested dictionary words before rare strings. Better results, in milliseconds.

One codebase, three deployments

The corpus is a roughly 144 MB SQLite file that ships in the repo as a 54 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 — corpus and accounts alike. 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: the decompressed file is traced into every function, so corpus reads never touch the network. Only account data — users, sessions, saved words — lives in Turso, via TURSO_DATABASE_URL.

The first production version used Turso for the corpus too, and metered row reads made that the expensive path. Moving corpus reads to the bundled file, and serving localized pages as static cache hits from the CDN, is what keeps the site on free tiers.

Bilingual by construction

The interface is fully bilingual — Hebrew and English — and that means the entire layout mirrors between RTL and LTR. Language lives in the URL (/he, /en) with hreflang alternates, and the theme is applied by a small inline script before first paint, so pages stay cacheable with no flash of the wrong direction or the wrong theme. Russian, French, Spanish, Portuguese and German interfaces followed; the data layer stays Hebrew-only. 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 231 Gates as both an interactive wheel and a 22×22 matrix, with real-word gates marked, 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 shipped after launch

Between July and October 2026 the search engine grew into a research tool. The user-visible additions:

  • Accounts by emailed magic link — no passwords — with saved words that sync across devices.
  • A free developer API: sign in, create a key, and get every value, word and search as JSON.
  • Learn: ten long-form articles on gematria, with every value checked against the engine and every claim tied to a source.
  • Reader suggestions for missing words, held for a maintainer's review before they reach the corpus.
  • A Pro tier — collections, batch lookup, CSV export — announced but not yet open for subscriptions; search stays free.

For people building with AI agents, the repo also ships a Model Context Protocol server that exposes the engine and local corpus as tools — values, word reports, search, the solver, the 231 Gates and the concordance.

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.