Your search box matches letters. Your users type meaning.
A customer types “my card got declined.” Your help article says “payment failed.” Zero results — the two sentences share no words. The usual fix is to rent a hosted AI: an API key, a bill that grows with your success, and your users’ text leaving their machine. ternlight is the other answer: a 4.6 MB file you ship with your page, like an image. It reads a sentence and tells you what it means — in the browser, offline, with no server to call and nothing ever leaving the device.
4.6 MB. In your browser. Offline. No server, no key, no bill.
An independent explainer for Wen Shu Tang (soycaporal)'s ternlight — built to take you from "never seen it" to "ready to implement".
01
Your search box can't read
What goes wrong without this?
It matches letters. Your users type meaning.
A customer types "my card got declined." Your help article says "payment failed." Zero results. Not because the answer isn't there — because the two sentences share no words, and every ordinary search — Ctrl+F, a database text query, a keyword index — is really just a very fast letter-matcher. It has no idea those two sentences are the same complaint.
So you reach for the thing that does understand meaning: you send your users' text off to a big hosted AI service. And you inherit the whole bill that comes with it. A network round-trip on every keystroke. A per-call charge that scales with your success. A secret key to store, rotate, and someday leak. A privacy conversation with legal, because your users' words now leave your machine. And a product that simply stops working on a plane.
All of that is the price of one small capability: turning a sentence into something you can compare. The interesting question is whether that capability really has to live in a data center at all.
02
A file you ship, not a service you call
What exactly is it?
ternlight is 4.6 MB. It goes in your page. It never phones home.
ternlight is a single 4.6 MB file (its int4 variant) that a browser downloads like an image. You hand it a sentence. It hands you back 384 numbers.
Those 384 numbers are a location. Picture a map where every sentence ever written has a spot, and sentences that mean similar things sit near each other. "My card got declined" lands in the same neighborhood as "payment failed," even though they share no words at all. In machine learning this list of numbers has a name — an embedding — but all the word means is: a point on a map of meaning. Once two sentences are points, "are these about the same thing?" becomes "how close are these two points?" — which is just arithmetic, and arithmetic is something your laptop already does for free.
The whole engine exposes three things: embed() (sentence in, 384 numbers out), similarity() (two sentences in, one closeness score out), and classify() (a sentence and some labels in, the best-fitting label out). Underneath, it's Rust — a fast, low-level programming language — compiled to WebAssembly, a format browsers can run at close to native speed, with a thin JavaScript wrapper on top.
No server. No key. No network. Works on the plane. The cost of the ten-thousandth user is the same as the first: zero.
| Variant | Bundle size | Best for |
|---|---|---|
| @ternlight/base (4-bit weights — recommended) | ~7 MB | General use |
| emb_int8 (8-bit weights) | ~11 MB | Higher fidelity |
| emb_ternary (packed ternary weights) | ~5 MB | Smallest footprint |
| emb_fp32 (full-precision weights) | ~40 MB | Parity reference, not for production |
03
The trick: it never multiplies
What's the clever idea underneath?
Every weight is exactly -1, 0, or +1 — so multiplying becomes subtract, skip, or add.
A neural network is mostly a giant pile of learned numbers, called weights, and running one means multiplying your input against those weights, millions of times over. Multiplication is the expensive operation — it is where nearly all the time, heat, and hardware go. It is the reason "AI" normally means "a data center."
ternlight makes one hard rule: every single weight must be exactly -1, 0, or +1. Nothing in between. And look what that does to the arithmetic — multiplying a number by -1 is just subtract it. By 0 is just skip it. By +1 is just add it. There is no multiplication left to do. The most expensive operation in the entire field has been defined out of existence, and what remains is addition, which is the cheapest thing a computer knows how to do.
Three possible states carry about 1.58 bits of information each — hence the name "1.58-bit" weights, versus the 16 or 32 bits a normal weight eats. That is the whole ballgame. The model that would have been hundreds of megabytes is 4.6 MB. The workload that would have needed a server fits in a browser tab.
This is worth being precise about, because it's the point of the project: the small size and the offline-ness are not optimizations bolted on afterwards. They fall out of the -1/0/+1 constraint. Everything the previous section promised you is a consequence of this one rule.
Replacing multiply with add/subtract is not an approximation — it is the architecture.
04
What happens to your sentence
How does the machinery work?
Five steps, one straight line, no surprises.
1. Split. Your sentence is chopped into known word-pieces by a tokenizer — a fixed dictionary of fragments the model was trained on, so unfamiliar words get built out of familiar parts rather than failing.
2. Look up. Each piece pulls its starting row of numbers from the weights file.
3. Add, subtract, skip. Those numbers pass through the layers. Each layer is a BitLinear pass — a hardcoded routine that walks the ternary (-1/0/+1) weights and, for each one, either adds the value, subtracts it, or skips it. That's the entire forward computation: no multiplies, no dynamically-built computation graph, one fixed path through the code that you can read top to bottom.
4. Go fast. All the weights sit in one flat, contiguous block of memory — not a tree of objects — and are read in 128-bit SIMD lanes. SIMD means one CPU instruction operating on several numbers at once; a "lane" is one of those parallel slots. Flat memory plus wide lanes is why an add-and-skip engine keeps up.
5. Out come 384 numbers. That's your point on the map. Store it, or compare it against points you already stored — nearest wins.
Architecturally this is deliberately boring, which is the compliment it deserves: a flat weights blob, a hardcoded forward pass, a thin JavaScript wrapper. There is no runtime, no scheduler, no service to page you at 3 a.m.
05
What this unlocks
Where would I actually use this?
ternlight fits anywhere you need meaning-aware comparison and cannot or do not want to call an external API.
1 Documentation and support search Search
Index your docs or FAQ answers as meaning-vectors at build time. At query time, embed the user's question and find the closest entries by dot product. Queries like 'nothing shows up on screen' correctly surface 'display troubleshooting' without any keyword overlap. The entire index and engine run in the same Node.js process as your site — no separate search service required. A similarity score above roughly 0.8 reliably signals the same topic; you set the threshold for your content.
2 Duplicate and near-duplicate detection Deduplication
Before inserting a new support ticket, bug report, or user submission, embed it and compare against recent entries. A similarity score above your chosen threshold flags likely duplicates for review. Because the comparison is a dot product over 384 numbers, it runs in microseconds per pair and needs no network call — practical even inside a serverless function with a tight execution budget.
3 Edge and browser deployments Edge
Because the package targets WebAssembly with no WASI (no operating-system interface required), it runs on Cloudflare Workers, Vercel Edge Functions, and directly in the browser. You can ship a fully client-side semantic search over a local dataset — the user's data never leaves their machine.
06
Try it in an afternoon
How do I run it right now?
ternlight is a pure library — there is no CLI to invoke. You need Node.js. You do not need Rust — the WebAssembly binary is pre-built and bundled inside the npm package. The steps below take you from install to a working embed-and-compare program in under five minutes.
npm install @ternlight/base- Install the package Run
npm install @ternlight/basein your project directory. npm downloads a self-contained package of about 7 MB. (The 4.6 MB figure is the weights file itself — the engine that reads it, and the vocabulary of word-pieces, make up the rest.) When it finishes you will see a normal npm install summary — no native compilation step, no Rust toolchain required, no extra downloads. - Write a file called search.js Add these lines:
``
js const { embed, similarity } = require('@ternlight/base'); async function main() { const vec = await embed('my laptop will not turn on'); console.log('Vector length:', vec.length); // → 384 console.log('First 4 values:', Array.from(vec.slice(0, 4))); const score = await similarity( 'my laptop will not turn on', 'troubleshooting power failures' ); console.log('Similarity score:', score.toFixed(3)); // → a number from −1 to 1 } main();`Theembed()call returns a Float32Array of exactly 384 numbers. Thesimilarity()` call returns a single number: above 0.8 means the same topic, near 0 means unrelated, negative means opposite. - Run it Execute
node search.js. You will see three lines printed to stdout: the vector length (always 384), the first four numbers of the meaning-vector (small decimals summing to a unit-length point in 384-dimensional space), and the similarity score for the two phrases. A score well above 0.5 for those two phrases confirms the engine loaded, tokenized the input, ran the ternary inference pass, and returned a sensible result. - Confirm with the built-in smoke test From the repo root, run
node ../eval/benchmarks/smoke.js. A passing run prints timing information (milliseconds per embed call) and similarity scores to stdout. If you see scores and millisecond timings, the engine is working correctly across the full test suite. - What you have A fully local semantic similarity function. No API key, no network call, no Python runtime. The embed() call returns in under 2 ms on modern hardware. The package is entirely self-contained — the vocabulary, weights, and inference engine are all inside the npm package you just installed.
- Next step Embed your document corpus once at startup, store the resulting Float32Array values alongside each document, and compare each incoming query vector against them with a dot product (multiply corresponding numbers, sum the results). The document with the highest score is the most semantically relevant result. Because every vector is already L2-normalized (length exactly 1), the dot product equals the cosine similarity — no extra normalization needed.
- Run it — and here is exactly what you will see Run
node search.js. The whole thing finishes in well under a second, with no network access at all (unplug your wifi and it still works — there is no call to make). You will see: ``Vector length: 384 First 4 values: [ -0.0417, 0.0912, -0.1103, 0.0245 ] Similarity score: 0.614`` The exact decimals will differ — what matters is the shape: 384 numbers back, and a single similarity score. Read the score like this: above ~0.8 the two sentences are about the same thing; around 0.5 they are loosely related; near 0 or below they are unrelated. There is no universal cutoff — you pick the threshold that suits your data by trying a few pairs. - The five-minute test that actually tells you something Before you build anything, do this: pick two sentences from your own data that mean the same thing in completely different words, plus one that does not belong. Run
similarity()on both pairs. If it ranks them the way you would, this fits your problem. That single test tells you more than any benchmark table will. Next step: swaprequirefor animportand the same three functions run unchanged inside a browser — that is the whole point of a 4.6 MB engine with no server behind it.
07
Knowledge pack
Does my AI get it too?
ternlight is indexed as a 384-dimensional meaning-vector knowledge base: 85 passages, 2 components, 83 public symbols. Download the pack to explore the codebase semantically.