<?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: Resonance</title>
    <description>The latest articles on DEV Community by Resonance (@resonance_dev).</description>
    <link>https://dev.to/resonance_dev</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%2F4068660%2F3c3ffffa-30ab-43e6-ad8d-a025177e466c.png</url>
      <title>DEV Community: Resonance</title>
      <link>https://dev.to/resonance_dev</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/resonance_dev"/>
    <language>en</language>
    <item>
      <title>I shipped an API client with semantic search and the semantic search was broken</title>
      <dc:creator>Resonance</dc:creator>
      <pubDate>Sat, 08 Aug 2026 10:30:21 +0000</pubDate>
      <link>https://dev.to/resonance_dev/i-shipped-an-api-client-with-semantic-search-and-the-semantic-search-was-broken-2h9h</link>
      <guid>https://dev.to/resonance_dev/i-shipped-an-api-client-with-semantic-search-and-the-semantic-search-was-broken-2h9h</guid>
      <description>&lt;p&gt;I built &lt;a href="https://github.com/d4r4ki4n/recon" rel="noopener noreferrer"&gt;Recon&lt;/a&gt;, a local API client with semantic search. The idea was simple: I had 200+ saved API requests across projects and could never find the right one by name. Now I type "the one that updates the user profile" and it finds it.&lt;/p&gt;

&lt;p&gt;The semantic search runs a local embedding model (all-MiniLM-L6-v2, ~23MB) through &lt;code&gt;@xenova/transformers&lt;/code&gt;. No API calls, no data leaves the machine. SQLite stores everything. One file, one database, no cloud.&lt;/p&gt;

&lt;p&gt;I shipped five releases. The semantic search was broken in all of them.&lt;/p&gt;

&lt;h2&gt;
  
  
  The bug
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;all-MiniLM-L6-v2&lt;/code&gt; produces a 384-dimensional embedding as a &lt;code&gt;Float32Array&lt;/code&gt;. To store it in SQLite, I converted it to a Buffer:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;embedding&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;generateEmbedding&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;text&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// Float32Array(384)&lt;/span&gt;
&lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;prepare&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;INSERT INTO requests (embedding) VALUES (?)&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;Buffer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;from&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;embedding&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;Buffer.from(Float32Array)&lt;/code&gt; creates a buffer viewing the TypedArray's memory. But here's the thing: it creates a buffer with length equal to the TypedArray's &lt;code&gt;length&lt;/code&gt; (384), not its &lt;code&gt;byteLength&lt;/code&gt; (1536). Each 4-byte float gets truncated to a single byte.&lt;/p&gt;

&lt;p&gt;The stored embedding is 384 bytes. The real embedding is 1536 bytes. Every embedding in the database is corrupted.&lt;/p&gt;

&lt;p&gt;When searching, the query embedding (correct, 1536 bytes) gets compared against stored embeddings (corrupted, 384 bytes). The cosine similarity returns NaN. Every search returns nothing.&lt;/p&gt;

&lt;p&gt;The fix:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Before (broken):&lt;/span&gt;
&lt;span class="nx"&gt;Buffer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;from&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;embedding&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;// After (correct):&lt;/span&gt;
&lt;span class="nx"&gt;Buffer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;from&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;embedding&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;buffer&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;embedding&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;byteOffset&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;embedding&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;byteLength&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Or more simply:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;Buffer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;from&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Uint8Array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;embedding&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;buffer&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This creates a buffer from the raw bytes of the underlying ArrayBuffer, preserving all 1536 bytes.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why I didn't catch it
&lt;/h2&gt;

&lt;p&gt;In dev mode, I tested semantic search by typing a query and seeing results. It worked. The model loaded, embeddings generated, results appeared. I never checked the actual cosine similarity scores. The results were there because the FTS5 keyword search was returning them, not the semantic search. The semantic results were silently NaN and filtered out. I was testing the feature and seeing the fallback.&lt;/p&gt;

&lt;p&gt;I wrote integration tests two weeks after shipping. The tests checked actual cosine similarity scores. The first test failed: &lt;code&gt;expected score &amp;gt; 0.1, received NaN&lt;/code&gt;. That's when I found it.&lt;/p&gt;

&lt;h2&gt;
  
  
  The ESM-only trap (the second bug)
&lt;/h2&gt;

&lt;p&gt;The semantic search wasn't the only thing broken. The first binary I shipped crashed on launch.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;@xenova/transformers&lt;/code&gt; is ESM-only. In dev mode, everything works fine. &lt;code&gt;electron-vite&lt;/code&gt; handles the import, the model loads, semantic search works. You ship the binary and it crashes before the window opens.&lt;/p&gt;

&lt;p&gt;The problem: &lt;code&gt;electron-vite&lt;/code&gt;'s &lt;code&gt;externalizeDepsPlugin()&lt;/code&gt; externalizes node_modules as &lt;code&gt;require()&lt;/code&gt; calls in the compiled output. But &lt;code&gt;@xenova/transformers&lt;/code&gt; can't be &lt;code&gt;require()&lt;/code&gt;'d. It's ESM. The compiled main process does &lt;code&gt;require("@xenova/transformers")&lt;/code&gt; and the process dies.&lt;/p&gt;

