<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Wen Shu Tang</title>
    <description>The latest articles on DEV Community by Wen Shu Tang (@soycaporal).</description>
    <link>https://dev.to/soycaporal</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4023385%2Fd6e8a4ca-becb-4645-b5bf-be5f9fbbce39.jpg</url>
      <title>DEV Community: Wen Shu Tang</title>
      <link>https://dev.to/soycaporal</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/soycaporal"/>
    <language>en</language>
    <item>
      <title>Like Fuse.js, but it matches on meaning: a 7 MB embedding model that runs in the browser</title>
      <dc:creator>Wen Shu Tang</dc:creator>
      <pubDate>Fri, 31 Jul 2026 16:24:29 +0000</pubDate>
      <link>https://dev.to/soycaporal/like-fusejs-but-it-matches-on-meaning-a-7-mb-embedding-model-that-runs-in-the-browser-1fgm</link>
      <guid>https://dev.to/soycaporal/like-fusejs-but-it-matches-on-meaning-a-7-mb-embedding-model-that-runs-in-the-browser-1fgm</guid>
      <description>&lt;p&gt;Semantic search — matching on &lt;em&gt;meaning&lt;/em&gt; instead of exact text — almost always means standing up infrastructure: an embedding API (plus a key, plus a per-call bill), a Python model server, or a vector database. For a lot of ordinary features, that's a surprising amount of backend for "make the search box understand synonyms."&lt;/p&gt;

&lt;p&gt;So I tried to delete all of it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;ternlight&lt;/strong&gt; is a sentence-embedding model where the model, tokenizer, and inference engine are &lt;strong&gt;~7 MB total and run entirely in the browser, on the CPU.&lt;/strong&gt; No server, no API, no GPU. You &lt;code&gt;npm install&lt;/code&gt; it and it runs where your users already are.&lt;/p&gt;

&lt;p&gt;The way I explain it simply: &lt;strong&gt;an upgraded Fuse.js.&lt;/strong&gt; Something you drop into the page - no service to stand up, no key to manage — that happens to match on &lt;em&gt;meaning&lt;/em&gt; instead of characters. &lt;a href="https://pagefind.app/" rel="noopener noreferrer"&gt;Pagefind&lt;/a&gt; made full-text search on a static site a one-line install; this is the same idea, but with semantic search unlocked.&lt;/p&gt;

&lt;p&gt;👉 &lt;strong&gt;Live demo - runs entirely in your browser: &lt;a href="https://ternlight.dev/demo/" rel="noopener noreferrer"&gt;https://ternlight.dev/demo/&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The whole integration
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm &lt;span class="nb"&gt;install&lt;/span&gt; @ternlight/base
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;embed&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;similar&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@ternlight/base&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;// text -&amp;gt; 384-dim vector, computed on the CPU, in this tab&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;v&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;embed&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;how do I cancel my subscription?&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// or rank a small corpus by meaning&lt;/span&gt;
&lt;span class="nf"&gt;similar&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;I want my money back&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;docs&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;topK&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="c1"&gt;// -&amp;gt; ranked matches · ~5 ms/embed · zero network&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No &lt;code&gt;fetch&lt;/code&gt;, no key, works offline. That's it.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fy450v91vzei0s1iz1io2.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fy450v91vzei0s1iz1io2.png" alt=" " width="549" height="815"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What "on-device" actually costs
&lt;/h2&gt;

&lt;p&gt;The interesting part isn't that it's small, it's that "on-device" makes &lt;strong&gt;four things bind at once&lt;/strong&gt; that normally trade off against each other:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;It has to fit:&lt;/strong&gt; engine + tokenizer + weights arrive as &lt;em&gt;one&lt;/em&gt; artifact the browser downloads once.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;It has to stay smart:&lt;/strong&gt; an embedding is only worth anything if it captures meaning (the one you can't fake).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;It has to run anywhere:&lt;/strong&gt; no GPU to assume, nothing to install; a runtime every browser already has.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;It has to be fast on a CPU:&lt;/strong&gt; a search box feels latency keystroke by keystroke.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Each is reasonable alone; holding all four is the hard part, and it's why the usual tools don't carry over. A transformer behind an API is great &lt;em&gt;when you have a server to call&lt;/em&gt;. A general runtime like ONNX Runtime Web or PyTorch is built to run almost anything — which is exactly what makes it heavy to ship into a tab.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F7zoqsw8esgdc2iaqlp3i.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F7zoqsw8esgdc2iaqlp3i.webp" alt="Four radar charts over the axes small, fast, portable, and good — Fuse.js, transformers.js, and a remote API each cover only some axes; ternlight covers all four" width="800" height="645"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Roughly how the options compare:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;&lt;strong&gt;ternlight&lt;/strong&gt;&lt;/th&gt;
&lt;th&gt;Remote embedding API&lt;/th&gt;
&lt;th&gt;transformers.js (in-browser)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;On the wire&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;~7 MB, once&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;~0, but a network call per query&lt;/td&gt;
&lt;td&gt;tens–hundreds of MB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Latency&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;~5 ms/embed, local&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;network round-trip each call&lt;/td&gt;
&lt;td&gt;heavier (general runtime)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Server / API key&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;none&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;required&lt;/td&gt;
&lt;td&gt;none&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Data leaves device&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;no&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;td&gt;no&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;GPU&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;not needed&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;n/a&lt;/td&gt;
&lt;td&gt;often wanted&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  How it fits in 7 MB
&lt;/h2&gt;