&lt;p&gt;The fix is a dynamic &lt;code&gt;import()&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;pipeline&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;getPipeline&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;pipeline&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;pipeline&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;p&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="k"&gt;import&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@xenova/transformers&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nx"&gt;pipeline&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;p&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;pipeline&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Dynamic &lt;code&gt;import()&lt;/code&gt; works in CommonJS output because it returns a promise. The module loads asynchronously at runtime instead of synchronously at require time. The binary ships, the model loads on first search, everything works.&lt;/p&gt;

&lt;p&gt;This took me four hours to figure out. The error message was &lt;code&gt;Error [ERR_REQUIRE_ESM]: require() of ES Module&lt;/code&gt;. Not obvious that the fix is "don't require it, import it dynamically" when your entire build pipeline is CommonJS.&lt;/p&gt;

&lt;h2&gt;
  
  
  FTS5 injection via search input
&lt;/h2&gt;

&lt;p&gt;Recon also has full-text search (SQLite FTS5) for keyword matching. Results from FTS and semantic search are merged. The FTS5 query takes user input directly:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;requests_fts&lt;/span&gt; &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;requests_fts&lt;/span&gt; &lt;span class="k"&gt;MATCH&lt;/span&gt; &lt;span class="o"&gt;?&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is parameterized, so SQL injection isn't the issue. The issue is FTS5 query syntax. A user searching for &lt;code&gt;user-or&lt;/code&gt; gets a syntax error because &lt;code&gt;OR&lt;/code&gt; is a FTS5 operator. A hyphen is &lt;code&gt;NOT&lt;/code&gt;. The search crashes and returns zero results.&lt;/p&gt;

&lt;p&gt;The fix: wrap the query in double quotes to force a phrase search, and catch any FTS5 errors to fall back to semantic-only:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;ftsResults&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;prepare&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;SELECT * FROM requests_fts WHERE requests_fts MATCH ?&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
  &lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;all&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`"&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;query&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/"/g&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;""&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)}&lt;/span&gt;&lt;span class="s2"&gt;"`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// FTS5 syntax error, fall back to semantic search only&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The double-quote escaping (&lt;code&gt;""&lt;/code&gt; inside a quoted string) handles queries that contain quotes. The try/catch is the safety net for anything FTS5 decides is invalid syntax.&lt;/p&gt;

&lt;h2&gt;
  
  
  What's in the box
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Save and organize requests by project&lt;/li&gt;
&lt;li&gt;Environment variables (dev/staging/prod)&lt;/li&gt;
&lt;li&gt;Full request/response history&lt;/li&gt;
&lt;li&gt;Semantic search (local embeddings) + keyword search (FTS5)&lt;/li&gt;
&lt;li&gt;Import Postman collections (JSON export)&lt;/li&gt;
&lt;li&gt;Export collections as .http files (Git-friendly, works with JetBrains HTTP Client and VS Code REST Client)&lt;/li&gt;
&lt;li&gt;Import .http files back into Recon&lt;/li&gt;
&lt;li&gt;No account, no login, no cloud sync&lt;/li&gt;
&lt;li&gt;MIT source&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Windows only for now. The source is MIT. If you can build it yourself, do it. If you want a prebuilt binary, grab it from the &lt;a href="https://github.com/d4r4ki4n/recon/releases" rel="noopener noreferrer"&gt;releases page&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Source: &lt;a href="https://github.com/d4r4ki4n/recon" rel="noopener noreferrer"&gt;https://github.com/d4r4ki4n/recon&lt;/a&gt;&lt;br&gt;
Landing: &lt;a href="https://d4r4ki4n.github.io/recon/" rel="noopener noreferrer"&gt;https://d4r4ki4n.github.io/recon/&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What's missing
&lt;/h2&gt;

&lt;p&gt;No WebSocket, no gRPC, no Mac/Linux builds. REST only. No team collaboration features. No scripting. No mock servers.&lt;/p&gt;

&lt;p&gt;This is a tool for one developer who has too many saved requests and can't find them. If that's you, try it.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I learned
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;Buffer.from(TypedArray)&lt;/code&gt; does not preserve byte data. It creates a byte-level view with length equal to the TypedArray's element count, not its byte length. Use &lt;code&gt;Buffer.from(typedArray.buffer)&lt;/code&gt; instead.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Dynamic &lt;code&gt;import()&lt;/code&gt; is the fix for ESM-only deps in Electron. Not &lt;code&gt;esbuild&lt;/code&gt; config, not &lt;code&gt;type: "module"&lt;/code&gt;, not a custom resolver. Just &lt;code&gt;await import()&lt;/code&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;FTS5 query syntax is user input. Treat it like SQL injection: sanitize, escape, and have a fallback.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Local embeddings are practical. 23MB model, 50ms per embedding, runs in Node.js. You don't need an API for semantic search. The model is small enough to ship in the binary.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Shipping a binary is not the same as running it in dev. The ESM bug only appeared in the production build. The embedding bug was invisible because FTS5 was masking it. Run the binary before you ship it. Write tests that check actual values, not just "does it return something."&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;If your product's core differentiator is a feature, test that feature specifically. Not "does search return results." "does semantic search return results with non-NaN scores." The difference is the difference between shipping a working product and shipping a broken one for two weeks.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;




&lt;p&gt;I'm one developer. The product is free. The source is MIT. If you have feedback, I want to hear it.&lt;/p&gt;

</description>
      <category>showdev</category>
      <category>electron</category>
      <category>sqlite</category>
      <category>webdev</category>
    </item>
  </channel>
</rss>