&lt;p&gt;There are three levers that unlocked this:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Ternary weights.&lt;/strong&gt; In deep neural nets, every weight has a floating point 32 representation. But here, every linear-layer weight is just &lt;code&gt;-1&lt;/code&gt;, &lt;code&gt;0&lt;/code&gt;, or &lt;code&gt;+1&lt;/code&gt; - trained that way &lt;em&gt;from the start&lt;/em&gt;. That's two bits per weight instead of thirty-two. And here's the part that is most cool: when weights are only &lt;code&gt;-1/0/+1&lt;/code&gt;, the matrix multiply that dominates inference stops needing multiplication - it collapses into &lt;strong&gt;add, subtract, skip&lt;/strong&gt;, exactly the shape a CPU's Single Instruction multiple data (SIMD) lanes chew through vector operations fast. So ternary does double duty: it shrinks the model &lt;em&gt;and&lt;/em&gt; makes it quick on a plain CPU.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fv74rkqru4c9m63yd5lec.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fv74rkqru4c9m63yd5lec.png" alt=" " width="709" height="270"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Left: 32-bit weights, a continuous range. Right: the same layer, ternary — every weight is one of three values.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Packing.&lt;/strong&gt; Three possible values pack into ~2 bits; squeezing thousands together is what turns a normal-sized network into a few-megabyte download.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. A from-scratch engine.&lt;/strong&gt; Instead of shipping PyTorch/ONNX and letting the runtime dwarf the weights, the whole inference path is hand-written Rust compiled to one small WASM module - where that add/subtract/skip hot loop actually lives.&lt;/p&gt;

&lt;p&gt;The surprise: &lt;em&gt;"small, fast, portable, good - pick two"&lt;/em&gt; feels like a law. But training ternary from the start doesn't slide you along that tradeoff curve, it &lt;strong&gt;moves the curve&lt;/strong&gt; — more quality per byte than the tradeoff should allow. That's why all four can hold at once.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fqkseagzp6g9t14mhhe1y.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fqkseagzp6g9t14mhhe1y.png" alt=" " width="716" height="496"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Measured: ~8× smaller for ~0.05 Spearman vs the pre-quantization student — the whole point of doing it in training rather than after.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  When to use it (and when not)
&lt;/h2&gt;

&lt;p&gt;Honest limits: It's &lt;strong&gt;English-only&lt;/strong&gt;, reads up to &lt;strong&gt;128 tokens&lt;/strong&gt; at a time, and won't top a retrieval leaderboard. It's tuned to be small and useful, not to win benchmarks.&lt;/p&gt;

&lt;p&gt;Where it shines is the ordinary, high-volume work: semantic search over your own docs, matching a question to an FAQ or an intent, finding near-duplicates, privacy-first tasks. For those, a small model that runs locally beats a higher benchmark number sitting behind an API, no network round-trip, no API keys, nothing to deploy, and the data never leaves the device.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try it
&lt;/h2&gt;

&lt;p&gt;It's &lt;strong&gt;MIT and fully open sourced&lt;/strong&gt; - the model, the training + quantization pipeline, and the Rust engine are all on &lt;a href="https://github.com/soycaporal/ternlight" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;. One line to try it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm &lt;span class="nb"&gt;install&lt;/span&gt; @ternlight/base
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I wrote the longer, illustrated version - with the craft-project backstory on my site: &lt;strong&gt;&lt;a href="https://ternlight.dev/blog/running-embeddings-in-the-browser/" rel="noopener noreferrer"&gt;Running embeddings in the browser: what it actually costs&lt;/a&gt;&lt;/strong&gt;. Deep dives on the training, the packing, and the engine are coming soon.&lt;/p&gt;

&lt;p&gt;Happy to answer anything in the comments 👇&lt;/p&gt;

</description>
      <category>javascript</category>
      <category>machinelearning</category>
      <category>webassembly</category>
      <category>rust</category>
    </item>
  </channel>
</rss>
