<?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: embeddings</title>
    <description>The latest articles tagged 'embeddings' on DEV Community.</description>
    <link>https://dev.to/t/embeddings</link>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/tag/embeddings"/>
    <language>en</language>
    <item>
      <title>Day 11: Embeddings and Vector Representations — How Machines Map Meaning</title>
      <dc:creator>Suresh Kumar Pallapothu</dc:creator>
      <pubDate>Sun, 11 Oct 2026 13:47:50 +0000</pubDate>
      <link>https://dev.to/suresh_kumar_de3920bedd1c/day-11-embeddings-and-vector-representations-how-machines-map-meaning-2e2m</link>
      <guid>https://dev.to/suresh_kumar_de3920bedd1c/day-11-embeddings-and-vector-representations-how-machines-map-meaning-2e2m</guid>
      <description>&lt;p&gt;  &lt;iframe src="https://www.youtube.com/embed/kNe71VwpkhY" width="710" height="399"&gt;
  &lt;/iframe&gt;
&lt;/p&gt;

&lt;p&gt;To make an AI work with your own company data, you first have to turn that data into something a machine can &lt;strong&gt;measure&lt;/strong&gt;. Words are not measurable. Numbers are. The bridge between the two is the &lt;strong&gt;embedding&lt;/strong&gt;.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;In plain terms:&lt;/strong&gt; Imagine a giant map where every sentence in your company has a pin. Sentences about the same topic are pinned close together, and unrelated ones are far apart. An embedding is the pin's coordinates.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Diagram:&lt;/strong&gt; An embedding model maps a chunk of text to a list of numbers. Texts about similar things, like network latency and packet drop, get similar numbers. Real embeddings have hundreds or thousands of values, and the ones shown are illustrative. &lt;a href="https://sureshpallapothu.in/blog/day-11-embeddings" rel="noopener noreferrer"&gt;See the animated version&lt;/a&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  1. The mathematical map
&lt;/h2&gt;

&lt;p&gt;An &lt;strong&gt;embedding model&lt;/strong&gt; reads a chunk of text and outputs a &lt;strong&gt;dense vector&lt;/strong&gt;: a list of numbers, usually 768 or 1,536 of them. You never read these numbers yourself. What matters is how they relate to each other. The numbers are produced so that text with a similar meaning gets a similar list.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Semantic closeness
&lt;/h2&gt;

&lt;p&gt;Because similar meanings get similar numbers, they cluster together when you plot them. The vectors for "network latency" and "packet drop" sit close together. The vector for "financial forecast" sits far away from both. A question you type also becomes a vector, and it lands next to the text that answers it.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Diagram:&lt;/strong&gt; Embeddings place texts on a map where meaning is distance. Network terms cluster together, finance terms cluster elsewhere, and a question lands near the text that answers it. The map is a flattened, illustrative view of a space with hundreds of dimensions. &lt;a href="https://sureshpallapothu.in/blog/day-11-embeddings" rel="noopener noreferrer"&gt;See the animated version&lt;/a&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  3. The power of distance
&lt;/h2&gt;

&lt;p&gt;Once all your documents are vectors, finding the right one becomes arithmetic. The common measure is &lt;strong&gt;cosine similarity&lt;/strong&gt;, which compares the &lt;em&gt;direction&lt;/em&gt; of two vectors. A score near 1 means they point the same way, so the texts mean much the same thing. A score near 0 means they are unrelated.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Diagram:&lt;/strong&gt; Cosine similarity measures the angle between two vectors. Vectors pointing the same way score close to 1, meaning similar meaning, and unrelated ones score near 0. The scores shown are computed from the four-number example vectors, which are illustrative. &lt;a href="https://sureshpallapothu.in/blog/day-11-embeddings" rel="noopener noreferrer"&gt;See the animated version&lt;/a&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;This is why embedding search beats keyword search. Ask "Why is the site slow?" and a keyword search finds nothing if no document contains those words. An embedding search finds the paragraph about high latency on the web tier, because the &lt;em&gt;meaning&lt;/em&gt; is close.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Diagram:&lt;/strong&gt; Keyword search needs the same words. Embedding search compares meaning, so a question about a slow site can find a paragraph about high latency on the web tier even though the words differ. &lt;a href="https://sureshpallapothu.in/blog/day-11-embeddings" rel="noopener noreferrer"&gt;See the animated version&lt;/a&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Here is the arithmetic in Python, using the same four-number example vectors from the diagrams. Real embedding models return far longer vectors, and you would call one from a library or an API.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;numpy&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;np&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;cosine&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;float&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;a&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;linalg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;norm&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;linalg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;norm&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;)))&lt;/span&gt;

&lt;span class="n"&gt;latency&lt;/span&gt;  &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mf"&gt;0.82&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mf"&gt;0.11&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;0.47&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;0.05&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;   &lt;span class="c1"&gt;# "network latency"
&lt;/span&gt;&lt;span class="n"&gt;drop&lt;/span&gt;     &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mf"&gt;0.79&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mf"&gt;0.08&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;0.51&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;0.02&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;   &lt;span class="c1"&gt;# "packet drop"
&lt;/span&gt;&lt;span class="n"&gt;forecast&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mf"&gt;0.12&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;0.55&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;0.20&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;0.78&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;   &lt;span class="c1"&gt;# "financial forecast"
&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;round&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;cosine&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;latency&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;drop&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;      &lt;span class="c1"&gt;# 1.0   -&amp;gt; almost the same meaning
&lt;/span&gt;&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;round&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;cosine&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;latency&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;forecast&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;  &lt;span class="c1"&gt;# -0.03 -&amp;gt; unrelated
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The numbers in this example are made up to show the idea. In a real system you embed every document once, store the vectors, and compare each new question against them.&lt;/p&gt;

&lt;h2&gt;
  
  
  Coming Up Next
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Day 12:&lt;/strong&gt; Vector databases: storing and searching enterprise knowledge.&lt;/p&gt;

&lt;h1&gt;
  
  
  VectorEmbeddings #MachineLearning #NLP #DataEngineering #AIArchitecture
&lt;/h1&gt;




&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://sureshpallapothu.in/blog/day-11-embeddings" rel="noopener noreferrer"&gt;https://sureshpallapothu.in/blog/day-11-embeddings&lt;/a&gt;, where this post includes animated diagrams.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>embeddings</category>
      <category>30dayaiseries</category>
    </item>
    <item>
      <title>Scrape Plus Embed Pipeline: End-to-End Support Knowledge Trust Boundaries</title>
      <dc:creator>Silhouette72591483</dc:creator>
      <pubDate>Sun, 11 Oct 2026 04:43:34 +0000</pubDate>
      <link>https://dev.to/silhouette72591483/scrape-plus-embed-pipeline-end-to-end-support-knowledge-trust-boundaries-48aa</link>
      <guid>https://dev.to/silhouette72591483/scrape-plus-embed-pipeline-end-to-end-support-knowledge-trust-boundaries-48aa</guid>
      <description>&lt;p&gt;Short answer: For an internal customer-support knowledge bot, search or list approved sources, deduplicate canonical URLs, fetch each page, extract readable text, chunk and embed it, then upsert chunks with the source URL. This scrape plus embed pipeline has five independently failing stages. Choose the processors' region, retention, and deletion boundaries before optimizing index cost at scale; a cheap index does not erase copies held elsewhere.&lt;/p&gt;

&lt;h2&gt;
  
  
  What decision are we recording?
&lt;/h2&gt;

&lt;p&gt;Every indexed chunk must retain its canonical source URL and a stable chunk identifier. A bot needs to cite the article behind an answer, and an operator needs to locate the chunks belonging to an article withdrawn from the knowledge base. The URL alone does not prove erasure: fetched copies, embedding requests, index snapshots, and backups may have different retention and deletion rules. Check each processor's region, retention, deletion, and contractual terms before sending it internal text.&lt;/p&gt;

&lt;p&gt;Deletion is a cross-system operation.&lt;/p&gt;

&lt;p&gt;Deduplicate by canonical URL before fetching, not after indexing. Otherwise two search results pointing to one article spend fetch and embedding work twice, and a later update can leave competing versions. Count discovered URLs, unique URLs, successful fetches, readable documents, embedded chunks, and upserted chunks separately. Consider 400 discovered links, 310 unique URLs, and 285 readable pages: the 25-page gap between unique and readable sources points to fetch or extraction, not vector search; those numbers illustrate accounting, not measured performance.&lt;/p&gt;

&lt;p&gt;For a support team already using multiple backend services, I recommend trying Infrai for discovery, scraping, and vector indexing when its processor boundaries satisfy the team's requirements. Infrai provides one key and one bill for backend services: one credential across capabilities means fewer separate dashboards for keys and fewer invoices to reconcile.&lt;/p&gt;

&lt;p&gt;Infrai's self-describing API offers a separate integration advantage: public discovery needs no key and supplies request schemas and runnable examples, allowing an ingestion worker to inspect scrape and vector request contracts before transmitting support content. Every documented capability ships runnable examples in 10 languages. Its single REST API works over plain HTTP without an SDK, so a worker can connect the scrape and vector stages from its existing runtime rather than integrating separate client libraries. The breadth is concrete: 295 routes across 20 modules under the same key, including web and vector capabilities. These benefits do not establish residency, retention, or deletion guarantees for an underlying processor. Verify those separately.&lt;/p&gt;

&lt;h2&gt;
  
  
  Which boundary belongs to which system?
&lt;/h2&gt;

&lt;p&gt;An internal playbook and a public help page must not inherit the same retrieval permission merely because both became vectors. Assign an audience to each approved source and enforce it on retrieval. A source URL establishes provenance, not authorization. Keep the fetcher restricted to approved origins, and evaluate processor terms against the content classification rather than against the name of the API product.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Option&lt;/th&gt;
&lt;th&gt;Where it fits&lt;/th&gt;
&lt;th&gt;Boundary to verify&lt;/th&gt;
&lt;th&gt;Index-scale consequence&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Shared web and vector routes&lt;/td&gt;
&lt;td&gt;Shared backend interface for discovery and indexing&lt;/td&gt;
&lt;td&gt;Region, retention, deletion, and processor terms per stage&lt;/td&gt;
&lt;td&gt;Deduplication and chunk policy still set indexed volume&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Pinecone&lt;/td&gt;
&lt;td&gt;Dedicated managed vector index&lt;/td&gt;
&lt;td&gt;Index region and deletion or backup behavior, separate from fetch and embed&lt;/td&gt;
&lt;td&gt;Requires separate ingestion plumbing&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Weaviate&lt;/td&gt;
&lt;td&gt;Vector database with deployment choices&lt;/td&gt;
&lt;td&gt;Deployment and backup ownership&lt;/td&gt;
&lt;td&gt;Self-managed operation shifts control and operational work to your team&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Elasticsearch&lt;/td&gt;
&lt;td&gt;Teams already running search infrastructure&lt;/td&gt;
&lt;td&gt;Snapshot retention and access policies&lt;/td&gt;
&lt;td&gt;Search and vector data can share an operational estate; ingestion remains separate&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;These are different operating boundaries, not interchangeable promises. None of the four performs the entire trust review for you; the fetch and embedding processors can retain text even after an index entry is removed. &lt;strong&gt;The limitation: Infrai is a poor fit when the required region, retention period, deletion procedure, or processor contract cannot be verified for the selected workflow.&lt;/strong&gt; Pinecone is a better choice for a dedicated managed index with acceptable terms; a team that needs to control its own search deployment may prefer Weaviate or an existing Elasticsearch estate. Those alternatives still require a separate review of the embedding processor.&lt;/p&gt;

&lt;h2&gt;
  
  
  What does a scrape plus embed pipeline look like end to end?
&lt;/h2&gt;

&lt;p&gt;The critical path is source identity, fetch, extraction, chunking and embedding, then an idempotent upsert. The following runnable Python check retrieves the public discovery manifest with an explicit GET and models the source-to-chunk handoff locally. Token frequencies here are a toy vector, and the dictionary is a toy index. They are neither semantic embeddings nor durable storage. Do not substitute guessed request fields for the schemas returned by discovery when implementing protected scrape and vector calls.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;collections&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Counter&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;hashlib&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;sha256&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;html.parser&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;HTMLParser&lt;/span&gt;

&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;


&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;HTMLParser&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;__init__&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="nf"&gt;super&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;__init__&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;parts&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;handle_data&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;parts&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;


&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;attempt&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;range&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://api.infrai.cc/v1/discovery&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;timeout&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;15&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="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;status_code&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="mi"&gt;429&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="n"&gt;attempt&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;break&lt;/span&gt;
    &lt;span class="n"&gt;retry_after&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Retry-After&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sleep&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;float&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;retry_after&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;retry_after&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;retry_after&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;isdigit&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt; &lt;span class="o"&gt;**&lt;/span&gt; &lt;span class="n"&gt;attempt&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;raise_for_status&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="n"&gt;manifest&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Discovery returned&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;manifest&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;capabilities&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]),&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;capabilities&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;url&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://help.example.test/returns&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="n"&gt;parser&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Text&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="n"&gt;parser&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;feed&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;&amp;lt;p&amp;gt;Return requests require an order number.&amp;lt;/p&amp;gt;&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;words&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt; &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;parser&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;parts&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;split&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="n"&gt;index&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;
&lt;span class="n"&gt;counts&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Counter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;discovered&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;unique&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;fetched&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;extracted&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;offset&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;range&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;words&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;chunk&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt; &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;words&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;offset&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;offset&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
    &lt;span class="n"&gt;vector&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Counter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;chunk&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;lower&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;split&lt;/span&gt;&lt;span class="p"&gt;()))&lt;/span&gt;
    &lt;span class="n"&gt;counts&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;embedded&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
    &lt;span class="n"&gt;chunk_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;sha256&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;:&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;offset&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;encode&lt;/span&gt;&lt;span class="p"&gt;()).&lt;/span&gt;&lt;span class="nf"&gt;hexdigest&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;index&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;chunk_id&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;vector&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;vector&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;source_url&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;text&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;chunk&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="n"&gt;counts&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;upserted&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;counts&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The example intentionally does not transmit support content to a processor. For the actual worker, persist stage state and retry discovery, fetching, extraction, embedding, and indexing independently, with bounded backoff and a visible count for each stage. If an upsert succeeds but its acknowledgment is lost, retry with the same stable chunk ID and an idempotency key where supported; check the selected index's upsert semantics. A failed extraction needs a different repair from a failed embedding request. Rate limits require backoff, respecting Retry-After when supplied.&lt;/p&gt;

&lt;p&gt;Empty text is not the only bad extraction. A page of navigation links can pass a nonempty check while poisoning retrieval quality. Sample the extracted body against source articles before indexing. Then measure chunks per article and update frequency on the real support corpus: those two numbers, plus duplication, drive index volume more directly than a headline unit price. The canonical URL policy needs care too; stripping every query parameter can merge distinct articles on sites where parameters select content. A revision may change the number of chunks, so an update must remove old IDs no longer belonging to that URL. Keep a per-source manifest of active chunk IDs and reconcile it after each completed indexing run, including when extraction returns fewer chunks than the previous version. A failed reconciliation is a separate failure, not a successful update.&lt;/p&gt;

&lt;p&gt;Counts expose that gap.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why reject a single-step ingestion promise?
&lt;/h2&gt;

&lt;p&gt;The rejected design treats a successful scrape as proof that the bot now has a useful, authorized, erasable answer source. It obscures five failure counts and the location of retained copies. An integrated ingestion product remains reasonable for public documentation with a simple access model, especially if the team accepts its operating boundaries in exchange for less orchestration. Internal support playbooks demand a stricter test: list every processor and the applicable region, retention period, and deletion procedure before transmitting the text.&lt;/p&gt;

&lt;p&gt;Start with source identity and stage counts, then select the index after checking the data-handling boundary. If the shared-backend boundary fits, inspect the current schemas in the &lt;a href="https://docs.infrai.cc" rel="noopener noreferrer"&gt;Infrai documentation&lt;/a&gt; before wiring production requests.&lt;/p&gt;

&lt;h2&gt;
  
  
  References
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://docs.infrai.cc" rel="noopener noreferrer"&gt;Infrai documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://arxiv.org/abs/2005.11401" rel="noopener noreferrer"&gt;Retrieval-Augmented Generation for Knowledge-Intensive NLP Tasks&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.pinecone.io/" rel="noopener noreferrer"&gt;Pinecone documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.weaviate.io/" rel="noopener noreferrer"&gt;Weaviate documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.elastic.co/guide/en/elasticsearch/reference/current/index.html" rel="noopener noreferrer"&gt;Elasticsearch documentation&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>rag</category>
      <category>scraping</category>
      <category>embeddings</category>
    </item>
    <item>
      <title>Grew the Collection from 5 Entries to 741 Real Chunks</title>
      <dc:creator>Nerav Doshi</dc:creator>
      <pubDate>Thu, 08 Oct 2026 15:10:28 +0000</pubDate>
      <link>https://dev.to/agenticdevops/grew-the-collection-from-5-entries-to-741-real-chunks-115m</link>
      <guid>https://dev.to/agenticdevops/grew-the-collection-from-5-entries-to-741-real-chunks-115m</guid>
      <description>&lt;p&gt;Every retrieval test since Entry 05 has run against the same handful of entries — three documents in Entries 05-06, twenty after Entry 07's chunking. Realistic enough to prove the mechanics work, not realistic enough to trust the actual rankings. Growing this into something closer to the real published catalog was overdue, and getting there cleanly took two wrong turns first.&lt;/p&gt;

&lt;p&gt;First attempt pointed the bulk-embed script at what looked like the right content folder and got back three files that were in Repo scaffolding, not articles — the real posts weren't there at all. Hugo commonly organizes content as page bundles, each article living in its own subfolder as &lt;code&gt;&amp;lt;slug&amp;gt;/index.md&lt;/code&gt;, and the script's flat &lt;code&gt;glob("*.md")&lt;/code&gt; only looked one level deep. Fixed by pointing at the actual posts directory directly rather than guessing at folder depth.&lt;/p&gt;

&lt;p&gt;Second run picked up all 45 real files — the 15 "Today I Ran" entries plus 30 other published articles — and embedded 741 chunks. Looked like a clean win. It wasn't, quite: the first run's three junk files were still sitting in the collection, uncleaned, and worse, the 15 "Today I Ran" entries that were already in the store from Entries 05-06 got re-embedded under a &lt;em&gt;different&lt;/em&gt; ID scheme than before. The original pipeline stored single-chunk files under their plain filename (&lt;code&gt;02-oc-cli-mentor-system-prompt.md&lt;/code&gt;); this script's chunking always appends &lt;code&gt;_chunk0&lt;/code&gt;, &lt;code&gt;_chunk1&lt;/code&gt;, even for short files. Same content, two ID formats, both present at once — a self-inflicted version of the exact cross-document ranking bias Entry 07 found by accident with a real multi-chunk document.&lt;/p&gt;

&lt;p&gt;Rather than hand-picking which of ~766 entries to delete, wiped the whole collection and rebuilt from nothing:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;chromadb&lt;/span&gt;
&lt;span class="n"&gt;client&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;chromadb&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;PersistentClient&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;./chroma_db&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;delete_collection&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;today_i_ran_notes&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then re-ran the bulk script fresh. Clean this time:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Files found:      45
Files processed:  45
Files failed:     0
Chunks embedded:  741
Collection size:  0 -&amp;gt; 741
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No duplicates, no scaffolding files, consistent chunking and ID scheme across every document. From 5 test entries to 741 real chunks drawn from the actual catalog, in one clean pass.&lt;/p&gt;

&lt;p&gt;One thing to glag before next test: this collection now includes the "Today I Ran" series' own back catalog — Entries 01 through 15 — alongside everything else. The index can now retrieve passages from its own past entries about building the index. Querying this store about chunking bias will likely surface Entry 07, which is the entry that discovered chunking bias, embedded into the very store that bias affects. Recursive in a way that's more interesting than useful, but worth noting before the next retrieval test runs against this and something from the series' own history shows up as the top result.&lt;/p&gt;

&lt;p&gt;The real payoff of this entry isn't the number 741 — it's that every retrieval test from here forward is running against something that resembles the actual size and shape of a real knowledge base, instead of a handful of hand-picked documents chosen to make a specific point work. Entry 07's chunking-bias fix (the per-document result cap flagged back then, still not built) matters a lot more now than it did against 20 entries.&lt;/p&gt;

</description>
      <category>ollama</category>
      <category>rag</category>
      <category>embeddings</category>
      <category>chromadb</category>
    </item>
    <item>
      <title>Voyage AI vs Cohere Embed v4 vs Nemotron 3 Embed: Production RAG Trade-offs</title>
      <dc:creator>Jangwook Kim</dc:creator>
      <pubDate>Thu, 08 Oct 2026 00:39:22 +0000</pubDate>
      <link>https://dev.to/jangwook_kim_e31e7291ad98/voyage-ai-vs-cohere-embed-v4-vs-nemotron-3-embed-production-rag-trade-offs-2bpp</link>
      <guid>https://dev.to/jangwook_kim_e31e7291ad98/voyage-ai-vs-cohere-embed-v4-vs-nemotron-3-embed-production-rag-trade-offs-2bpp</guid>
      <description>&lt;h2&gt;
  
  
  Why We Evaluated These Embedding Models
&lt;/h2&gt;

&lt;p&gt;Embedding selection looks reversible until a production index contains tens of millions of vectors. At that point, changing the model can mean re-reading every source document, reproducing the original chunking pipeline, paying for another embedding pass, building a second index, and switching traffic without mixing incompatible vector spaces.&lt;/p&gt;

&lt;p&gt;That operational commitment—not a leaderboard position—was why we evaluated Voyage 4, Cohere Embed v4, and NVIDIA Nemotron 3 Embed.&lt;/p&gt;

&lt;p&gt;The three options represent materially different deployment choices:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Voyage 4 is a managed, text-focused family with selectable dimensions and quantized output formats.&lt;/li&gt;
&lt;li&gt;Cohere Embed v4 is a managed multimodal model for text, images, and mixed documents.&lt;/li&gt;
&lt;li&gt;Nemotron 3 Embed is positioned as an open-weight, self-hosted family where licensing cost is replaced by GPU and operations cost.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Under the hood, each model converts documents and queries into vectors that an index compares by cosine similarity, dot product, or an equivalent distance function. The implementation details differ, but the production contract is the same: document and query preprocessing must remain stable, dimensions must match, and we need continued access to a query encoder compatible with the stored vectors, rather than necessarily the exact model version originally used.&lt;/p&gt;

&lt;p&gt;We found an immediate problem with the public comparison narrative. The available benchmark figures were not generated on one common evaluation suite.&lt;/p&gt;

&lt;p&gt;The Nemotron 3 figures we examined included 78.46 on RTEB and 75.45 on MMTEB Retrieval for the 8B checkpoint. The 1B checkpoint was listed at 72.38 RTEB and 71.04 MMTEB Retrieval. Figures circulating for Voyage and Cohere were general MTEB aggregates, not directly comparable RTEB runs. We therefore refused to place those values into a single “winner” ranking.&lt;/p&gt;

&lt;p&gt;A 78.46 retrieval score and a roughly 67 general MTEB aggregate do not measure the same thing. MTEB can blend retrieval, clustering, classification, and semantic-similarity tasks. RTEB is retrieval-specific. Different task sets, relevance judgments, languages, pooling policies, and gain conventions can reverse an apparent lead.&lt;/p&gt;

&lt;p&gt;This matters because a model can perform well on public web retrieval and still miss exact clauses in contracts, versioned API behavior, product identifiers, or cross-language support content. Our deployment recommendation consequently depends on a customer’s labeled corpus rather than the headline score.&lt;/p&gt;

&lt;p&gt;We treat the model choice as an index architecture decision. Teams evaluating the surrounding stack can compare it with the vector and retrieval tools in our &lt;a href="https://dev.to/tools"&gt;AI tools collection&lt;/a&gt;, but they should freeze the embedding contract before optimizing the database.&lt;/p&gt;

&lt;h2&gt;
  
  
  Hands-On Walkthrough: Setup, Execution &amp;amp; Output
&lt;/h2&gt;

&lt;p&gt;We started by validating the metric harness rather than sending three sets of vectors into an untrusted scorer. That step caught a class of benchmark errors more damaging than a slow API: document-ID misalignment can turn perfect retrieval into a score of zero while every individual array still has the expected shape.&lt;/p&gt;

&lt;p&gt;We ran the tests in Python 3.12 with these pinned dependencies:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;python &lt;span class="nt"&gt;-m&lt;/span&gt; venv .venv
&lt;span class="nb"&gt;source&lt;/span&gt; .venv/bin/activate

python &lt;span class="nt"&gt;-m&lt;/span&gt; pip &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nv"&gt;numpy&lt;/span&gt;&lt;span class="o"&gt;==&lt;/span&gt;2.1.3 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nv"&gt;scipy&lt;/span&gt;&lt;span class="o"&gt;==&lt;/span&gt;1.14.1 &lt;span class="se"&gt;\&lt;/span&gt;
  scikit-learn&lt;span class="o"&gt;==&lt;/span&gt;1.5.2

python benchmark_metric_contract.py
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The following compact fixture reproduces the key cutoff and all-zero-relevance checks; the document-ID alignment results are reported separately below. It is runnable without an API key or GPU.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;numpy&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;np&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;sklearn.metrics&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;ndcg_score&lt;/span&gt;

&lt;span class="n"&gt;cutoff&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;
&lt;span class="n"&gt;document_count&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;12&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;score&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;relevance&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ranking_scores&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;y_true&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;asarray&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="n"&gt;relevance&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;dtype&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;float&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;y_score&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;asarray&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="n"&gt;ranking_scores&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;dtype&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;float&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;float&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;ndcg_score&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;y_true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;y_score&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;k&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;cutoff&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;

&lt;span class="c1"&gt;# Higher retrieval scores produce earlier ranks.
&lt;/span&gt;&lt;span class="n"&gt;descending_scores&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;list&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;range&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;document_count&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;one_relevant_at_rank&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;rank&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;relevance&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mf"&gt;0.0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;document_count&lt;/span&gt;
    &lt;span class="n"&gt;relevance&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;rank&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;1.0&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;score&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;relevance&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;descending_scores&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;results&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;single_relevant_at_rank_1&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;one_relevant_at_rank&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;single_relevant_at_rank_2&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;one_relevant_at_rank&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;single_relevant_at_rank_10&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;one_relevant_at_rank&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;single_relevant_at_rank_11&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;one_relevant_at_rank&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;11&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;all_zero_relevance_query&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;score&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mf"&gt;0.0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;document_count&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;descending_scores&lt;/span&gt;&lt;span class="p"&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;assert&lt;/span&gt; &lt;span class="n"&gt;results&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;single_relevant_at_rank_1&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mf"&gt;1.0&lt;/span&gt;
&lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="n"&gt;results&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;single_relevant_at_rank_10&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mf"&gt;0.0&lt;/span&gt;
&lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="n"&gt;results&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;single_relevant_at_rank_11&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mf"&gt;0.0&lt;/span&gt;
&lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="n"&gt;results&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;all_zero_relevance_query&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mf"&gt;0.0&lt;/span&gt;

&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;dumps&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;results&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;indent&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Our executed fixture produced:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"single_relevant_at_rank_1"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;1.0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"single_relevant_at_rank_2"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;0.6309297535714574&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"single_relevant_at_rank_10"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;0.2890648263178878&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"single_relevant_at_rank_11"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;0.0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"all_zero_relevance_query"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;0.0&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We also tested graded relevance. Swapping two graded results produced an nDCG@10 of &lt;code&gt;0.9224945116765988&lt;/code&gt; with linear gains and &lt;code&gt;0.842828264880938&lt;/code&gt; when we supplied exponential gains. That absolute difference of &lt;code&gt;0.07966624679566081&lt;/code&gt; is large enough to manufacture a benchmark “win” if two runs use different gain conventions.&lt;/p&gt;

&lt;p&gt;The alignment fixture exposed an even larger scoring error:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;aligned_score=1.0
joint_column_permutation_score=1.0
judgments_only_permutation_score=0.0
all_assertions_passed=true
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Permuting document IDs in both predictions and judgments preserved the score. Permuting only the relevance judgments reduced it from 1.0 to 0.0. We now require one canonical document-ID map across chunk storage, relevance judgments, cached vectors, and result files.&lt;/p&gt;

&lt;p&gt;This was a measurement-contract test, not a model-quality run. We did not produce vendor embeddings in this executed sandbox, so we do not present invented nDCG, recall, latency, throughput, or multilingual results for Voyage, Cohere, or Nemotron.&lt;/p&gt;

&lt;p&gt;For a real head-to-head, our required run manifest is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"corpus"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"chunks"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2000-5000"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"chunker_version"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"frozen"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"content_hashes"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"required"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"queries"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"relevance_judgments"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"graded"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"zero_relevance_policy"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"declared"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"languages"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"stratified"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"retrieval"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"metric"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"nDCG@10"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Recall@5"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Recall@10"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Recall@50"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"similarity"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"cosine"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"normalization"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"explicit"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"reranker"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"disabled"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"execution"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"model_version"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"pinned"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"dimension"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"recorded"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"input_type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"recorded"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"truncation"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"disabled-or-counted"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"batch_and_realtime"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"separate"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Voyage’s &lt;a href="https://docs.voyageai.com/docs/embeddings" rel="noopener noreferrer"&gt;embedding reference&lt;/a&gt; and &lt;a href="https://github.com/voyage-ai/voyageai-python" rel="noopener noreferrer"&gt;official Python client&lt;/a&gt; expose the necessary model, input type, output dimension, truncation, and output data type controls. Teams should pin Cohere’s equivalent configuration using its &lt;a href="https://docs.cohere.com/docs/embed" rel="noopener noreferrer"&gt;Embed reference&lt;/a&gt;, rather than infer it from a framework wrapper.&lt;/p&gt;

&lt;h2&gt;
  
  
  Benchmark Limitations and Production Risks
&lt;/h2&gt;

&lt;p&gt;The first failure was not an API error. It was evidence comparability. We could not reproduce a valid three-model leaderboard from the supplied material because no common fixed-corpus run existed. Any article naming an unconditional winner from these inputs would be overstating the evidence.&lt;/p&gt;

&lt;p&gt;We hit six additional production hazards.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Dimensions are a schema, not a tuning flag.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Voyage 4 supports 256, 512, 1,024, and 2,048 dimensions, with 1,024 as the default. Cohere Embed v4 configurations include 256, 512, 1,024, and 1,536 dimensions. The Nemotron 3 material we reviewed described 2,048 dimensions for the 1B model and 4,096 for 8B.&lt;/p&gt;

&lt;p&gt;A collection created at 1,024 dimensions cannot accept a 1,536- or 4,096-element vector. We plan a new collection, validation, and traffic cutover for a dimension change. Switching to an incompatible embedding space requires re-embedding; reducing compatible Matryoshka vectors may allow us to reuse existing embeddings after validating the transformation and retrieval quality. We store model, revision, dimension, normalization, data type, and preprocessing hash beside every index.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Voyage silently truncates by default.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Voyage’s &lt;code&gt;truncation&lt;/code&gt; option defaults to &lt;code&gt;True&lt;/code&gt;. An oversized input can therefore return a valid vector for incomplete content. That is worse than an explicit failure when the missing text contains the relevant clause.&lt;/p&gt;

&lt;p&gt;We set &lt;code&gt;truncation=False&lt;/code&gt; in ingestion jobs and route over-length chunks to a dead-letter queue. We would rather fail visibly than index partial documents silently.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Query and document modes are operationally significant.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Voyage prepends different retrieval prompts when &lt;code&gt;input_type&lt;/code&gt; is &lt;code&gt;query&lt;/code&gt; or &lt;code&gt;document&lt;/code&gt;. Cohere also distinguishes retrieval input types. We treat this field as part of the model version. A missing query mode is not harmless configuration drift.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. Request size is not the same as rate limit.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Voyage accepts at most 1,000 texts per request, with additional token ceilings. The documented total is up to one million tokens for &lt;code&gt;voyage-4-lite&lt;/code&gt;, 320,000 for &lt;code&gt;voyage-4&lt;/code&gt;, and 120,000 for &lt;code&gt;voyage-4-large&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Those are payload limits, not throughput guarantees. We could not verify per-key requests-per-minute, token-per-minute limits, cooldown behavior, or reserved-capacity terms across all three providers. Teams should load-test their own account tier rather than extrapolate from maximum batch size.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;5. Quantization support is uneven.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Voyage’s hosted 4-series can return float, int8, uint8, binary, and unsigned-binary output. The local &lt;code&gt;voyage-4-nano&lt;/code&gt; path does not support int8 or uint8 because its required calibration ranges are not shipped; those modes raise &lt;code&gt;NotImplementedError&lt;/code&gt;. Float and binary formats remain available locally.&lt;/p&gt;

&lt;p&gt;The local extra also requires Python 3.10 or newer and installs PyTorch plus Sentence Transformers. We kept API-only and local-inference environments separate to avoid turning a lightweight client image into a large ML runtime.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;6. Nemotron naming can produce the wrong comparison.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;We found material mixing Nemotron 3 Embed with the older &lt;code&gt;llama-nemotron-embed-1b-v2&lt;/code&gt;. The older model has an 8,192-token input limit and Matryoshka dimensions down from 2,048. Those specifications must not be copied onto the newer Nemotron 3 family described with a 32,768-token context.&lt;/p&gt;

&lt;p&gt;We pin exact model identifiers and artifact revisions. “Nemotron Embed” is not precise enough for an index manifest.&lt;/p&gt;

&lt;h2&gt;
  
  
  Scale, Latency &amp;amp; Cost vs. Alternatives
&lt;/h2&gt;

&lt;p&gt;We normalized the product configurations available during the review, but we did not measure service latency or GPU throughput. The latency column is therefore deliberately absent rather than filled with synthetic numbers.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Option&lt;/th&gt;
&lt;th&gt;Deployment&lt;/th&gt;
&lt;th&gt;Context limit&lt;/th&gt;
&lt;th&gt;Dimensions reviewed&lt;/th&gt;
&lt;th&gt;Multimodal&lt;/th&gt;
&lt;th&gt;Public price used for planning&lt;/th&gt;
&lt;th&gt;Main operational trade-off&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Voyage 4 Lite&lt;/td&gt;
&lt;td&gt;Managed API&lt;/td&gt;
&lt;td&gt;32,000&lt;/td&gt;
&lt;td&gt;256-2,048&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;$0.02 per 1M tokens&lt;/td&gt;
&lt;td&gt;Lowest listed API rate; quality must be tested by domain&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Voyage 4&lt;/td&gt;
&lt;td&gt;Managed API&lt;/td&gt;
&lt;td&gt;32,000&lt;/td&gt;
&lt;td&gt;256-2,048&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;$0.06 per 1M tokens&lt;/td&gt;
&lt;td&gt;Balanced managed text retrieval&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Voyage 4 Large&lt;/td&gt;
&lt;td&gt;Managed API&lt;/td&gt;
&lt;td&gt;32,000&lt;/td&gt;
&lt;td&gt;256-2,048&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;$0.12 per 1M tokens&lt;/td&gt;
&lt;td&gt;Higher-cost quality tier&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cohere Embed v4&lt;/td&gt;
&lt;td&gt;Managed API&lt;/td&gt;
&lt;td&gt;128,000&lt;/td&gt;
&lt;td&gt;256-1,536&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;$0.12 per 1M text tokens&lt;/td&gt;
&lt;td&gt;Best fit here for mixed text and image retrieval&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Nemotron 3 Embed 1B&lt;/td&gt;
&lt;td&gt;Self-hosted&lt;/td&gt;
&lt;td&gt;32,768&lt;/td&gt;
&lt;td&gt;2,048&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;No per-token license charge&lt;/td&gt;
&lt;td&gt;GPU serving, observability, and capacity become our responsibility&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Nemotron 3 Embed 8B&lt;/td&gt;
&lt;td&gt;Self-hosted&lt;/td&gt;
&lt;td&gt;32,768&lt;/td&gt;
&lt;td&gt;4,096&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;No per-token license charge&lt;/td&gt;
&lt;td&gt;Largest raw vector payload at the reviewed dimensions; serving capacity must be measured&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;At one billion text tokens, the simple API bill is approximately:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Voyage 4 Lite: $20&lt;/li&gt;
&lt;li&gt;Voyage 4: $60&lt;/li&gt;
&lt;li&gt;Voyage 4 Large: $120&lt;/li&gt;
&lt;li&gt;Cohere Embed v4: $120&lt;/li&gt;
&lt;li&gt;Nemotron 3: infrastructure-dependent&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These numbers expose an important reality: initial embedding API charges can be smaller than vector storage, indexing, replicas, backups, and migration engineering. At float32, one million raw vectors require approximately:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;256 dimensions: 0.95 GiB&lt;/li&gt;
&lt;li&gt;1,024 dimensions: 3.81 GiB&lt;/li&gt;
&lt;li&gt;1,536 dimensions: 5.72 GiB&lt;/li&gt;
&lt;li&gt;2,048 dimensions: 7.63 GiB&lt;/li&gt;
&lt;li&gt;4,096 dimensions: 15.26 GiB&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That excludes IDs, graph edges, metadata, allocator overhead, replicas, and snapshots. Moving from 1,024 to 4,096 dimensions quadruples the raw vector payload before the vector database adds its own structures.&lt;/p&gt;

&lt;p&gt;For Nemotron, we use a break-even equation rather than pretending open weights are free:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;self_hosted_cost_per_million = gpu_hourly_cost / millions_of_tokens_per_hour
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;At a hypothetical $2 per GPU-hour, self-hosting must sustain more than 16.67 million tokens per hour to beat a $0.12-per-million API on raw compute. It must exceed 33.33 million tokens per hour to beat a $0.06-per-million API. Engineering labor, idle capacity, failover, deployment, and monitoring move the real threshold higher.&lt;/p&gt;

&lt;p&gt;We did not measure Nemotron throughput, memory use, or NVFP4 quality loss, so those break-even examples are formulas, not performance claims.&lt;/p&gt;

&lt;p&gt;For teams planning a migration or capacity model, our &lt;a href="https://dev.to/services"&gt;AI infrastructure services&lt;/a&gt; focus on the complete cost boundary: embedding generation, index memory, replicas, refresh cadence, and rollback—not just the token invoice.&lt;/p&gt;

&lt;h2&gt;
  
  
  Our Final Verdict: When to Deploy, When to Skip
&lt;/h2&gt;

&lt;p&gt;We cannot honestly name a universal retrieval-quality winner from the available evidence. Nemotron 3 has the strongest retrieval-specific public numbers in this comparison, but those numbers were not generated in the same run as Voyage 4 and Cohere Embed v4. Our executed work validated the metric harness, not the three models.&lt;/p&gt;

&lt;p&gt;That distinction defines our verdict.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Deploy Voyage 4 if:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;We need managed text embeddings with low list pricing.&lt;/li&gt;
&lt;li&gt;We want selectable dimensions and native quantized output.&lt;/li&gt;
&lt;li&gt;A 32,000-token context covers the chunking strategy.&lt;/li&gt;
&lt;li&gt;We can pin &lt;code&gt;input_type&lt;/code&gt;, reject truncation, and keep a model manifest.&lt;/li&gt;
&lt;li&gt;We have domain labels proving that Lite, standard, or Large meets recall targets.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Deploy Cohere Embed v4 if:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;We need text, images, and mixed documents in one retrieval space.&lt;/li&gt;
&lt;li&gt;A 128,000-token input window materially simplifies document handling.&lt;/li&gt;
&lt;li&gt;We value a managed endpoint more than control over model weights.&lt;/li&gt;
&lt;li&gt;We can absorb the API price and validate multilingual performance on our own language mix.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Deploy Nemotron 3 Embed if:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;We already operate NVIDIA inference infrastructure.&lt;/li&gt;
&lt;li&gt;Data residency or service independence makes self-hosting valuable.&lt;/li&gt;
&lt;li&gt;Query volume is high and steady enough to amortize GPUs.&lt;/li&gt;
&lt;li&gt;We can benchmark the exact checkpoint, precision, and dimension used in production.&lt;/li&gt;
&lt;li&gt;We accept responsibility for capacity, upgrades, observability, and rollback.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Hold off or avoid all three as a final choice if:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;We do not have labeled queries from the target domain.&lt;/li&gt;
&lt;li&gt;We are comparing RTEB and general MTEB scores as if they were interchangeable.&lt;/li&gt;
&lt;li&gt;We have not calculated index memory at the chosen dimension and replica count.&lt;/li&gt;
&lt;li&gt;The ingestion pipeline allows silent truncation.&lt;/li&gt;
&lt;li&gt;Model names, preprocessing prefixes, or revisions are not pinned.&lt;/li&gt;
&lt;li&gt;We are inferring multilingual quality from an aggregate score.&lt;/li&gt;
&lt;li&gt;We have not tested rate-limit and batch assumptions against the production account.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Our practical default is Voyage 4 at 1,024 dimensions for managed, text-only evaluation; Cohere Embed v4 when multimodal retrieval is a real requirement; and Nemotron 3 only when self-hosting is an architectural objective rather than a reaction to per-token pricing.&lt;/p&gt;

&lt;p&gt;We would not approve production deployment until all candidates had run over the same 2,000-to-5,000-chunk corpus, the same graded queries, the same distance metric, and the same nDCG and recall implementation. The winning model is the one that clears the application’s recall threshold at the lowest total operating cost—not the one with the largest vector or the most flattering public benchmark.&lt;/p&gt;

&lt;p&gt;If the index migration risk is already material, &lt;a href="https://dev.to/contact"&gt;contact our engineering team&lt;/a&gt; before changing dimensions or model families. Rebuilding a benchmark is cheap. Rebuilding a live retrieval index under customer traffic is not.&lt;/p&gt;

</description>
      <category>embeddings</category>
      <category>rag</category>
      <category>vectorsearch</category>
      <category>aiinfrastructure</category>
    </item>
    <item>
      <title>Vector Search &amp; Embeddings in Java: Building Semantic Search Engines</title>
      <dc:creator>Said Olano</dc:creator>
      <pubDate>Fri, 02 Oct 2026 01:41:21 +0000</pubDate>
      <link>https://dev.to/said_olano/vector-search-embeddings-in-java-building-semantic-search-engines-279c</link>
      <guid>https://dev.to/said_olano/vector-search-embeddings-in-java-building-semantic-search-engines-279c</guid>
      <description>&lt;h1&gt;
  
  
  Vector Search &amp;amp; Embeddings in Java: Building Semantic Search Engines
&lt;/h1&gt;

&lt;h2&gt;
  
  
  Introduction
&lt;/h2&gt;

&lt;p&gt;Traditional keyword-based search is dead. When users search for "best restaurants near me," they don't expect results matching those exact words—they expect restaurants that &lt;em&gt;mean&lt;/em&gt; the right thing.&lt;/p&gt;

&lt;p&gt;This is where &lt;strong&gt;vector search and embeddings&lt;/strong&gt; enter the picture.&lt;/p&gt;

&lt;p&gt;Vector search is the technology powering modern AI applications: ChatGPT's retrieval-augmented generation (RAG), Netflix's recommendation engine, Spotify's "Discover Weekly," and enterprise semantic search platforms. Yet many Java developers still think of search as Elasticsearch queries with exact terms.&lt;/p&gt;

&lt;p&gt;This gap is costing you:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Missed user intent&lt;/strong&gt;: Returning exact matches instead of semantic matches&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Irrelevant results&lt;/strong&gt;: Users abandon apps when search doesn't understand context&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Lost competitive advantage&lt;/strong&gt;: Your competitors' AI-powered features work better&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In this guide, you'll learn:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;What embeddings and vector search actually are&lt;/strong&gt; (the math isn't complex, the concepts are elegant)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;How to implement semantic search in Java&lt;/strong&gt; using production libraries&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Integration patterns&lt;/strong&gt; with PostgreSQL (pgvector), Elasticsearch, and Pinecone&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Real-world use cases&lt;/strong&gt; from e-commerce to support chatbots&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Performance tuning&lt;/strong&gt; for millions of vectors&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;By the end, you'll understand why vector search is essential for modern applications and how to build it with Java.&lt;/p&gt;




&lt;h2&gt;
  
  
  Part 1: Understanding Embeddings and Vector Space
&lt;/h2&gt;

&lt;h3&gt;
  
  
  What Is an Embedding?
&lt;/h3&gt;

&lt;p&gt;An &lt;strong&gt;embedding&lt;/strong&gt; is a numerical representation of text, images, or other data. Instead of storing "restaurant recommendations," you store a vector of numbers: &lt;code&gt;[0.25, -0.15, 0.82, ..., 0.41]&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;These aren't random numbers. They're learned through &lt;strong&gt;neural networks&lt;/strong&gt; trained on massive datasets. Similar concepts produce similar vectors. This property is the entire foundation of vector search.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Example:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Query: "best pizza place"&lt;/li&gt;
&lt;li&gt;Vector: &lt;code&gt;[0.12, 0.88, -0.31, 0.45, ...]&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Document: "authentic Italian pizzeria"&lt;/li&gt;
&lt;li&gt;Vector: &lt;code&gt;[0.14, 0.87, -0.29, 0.46, ...]&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These vectors are &lt;em&gt;close&lt;/em&gt; in vector space. Measuring that distance (using cosine similarity, Euclidean distance, etc.) gives you a relevance score.&lt;/p&gt;

&lt;h3&gt;
  
  
  How Modern Embedding Models Work
&lt;/h3&gt;

&lt;p&gt;In production, you don't hand-craft embeddings. You use pre-trained embedding models:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;OpenAI's text-embedding-ada-002&lt;/strong&gt;: 1,536 dimensions, state-of-the-art for general text&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Sentence Transformers (open-source)&lt;/strong&gt;: 384-768 dimensions, fast inference, great for local deployments&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cohere's embedding API&lt;/strong&gt;: 4,096 dimensions, excellent domain-specific options&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Google's Gecko Embedding&lt;/strong&gt;: Lightweight, optimized for cost&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These models map text → vector in a way that preserves semantic meaning.&lt;/p&gt;

&lt;h3&gt;
  
  
  Vector Space Fundamentals
&lt;/h3&gt;

&lt;p&gt;All vectors live in an &lt;strong&gt;N-dimensional space&lt;/strong&gt;. When you have 1,536-dimensional vectors (from OpenAI), you're working in 1,536-dimensional space.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Similarity metrics:&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Cosine Similarity&lt;/strong&gt; (most common)&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Measures angle between vectors&lt;/li&gt;
&lt;li&gt;Range: -1 to 1 (1 = identical direction)&lt;/li&gt;
&lt;li&gt;Formula: &lt;code&gt;(A · B) / (||A|| × ||B||)&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Euclidean Distance&lt;/strong&gt; (L2)&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Measures straight-line distance&lt;/li&gt;
&lt;li&gt;Smaller = more similar&lt;/li&gt;
&lt;li&gt;Formula: &lt;code&gt;√(Σ(ai - bi)²)&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Manhattan Distance&lt;/strong&gt; (L1)&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Sum of absolute differences&lt;/li&gt;
&lt;li&gt;Faster but less intuitive than Euclidean&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;For text search, &lt;strong&gt;cosine similarity&lt;/strong&gt; is almost always the right choice.&lt;/p&gt;




&lt;h2&gt;
  
  
  Part 2: Building Semantic Search in Java
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Architecture Overview
&lt;/h3&gt;

&lt;p&gt;A semantic search system has these components:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;┌─────────────────────────────────────────────┐
│  User Query                                  │
└─────────────┬───────────────────────────────┘
              │
┌─────────────▼───────────────────────────────┐
│  1. Embedding Model (Convert text → vector) │
│     (OpenAI API / Local Sentence Transformer)│
└─────────────┬───────────────────────────────┘
              │
┌─────────────▼───────────────────────────────┐
│  2. Vector Search Engine                     │
│     (Pinecone / PostgreSQL pgvector)        │
└─────────────┬───────────────────────────────┘
              │
┌─────────────▼───────────────────────────────┐
│  3. Similarity Ranking                       │
│     Return top-K most relevant results      │
└─────────────┬───────────────────────────────┘
              │
┌─────────────▼───────────────────────────────┐
│  Results with scores (0.0 - 1.0)            │
└─────────────────────────────────────────────┘
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Example 1: Using OpenAI Embeddings + Pinecone
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Step 1: Add Dependencies&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight xml"&gt;&lt;code&gt;&lt;span class="c"&gt;&amp;lt;!-- pom.xml --&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;dependency&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;groupId&amp;gt;&lt;/span&gt;com.theokanning.openai-gpt3-java&lt;span class="nt"&gt;&amp;lt;/groupId&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;artifactId&amp;gt;&lt;/span&gt;api&lt;span class="nt"&gt;&amp;lt;/artifactId&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;version&amp;gt;&lt;/span&gt;0.18.1&lt;span class="nt"&gt;&amp;lt;/version&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/dependency&amp;gt;&lt;/span&gt;

&lt;span class="nt"&gt;&amp;lt;dependency&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;groupId&amp;gt;&lt;/span&gt;io.pinecone&lt;span class="nt"&gt;&amp;lt;/groupId&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;artifactId&amp;gt;&lt;/span&gt;pinecone-client&lt;span class="nt"&gt;&amp;lt;/artifactId&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;version&amp;gt;&lt;/span&gt;0.1.0&lt;span class="nt"&gt;&amp;lt;/version&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/dependency&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step 2: Create Embedding Service&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;com.theokanning.openai.embedding.Embedding&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;com.theokanning.openai.embedding.EmbeddingRequest&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;com.theokanning.openai.embedding.EmbeddingResult&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;com.theokanning.openai.service.OpenAiService&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;EmbeddingService&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="nc"&gt;OpenAiService&lt;/span&gt; &lt;span class="n"&gt;openAiService&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
    &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;modelId&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"text-embedding-ada-002"&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;

    &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="nf"&gt;EmbeddingService&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;apiKey&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;openAiService&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;OpenAiService&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;apiKey&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;

    &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="nc"&gt;List&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Double&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;embedText&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="nc"&gt;EmbeddingRequest&lt;/span&gt; &lt;span class="n"&gt;request&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;EmbeddingRequest&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;builder&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt;
            &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;model&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;modelId&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
            &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;input&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Collections&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;singletonList&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="o"&gt;))&lt;/span&gt;
            &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;build&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;

        &lt;span class="nc"&gt;EmbeddingResult&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;openAiService&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;createEmbeddings&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getData&lt;/span&gt;&lt;span class="o"&gt;().&lt;/span&gt;&lt;span class="na"&gt;get&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="o"&gt;).&lt;/span&gt;&lt;span class="na"&gt;getEmbedding&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;

    &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="nc"&gt;List&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;List&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Double&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;embedTexts&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;List&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;texts&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="nc"&gt;EmbeddingRequest&lt;/span&gt; &lt;span class="n"&gt;request&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;EmbeddingRequest&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;builder&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt;
            &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;model&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;modelId&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
            &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;input&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;texts&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
            &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;build&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;

        &lt;span class="nc"&gt;EmbeddingResult&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;openAiService&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;createEmbeddings&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getData&lt;/span&gt;&lt;span class="o"&gt;().&lt;/span&gt;&lt;span class="na"&gt;stream&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt;
            &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;sorted&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Comparator&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;comparingInt&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nl"&gt;Embedding:&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="n"&gt;getIndex&lt;/span&gt;&lt;span class="o"&gt;))&lt;/span&gt;
            &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;map&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nl"&gt;Embedding:&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="n"&gt;getEmbedding&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
            &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;collect&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Collectors&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;toList&lt;/span&gt;&lt;span class="o"&gt;());&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step 3: Pinecone Vector Search&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;io.pinecone.clients.Pinecone&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;io.pinecone.clients.Index&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;PineconeVectorStore&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="nc"&gt;Index&lt;/span&gt; &lt;span class="n"&gt;index&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
    &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="nc"&gt;EmbeddingService&lt;/span&gt; &lt;span class="n"&gt;embeddingService&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;

    &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="nf"&gt;PineconeVectorStore&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;apiKey&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;projectName&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; 
                               &lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;indexName&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;embeddingApiKey&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="nc"&gt;Pinecone&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Pinecone&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;Builder&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt;
            &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;withApiKey&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;apiKey&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
            &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;build&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;

        &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;index&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getIndex&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;projectName&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;indexName&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
        &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;embeddingService&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;EmbeddingService&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;embeddingApiKey&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;

    &lt;span class="c1"&gt;// Index documents with embeddings&lt;/span&gt;
    &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;indexDocument&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;docId&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; 
                             &lt;span class="nc"&gt;Map&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;metadata&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="nc"&gt;List&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Double&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;embedding&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;embeddingService&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;embedText&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;

        &lt;span class="n"&gt;index&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;upsert&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;docId&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;embedding&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;metadata&lt;/span&gt;
        &lt;span class="o"&gt;);&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;

    &lt;span class="c1"&gt;// Search: returns top K similar documents&lt;/span&gt;
    &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="nc"&gt;List&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;SearchResult&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;search&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;topK&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="nc"&gt;List&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Double&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;queryEmbedding&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;embeddingService&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;embedText&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;

        &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;results&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;index&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;query&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;queryEmbedding&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
            &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;withTopK&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;topK&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
            &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;withIncludeMetadata&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
            &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;execute&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;

        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;results&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getMatches&lt;/span&gt;&lt;span class="o"&gt;().&lt;/span&gt;&lt;span class="na"&gt;stream&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt;
            &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;map&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;match&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;SearchResult&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;
                &lt;span class="n"&gt;match&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getId&lt;/span&gt;&lt;span class="o"&gt;(),&lt;/span&gt;
                &lt;span class="n"&gt;match&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getScore&lt;/span&gt;&lt;span class="o"&gt;(),&lt;/span&gt;
                &lt;span class="n"&gt;match&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getMetadata&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt;
            &lt;span class="o"&gt;))&lt;/span&gt;
            &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;collect&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Collectors&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;toList&lt;/span&gt;&lt;span class="o"&gt;());&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;

&lt;span class="n"&gt;record&lt;/span&gt; &lt;span class="nf"&gt;SearchResult&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;Double&lt;/span&gt; &lt;span class="n"&gt;score&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;Map&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;metadata&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step 4: End-to-End Usage&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;SemanticSearchApp&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;static&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;main&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="o"&gt;[]&lt;/span&gt; &lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="nc"&gt;PineconeVectorStore&lt;/span&gt; &lt;span class="n"&gt;store&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;PineconeVectorStore&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;
            &lt;span class="nc"&gt;System&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getenv&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"PINECONE_API_KEY"&lt;/span&gt;&lt;span class="o"&gt;),&lt;/span&gt;
            &lt;span class="s"&gt;"my-project"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;
            &lt;span class="s"&gt;"restaurant-index"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;
            &lt;span class="nc"&gt;System&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getenv&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"OPENAI_API_KEY"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
        &lt;span class="o"&gt;);&lt;/span&gt;

        &lt;span class="c1"&gt;// Index documents&lt;/span&gt;
        &lt;span class="n"&gt;store&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;indexDocument&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"rest-1"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"Best pizza in town, authentic Italian, family-owned"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; 
            &lt;span class="nc"&gt;Map&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;of&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"name"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"Pasta Paradise"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"type"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"pizza"&lt;/span&gt;&lt;span class="o"&gt;));&lt;/span&gt;

        &lt;span class="n"&gt;store&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;indexDocument&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"rest-2"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"Fast casual ramen, Tokyo-style noodles, great broth"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;
            &lt;span class="nc"&gt;Map&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;of&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"name"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"Noodle House"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"type"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"ramen"&lt;/span&gt;&lt;span class="o"&gt;));&lt;/span&gt;

        &lt;span class="c1"&gt;// Search with semantic understanding&lt;/span&gt;
        &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;results&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;store&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;search&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"where to find good Italian food"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;

        &lt;span class="n"&gt;results&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;forEach&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; 
            &lt;span class="nc"&gt;System&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;out&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;printf&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"ID: %s, Score: %.4f, Name: %s%n"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;
                &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="o"&gt;(),&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;score&lt;/span&gt;&lt;span class="o"&gt;(),&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;metadata&lt;/span&gt;&lt;span class="o"&gt;().&lt;/span&gt;&lt;span class="na"&gt;get&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"name"&lt;/span&gt;&lt;span class="o"&gt;))&lt;/span&gt;
        &lt;span class="o"&gt;);&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Example 2: Local Vector Search with PostgreSQL + pgvector
&lt;/h3&gt;

&lt;p&gt;For privacy-sensitive applications, you might want embeddings to stay within your infrastructure.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 1: Setup PostgreSQL pgvector&lt;/strong&gt;&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="c1"&gt;-- Enable pgvector extension&lt;/span&gt;
&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="n"&gt;EXTENSION&lt;/span&gt; &lt;span class="n"&gt;IF&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;EXISTS&lt;/span&gt; &lt;span class="n"&gt;vector&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;-- Create table&lt;/span&gt;
&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;documents&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="nb"&gt;SERIAL&lt;/span&gt; &lt;span class="k"&gt;PRIMARY&lt;/span&gt; &lt;span class="k"&gt;KEY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;content&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;embedding&lt;/span&gt; &lt;span class="n"&gt;vector&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1536&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="n"&gt;metadata&lt;/span&gt; &lt;span class="n"&gt;JSONB&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;created_at&lt;/span&gt; &lt;span class="nb"&gt;TIMESTAMP&lt;/span&gt; &lt;span class="k"&gt;DEFAULT&lt;/span&gt; &lt;span class="n"&gt;NOW&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;-- Create HNSW index for fast similarity search&lt;/span&gt;
&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;INDEX&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;documents&lt;/span&gt; &lt;span class="k"&gt;USING&lt;/span&gt; &lt;span class="n"&gt;hnsw&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;embedding&lt;/span&gt; &lt;span class="n"&gt;vector_cosine_ops&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step 2: Java Implementation with Sentence Transformers&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;org.springframework.ai.document.Document&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;org.springframework.ai.embedding.Embedding&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;org.springframework.ai.embedding.EmbeddingModel&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;org.springframework.jdbc.core.JdbcTemplate&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;

&lt;span class="nd"&gt;@Component&lt;/span&gt;
&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;PostgresVectorStore&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="nc"&gt;JdbcTemplate&lt;/span&gt; &lt;span class="n"&gt;jdbcTemplate&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
    &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="nc"&gt;EmbeddingModel&lt;/span&gt; &lt;span class="n"&gt;embeddingModel&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;

    &lt;span class="nd"&gt;@Autowired&lt;/span&gt;
    &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="nf"&gt;PostgresVectorStore&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;JdbcTemplate&lt;/span&gt; &lt;span class="n"&gt;jdbcTemplate&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; 
                               &lt;span class="nc"&gt;EmbeddingModel&lt;/span&gt; &lt;span class="n"&gt;embeddingModel&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;jdbcTemplate&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;jdbcTemplate&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
        &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;embeddingModel&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;embeddingModel&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;

    &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;indexDocument&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;metadata&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="nc"&gt;Embedding&lt;/span&gt; &lt;span class="n"&gt;embedding&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;embeddingModel&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;embed&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;

        &lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;sql&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"INSERT INTO documents (content, embedding, metadata) "&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt;
                     &lt;span class="s"&gt;"VALUES (?, ?::vector, ?::jsonb)"&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;

        &lt;span class="n"&gt;jdbcTemplate&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;update&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sql&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;vectorToString&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;embedding&lt;/span&gt;&lt;span class="o"&gt;),&lt;/span&gt; &lt;span class="n"&gt;metadata&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;

    &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="nc"&gt;List&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;DocumentResult&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;similaritySearch&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;limit&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="nc"&gt;Embedding&lt;/span&gt; &lt;span class="n"&gt;queryEmbedding&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;embeddingModel&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;embed&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;

        &lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;sql&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"SELECT id, content, metadata, "&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt;
                     &lt;span class="s"&gt;"1 - (embedding &amp;lt;=&amp;gt; ?::vector) as similarity "&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt;
                     &lt;span class="s"&gt;"FROM documents "&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt;
                     &lt;span class="s"&gt;"ORDER BY embedding &amp;lt;=&amp;gt; ?::vector "&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt;
                     &lt;span class="s"&gt;"LIMIT ?"&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;

        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;jdbcTemplate&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;query&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sql&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Object&lt;/span&gt;&lt;span class="o"&gt;[]{&lt;/span&gt;
            &lt;span class="n"&gt;vectorToString&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;queryEmbedding&lt;/span&gt;&lt;span class="o"&gt;),&lt;/span&gt;
            &lt;span class="n"&gt;vectorToString&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;queryEmbedding&lt;/span&gt;&lt;span class="o"&gt;),&lt;/span&gt;
            &lt;span class="n"&gt;limit&lt;/span&gt;
        &lt;span class="o"&gt;},&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;rs&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;rowNum&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;DocumentResult&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;rs&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getInt&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"id"&lt;/span&gt;&lt;span class="o"&gt;),&lt;/span&gt;
            &lt;span class="n"&gt;rs&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getString&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"content"&lt;/span&gt;&lt;span class="o"&gt;),&lt;/span&gt;
            &lt;span class="n"&gt;rs&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getDouble&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"similarity"&lt;/span&gt;&lt;span class="o"&gt;),&lt;/span&gt;
            &lt;span class="n"&gt;rs&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getString&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"metadata"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
        &lt;span class="o"&gt;));&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;

    &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="nf"&gt;vectorToString&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Embedding&lt;/span&gt; &lt;span class="n"&gt;embedding&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="s"&gt;"["&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;embedding&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getOutput&lt;/span&gt;&lt;span class="o"&gt;().&lt;/span&gt;&lt;span class="na"&gt;stream&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt;
            &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;map&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nl"&gt;String:&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="n"&gt;valueOf&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
            &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;collect&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Collectors&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;joining&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;","&lt;/span&gt;&lt;span class="o"&gt;))&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="s"&gt;"]"&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;

&lt;span class="n"&gt;record&lt;/span&gt; &lt;span class="nf"&gt;DocumentResult&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;double&lt;/span&gt; &lt;span class="n"&gt;score&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;metadata&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Part 3: Real-World Use Cases
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. E-Commerce Product Search
&lt;/h3&gt;

&lt;p&gt;Traditional: Search for "comfortable shoes" → matches only products with those exact words&lt;/p&gt;

&lt;p&gt;Semantic: Matches "ergonomic footwear," "supportive sneakers," "cushioned athletic shoes"&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;ProductSearchService&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="nc"&gt;VectorStore&lt;/span&gt; &lt;span class="n"&gt;vectorStore&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;

    &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="nc"&gt;List&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Product&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;findSimilarProducts&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;vectorStore&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;search&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="o"&gt;).&lt;/span&gt;&lt;span class="na"&gt;stream&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt;
            &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;map&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;toProduct&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
            &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;collect&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Collectors&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;toList&lt;/span&gt;&lt;span class="o"&gt;());&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  2. Support Chatbot with RAG
&lt;/h3&gt;

&lt;p&gt;Retrieval-Augmented Generation: Embed your knowledge base, find relevant docs, feed them to LLM&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="nd"&gt;@RestController&lt;/span&gt;
&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;SupportChatbot&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="nc"&gt;VectorStore&lt;/span&gt; &lt;span class="n"&gt;knowledgeBase&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
    &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="nc"&gt;OpenAiService&lt;/span&gt; &lt;span class="n"&gt;openAiService&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;

    &lt;span class="nd"&gt;@PostMapping&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"/ask"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
    &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="nc"&gt;ResponseEntity&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;ask&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nd"&gt;@RequestBody&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;question&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="c1"&gt;// Step 1: Find relevant docs using vector search&lt;/span&gt;
        &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;relevantDocs&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;knowledgeBase&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;search&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;question&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;

        &lt;span class="c1"&gt;// Step 2: Build context from retrieved docs&lt;/span&gt;
        &lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;relevantDocs&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;stream&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt;
            &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;map&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nl"&gt;SearchResult:&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
            &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;collect&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Collectors&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;joining&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"\n\n"&lt;/span&gt;&lt;span class="o"&gt;));&lt;/span&gt;

        &lt;span class="c1"&gt;// Step 3: Ask LLM with context&lt;/span&gt;
        &lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;prompt&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;format&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;
            &lt;span class="s"&gt;"Based on this knowledge base:\n%s\n\nAnswer: %s"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;question&lt;/span&gt;
        &lt;span class="o"&gt;);&lt;/span&gt;

        &lt;span class="nc"&gt;ChatCompletionRequest&lt;/span&gt; &lt;span class="n"&gt;request&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;ChatCompletionRequest&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;builder&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt;
            &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;model&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"gpt-4"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
            &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;messages&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;List&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;of&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;ChatMessage&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;ChatMessageRole&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;USER&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="o"&gt;(),&lt;/span&gt; &lt;span class="n"&gt;prompt&lt;/span&gt;&lt;span class="o"&gt;)))&lt;/span&gt;
            &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;build&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;

        &lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;answer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;openAiService&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;createChatCompletion&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
            &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getChoices&lt;/span&gt;&lt;span class="o"&gt;().&lt;/span&gt;&lt;span class="na"&gt;get&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="o"&gt;).&lt;/span&gt;&lt;span class="na"&gt;getMessage&lt;/span&gt;&lt;span class="o"&gt;().&lt;/span&gt;&lt;span class="na"&gt;getContent&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;

        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nc"&gt;ResponseEntity&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;ok&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;answer&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  3. Duplicate Detection
&lt;/h3&gt;

&lt;p&gt;Find near-duplicate documents or suspicious fraud patterns&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;DuplicateDetector&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="nc"&gt;VectorStore&lt;/span&gt; &lt;span class="n"&gt;vectorStore&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;

    &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;boolean&lt;/span&gt; &lt;span class="nf"&gt;isProbablyDuplicate&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;document&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;double&lt;/span&gt; &lt;span class="n"&gt;threshold&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;similarDocs&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;vectorStore&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;search&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;document&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;

        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="n"&gt;similarDocs&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;isEmpty&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; 
               &lt;span class="n"&gt;similarDocs&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;get&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="o"&gt;).&lt;/span&gt;&lt;span class="na"&gt;score&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;threshold&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Part 4: Performance Optimization
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Batching Embeddings
&lt;/h3&gt;

&lt;p&gt;Don't embed one document at a time. Batch them.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;indexManyDocuments&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;List&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Document&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;documents&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// ❌ Slow: N API calls&lt;/span&gt;
    &lt;span class="c1"&gt;// documents.forEach(doc -&amp;gt; index(doc));&lt;/span&gt;

    &lt;span class="c1"&gt;// ✅ Fast: 1 API call per batch&lt;/span&gt;
    &lt;span class="nc"&gt;Iterables&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;partition&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;documents&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="o"&gt;).&lt;/span&gt;&lt;span class="na"&gt;forEach&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;batch&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="nc"&gt;List&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;texts&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;batch&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;stream&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt;
            &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;map&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nl"&gt;Document:&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="n"&gt;getContent&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
            &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;collect&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Collectors&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;toList&lt;/span&gt;&lt;span class="o"&gt;());&lt;/span&gt;

        &lt;span class="nc"&gt;List&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;List&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Double&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;embeddings&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;embeddingService&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;embedTexts&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;texts&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;

        &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;batch&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;size&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="o"&gt;++)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;vectorStore&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;index&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;batch&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;get&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="o"&gt;).&lt;/span&gt;&lt;span class="na"&gt;getId&lt;/span&gt;&lt;span class="o"&gt;(),&lt;/span&gt; &lt;span class="n"&gt;embeddings&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;get&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="o"&gt;));&lt;/span&gt;
        &lt;span class="o"&gt;}&lt;/span&gt;
    &lt;span class="o"&gt;});&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Caching Embeddings
&lt;/h3&gt;

&lt;p&gt;Store computed embeddings to avoid redundant API calls&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="nd"&gt;@Component&lt;/span&gt;
&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;CachedEmbeddingService&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="nc"&gt;EmbeddingService&lt;/span&gt; &lt;span class="n"&gt;service&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
    &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="nc"&gt;Map&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;List&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Double&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;cache&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;ConcurrentHashMap&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&amp;gt;();&lt;/span&gt;

    &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="nc"&gt;List&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Double&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;embed&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;cache&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;computeIfAbsent&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;key&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;service&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;embedText&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="o"&gt;));&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Dimension Reduction
&lt;/h3&gt;

&lt;p&gt;Not all 1,536 dimensions are necessary for your use case. PCA can reduce them:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="nc"&gt;List&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Double&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;reduceDimensions&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;List&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Double&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;embedding&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;targetDim&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// Use Apache Commons Math or similar&lt;/span&gt;
    &lt;span class="c1"&gt;// This trades accuracy for speed/storage&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="no"&gt;PCA&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;reduce&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;embedding&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;targetDim&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Part 5: Production Considerations
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. Latency Budget
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Embedding API call: 50-200ms&lt;/li&gt;
&lt;li&gt;Vector search: 10-50ms (with proper indexing)&lt;/li&gt;
&lt;li&gt;Total for user query: &amp;lt;500ms&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If this is too slow, cache frequent queries.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Cost Management
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;OpenAI embeddings: ~$0.02 per million tokens&lt;/li&gt;
&lt;li&gt;Pinecone: ~$0.70 per 1M vectors/month&lt;/li&gt;
&lt;li&gt;Self-hosted (pgvector): Your infrastructure cost&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For high-volume applications, self-hosting saves money.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Handling Updates
&lt;/h3&gt;

&lt;p&gt;When document content changes, re-embed and update:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;updateDocument&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;docId&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;newContent&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="nc"&gt;List&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Double&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;newEmbedding&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;embeddingService&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;embedText&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;newContent&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
    &lt;span class="n"&gt;vectorStore&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;update&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;docId&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;newEmbedding&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;newContent&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  4. Monitoring
&lt;/h3&gt;

&lt;p&gt;Track embedding quality:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="nd"&gt;@Component&lt;/span&gt;
&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;EmbeddingQualityMonitor&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="nc"&gt;MeterRegistry&lt;/span&gt; &lt;span class="n"&gt;meterRegistry&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;

    &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;recordSimilarityScore&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;double&lt;/span&gt; &lt;span class="n"&gt;score&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="nc"&gt;Timer&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;builder&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"search.similarity.score"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
            &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;publishPercentiles&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;0.5&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;0.95&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;0.99&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
            &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;register&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;meterRegistry&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
            &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;record&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Duration&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;ofMillis&lt;/span&gt;&lt;span class="o"&gt;((&lt;/span&gt;&lt;span class="kt"&gt;long&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;score&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;1000&lt;/span&gt;&lt;span class="o"&gt;)));&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;Vector search and embeddings are no longer bleeding-edge. They're essential for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Better search relevance&lt;/strong&gt; (understand user intent, not just keywords)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Semantic recommendations&lt;/strong&gt; (similarity-based, not just collaborative filtering)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;RAG-powered chatbots&lt;/strong&gt; (ground LLMs in your data)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Anomaly detection&lt;/strong&gt; (spot unusual patterns in vector space)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Key Takeaways:&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Embeddings are numerical representations where similar concepts = close vectors&lt;/li&gt;
&lt;li&gt;Use cosine similarity to measure relevance&lt;/li&gt;
&lt;li&gt;Choose between managed (Pinecone, Weaviate) or self-hosted (PostgreSQL + pgvector)&lt;/li&gt;
&lt;li&gt;Batch API calls, cache embeddings, monitor quality&lt;/li&gt;
&lt;li&gt;Start with OpenAI embeddings; optimize to self-hosted later if cost-prohibitive&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Your next semantic search implementation is just these components away. Build it today.&lt;/p&gt;




&lt;h2&gt;
  
  
  Further Reading
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Pinecone Docs: &lt;a href="https://docs.pinecone.io/" rel="noopener noreferrer"&gt;https://docs.pinecone.io/&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Sentence Transformers: &lt;a href="https://www.sbert.net/" rel="noopener noreferrer"&gt;https://www.sbert.net/&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;pgvector Extension: &lt;a href="https://github.com/pgvector/pgvector" rel="noopener noreferrer"&gt;https://github.com/pgvector/pgvector&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;RAG Best Practices: &lt;a href="https://docs.anthropic.com/en/api/guides/vision" rel="noopener noreferrer"&gt;https://docs.anthropic.com/en/api/guides/vision&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>java</category>
      <category>vectorsearch</category>
      <category>ai</category>
      <category>embeddings</category>
    </item>
    <item>
      <title>Beyond Generative AI: Why Streaming LLMs Fail Structured Voice, and JEV Wins</title>
      <dc:creator>Felona Voice</dc:creator>
      <pubDate>Thu, 01 Oct 2026 18:42:13 +0000</pubDate>
      <link>https://dev.to/feleona_voice/beyond-generative-ai-why-streaming-llms-fail-structured-voice-and-jev-wins-4h1</link>
      <guid>https://dev.to/feleona_voice/beyond-generative-ai-why-streaming-llms-fail-structured-voice-and-jev-wins-4h1</guid>
      <description>&lt;p&gt;The promise of conversational AI has always been grand: natural, fluid interactions that feel human. Yet, for all the advancements in large language models (LLMs), a glaring gap persists in real-time, structured voice AI. We've been trying to fit a square peg in a round hole, using auto-regressive, token-generating LLMs for tasks that demand precision, speed, and determinism. It's time to acknowledge the fundamental mismatch and embrace a new paradigm: &lt;strong&gt;Felona Voice&lt;/strong&gt;, powered by Joint Embedding Vectors (JEV) and stateful conversational transition graphs (VoiceGraph).&lt;/p&gt;

&lt;h2&gt;
  
  
  The LLM Paradigm: A Costly Latency Trap for Structured Voice
&lt;/h2&gt;

&lt;p&gt;Traditional voice agents, often built around a loop of ASR -&amp;gt; LLM -&amp;gt; TTS, face inherent limitations when applied to structured conversational flows like booking a table, checking an account balance, or routing a customer service call. Here's why the streaming LLM approach fundamentally struggles:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Crippling Latency&lt;/strong&gt;: LLMs are auto-regressive, meaning they generate responses token by token. This process involves: &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  &lt;strong&gt;Time To First Token (TTFT)&lt;/strong&gt;: The initial delay before any output begins (often 200-500ms).&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Token Generation Time&lt;/strong&gt;: Subsequent tokens are generated sequentially. 
For a typical conversational turn, this translates to an &lt;strong&gt;850ms to 1,800ms+&lt;/strong&gt; delay for the LLM to process input and formulate a response (even if it's just a decision for the next action). This latency breaks natural human conversation, making barge-in difficult and creating awkward silences.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Astronomical Costs at Scale&lt;/strong&gt;: Every interaction with an LLM, no matter how simple the intent resolution, consumes tokens. At scale, this becomes a significant infrastructure cost. Imagine a contact center handling tens of thousands or hundreds of thousands of calls a month, each with multiple conversational turns. Every turn means paying for input and output tokens, even for simple 'yes/no' or intent confirmation. These costs quickly escalate into tens of thousands of dollars monthly for intent resolution alone, let alone full generative responses.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Hallucinations and Lack of Determinism&lt;/strong&gt;: LLMs are probabilistic by nature. While fantastic for creative generation, this probabilistic approach leads to 'hallucinations' – generating plausible but incorrect or off-script responses. For enterprise applications in banking, healthcare, or critical customer support, non-deterministic behavior and the risk of generating inaccurate information are simply unacceptable. Compliance and user trust demand predictable outcomes.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Heavy Network Dependency&lt;/strong&gt;: Relying on external cloud LLM APIs means constant network round-trips for every decision. This adds to latency and introduces points of failure, making local testing and development cumbersome without API keys.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  The Felona Voice Revolution: Precision with Joint Embedding Vectors (JEV)
&lt;/h2&gt;

&lt;p&gt;Felona Voice (&lt;a href="https://github.com/mohitjoer/felona_voice" rel="noopener noreferrer"&gt;https://github.com/mohitjoer/felona_voice&lt;/a&gt;) introduces a paradigm shift by recognizing that &lt;em&gt;structured voice AI doesn't need generative reasoning for intent resolution&lt;/em&gt;. Instead, it leverages &lt;strong&gt;Joint Embedding Vectors (JEV)&lt;/strong&gt; and &lt;strong&gt;VoiceGraph&lt;/strong&gt; to deliver unparalleled performance:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Joint Embedding Vectors (JEV)&lt;/strong&gt;: Instead of generating text, Felona Voice embeds both the user's spoken input and the predefined actions/intents of your agent into a shared vector space. When a user speaks, their utterance is embedded, and a lightning-fast similarity search determines the closest matching action. This completely bypasses the slow, token-by-token generation process of LLMs for intent resolution.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;VoiceGraph: Stateful Conversational Transition Graphs&lt;/strong&gt;: Felona Voice uses a &lt;code&gt;VoiceGraph&lt;/code&gt; – a deterministic, stateful graph that defines the conversational flow. JEV similarity matching then guides the agent through this graph, ensuring predictable and precise transitions. This eliminates hallucinations entirely because the agent's actions are explicitly defined and matched, not probabilistically generated.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  The Result: Unprecedented Speed and Efficiency
&lt;/h3&gt;

&lt;p&gt;By leveraging JEV for intent resolution, Felona Voice achieves:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  &lt;strong&gt;Ultra-low Latency&lt;/strong&gt;: Intent decisions are made in &lt;strong&gt;sub-10ms (~5ms)&lt;/strong&gt;. This is an order of magnitude faster than LLMs, enabling instant barge-in and truly natural, real-time conversational turns.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Zero Cost for Intent Resolution&lt;/strong&gt;: JEV matching is an in-memory operation. There are no tokens to generate, no external API calls for intent. This means &lt;strong&gt;$0.00 per conversational turn&lt;/strong&gt; for intent resolution.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Zero Hallucinations&lt;/strong&gt;: With a deterministic &lt;code&gt;VoiceGraph&lt;/code&gt; guided by precise JEV matching, your agent will always follow defined paths, ensuring compliance and reliability.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Local-First Development&lt;/strong&gt;: Test and develop your voice agents locally without needing any external API keys for deterministic routing.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Felona Voice in Action: TypeScript Simplicity
&lt;/h2&gt;

&lt;p&gt;Felona Voice is built for developers, offering a fluent TypeScript API that makes building sophisticated voice agents intuitive. Let's see how simple it is to define an agent with precise actions:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&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;createAgent&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="s2"&gt;felona-voice&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kr"&gt;interface&lt;/span&gt; &lt;span class="nx"&gt;MyAgentContext&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;userName&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;tableSize&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="kr"&gt;number&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="nx"&gt;agent&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;createAgent&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;MyAgentContext&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;RestaurantConcierge&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;system&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;You are an intelligent voice concierge for 'The Golden Spoon' restaurant. Your primary goal is to book tables and answer FAQs.&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;action&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;book_table&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Book a restaurant reservation. I want to reserve a table. Can I get a booking?&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;input&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="c1"&gt;// In a real app, you'd extract parameters from 'input' or prompt for them&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;ctx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;tableSize&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="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Certainly! For how many people would you like to book a table?&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;
      &lt;span class="c1"&gt;// Simulate booking logic&lt;/span&gt;
      &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`Booking a table for &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;tableSize&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; people.`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="s2"&gt;`Okay, I've booked a table for &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;tableSize&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; people. Is there anything else?`&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;action&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;check_hours&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;What are your operating hours? When are you open?&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;The Golden Spoon is open Monday to Saturday, from 5 PM to 11 PM.&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
  &lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;action&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;greet_user&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Hello. Hi there. Good morning. Good afternoon. Good evening.&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;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="nx"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;userName&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="s2"&gt;`Welcome back, &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;userName&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;! How can I assist you today?`&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="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Hello! Welcome to The Golden Spoon. How may I help you?&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fallback&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;I'm sorry, I didn't quite catch that. How can I assist you today?&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// Example interaction&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;runConversation&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;currentContext&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;MyAgentContext&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{};&lt;/span&gt;

  &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;reply&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;agent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;interact&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Hi&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;currentContext&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`Agent: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;reply&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;response&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="c1"&gt;// Agent: Hello! Welcome to The Golden Spoon. How may I help you?&lt;/span&gt;

  &lt;span class="nx"&gt;currentContext&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="nx"&gt;currentContext&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="nx"&gt;reply&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;context&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;

  &lt;span class="nx"&gt;reply&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;agent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;interact&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;I'd like to book a table&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;currentContext&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`Agent: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;reply&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;response&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="c1"&gt;// Agent: Certainly! For how many people would you like to book a table?&lt;/span&gt;

  &lt;span class="nx"&gt;currentContext&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="nx"&gt;currentContext&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="nx"&gt;reply&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;context&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;

  &lt;span class="nx"&gt;reply&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;agent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;interact&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;For four people&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;currentContext&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nx"&gt;currentContext&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;tableSize&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// Manually update context for this example&lt;/span&gt;
  &lt;span class="nx"&gt;reply&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;agent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;interact&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;""&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;currentContext&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// Re-interact to trigger action with updated context&lt;/span&gt;
  &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`Agent: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;reply&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;response&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="c1"&gt;// Agent: Okay, I've booked a table for 4 people. Is there anything else?&lt;/span&gt;

  &lt;span class="nx"&gt;currentContext&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="nx"&gt;currentContext&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="nx"&gt;reply&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;context&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;

  &lt;span class="nx"&gt;reply&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;agent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;interact&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;What are your hours?&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;currentContext&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`Agent: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;reply&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;response&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="c1"&gt;// Agent: The Golden Spoon is open Monday to Saturday, from 5 PM to 11 PM.&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nf"&gt;runConversation&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Felona Voice also offers pluggable audio pipelines for WebSockets, WebRTC, Deepgram, Whisper, ElevenLabs, and Cartesia, ensuring flexibility for your production environment.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Stark Reality: Traditional Voice Agent (LLM Loop) vs. Felona Voice (JEV + VoiceGraph)
&lt;/h2&gt;

&lt;p&gt;Let's put the architectural differences into perspective:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Metric&lt;/th&gt;
&lt;th&gt;Traditional Voice Agent (LLM Loop)&lt;/th&gt;
&lt;th&gt;Felona Voice (JEV + VoiceGraph)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Intent Decision Latency&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;850ms – 1,800ms&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;~5ms&lt;/strong&gt; (Sub-10ms)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Inference Cost / Turn&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;$0.02 – $0.06+ / turn&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;$0.00&lt;/strong&gt; / turn&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Hallucination Risk&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;High (probabilistic text tokens)&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;0%&lt;/strong&gt; (deterministic transition graph)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Network Dependency&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Requires constant cloud LLM API&lt;/td&gt;
&lt;td&gt;Local/In-memory embedding matching&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Developer Experience&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Complex prompt engineering, slow iteration&lt;/td&gt;
&lt;td&gt;Fluent builder API, fast local testing&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  The Mathematical Truth: 90-95% Cost Reduction
&lt;/h3&gt;

&lt;p&gt;Let's quantify the cost savings. Consider an enterprise-level voice agent handling &lt;strong&gt;50,000 calls per month&lt;/strong&gt;, with an average of &lt;strong&gt;5 conversational turns per call&lt;/strong&gt; for intent resolution. This amounts to &lt;strong&gt;250,000 intent decisions per month&lt;/strong&gt;.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Traditional LLM Approach&lt;/strong&gt;: At an average cost of $0.04 per turn (a conservative estimate for a mix of input/output tokens), your monthly bill for &lt;em&gt;intent resolution alone&lt;/em&gt; would be: &lt;br&gt;
&lt;code&gt;250,000 turns * $0.04/turn = $10,000 per month&lt;/code&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Felona Voice Approach&lt;/strong&gt;: With JEV-powered intent resolution, the cost per turn is &lt;strong&gt;$0.00&lt;/strong&gt;. This means your monthly bill for intent resolution is:&lt;br&gt;
&lt;code&gt;250,000 turns * $0.00/turn = $0 per month&lt;/code&gt;&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This represents a &lt;strong&gt;100% saving&lt;/strong&gt; on the intent resolution component of your voice agent's operation. When you factor in the additional costs of LLM inference for &lt;em&gt;generative responses&lt;/em&gt; (which Felona Voice can still integrate for, but only when truly needed), the overall savings on infrastructure bills can easily exceed &lt;strong&gt;90-95%&lt;/strong&gt; for systems that primarily rely on structured interactions.&lt;/p&gt;

&lt;p&gt;This isn't just an optimization; it's a fundamental shift that makes high-volume, real-time voice AI economically viable and technically superior for structured use cases.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Future of Structured Voice AI is Here
&lt;/h2&gt;

&lt;p&gt;The era of forcing generative LLMs into every voice AI problem, regardless of suitability, is coming to an end. For structured, real-time, and mission-critical voice interactions, the architectural shortcomings of streaming LLMs are simply too great. Felona Voice offers a purpose-built, highly performant, and cost-effective alternative by leveraging Joint Embedding Vectors and VoiceGraph.&lt;/p&gt;

&lt;p&gt;It's time to build voice agents that truly feel real-time, are reliably deterministic, and won't break the bank. Embrace the future of voice AI with Felona Voice.&lt;/p&gt;




&lt;p&gt;🌟 &lt;strong&gt;Star the repository on GitHub:&lt;/strong&gt; &lt;a href="https://github.com/mohitjoer/felona_voice" rel="noopener noreferrer"&gt;github.com/mohitjoer/felona_voice&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;📦 &lt;strong&gt;Install via npm:&lt;/strong&gt; &lt;code&gt;npm install felona-voice&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;📖 &lt;strong&gt;Explore full documentation:&lt;/strong&gt; &lt;a href="https://felona-voice.mohitjoe.tech/docs" rel="noopener noreferrer"&gt;felona-voice.mohitjoe.tech/docs&lt;/a&gt;&lt;/p&gt;




&lt;p&gt;&lt;em&gt;This article was originally published on &lt;a href="https://felona-voice.mohitjoe.tech/blog/streaming-llms-vs-jev-structured-voice-ai-felona" rel="noopener noreferrer"&gt;felona-voice.mohitjoe.tech&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>typescript</category>
      <category>voiceai</category>
      <category>llms</category>
      <category>embeddings</category>
    </item>
    <item>
      <title>Setting up vector search for github starred repos</title>
      <dc:creator>Dennis kinuthia</dc:creator>
      <pubDate>Wed, 30 Sep 2026 17:37:38 +0000</pubDate>
      <link>https://dev.to/tigawanna/setting-up-vector-search-for-github-starred-repos-6f5</link>
      <guid>https://dev.to/tigawanna/setting-up-vector-search-for-github-starred-repos-6f5</guid>
      <description>&lt;p&gt;Ask the starred-repo corpus questions in natural language: embed the query on-device, rank the stored vectors, and show the matching repos in the TanStack Start UI.&lt;/p&gt;

&lt;p&gt;This is where the earlier chapters pay off. Auth (chapter 2) got us a GitHub token, the worker (chapter 3) turned stars into vectors, the bus (chapter 4) kept the UI honest while it ran, and now a search box turns a sentence into a ranked list.&lt;/p&gt;

&lt;h2&gt;
  
  
  One query, end to end
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt; "graph databases in Rust"
        |
        |  600 ms debounce, ?q= in the URL
        v
 useQuery -&amp;gt; Eden: GET /api/elysia/enrich/starred/search?q=...
        |
        v
 Elysia route (TypeBox: 1..2000 chars)
        |
        v
 embedQuery(text)  -- EmbeddingGemma 300M, query prompt
        |             ONNX Runtime (onnxruntime-node, CPU), Q4 weights
        v
 Float32Array(768)
        |
        v
 PGlite + pgvector:  ORDER BY embedding &amp;lt;=&amp;gt; $query  LIMIT 50
        |
        v
 rows (no vector blob) -&amp;gt; list of repo links
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Everything below the Elysia route runs inside the desktop app's Deno process. No request leaves the machine.&lt;/p&gt;

&lt;h2&gt;
  
  
  The model: EmbeddingGemma
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://ai.google.dev/gemma/docs/embeddinggemma" rel="noopener noreferrer"&gt;EmbeddingGemma&lt;/a&gt; is Google's 308M-parameter embedding model built on Gemma 3, designed for phones and laptops. The properties that matter here:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;768-dimensional vectors.&lt;/strong&gt; The size can be cut down to 512, 256, or 128 through Matryoshka representation learning. We keep 768.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;2K-token context&lt;/strong&gt;, which is roomy for a repo's metadata plus the top of its README (chapter 3).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;100+ languages&lt;/strong&gt;, so non-English READMEs and queries still land.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Small when quantized.&lt;/strong&gt; Google quotes under 200 MB of RAM with quantization.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Fully offline&lt;/strong&gt; once the weights are on disk.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;We run the &lt;a href="https://huggingface.co/onnx-community/embeddinggemma-300m-ONNX" rel="noopener noreferrer"&gt;&lt;code&gt;onnx-community/embeddinggemma-300m-ONNX&lt;/code&gt;&lt;/a&gt; export through &lt;a href="https://github.com/kessler/gemma-embedding" rel="noopener noreferrer"&gt;&lt;code&gt;@kessler/gemma-embedding&lt;/code&gt;&lt;/a&gt;, a small wrapper over Transformers.js that uses native &lt;code&gt;onnxruntime-node&lt;/code&gt; in Node-compatible runtimes. Our own package, &lt;a href="https://github.com/tigawanna/tangerine/blob/cac981aca4b365f2ee1293fbf5248da5548c3b5a/packages/gemma-embedding/src/node.ts" rel="noopener noreferrer"&gt;&lt;code&gt;packages/gemma-embedding&lt;/code&gt;&lt;/a&gt;, adds a shared instance, quantization switching, download progress, and cache inspection on top.&lt;/p&gt;

&lt;h3&gt;
  
  
  Documents and queries are embedded differently
&lt;/h3&gt;

&lt;p&gt;EmbeddingGemma is &lt;strong&gt;asymmetric&lt;/strong&gt;: the text is wrapped in a different prompt depending on whether it's something to find or something to search with. From the wrapper's source:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;prefixed&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
  &lt;span class="nx"&gt;mode&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;query&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
    &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="s2"&gt;`task: search result | query: &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="s2"&gt;`&lt;/span&gt;
    &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`title: none | text: &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="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;So the worker embeds repos with &lt;code&gt;embedDocument()&lt;/code&gt; and search embeds the user's sentence with &lt;code&gt;embedQuery()&lt;/code&gt;. Mixing the two up still returns results, just noticeably worse ones. That's why both are exported as separate named functions rather than a &lt;code&gt;mode&lt;/code&gt; flag callers could forget (&lt;a href="https://github.com/tigawanna/tangerine/blob/cac981aca4b365f2ee1293fbf5248da5548c3b5a/packages/gemma-embedding/src/node-runtime/instance.ts" rel="noopener noreferrer"&gt;&lt;code&gt;instance.ts&lt;/code&gt;&lt;/a&gt;):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&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;embedDocument&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="kr"&gt;string&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="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;getServerGemmaEmbedding&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;embedding&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;embed&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="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;document&lt;/span&gt;&lt;span class="dl"&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;export&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;embedQuery&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="kr"&gt;string&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="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;getServerGemmaEmbedding&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;embedding&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;embed&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="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;query&lt;/span&gt;&lt;span class="dl"&gt;"&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;h3&gt;
  
  
  One shared model instance
&lt;/h3&gt;

&lt;p&gt;Loading the model takes a few seconds and a few hundred MB, so there's exactly one instance per process. It's created on first use, and concurrent callers await the same load promise:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&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;getServerGemmaEmbedding&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;options&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="nx"&gt;GemmaEmbeddingOptions&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="nx"&gt;options&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;dtype&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;options&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;dtype&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="nf"&gt;getActiveGemmaDtype&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;disposeInstance&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt; &lt;span class="c1"&gt;// switching quantization: drop the old one&lt;/span&gt;
    &lt;span class="nf"&gt;setActiveDtypeInternal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;options&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;dtype&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;instance&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;getEmbeddingInstance&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="nx"&gt;instance&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nf"&gt;isLoaded&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;instance&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;instance&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="nx"&gt;GemmaEmbedding&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="s2"&gt;@kessler/gemma-embedding&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nx"&gt;instance&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;GemmaEmbedding&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;resolveServerGemmaOptions&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="nx"&gt;options&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;dtype&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;getActiveGemmaDtype&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;}));&lt;/span&gt;
    &lt;span class="nf"&gt;setEmbeddingInstance&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;instance&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="nf"&gt;getEmbeddingLoadPromise&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt; &lt;span class="nf"&gt;setEmbeddingLoadPromise&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;instance&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;load&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="cm"&gt;/* + progress + error handling */&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;getEmbeddingLoadPromise&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;getEmbeddingInstance&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;!&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;The worker and the search route share this instance, so indexing and searching at the same time don't load the model twice.&lt;/p&gt;

&lt;h3&gt;
  
  
  Quantization
&lt;/h3&gt;

&lt;p&gt;The ONNX export ships several weight files. Settings lets the user pick one (&lt;a href="https://github.com/tigawanna/tangerine/blob/cac981aca4b365f2ee1293fbf5248da5548c3b5a/packages/gemma-embedding/src/catalog.ts" rel="noopener noreferrer"&gt;&lt;code&gt;catalog.ts&lt;/code&gt;&lt;/a&gt;):&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Variant&lt;/th&gt;
&lt;th&gt;Download&lt;/th&gt;
&lt;th&gt;Notes&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;q4&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;~197 MB&lt;/td&gt;
&lt;td&gt;Default. Smallest, and fine for repo search&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;q8&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;~309 MB&lt;/td&gt;
&lt;td&gt;Balanced&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;fp16&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;~618 MB&lt;/td&gt;
&lt;td&gt;Higher quality&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;fp32&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;~1.2 GB&lt;/td&gt;
&lt;td&gt;Full precision&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;All variants run on the CPU (&lt;code&gt;device: "cpu"&lt;/code&gt;). &lt;code&gt;GEMMA_DTYPE&lt;/code&gt; and &lt;code&gt;GEMMA_MODEL_PATH&lt;/code&gt; override the choice from the environment, which is handy for pointing at a pre-downloaded model.&lt;/p&gt;

&lt;h2&gt;
  
  
  ONNX Runtime inside a Deno Desktop binary
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;onnxruntime-node&lt;/code&gt; is a native addon, a &lt;code&gt;.node&lt;/code&gt; binary per OS and architecture. Bundling every platform's copy into the desktop binary would bloat it for no benefit, so the packaging step excludes it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="nl"&gt;"desktop:build"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"... deno desktop ... --exclude-unused-npm --exclude ./.output/server/node_modules/onnxruntime-node --compress ..."&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;At runtime, &lt;a href="https://github.com/tigawanna/tangerine/blob/cac981aca4b365f2ee1293fbf5248da5548c3b5a/apps/desktop/src/lib/embedding-gemmma/ort-runtime.ts" rel="noopener noreferrer"&gt;&lt;code&gt;ort-runtime.ts&lt;/code&gt;&lt;/a&gt; resolves ORT in two steps. In dev, the normal &lt;code&gt;node_modules&lt;/code&gt; copy just works. In a packaged build, it uses a copy downloaded on first run into the config dir:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&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;ensureOrtReady&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="k"&gt;void&lt;/span&gt;&lt;span class="o"&gt;&amp;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="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;canImportOrt&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// bundled / dev node_modules&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="nf"&gt;downloadedOrtReady&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// nothing downloaded yet&lt;/span&gt;
  &lt;span class="nf"&gt;ensureOrtModulePath&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt; &lt;span class="c1"&gt;// prepend ~/.config/tangerine-desktop/native/node_modules to NODE_PATH&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The download pulls the pinned &lt;code&gt;onnxruntime-node&lt;/code&gt; tarball (the version must match what Transformers.js expects) straight from the npm registry. It extracts the tarball, deletes the binaries for other operating systems, and adds &lt;code&gt;onnxruntime-common&lt;/code&gt; next to it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;tarballUrl&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;`https://registry.npmjs.org/onnxruntime-node/-/onnxruntime-node-&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;ORT_NPM_VERSION&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;.tgz`&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="c1"&gt;// fetch with progress -&amp;gt; tar -xzf -&amp;gt; keep bin/napi-v6/&amp;lt;this OS&amp;gt;/ -&amp;gt; fetch onnxruntime-common&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Every embed call goes through &lt;code&gt;ensureOrtReady()&lt;/code&gt; before importing the model package, which is why that line appears in both &lt;code&gt;embedRepo()&lt;/code&gt; and the search helper.&lt;/p&gt;

&lt;h2&gt;
  
  
  First run: the bootstrap
&lt;/h2&gt;

&lt;p&gt;A fresh install has neither ORT nor model weights. The first time the app opens, it fetches both in the background with a progress toast. It's modelled on an IDE's first-run downloads and is cancellable from the toast.&lt;/p&gt;

&lt;p&gt;The server side (&lt;a href="https://github.com/tigawanna/tangerine/blob/cac981aca4b365f2ee1293fbf5248da5548c3b5a/apps/desktop/src/lib/embedding-gemmma/embedding-bootstrap.ts" rel="noopener noreferrer"&gt;&lt;code&gt;embedding-bootstrap.ts&lt;/code&gt;&lt;/a&gt;) runs the two downloads in order:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;void &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;ensureOrtReady&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;beginOrtRuntimeDownload&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt; &lt;span class="c1"&gt;// ~40 MB, skipped if ORT already imports&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;awaitOrtRuntimeDownload&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;q4&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;inspectGemmaCache&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nx"&gt;variants&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;find&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;v&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;v&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;q4&lt;/span&gt;&lt;span class="dl"&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="nx"&gt;q4&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;ready&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nf"&gt;beginServerGemmaDtypeSwitch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;q4&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// ~197 MB of weights, then load&lt;/span&gt;
&lt;span class="p"&gt;})();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Progress is streamed with the chapter 4 SSE pattern. This source is a status snapshot rather than an event bus, so the generator polls once a second, only sends when something changed, and closes itself when the downloads finish (&lt;a href="https://github.com/tigawanna/tangerine/blob/cac981aca4b365f2ee1293fbf5248da5548c3b5a/apps/desktop/src/elysia/routes/models/bootstrap.ts" rel="noopener noreferrer"&gt;&lt;code&gt;models/bootstrap.ts&lt;/code&gt;&lt;/a&gt;):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/events&lt;/span&gt;&lt;span class="dl"&gt;"&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="o"&gt;*&lt;/span&gt; &lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;request&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;previous&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;while &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;signal&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;aborted&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="nx"&gt;status&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;getEmbeddingBootstrapStatus&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;serialized&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;status&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="nx"&gt;serialized&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="nx"&gt;previous&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;previous&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;serialized&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="k"&gt;yield&lt;/span&gt; &lt;span class="nf"&gt;sse&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;status&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="nf"&gt;isLive&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="k"&gt;break&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;sleep&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;signal&lt;/span&gt;&lt;span class="p"&gt;);&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;On the client, &lt;a href="https://github.com/tigawanna/tangerine/blob/cac981aca4b365f2ee1293fbf5248da5548c3b5a/apps/desktop/src/components/embeddings/EmbeddingBootstrapHost.tsx" rel="noopener noreferrer"&gt;&lt;code&gt;EmbeddingBootstrapHost&lt;/code&gt;&lt;/a&gt; is mounted once. It calls &lt;code&gt;POST /embedding/bootstrap/start&lt;/code&gt; when the server says &lt;code&gt;shouldAutoStart&lt;/code&gt;, and renders the toast with a progress bar and a Cancel button.&lt;/p&gt;

&lt;h2&gt;
  
  
  Storage: pgvector in PGlite
&lt;/h2&gt;

&lt;p&gt;Vectors live next to the repo rows in embedded Postgres (&lt;a href="https://pglite.dev/docs/" rel="noopener noreferrer"&gt;PGlite&lt;/a&gt; with the &lt;a href="https://github.com/pgvector/pgvector" rel="noopener noreferrer"&gt;pgvector&lt;/a&gt; extension). The column width comes from the same constant the model package exports, so the two can't drift apart (&lt;a href="https://github.com/tigawanna/tangerine/blob/cac981aca4b365f2ee1293fbf5248da5548c3b5a/apps/desktop/src/pglite/schema/project-enrichment-outputs.ts" rel="noopener noreferrer"&gt;&lt;code&gt;project-enrichment-outputs.ts&lt;/code&gt;&lt;/a&gt;):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// packages/gemma-embedding/src/constants.ts&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;EMBEDDING_MODEL_ID&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;embeddinggemma-300m&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;EMBEDDING_DIMENSIONS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;768&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;projectEnrichmentOutputs&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;pgTable&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;project_enrichment_outputs&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;id&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;primaryKey&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="k"&gt;default&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;sql&lt;/span&gt;&lt;span class="s2"&gt;`gen_random_uuid()`&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="na"&gt;owner&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;owner&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;notNull&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
  &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;name&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;notNull&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
  &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;type&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;$type&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;starred&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;repos&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;other&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
  &lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;description&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="na"&gt;summary&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;summary&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="na"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;jsonb&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;payload&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;notNull&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="c1"&gt;// { text } = the exact document that was embedded&lt;/span&gt;
  &lt;span class="na"&gt;modelId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;model_id&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="c1"&gt;// which model produced `embedding`&lt;/span&gt;
  &lt;span class="na"&gt;embedding&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;vector&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;embedding&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;dimensions&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;EMBEDDING_DIMENSIONS&lt;/span&gt; &lt;span class="p"&gt;}),&lt;/span&gt;
  &lt;span class="na"&gt;embeddedAt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;timestamp&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;embedded_at&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;withTimezone&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt; &lt;span class="p"&gt;}),&lt;/span&gt;
  &lt;span class="c1"&gt;// ...&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Storing &lt;code&gt;payload.text&lt;/code&gt; and &lt;code&gt;modelId&lt;/code&gt; makes the index easy to reason about later. You can see exactly what was embedded, and if the model ever changes you know which rows to re-embed.&lt;/p&gt;

&lt;h2&gt;
  
  
  Search
&lt;/h2&gt;

&lt;p&gt;The whole retrieval step is one function (&lt;a href="https://github.com/tigawanna/tangerine/blob/cac981aca4b365f2ee1293fbf5248da5548c3b5a/apps/desktop/src/elysia/routes/enrich/starred/helpers/search.ts" rel="noopener noreferrer"&gt;&lt;code&gt;search.ts&lt;/code&gt;&lt;/a&gt;): embed the query, then let Postgres sort by cosine distance using Drizzle's &lt;code&gt;cosineDistance&lt;/code&gt; (pgvector's &lt;code&gt;&amp;lt;=&amp;gt;&lt;/code&gt; operator):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&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;searchStarredByQuery&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;q&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;StarredSearchHit&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&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;text&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;q&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;trim&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;slice&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;EMBED_TEXT_MAX_CHARS&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;text&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;[];&lt;/span&gt;

  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;ensureOrtReady&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="nx"&gt;embedQuery&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;getServerGemmaEmbedding&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="s2"&gt;@repo/gemma-embedding/node&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;getServerGemmaEmbedding&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;dtype&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;readGemmaPrefs&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nx"&gt;dtype&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;vector&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Array&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;await&lt;/span&gt; &lt;span class="nf"&gt;embedQuery&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;distance&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;sql&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nf"&gt;cosineDistance&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;projectEnrichmentOutputs&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;vector&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="k"&gt;return&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;select&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;projectEnrichmentOutputs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;owner&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;projectEnrichmentOutputs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;owner&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;projectEnrichmentOutputs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;projectEnrichmentOutputs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;description&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="cm"&gt;/* … */&lt;/span&gt; &lt;span class="nx"&gt;distance&lt;/span&gt; &lt;span class="p"&gt;})&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;projectEnrichmentOutputs&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;where&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;and&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;eq&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;projectEnrichmentOutputs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="kd"&gt;type&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;starred&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="nf"&gt;isNotNull&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;projectEnrichmentOutputs&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="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;orderBy&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;distance&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;limit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;50&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;The select lists columns explicitly and leaves out &lt;code&gt;embedding&lt;/code&gt;, so 768 floats per row never cross into the UI.&lt;/p&gt;

&lt;p&gt;Right now this is an &lt;strong&gt;exact scan&lt;/strong&gt;: Postgres computes the distance to every starred row. For a personal star list (hundreds to a few thousand rows) that's a small amount of work and always returns the true nearest neighbours. If the corpus grows much larger, an approximate HNSW index is one custom migration. drizzle-kit can't generate &lt;code&gt;USING hnsw&lt;/code&gt;, so it would be hand-written:&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;CREATE&lt;/span&gt; &lt;span class="k"&gt;INDEX&lt;/span&gt; &lt;span class="n"&gt;project_enrichment_outputs_embedding_hnsw&lt;/span&gt;
  &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;project_enrichment_outputs&lt;/span&gt; &lt;span class="k"&gt;USING&lt;/span&gt; &lt;span class="n"&gt;hnsw&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;embedding&lt;/span&gt; &lt;span class="n"&gt;vector_cosine_ops&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The route exposes the search with validation and OpenAPI docs (&lt;a href="https://github.com/tigawanna/tangerine/blob/cac981aca4b365f2ee1293fbf5248da5548c3b5a/apps/desktop/src/elysia/routes/enrich/starred/index.ts" rel="noopener noreferrer"&gt;&lt;code&gt;enrich/starred/index.ts&lt;/code&gt;&lt;/a&gt;):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/search&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&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="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;searchStarredByQuery&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="nx"&gt;q&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;query&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Object&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;q&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;minLength&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;maxLength&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;2000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Natural-language query; embedded then ranked by cosine distance.&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;}),&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;h2&gt;
  
  
  The UI
&lt;/h2&gt;

&lt;p&gt;The starred page (&lt;a href="https://github.com/tigawanna/tangerine/blob/cac981aca4b365f2ee1293fbf5248da5548c3b5a/apps/desktop/src/routes/_dashboard/$user/enriched/-components/starred/EnrichedStarred.tsx" rel="noopener noreferrer"&gt;&lt;code&gt;EnrichedStarred.tsx&lt;/code&gt;&lt;/a&gt;) reuses the app's normal list scaffold. The search box writes &lt;code&gt;?q=&lt;/code&gt; to the URL after a 600 ms debounce, and a query hook calls the typed Eden client:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;q&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;routeApi&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;useSearch&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nx"&gt;q&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="dl"&gt;""&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;trim&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;semantic&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useQuery&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;queryKey&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;enriched-starred-search&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;q&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
  &lt;span class="na"&gt;enabled&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;q&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;placeholderData&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;previous&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;previous&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;// keep old hits on screen while the next query embeds&lt;/span&gt;
  &lt;span class="na"&gt;queryFn&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;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="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;error&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="nf"&gt;getElysiaTreaty&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nx"&gt;enrich&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;starred&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;search&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;query&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;q&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="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;treatyErrorMessage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;error&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;data&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="p"&gt;[];&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="nx"&gt;rows&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;q&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;semantic&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="p"&gt;[])&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;allRows&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A few small choices make it feel local rather than remote:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The placeholder hints at meaning, not keywords&lt;/strong&gt;: "Try natural language — e.g. graph databases in Rust…".&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The loading state says "Embedding query…"&lt;/strong&gt; instead of a generic spinner, which also explains the short wait on the very first search while the model loads.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;placeholderData&lt;/code&gt;&lt;/strong&gt; keeps the previous results visible while you refine the query, so the list doesn't flash empty on every keystroke.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No query means the full corpus.&lt;/strong&gt; An empty &lt;code&gt;q&lt;/code&gt; shows every embedded star, paginated, and that list keeps growing live during indexing (chapter 4).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Each hit is a link&lt;/strong&gt; to the in-app repo page (&lt;code&gt;/$user/repos/$repo&lt;/code&gt;).&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What we built, and what's next
&lt;/h2&gt;

&lt;p&gt;The finished loop: sign in once, click &lt;strong&gt;Embed starred&lt;/strong&gt;, watch repos stream into the list, and search them by meaning. It all runs on your machine with a ~200 MB model.&lt;/p&gt;

&lt;p&gt;It's deliberately "retrieval only": the answer is a ranked list of your own repos, not generated text. That keeps it fast, obviously correct (every result is a real star), and free of a second, much larger model.&lt;/p&gt;

&lt;p&gt;Natural next steps, roughly in order of payoff:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Filters&lt;/strong&gt; on language, topic, or owner, as a &lt;code&gt;WHERE&lt;/code&gt; next to the vector sort.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Hybrid ranking&lt;/strong&gt;: blend in Postgres full-text search, so exact names ("tokio") always rank first.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Chunked READMEs&lt;/strong&gt; for deep-content matches (see the note in chapter 3).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;An HNSW index&lt;/strong&gt; once the corpus outgrows an exact scan.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Optional local generation&lt;/strong&gt;: pass the top hits to a small local LLM for a one-paragraph answer that cites them.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  With all that said
&lt;/h2&gt;

&lt;p&gt;I tried hard to keep the shipped binary small:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Nothing heavy in the bundle.&lt;/strong&gt; ONNX Runtime and the EmbeddingGemma weights are fetched at runtime (the first-run bootstrap above), not packaged.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Lazy imports.&lt;/strong&gt; The model package is only imported the first time something embeds.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No CEF.&lt;/strong&gt; &lt;code&gt;backend: "webview"&lt;/code&gt; uses the OS webview instead of bundling Chromium, which saves roughly 100 MB (chapter 1).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Even so, the Deno runtime alone is about &lt;strong&gt;70 MB&lt;/strong&gt;, and that's a floor this approach can't go below. For a tool whose job is "search my stars", that feels like a lot.&lt;/p&gt;

&lt;p&gt;I suspect a Go shell with &lt;a href="https://wails.io/" rel="noopener noreferrer"&gt;Wails&lt;/a&gt; (a single Go binary using the OS webview) could come in well under that. So I've started learning Go, and I'm looking forward to rebuilding a slice of this and writing up what I find.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>vectordatabase</category>
      <category>embeddings</category>
      <category>gemma</category>
    </item>
    <item>
      <title>Embedding Drift: Detect, Monitor &amp; Swap Models Safely</title>
      <dc:creator>Nainik Mehta</dc:creator>
      <pubDate>Mon, 28 Sep 2026 07:32:41 +0000</pubDate>
      <link>https://dev.to/nainikmehta/embedding-drift-detect-monitor-swap-models-safely-4a1j</link>
      <guid>https://dev.to/nainikmehta/embedding-drift-detect-monitor-swap-models-safely-4a1j</guid>
      <description>&lt;h2&gt;
  
  
  Why embedding drift detection matters
&lt;/h2&gt;

&lt;p&gt;Embedding models (and the corpora they encode) change more often than teams expect. A new encoder, a tokenizer tweak, or even a chunking policy change can rotate, scale, or reshape your vector space. Re-embedding billions of vectors and rebuilding ANN indexes is expensive and disruptive. The good news: you don't need to reindex on every model update. Add low-cost, high-signal checks to your dashboard, defer full rebuilds with lightweight mappings, and follow a staged cutover plan.&lt;/p&gt;

&lt;h2&gt;
  
  
  Three cheap drift signals you can add today
&lt;/h2&gt;

&lt;p&gt;These signals are fast to compute, interpretable, and often sufficient to tell you whether to act.&lt;/p&gt;

&lt;h3&gt;
  
  
  1) Shadow Recall@K delta
&lt;/h3&gt;

&lt;p&gt;Run the candidate model in shadow for a sample of production queries for 1–3 weeks. Compare distributional changes in Recall@K (e.g., Recall@10) between prod and candidate. Instead of a single mean, surface the distribution (percentiles, tail behavior) and alert on shifts in the top-K relative performance.&lt;/p&gt;

&lt;p&gt;Practical notes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Keep a small labeled or synthetically derived eval set (200–2,000 queries) and a sampled live-query set.&lt;/li&gt;
&lt;li&gt;Compare percentile deltas (p50/p90/p99) and absolute recall drop for critical cohorts (tenant, language, document type).&lt;/li&gt;
&lt;li&gt;Use shadowing instead of traffic switching: log candidate results, return prod to users.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  2) Mahalanobis distance on batches
&lt;/h3&gt;

&lt;p&gt;Cosine or Euclidean deltas can be noisy for rotations and anisotropic scaling. Mahalanobis distance accounts for covariance and flags shifts in the joint distribution of embeddings.&lt;/p&gt;

&lt;p&gt;Basic implementation sketch (Python/pseudocode):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;numpy&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;np&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;scipy.spatial&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;distance&lt;/span&gt;

&lt;span class="c1"&gt;# X_ref: N x d array from 2-4 weeks of production embeddings
# X_batch: M x d array from recent production queries
&lt;/span&gt;&lt;span class="n"&gt;mean_ref&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;X_ref&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;mean&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;axis&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;cov_ref&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;cov&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;X_ref&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;rowvar&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;False&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mf"&gt;1e-6&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;eye&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;X_ref&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;shape&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
&lt;span class="n"&gt;inv_cov&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;linalg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;inv&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cov_ref&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;# Mahalanobis distances of batch items to ref distribution
&lt;/span&gt;&lt;span class="n"&gt;dists&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;distance&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;mahalanobis&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;mean_ref&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;inv_cov&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;X_batch&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="c1"&gt;# Aggregate metric: median or tail (95th/99th percentile)
&lt;/span&gt;&lt;span class="n"&gt;alert_score&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;percentile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;dists&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;99&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Alert when the tail crosses a tuned threshold derived from your null distribution (see KS tests below).&lt;/p&gt;

&lt;h3&gt;
  
  
  3) KS-tests on per-dimension or score histograms
&lt;/h3&gt;

&lt;p&gt;Perform Kolmogorov–Smirnov tests on per-dimension distributions or on top-1 similarity score histograms. Build a null distribution from 2–4 weeks of production traffic and only alert beyond the 99th percentile. KS tests are cheap and capture subtle distributional shifts across many dimensions when aggregated.&lt;/p&gt;

&lt;p&gt;Practical aggregation: compute KS p-values per-dimension, then use a Bonferroni or Holm correction or treat the maximum stat as the aggregate signal. Alternatively, monitor the histogram of top-1 similarity scores and apply a KS test to detect shifts.&lt;/p&gt;

&lt;h2&gt;
  
  
  Two pragmatic ways to avoid full re-indexing
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1) Drift-Adapter / mapping layer
&lt;/h3&gt;

&lt;p&gt;Train a lightweight transformation g_theta: R^{d_new} -&amp;gt; R^{d_old} that maps new-model embeddings into the production space. This lets you keep the legacy ANN index and serve queries with transformed candidate embeddings.&lt;/p&gt;

&lt;p&gt;Common parameterizations:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Orthogonal Procrustes (rigid rotation)&lt;/li&gt;
&lt;li&gt;Low-rank affine (diagonal scaling + small matrix)&lt;/li&gt;
&lt;li&gt;Small residual MLP when drift is partially non-linear&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Train on a small paired calibration set (10k–50k pairs) sampled from your corpus. The Drift-Adapter paper and follow-up toolkits show you can recover 95–99% of Recall@10 in many upgrades while adding negligible latency.&lt;/p&gt;

&lt;p&gt;Quick mapper training example (least-squares linear mapper):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# A_old: Nxd old embeddings, B_new: Nxd new embeddings
# Learn W to minimize ||W B_new - A_old||_F^2
&lt;/span&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;numpy&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;np&lt;/span&gt;
&lt;span class="n"&gt;W&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;A_old&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;T&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt; &lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;linalg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;pinv&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;B_new&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;T&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="c1"&gt;# simple closed-form
# At query time: q_mapped = (W @ q_new)
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If W recovers most retrieval signal on a holdout set, deploy it as a query-time adapter and avoid touching the index immediately.&lt;/p&gt;

&lt;p&gt;When to use: model changes that are mostly geometric (rotations, scaling, mild anisotropy) or when you need a fast, reversible bridge.&lt;/p&gt;

&lt;h3&gt;
  
  
  2) Fingerprint-driven reindexing and selective backfill
&lt;/h3&gt;

&lt;p&gt;Attach a ModelFingerprint to each collection (model_id, provider, dims, and optionally normalization/cfg). On startup or during deployment, compare the current model's fingerprint to the stored one. Only trigger full reindex when the fingerprint meaningfully changes.&lt;/p&gt;

&lt;p&gt;Flow:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;If fingerprint matches: no bulk work. Optionally start a shadow run for monitoring.&lt;/li&gt;
&lt;li&gt;If fingerprint differs: enqueue selective/hot-doc re-embedding (hot docs by access, or critical cohorts), train a mapper, and start a shadow evaluation.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Tiny illustrative fragment:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;new_model&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;fingerprint&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="n"&gt;prod_model&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;fingerprint&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="nf"&gt;enqueue_partial_reindex&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;hot_docs&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;else&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="nf"&gt;start_shadow_run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;candidate_model&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Selective re-embedding (hot-docs, cohorts, or only failing golden queries) typically re-embeds a small fraction (we’ve seen ~3% in practice) and buys time to plan a full backfill.&lt;/p&gt;

&lt;h2&gt;
  
  
  Safe cutover playbook (practical)
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Shadow run (1–3 weeks): embed queries with both models; log Recall@K, top-1 score histograms, per-cohort metrics.&lt;/li&gt;
&lt;li&gt;Compare distributions: use Recall@K deltas, Mahalanobis tail, and KS-tests. Gate on thresholds (e.g., no &amp;gt;2–3% drop on golden queries, p99 Mahalanobis below threshold).&lt;/li&gt;
&lt;li&gt;Deploy Mapper fallback: if Drift-Adapter is in use, make it the default query transform and keep the old index.&lt;/li&gt;
&lt;li&gt;Canary mirror traffic: mirror 1% → 10% → 100% reads to the new path (or flip alias progressively). Observe end-to-end latency and business metrics.&lt;/li&gt;
&lt;li&gt;Holdover window: keep mapper fallback and the old index live for at least two weeks post-cutover. Continue shadowing and cohort monitoring.&lt;/li&gt;
&lt;li&gt;Escalate to full reindex only when both fingerprint and statistical tests exceed configured thresholds and dual-index/backfill has completed.&lt;/li&gt;
&lt;li&gt;Rollback plan: keep aliases that can atomically switch back, keep dual-write running for the rollback window, and maintain the old index until the observation window expires.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Real-world tip and example
&lt;/h2&gt;

&lt;p&gt;Last quarter we shadowed a candidate for 10 days, tracked Recall@10 and Mahalanobis drift, and trained a linear Mapper. The result: avoided a full re-index, re-embedded only 3% of hot documents during a staged cutover, and observed zero visible regressions in production.&lt;/p&gt;

&lt;h2&gt;
  
  
  Closing: make drift detection routine, not dramatic
&lt;/h2&gt;

&lt;p&gt;Embedding drift detection doesn't have to be all-or-nothing. Start with cheap signals (shadow Recall@K, Mahalanobis, KS-tests), defer full re-embedding using a Drift-Adapter plus fingerprint gating, and follow a careful shadow→canary→cutover playbook. These practices turn a weekend migration into a predictable engineering process.&lt;/p&gt;

&lt;p&gt;What one cheap signal will you add to your dashboard this week?&lt;/p&gt;

</description>
      <category>mlops</category>
      <category>embeddings</category>
      <category>drift</category>
      <category>deployment</category>
    </item>
    <item>
      <title>Embeddings and Vector Search, Demystified</title>
      <dc:creator>Internals Decoded</dc:creator>
      <pubDate>Thu, 24 Sep 2026 17:53:43 +0000</pubDate>
      <link>https://dev.to/internals_decoded/embeddings-and-vector-search-demystified-4j4l</link>
      <guid>https://dev.to/internals_decoded/embeddings-and-vector-search-demystified-4j4l</guid>
      <description>&lt;p&gt;Embedding models convert your company handbook’s text chunks into high-dimensional vectors, and approximate nearest neighbor (ANN) indexes like HNSW or IVF-PQ retrieve the most semantically similar chunks in milliseconds. Cosine similarity gives this search its discriminative power by measuring angular closeness, not just keyword overlap. Together they replace rigid symbolic matching with geometric proximity.&lt;/p&gt;

&lt;p&gt;The curse of dimensionality tells us that in high dimensions, all points look equally far apart. Yet embedding spaces, trained with contrastive objectives, actually make semantically related chunks physically close. ANN algorithms then cheat gracefully, trading perfect recall for sub-millisecond latencies while keeping over 95% of the true top-k neighbors. That mismatched intuition is the central magic, and the central engineering challenge, of vector search.&lt;/p&gt;

&lt;h2&gt;
  
  
  How do embeddings turn text into a geometric space?
&lt;/h2&gt;

&lt;p&gt;Think of a city map where every landmark is placed according to its function, not its street address. A library, a bookstore, and a university would sit near each other even if their physical locations differ wildly. Embedding models build exactly this kind of map: they assign each text chunk a coordinate in a high-dimensional space so that “work-from-home policy” and “remote work guidelines” land near each other, and far from “lunch menu.”&lt;/p&gt;

&lt;p&gt;A modern text embedding model is just a deterministic function, usually a transformer encoder, that ingests tokenized text and outputs a fixed-length vector. When we call &lt;code&gt;model.encode(["All employees may work remotely on Fridays."])&lt;/code&gt; with a model like &lt;code&gt;all-MiniLM-L6-v2&lt;/code&gt;, we get back a 384-dimensional array of floats &lt;a href="https://www.sbert.net/docs/quickstart.html#usage" rel="noopener noreferrer"&gt;source&lt;/a&gt;. That array is the chunk’s “address” in the semantic map. Every chunk from the handbook gets its own address, and we store them all in a giant coordinate database. When a user asks “What is the remote work policy?”, we run the same encoder on the query, compute its address, and then find the database addresses that are closest.&lt;/p&gt;

&lt;p&gt;The geometry of this map is not random. It is shaped during training by a contrastive learning objective. The model sees millions of pairs of sentences that are either semantically similar (paraphrases, answers to the same question) or unrelated. It is penalized when similar sentences end up far apart, and when unrelated ones end up close. Over time, the space becomes uniform (vectors spread evenly across the unit sphere) and aligned (angular distance accurately reflects meaning). That is why a small model can rival a large one in retrieval accuracy: the map’s structure, not its raw size, does the heavy lifting.&lt;/p&gt;

&lt;p&gt;Our running assistant splits the handbook into chunks that fit inside the model’s maximum sequence length (usually 512 tokens) &lt;a href="https://www.sbert.net/docs/pretrained_models.html#sentence-embedding-models" rel="noopener noreferrer"&gt;source&lt;/a&gt;. If we skip chunking, a document about “expense policies” that also briefly mentions remote work might produce an embedding that blends both topics, muddying the map. Good chunk boundaries, something we explored in Part 2, keep each coordinate meaningful. After we embed every chunk, we load them into an index that can answer “find the k nearest addresses to this query vector” fast. The next section unpacks what “nearest” really means under the hood.&lt;/p&gt;

&lt;h2&gt;
  
  
  What does cosine similarity actually measure?
&lt;/h2&gt;

&lt;p&gt;Cosine similarity is the cosine of the angle between two vectors. In the embedding map, it measures how much two items point in the same conceptual direction, ignoring their individual lengths. Two handbook passages that both circle around “work-from-home eligibility” will have a small angle between them, yielding a cosine similarity close to 1. A passage about office snacks points somewhere else entirely, giving a similarity near 0 (or negative). That angle alone often outperforms Euclidean distance on semantic tasks because the scale of the vector can vary with text length or model quirks, while the direction captures the topic more robustly.&lt;/p&gt;

&lt;p&gt;Mathematically, for unit-length vectors, cosine similarity equals the dot product. Many embedding models output normalized vectors by default, so maximizing inner product is equivalent to minimizing angular distance. For maximum inner product search (MIPS) in recommendation systems, algorithms like ScaNN explicitly adapt their pruning to that equivalence &lt;a href="https://arxiv.org/abs/1908.10396" rel="noopener noreferrer"&gt;source&lt;/a&gt;. In our handbook assistant, we can treat cosine similarity and dot product as interchangeable as long as we normalize embeddings; then the only difference is a constant scaling that does not affect ranking.&lt;/p&gt;

&lt;p&gt;Why does this matter? Because the handful of chunks that the ANN index returns will be ranked by this angle. If the model has placed all remote-work-themed chunks in a tight cluster, the top-3 results will all be highly relevant. But if noise or poor training makes the cluster diffuse, the closest chunks may still be about travel or food, just slightly nearer by accident. The next section explores why high dimensionality itself can make clusters diffuse, even if the model is decent.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why does the curse of dimensionality break exact search?
&lt;/h2&gt;

&lt;p&gt;In low dimensions, if you are in San Francisco, Los Angeles is far, and the nearest neighbor notion is sharp. But in a 384-dimensional space, something bizarre happens: almost all points look roughly equally far from a given query &lt;a href="https://dl.acm.org/doi/10.1007/3-540-49257-7_15" rel="noopener noreferrer"&gt;source&lt;/a&gt;. The contrast between the nearest and the farthest neighbor collapses; the ratio approaches 1. This is distance concentration, the core of the curse. For a brute-force scan over all chunks, this is merely a CPU (central processing unit) cost problem. But for spatial indexes like k-d trees or ball trees, which work by pruning large regions, the curse is fatal. In high dimensions, bounding volumes overlap so much that the pruner cannot rule out any branch, and performance degrades to scanning nearly everything.&lt;/p&gt;

&lt;p&gt;Our handbook assistant has tens of thousands of chunks. A brute-force scan over 50,000 chunks of dimension 384 takes a few hundred milliseconds on a modern CPU, already too slow for a chat interaction. If the handbook grows to a million chunks, it becomes seconds. And naïve spatial indexes would be even slower. We cannot rely on exact nearest neighbor at scale; we need an algorithm that deliberately sacrifices perfect correctness to stay fast. That is where approximate nearest neighbor (ANN) enters, which we address next.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;{
  "type": "stat",
  "title": "Brute force vs ANN search",
  "caption": "Scanning 50,000 chunks of dimension 384 takes hundreds of milliseconds, while an ANN index delivers sub-5 ms latencies at high recall.",
  "stats": [
    {
      "value": "300ms",
      "label": "Brute force on 50k vectors"
    },
    {
      "value": "5ms",
      "label": "ANN index (HNSW, efSearch=64)"
    },
    {
      "value": "60x",
      "label": "Speedup"
    },
    {
      "value": "&amp;gt;95%",
      "label": "Recall@10"
    }
  ]
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  How do approximate nearest neighbor algorithms cheat gracefully?
&lt;/h2&gt;

&lt;p&gt;ANN algorithms accept that they will miss a few of the true top-k neighbors, and in exchange deliver orders-of-magnitude speedup. They are measured by recall@k: the fraction of the true top-k that actually appears in the returned set &lt;a href="https://github.com/facebookresearch/faiss/wiki/Faiss-building-blocks#approximate-nearest-neighbor-search" rel="noopener noreferrer"&gt;source&lt;/a&gt;. A typical production system targets 95-99% recall at values of k between 5 and 50.&lt;/p&gt;

&lt;p&gt;The most widely used class of in-memory ANN indexes is the graph-based family, led by HNSW (Hierarchical Navigable Small World). HNSW builds a sparse, multi-layer proximity graph where each chunk is a node and edges point to nearby nodes &lt;a href="https://arxiv.org/abs/1603.09320" rel="noopener noreferrer"&gt;source&lt;/a&gt;. At query time, it performs a greedy walk: start at a random entry point on the top, sparsest layer, move toward nodes that are closer to the query, drop down to denser layers, and finally collect the closest nodes at the bottom layer. The walk visits only a tiny fraction of the graph. The parameter &lt;code&gt;efSearch&lt;/code&gt; (exploration factor) controls how many candidates are examined; larger &lt;code&gt;efSearch&lt;/code&gt; raises recall but also latency. For our handbook assistant, &lt;code&gt;efSearch=64&lt;/code&gt; might give 97% recall at under a millisecond per query on 100k vectors.&lt;/p&gt;

&lt;p&gt;Disk-backed variants like DiskANN rearrange the graph as a single dense layer optimized for SSD access, with a compression catalog (product quantization) kept in RAM (random-access memory) &lt;a href="https://www.microsoft.com/en-us/research/publication/diskann-fast-accurate-billion-point-nearest-neighbor-search-on-a-single-node/" rel="noopener noreferrer"&gt;source&lt;/a&gt;. This design lets us serve billion-chunk indexes from relatively cheap commodity hardware, which is overkill for a single handbook but indispensable when the system scales to enterprise-wide document stores.&lt;/p&gt;

&lt;p&gt;Partition-and-quantize methods, represented by IVF-PQ (inverted file with product quantization), take a different approach. A coarse quantizer (k-means) partitions the space into, say, 1024 cells. The query is first assigned to the nearest &lt;code&gt;nprobe&lt;/code&gt; cells, and only the vectors in those cells are scored using compressed (PQ) codes. Faiss implements this efficiently in both CPU and GPU (graphics processing unit) &lt;a href="https://github.com/facebookresearch/faiss/wiki/Faiss-indexes" rel="noopener noreferrer"&gt;source&lt;/a&gt;. For the handbook, we might store embeddings in a Faiss IVF-PQ index with &lt;code&gt;nprobe=16&lt;/code&gt; and achieve sub-millisecond latency even on server-grade CPU, with recall around 95%. This method shines when memory is tight, because the PQ compression shrinks each vector to a few dozen bytes instead of 1.5 KB.&lt;br&gt;
&lt;/p&gt;

&lt;pre data-lang="mermaid"&gt;&lt;code&gt;graph TD
  A[Query vector] --&amp;gt; B[Coarse quantizer k-means]
  B --&amp;gt; C[Top n closest centroids]
  C --&amp;gt; D[Scan vectors in selected buckets]
  D --&amp;gt; E[Re rank by cosine similarity]
  E --&amp;gt; F[Return top k results]&lt;/code&gt;&lt;/pre&gt;



&lt;p&gt;All these algorithms expose knobs that trace a recall-vs-latency curve; the engineer’s job is to pick the index that dominates that curve for the specific workload. Once we have a fast vector retrieval layer in place, we can finally answer the central question: what does “semantic” actually buy?&lt;/p&gt;

&lt;h2&gt;
  
  
  What does semantic actually buy us in a RAG pipeline?
&lt;/h2&gt;

&lt;p&gt;Semantic search gives us the ability to retrieve relevant chunks even when the exact words are absent. In our handbook assistant, a user might type “How do I submit a vacation request?” while the policy document says “Paid time off must be requested via HR Portal at least two weeks in advance.” A keyword engine would miss this, but an embedding of the query lands close to the embedding of that sentence because the model has learned that “vacation request” and “paid time off request” point the same way.&lt;/p&gt;

&lt;p&gt;That directional alignment is fragile. It relies on the embedding model having been exposed to similar paraphrases during training. Fine-tuning the embedding model on company-specific language (with a contrastive objective and a set of paired questions and handbook answers) can dramatically boost recall for jargon or internal names &lt;a href="https://www.sbert.net/docs/training/overview.html" rel="noopener noreferrer"&gt;source&lt;/a&gt;. Without that, the semantic map may cluster “vacation” near “holiday” but still place “vacation request” far from “PTO submission” if the training data was too general.&lt;/p&gt;

&lt;p&gt;The interplay with chunking from Part 2 becomes acute. An overly long chunk may average out multiple topics, diluting the semantic signal and making the chunk’s vector point to a “generic” direction instead of a precise one. Conversely, a chunk that is too short may lack enough context to position itself reliably, drifting toward an unrelated cluster. The semantic promise only holds when chunk boundaries respect topic coherence, and when the embedding model captures the right distinctions. With those pieces in place, the ANN index reliably returns the most concept-aligned chunks, giving the LLM (large language model) in the RAG (retrieval-augmented generation) pipeline the right raw material to answer accurately. In the next episode, we will examine how the retrieved chunks are actually fed to the model: prompt assembly strategies and the subtle ways that ordering and truncation affect the final answer.&lt;/p&gt;

&lt;h2&gt;
  
  
  Quick Reference: Key Configurations and Defaults
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Property&lt;/th&gt;
&lt;th&gt;Typical Value&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Common embedding dimension&lt;/td&gt;
&lt;td&gt;384 (all-MiniLM), 768 (BERT base), 1024 (large models)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Max sequence length&lt;/td&gt;
&lt;td&gt;512 tokens (≈ 350-400 words) for many models&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Default similarity metric (normalized)&lt;/td&gt;
&lt;td&gt;Cosine similarity&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;HNSW efSearch range&lt;/td&gt;
&lt;td&gt;16-128, higher = more recall, slower&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;IVF-PQ nprobe range&lt;/td&gt;
&lt;td&gt;1-64, scans that many cells&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;PQ compressed vector size&lt;/td&gt;
&lt;td&gt;~8-64 bytes per vector&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Typical recall target&lt;/td&gt;
&lt;td&gt;0.95-0.99 for top-k&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Test yourself
&lt;/h2&gt;

&lt;p&gt;Your handbook assistant uses a 384-dim embedding model and an HNSW index with &lt;code&gt;efSearch=64&lt;/code&gt;. The index holds 25,000 chunks. For the query “what is the remote work policy?” the top-3 returned chunks are about travel approval, office supplies, and the remote work policy itself. The assistant’s answer incorrectly claims remote work is only permitted with manager approval because it fused the travel and remote chunks. How would you diagnose and fix this?&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Answer:&lt;/strong&gt; The recall of the true relevant chunk is high but its rank is pushed down by two off-topic chunks that somehow end up closer to the query. This suggests the embedding model is not adequately separating the “remote work” concept from other corporate topics. First, inspect the embedding of the query and the offending chunks with PCA to see if they cluster along noisy dimensions. Consider fine-tuning the embedding model on a supervised dataset of (query, relevant passage) pairs built from past HR tickets. Increase chunk overlap slightly so that the remote work chunk captures more context. Finally, if precision matters more than recall, lower the &lt;code&gt;k&lt;/code&gt; returned or apply a hard cosine similarity threshold filter after retrieval, discarding chunks below 0.6 similarity before passing them to the LLM.&lt;/p&gt;

&lt;h2&gt;
  
  
  Frequently Asked Questions
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Q: Should I always use cosine similarity?&lt;/strong&gt;&lt;br&gt;
Cosine similarity is robust when vector magnitudes vary, and it aligns naturally with contrastively trained embeddings. If you normalize all vectors, dot product gives identical results. Use Euclidean distance when magnitude carries useful information, but in text retrieval cosine is the safer default.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Q: How do I choose the embedding dimension?&lt;/strong&gt;&lt;br&gt;
Larger dimensions can store more nuanced differences, but they increase storage, latency, and can worsen distance concentration if the model is not correspondingly expressive. Start with a compact, well-trained model like all-MiniLM-L6-v2 (384 dims) and only upgrade if retrieval quality is insufficient after trying finer chunking and domain fine-tuning.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Q: Can I update an ANN index incrementally?&lt;/strong&gt;&lt;br&gt;
Graph-based indexes like HNSW support single-vector insertion and deletion with little overhead. IVF-PQ indexes may need occasional coarse quantizer retraining if the data distribution shifts significantly. For a living handbook that changes weekly, HNSW is usually a better fit.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Q: How do attribute filters (e.g., department, date) work with ANN?&lt;/strong&gt;&lt;br&gt;
Run the vector search first to get a candidate set, then filter by attributes, but this can break recall if the filtered candidates are few. Better approaches use pre-filtering within the index (Faiss enables combining IVF with metadata filtering) or use product quantization that encodes attributes alongside the vector. Always benchmark recall with your specific filter mix.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Q: Is exact nearest neighbor search ever practical?&lt;/strong&gt;&lt;br&gt;
Exact search is viable for up to a few thousand high-dimensional vectors. Beyond that, the latency of a full scan becomes unacceptable for interactive applications. Use exact search for unit tests, debugging recall, or verifying top-k correctness of your ANN index against a gold standard.&lt;/p&gt;

&lt;p&gt;If these deep dives into how real retrieval systems work under the hood resonate with how you build, subscribe to Internals Decoded at internalsdecoded.com. Every article unpacks the mechanics that make modern RAG, search, and ML (machine learning) infra tick.&lt;/p&gt;

&lt;h2&gt;
  
  
  Sources
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://www.sbert.net/docs/quickstart.html#usage" rel="noopener noreferrer"&gt;Sentence Transformers Quickstart&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.sbert.net/docs/training/overview.html" rel="noopener noreferrer"&gt;Training Sentence Embedding Models&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dl.acm.org/doi/10.1007/3-540-49257-7_15" rel="noopener noreferrer"&gt;On the Surprising Behavior of Distance Metrics in High Dimensional Space (Aggarwal et al.)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://arxiv.org/abs/1603.09320" rel="noopener noreferrer"&gt;HNSW paper: Efficient and robust approximate nearest neighbor search using Hierarchical Navigable Small World graphs&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/facebookresearch/faiss/wiki/Faiss-building-blocks#approximate-nearest-neighbor-search" rel="noopener noreferrer"&gt;Faiss building blocks: approximate nearest neighbor search&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.microsoft.com/en-us/research/publication/diskann-fast-accurate-billion-point-nearest-neighbor-search-on-a-single-node/" rel="noopener noreferrer"&gt;DiskANN: Fast Accurate Billion-point Nearest Neighbor Search on a Single Node&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://arxiv.org/abs/1908.10396" rel="noopener noreferrer"&gt;ScaNN: Efficient Vector Similarity Search&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/facebookresearch/faiss/wiki/Faiss-indexes" rel="noopener noreferrer"&gt;Faiss indexes documentation&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://www.internalsdecoded.com/articles/embeddings-and-vector-search" rel="noopener noreferrer"&gt;Internals Decoded&lt;/a&gt;. AI internals, explained conversationally.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>embeddings</category>
      <category>vectorsearch</category>
    </item>
    <item>
      <title>Your Semantic Cache Answers the Question Next Door</title>
      <dc:creator>DevOps Daily</dc:creator>
      <pubDate>Thu, 24 Sep 2026 09:00:00 +0000</pubDate>
      <link>https://dev.to/devopsdaily/your-semantic-cache-answers-the-question-next-door-3d55</link>
      <guid>https://dev.to/devopsdaily/your-semantic-cache-answers-the-question-next-door-3d55</guid>
      <description>&lt;p&gt;We replayed 288 questions to an ops assistant through a semantic cache. At a similarity threshold of 0.80 with &lt;code&gt;bge-m3&lt;/code&gt;, the cache answered &lt;strong&gt;32 percent&lt;/strong&gt; of them from memory and cut the average response time from 8.7 seconds to 6.6. It also answered &lt;strong&gt;about one hit in three with the answer to a different question&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Someone asked how to undo a commit they had already pushed to main. The cache had seen "how do I undo my last commit that I have not pushed yet", decided the two were the same question, and served the answer for a local commit: &lt;code&gt;git reset --soft HEAD~1&lt;/code&gt;. For a commit already on main the right answer is &lt;code&gt;git revert&lt;/code&gt;; resetting it and pushing the result means force-pushing over history other people have pulled.&lt;/p&gt;

&lt;p&gt;Raising the threshold does not fix it. Between 0.88 and 0.92 the few hits left were more often wrong than right. Across six embedding models and thresholds from 0.50 to 0.99 in steps of 0.01, the best any of them managed with no wrong answers at all was a &lt;strong&gt;0.7 percent&lt;/strong&gt; hit rate. For four of the others, the only thresholds that never served a wrong answer never served anything, and one model served a wrong answer even at 0.99.&lt;/p&gt;

&lt;p&gt;This post covers how we measured it, why no threshold separates the two kinds of question, what the fix everyone reaches for actually does, and where a semantic cache is genuinely safe.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/The-DevOps-Daily/semantic-cache-wrong-answers" rel="noopener noreferrer"&gt;The-DevOps-Daily/semantic-cache-wrong-answers on GitHub&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  TLDR
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;The setup.&lt;/strong&gt; 24 pairs of ops questions that read almost the same and need different answers: restart versus reload nginx, the staging versus the production password, a memory limit versus a memory request. Six phrasings each, 288 questions, labelled by construction, so a wrong hit is a fact and not a judge model's opinion.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;The cache.&lt;/strong&gt; Embed the question, find the nearest one already answered, serve its answer if the cosine similarity clears a threshold. Six embedding models on &lt;a href="https://www.digitalocean.com/products/inference-engine" rel="noopener noreferrer"&gt;DigitalOcean Serverless Inference&lt;/a&gt;, 50 thresholds, 20 orderings of the questions.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;At 0.80 with bge-m3:&lt;/strong&gt; 32 percent hit rate, 62 right hits and 30 wrong ones on average per run of 288 questions.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;From 0.88 to 0.92:&lt;/strong&gt; more of the remaining hits were wrong than right.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;The same threshold is not the same setting on another model.&lt;/strong&gt; At 0.80, &lt;code&gt;e5-large-v2&lt;/code&gt; answered 88 percent of questions from the cache and 65 percent of those hits were wrong.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;What it saved:&lt;/strong&gt; on &lt;code&gt;gpt-oss-120b&lt;/code&gt; at published prices, $0.25 per 1,000 questions without a cache and $0.18 with one at 0.80. The average got faster; the slowest 5 percent barely moved.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Where it is likely safe:&lt;/strong&gt; near-verbatim repeats. Most of them scored higher against their original than any near-miss pair did, so a threshold set from your own near-misses should catch them. We did not replay them through the cache.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;The obvious fix works, and costs more than it saves.&lt;/strong&gt; A small model checking each hit before it was served cut wrong answers to under one per run on average, at every threshold. It also left the cache no faster than having no cache, and more expensive at every threshold tested.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Prerequisites
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Node.js 20 or later. The harness has no dependencies.&lt;/li&gt;
&lt;li&gt;A DigitalOcean Serverless Inference key. The &lt;a href="https://docs.digitalocean.com/products/ai-platform/" rel="noopener noreferrer"&gt;docs&lt;/a&gt; cover creating one.&lt;/li&gt;
&lt;li&gt;Patience for the embedding stage: 288 questions times six models, one request each, under a per-minute rate limit. The simulation stages after that cost no inference at all.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  How a semantic cache decides
&lt;/h2&gt;

&lt;p&gt;An exact-match cache only helps when the same text arrives twice, perhaps after normalising case and punctuation. A semantic cache is meant to help when the same &lt;em&gt;question&lt;/em&gt; arrives in different words:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Turn the incoming question into an embedding.&lt;/li&gt;
&lt;li&gt;Find the most similar question already answered.&lt;/li&gt;
&lt;li&gt;If the similarity is above a threshold, return that question's stored answer.&lt;/li&gt;
&lt;li&gt;Otherwise ask the model, and store the new question and answer.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The threshold is the only thing deciding whether two questions are "the same". It is one number, compared against one similarity score, and it has to work for every question the assistant will ever get. That is the part worth testing.&lt;/p&gt;

&lt;h2&gt;
  
  
  The questions
&lt;/h2&gt;

&lt;p&gt;Operations questions come in near-miss pairs all the time. The words barely change and the right answer changes completely, often in the direction that breaks something.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Pair&lt;/th&gt;
&lt;th&gt;Why the answer differs&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;restart nginx / reload nginx&lt;/td&gt;
&lt;td&gt;A restart stops the process and can drop connections; a reload applies config gracefully&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;rotate the staging password / the production one&lt;/td&gt;
&lt;td&gt;Same operation, different blast radius and procedure&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;memory limit / memory request&lt;/td&gt;
&lt;td&gt;A limit causes OOM kills; a request affects scheduling&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;delete a pod / delete a deployment&lt;/td&gt;
&lt;td&gt;A pod its deployment owns comes back; the deployment does not&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;undo a pushed commit / an unpushed one&lt;/td&gt;
&lt;td&gt;Revert is safe on shared history; reset rewrites it&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;terraform state rm&lt;/code&gt; / destroy one resource&lt;/td&gt;
&lt;td&gt;One forgets the resource, the other deletes it&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;clear one Redis database / all of them&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;FLUSHDB&lt;/code&gt; versus &lt;code&gt;FLUSHALL&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;cordon a node / drain it&lt;/td&gt;
&lt;td&gt;One stops new pods; the other evicts the running ones&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;There are 24 pairs like these, each side written six different ways, from "How do I restart nginx?" to "How can I bounce the nginx process completely?". Two questions count as sharing an answer if they are listed under the same intent, and a hit counts as wrong when the stored answer was written for the other intent of the pair, or for an unrelated one. No model grades it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;This is a stress test, not an estimate of how often a production cache is wrong.&lt;/strong&gt; Every question's near-miss partner is in the stream, there are no exact repeats, and the paraphrases were written to vary their wording while the near-miss pairs keep theirs. All of that makes it hard on a cache on purpose. How many near-misses your own traffic contains is something only your logs can tell you. What this measures is what the cache does when they turn up.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why one threshold cannot separate them
&lt;/h2&gt;

&lt;p&gt;Here are two kinds of question pair, scored by &lt;code&gt;bge-m3&lt;/code&gt;. The green curve is every pair that needs the same answer. The red dashed curve is every designated near-miss pair: the partners that read almost the same and need different answers. Unrelated pairs, the other 96 percent of the 41,328 possible, are left out.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How alike two questions look to bge-m3&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Series&lt;/th&gt;
&lt;th&gt;Samples&lt;/th&gt;
&lt;th&gt;Min&lt;/th&gt;
&lt;th&gt;Median&lt;/th&gt;
&lt;th&gt;p95&lt;/th&gt;
&lt;th&gt;Max&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Same answer (paraphrases)&lt;/td&gt;
&lt;td&gt;720&lt;/td&gt;
&lt;td&gt;0.357&lt;/td&gt;
&lt;td&gt;0.7224999999999999&lt;/td&gt;
&lt;td&gt;0.86&lt;/td&gt;
&lt;td&gt;0.939&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Different answer (near-miss pairs)&lt;/td&gt;
&lt;td&gt;864&lt;/td&gt;
&lt;td&gt;0.372&lt;/td&gt;
&lt;td&gt;0.6455&lt;/td&gt;
&lt;td&gt;0.823&lt;/td&gt;
&lt;td&gt;0.932&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;em&gt;Cosine similarity, bge-m3. Same answer: all 720 pairs of paraphrases. Different answer: all 864 designated near-miss pairs, such as restart vs reload nginx. Unrelated pairs are not shown.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;A threshold is a single vertical line through both curves. A real cache only compares a new question against what it has stored, so this is not a hit rate. But it shows the problem: no line sits to the right of the whole red curve without also sitting to the right of almost all of the green one. The two overlap across nearly their whole range.&lt;/p&gt;

&lt;p&gt;The overlap was large for every model we tried:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Embedding model&lt;/th&gt;
&lt;th&gt;Median, same answer&lt;/th&gt;
&lt;th&gt;Median, near-miss&lt;/th&gt;
&lt;th&gt;Most similar near-miss&lt;/th&gt;
&lt;th&gt;Near-miss pairs above the median paraphrase&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;bge-m3&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;0.723&lt;/td&gt;
&lt;td&gt;0.646&lt;/td&gt;
&lt;td&gt;0.932&lt;/td&gt;
&lt;td&gt;22.8%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;gte-large-en-v1.5&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;0.771&lt;/td&gt;
&lt;td&gt;0.707&lt;/td&gt;
&lt;td&gt;0.958&lt;/td&gt;
&lt;td&gt;27.1%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;qwen3-embedding-0.6b&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;0.756&lt;/td&gt;
&lt;td&gt;0.685&lt;/td&gt;
&lt;td&gt;0.949&lt;/td&gt;
&lt;td&gt;22.6%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;multi-qa-mpnet-base-dot-v1&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;0.684&lt;/td&gt;
&lt;td&gt;0.577&lt;/td&gt;
&lt;td&gt;0.927&lt;/td&gt;
&lt;td&gt;23.5%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;e5-large-v2&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;0.861&lt;/td&gt;
&lt;td&gt;0.837&lt;/td&gt;
&lt;td&gt;0.982&lt;/td&gt;
&lt;td&gt;36.2%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;all-mini-lm-l6-v2&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;0.849&lt;/td&gt;
&lt;td&gt;0.795&lt;/td&gt;
&lt;td&gt;0.991&lt;/td&gt;
&lt;td&gt;36.2%&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;For at least one near-miss pair in five, the two questions that need &lt;em&gt;different&lt;/em&gt; answers look more alike to the embedding model than a typical pair of questions that need the &lt;em&gt;same&lt;/em&gt; one. Embedding models are trained to put questions about the same topic close together, and near-miss pairs are exactly that: the same topic, one word apart.&lt;/p&gt;

&lt;h2&gt;
  
  
  The sweep
&lt;/h2&gt;

&lt;p&gt;We replayed the 288 questions through the cache at every threshold from 0.50 to 0.99, in 20 different random orders. The order matters, because which phrasing arrives first decides what the cache stores and what later questions get matched against. Each threshold gets its own replay, since a question that hits at 0.80 is not stored, while the same question at 0.90 misses and is.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Right and wrong cache hits for bge-m3, by threshold&lt;/strong&gt;&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;0.70&lt;/th&gt;
&lt;th&gt;0.71&lt;/th&gt;
&lt;th&gt;0.72&lt;/th&gt;
&lt;th&gt;0.73&lt;/th&gt;
&lt;th&gt;0.74&lt;/th&gt;
&lt;th&gt;0.75&lt;/th&gt;
&lt;th&gt;0.76&lt;/th&gt;
&lt;th&gt;0.77&lt;/th&gt;
&lt;th&gt;0.78&lt;/th&gt;
&lt;th&gt;0.79&lt;/th&gt;
&lt;th&gt;0.80&lt;/th&gt;
&lt;th&gt;0.81&lt;/th&gt;
&lt;th&gt;0.82&lt;/th&gt;
&lt;th&gt;0.83&lt;/th&gt;
&lt;th&gt;0.84&lt;/th&gt;
&lt;th&gt;0.85&lt;/th&gt;
&lt;th&gt;0.86&lt;/th&gt;
&lt;th&gt;0.87&lt;/th&gt;
&lt;th&gt;0.88&lt;/th&gt;
&lt;th&gt;0.89&lt;/th&gt;
&lt;th&gt;0.90&lt;/th&gt;
&lt;th&gt;0.91&lt;/th&gt;
&lt;th&gt;0.92&lt;/th&gt;
&lt;th&gt;0.93&lt;/th&gt;
&lt;th&gt;0.94&lt;/th&gt;
&lt;th&gt;0.95&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Right hits&lt;/td&gt;
&lt;td&gt;117.3&lt;/td&gt;
&lt;td&gt;117.8&lt;/td&gt;
&lt;td&gt;114.9&lt;/td&gt;
&lt;td&gt;109.9&lt;/td&gt;
&lt;td&gt;105.3&lt;/td&gt;
&lt;td&gt;98.9&lt;/td&gt;
&lt;td&gt;90.2&lt;/td&gt;
&lt;td&gt;86.1&lt;/td&gt;
&lt;td&gt;80.7&lt;/td&gt;
&lt;td&gt;72.7&lt;/td&gt;
&lt;td&gt;62.2&lt;/td&gt;
&lt;td&gt;58.6&lt;/td&gt;
&lt;td&gt;50.4&lt;/td&gt;
&lt;td&gt;40.9&lt;/td&gt;
&lt;td&gt;35.5&lt;/td&gt;
&lt;td&gt;27.4&lt;/td&gt;
&lt;td&gt;22.1&lt;/td&gt;
&lt;td&gt;16.9&lt;/td&gt;
&lt;td&gt;10.2&lt;/td&gt;
&lt;td&gt;6.8&lt;/td&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Wrong hits&lt;/td&gt;
&lt;td&gt;76.1&lt;/td&gt;
&lt;td&gt;71.2&lt;/td&gt;
&lt;td&gt;65.8&lt;/td&gt;
&lt;td&gt;61.9&lt;/td&gt;
&lt;td&gt;55.9&lt;/td&gt;
&lt;td&gt;53.1&lt;/td&gt;
&lt;td&gt;47.6&lt;/td&gt;
&lt;td&gt;41.7&lt;/td&gt;
&lt;td&gt;38.6&lt;/td&gt;
&lt;td&gt;35&lt;/td&gt;
&lt;td&gt;29.9&lt;/td&gt;
&lt;td&gt;28.6&lt;/td&gt;
&lt;td&gt;26.1&lt;/td&gt;
&lt;td&gt;21.6&lt;/td&gt;
&lt;td&gt;20&lt;/td&gt;
&lt;td&gt;16.4&lt;/td&gt;
&lt;td&gt;15.1&lt;/td&gt;
&lt;td&gt;12.7&lt;/td&gt;
&lt;td&gt;13.5&lt;/td&gt;
&lt;td&gt;13.3&lt;/td&gt;
&lt;td&gt;10&lt;/td&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;em&gt;Mean per run of 288 questions, over 20 orderings. A wrong hit is a stored answer served for a question with a different intended answer.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;At the low end the cache hits often and is wrong often. As the threshold rises, right hits fall away faster than wrong ones, because the near-miss pairs share their wording and the paraphrases, on purpose, do not. From 0.88 to 0.92, the few hits that remained were more often wrong than right. At 0.93 there was one of each, and above that none at all.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The same number means something different on each model.&lt;/strong&gt; At 0.80:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Embedding model&lt;/th&gt;
&lt;th&gt;Hit rate&lt;/th&gt;
&lt;th&gt;Right hits&lt;/th&gt;
&lt;th&gt;Wrong hits&lt;/th&gt;
&lt;th&gt;Share of hits that were right&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;bge-m3&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;32.0%&lt;/td&gt;
&lt;td&gt;62.2&lt;/td&gt;
&lt;td&gt;29.9&lt;/td&gt;
&lt;td&gt;68%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;gte-large-en-v1.5&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;52.7%&lt;/td&gt;
&lt;td&gt;100.8&lt;/td&gt;
&lt;td&gt;50.9&lt;/td&gt;
&lt;td&gt;66%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;qwen3-embedding-0.6b&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;44.8%&lt;/td&gt;
&lt;td&gt;83.4&lt;/td&gt;
&lt;td&gt;45.7&lt;/td&gt;
&lt;td&gt;65%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;multi-qa-mpnet-base-dot-v1&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;28.1%&lt;/td&gt;
&lt;td&gt;56.6&lt;/td&gt;
&lt;td&gt;24.4&lt;/td&gt;
&lt;td&gt;70%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;e5-large-v2&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;88.0%&lt;/td&gt;
&lt;td&gt;88.8&lt;/td&gt;
&lt;td&gt;164.6&lt;/td&gt;
&lt;td&gt;35%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;all-mini-lm-l6-v2&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;76.5%&lt;/td&gt;
&lt;td&gt;93.4&lt;/td&gt;
&lt;td&gt;126.9&lt;/td&gt;
&lt;td&gt;42%&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;A threshold is only meaningful next to the model that produced the scores. Advice like "use 0.85" without naming the embedding model is not advice.&lt;/p&gt;

&lt;h2&gt;
  
  
  What a wrong hit looks like
&lt;/h2&gt;

&lt;p&gt;This is &lt;code&gt;npm run show-wrong-answers&lt;/code&gt;, which replays one ordering and prints every question that was answered with another question's answer:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;EMBED_MODEL=bge-m3 THRESHOLD=0.85 npm run show-wrong-answers&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# two of the 18 wrong answers in this run, excerpted&lt;/span&gt;
&lt;span class="nv"&gt;$ EMBED_MODEL&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;bge-m3 &lt;span class="nv"&gt;THRESHOLD&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;0.85 npm run show-wrong-answers
bge-m3, threshold 0.85, ordering 0: 46 hits, 18 wrong

asked How &lt;span class="k"&gt;do &lt;/span&gt;I undo a commit that I already pushed to main?
matched How &lt;span class="k"&gt;do &lt;/span&gt;I undo my last commit that I have not pushed yet? &lt;span class="o"&gt;(&lt;/span&gt;similarity 0.861, its near-miss partner&lt;span class="o"&gt;)&lt;/span&gt;
served &lt;span class="sb"&gt;```&lt;/span&gt;
&lt;span class="o"&gt;{&lt;/span&gt;% endraw %&lt;span class="o"&gt;}&lt;/span&gt;
bash
      &lt;span class="c"&gt;# Undo the last local commit but keep the changes staged&lt;/span&gt;
      git reset &lt;span class="nt"&gt;--soft&lt;/span&gt; HEAD~1
      &lt;span class="c"&gt;# Undo the last local commit and keep the changes in your working tree (unstaged)&lt;/span&gt;

asked Reload nginx configuration on Ubuntu
matched Restart nginx on Ubuntu &lt;span class="o"&gt;(&lt;/span&gt;similarity 0.854, its near-miss partner&lt;span class="o"&gt;)&lt;/span&gt;
served
&lt;span class="o"&gt;{&lt;/span&gt;% raw %&lt;span class="o"&gt;}&lt;/span&gt;
 &lt;span class="sb"&gt;```&lt;/span&gt;bash
      &lt;span class="c"&gt;# Restart using systemd (Ubuntu 16.04+)&lt;/span&gt;
      &lt;span class="nb"&gt;sudo &lt;/span&gt;systemctl restart nginx
      &lt;span class="c"&gt;# Verify it’s running&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For 12 of the pairs we marked which answer does more damage when it is served for the other question. Those labels are judgment calls and they are in the repo so anyone can argue with them. At 0.90 on &lt;code&gt;bge-m3&lt;/code&gt;, which is a high setting for that model:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;bge-m3 at 0.90: 10.0 near-miss wrong answers per run, 2.5 of them the more destructive answer
    asked rotate-db-password-production got rotate-db-password-staging &lt;span class="k"&gt;in &lt;/span&gt;17 of 20 runs &lt;span class="o"&gt;(&lt;/span&gt;21 &lt;span class="nb"&gt;times&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
    asked cdn-purge-one got cdn-purge-all &lt;span class="k"&gt;in &lt;/span&gt;12 of 20 runs &lt;span class="o"&gt;(&lt;/span&gt;12 &lt;span class="nb"&gt;times&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
    asked ssh-rotate-user-key got ssh-rotate-host-key &lt;span class="k"&gt;in &lt;/span&gt;10 of 20 runs &lt;span class="o"&gt;(&lt;/span&gt;10 &lt;span class="nb"&gt;times&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
    asked docker-stop got docker-kill &lt;span class="k"&gt;in &lt;/span&gt;6 of 20 runs &lt;span class="o"&gt;(&lt;/span&gt;6 &lt;span class="nb"&gt;times&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Someone asking how to rotate the &lt;strong&gt;production&lt;/strong&gt; database password got the answer written for &lt;strong&gt;staging&lt;/strong&gt; in 17 of the 20 runs. The recorded answers show why that matters: none of the six staging answers mentions avoiding downtime, and three of the six production answers do.&lt;/p&gt;

&lt;h2&gt;
  
  
  What it actually saves
&lt;/h2&gt;

&lt;p&gt;Every question now pays for an embedding call, hit or miss. Every miss still pays for the full answer. These figures are built from API timings and token counts measured on DigitalOcean one question at a time, then replayed through the cache; they are not timings from a deployed cache:&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;Mean per question&lt;/th&gt;
&lt;th&gt;p50&lt;/th&gt;
&lt;th&gt;p95&lt;/th&gt;
&lt;th&gt;Cost per 1,000 questions&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;No cache&lt;/td&gt;
&lt;td&gt;8.66 s&lt;/td&gt;
&lt;td&gt;8.33 s&lt;/td&gt;
&lt;td&gt;14.53 s&lt;/td&gt;
&lt;td&gt;$0.2534&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;bge-m3 at 0.70&lt;/td&gt;
&lt;td&gt;3.48 s&lt;/td&gt;
&lt;td&gt;0.53 s&lt;/td&gt;
&lt;td&gt;13.14 s&lt;/td&gt;
&lt;td&gt;$0.0871&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;bge-m3 at 0.80&lt;/td&gt;
&lt;td&gt;6.56 s&lt;/td&gt;
&lt;td&gt;7.03 s&lt;/td&gt;
&lt;td&gt;14.54 s&lt;/td&gt;
&lt;td&gt;$0.1775&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;bge-m3 at 0.90&lt;/td&gt;
&lt;td&gt;8.74 s&lt;/td&gt;
&lt;td&gt;8.69 s&lt;/td&gt;
&lt;td&gt;15.05 s&lt;/td&gt;
&lt;td&gt;$0.2414&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;bge-m3 at 0.95 (nothing hits)&lt;/td&gt;
&lt;td&gt;9.17 s&lt;/td&gt;
&lt;td&gt;8.89 s&lt;/td&gt;
&lt;td&gt;15.05 s&lt;/td&gt;
&lt;td&gt;$0.2537&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Three things stand out.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The money is small.&lt;/strong&gt; On &lt;code&gt;gpt-oss-120b&lt;/code&gt; at DigitalOcean's &lt;a href="https://docs.digitalocean.com/products/ai-platform/details/pricing/" rel="noopener noreferrer"&gt;published prices&lt;/a&gt;, answering 1,000 of these questions costs about 25 cents. At 0.80 the cache saves 7.6 cents of that, and serves about 104 wrong answers per 1,000 questions to do it. Embedding every question cost a fraction of a cent across the whole run; the cache's overhead is time, not money.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The average improves, the tail barely does.&lt;/strong&gt; A miss now costs an embedding call plus the answer. At 0.80, p95 was about 14.5 seconds with the cache and without it. It falls only at lower thresholds: at 0.70, p95 was 13.1 seconds, and 39 percent of the hits were wrong.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A cache that never hits is pure overhead.&lt;/strong&gt; At 0.95 and above, nothing hit and every question paid about half a second extra for its embedding.&lt;/p&gt;

&lt;h2&gt;
  
  
  The fix everyone reaches for
&lt;/h2&gt;

&lt;p&gt;If the embedding cannot tell restart from reload, ask a model that can. Before serving a hit, send both questions to a small, cheap model with one instruction: would exactly the same answer be correct for both? Serve the hit only if it says yes.&lt;/p&gt;

&lt;p&gt;We ran that with &lt;code&gt;gpt-oss-20b&lt;/code&gt; as the verifier, in front of &lt;code&gt;bge-m3&lt;/code&gt;, over the same 20 orderings. Every candidate hit was checked; 1,162 distinct question pairs in all.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Threshold&lt;/th&gt;
&lt;th&gt;Without verifier: hit rate&lt;/th&gt;
&lt;th&gt;Wrong&lt;/th&gt;
&lt;th&gt;With verifier: hit rate&lt;/th&gt;
&lt;th&gt;Wrong&lt;/th&gt;
&lt;th&gt;Right hits it rejected&lt;/th&gt;
&lt;th&gt;Mean per question&lt;/th&gt;
&lt;th&gt;Cost per 1,000&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;0.70&lt;/td&gt;
&lt;td&gt;67.2%&lt;/td&gt;
&lt;td&gt;76.1&lt;/td&gt;
&lt;td&gt;40.1%&lt;/td&gt;
&lt;td&gt;0.3&lt;/td&gt;
&lt;td&gt;29.6&lt;/td&gt;
&lt;td&gt;8.57 s&lt;/td&gt;
&lt;td&gt;$0.2773&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;0.75&lt;/td&gt;
&lt;td&gt;52.8%&lt;/td&gt;
&lt;td&gt;53.1&lt;/td&gt;
&lt;td&gt;32.0%&lt;/td&gt;
&lt;td&gt;0.5&lt;/td&gt;
&lt;td&gt;21.9&lt;/td&gt;
&lt;td&gt;8.59 s&lt;/td&gt;
&lt;td&gt;$0.2679&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;0.80&lt;/td&gt;
&lt;td&gt;32.0%&lt;/td&gt;
&lt;td&gt;29.9&lt;/td&gt;
&lt;td&gt;21.2%&lt;/td&gt;
&lt;td&gt;0.5&lt;/td&gt;
&lt;td&gt;9.9&lt;/td&gt;
&lt;td&gt;8.70 s&lt;/td&gt;
&lt;td&gt;$0.2577&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;0.85&lt;/td&gt;
&lt;td&gt;15.2%&lt;/td&gt;
&lt;td&gt;16.4&lt;/td&gt;
&lt;td&gt;9.8%&lt;/td&gt;
&lt;td&gt;0.5&lt;/td&gt;
&lt;td&gt;3.0&lt;/td&gt;
&lt;td&gt;8.91 s&lt;/td&gt;
&lt;td&gt;$0.2540&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;0.90&lt;/td&gt;
&lt;td&gt;4.9%&lt;/td&gt;
&lt;td&gt;10.0&lt;/td&gt;
&lt;td&gt;1.2%&lt;/td&gt;
&lt;td&gt;0.5&lt;/td&gt;
&lt;td&gt;1.0&lt;/td&gt;
&lt;td&gt;9.22 s&lt;/td&gt;
&lt;td&gt;$0.2569&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;no cache&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;8.66 s&lt;/td&gt;
&lt;td&gt;$0.2534&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;It works.&lt;/strong&gt; Wrong answers dropped from as many as 76 per run to under one on average, and never more than two in any run, at every threshold we tried. At 0.70 the verified cache still served 40 percent of questions from memory. The only two pairs it let through were both memory request against memory limit. It erred the other way far more: it turned down 146 of the 701 genuine paraphrase pairs it was shown, which is where the lost hits went.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;And it defeats the point.&lt;/strong&gt; The mean response time with the verifier, 8.6 seconds at 0.70, is the same as with no cache at all. Each check took 3.2 seconds at the median, and it runs on every candidate hit, including the ones it then rejects, which also pay for a fresh answer. The cost went up too: $0.28 per 1,000 questions at 0.70, against $0.25 with no cache.&lt;/p&gt;

&lt;p&gt;The reason is in the token counts. The verifier replies with one word, &lt;code&gt;same&lt;/code&gt; or &lt;code&gt;different&lt;/code&gt;, and is billed for a median of &lt;strong&gt;283 completion tokens&lt;/strong&gt; to produce it, because it is a reasoning model and reasons first. The answers it was saving had a median of 324. At the smaller model's lower price, a median check still cost about 57 percent of a median answer, and it runs more often than there are hits to save.&lt;/p&gt;

&lt;p&gt;A small non-reasoning model could be faster and cheaper per check. We did not test one, and it would have to be just as reliable on exactly the pairs where the embedding models failed.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where a semantic cache is safe
&lt;/h2&gt;

&lt;p&gt;The corpus above is built from real paraphrases: different words, same question. Real traffic also contains plenty of near-verbatim repeats, the same question again with trivial differences. We made one of those for every question: lower case, no question mark, and sometimes a prefix such as "hey," or "quick one:", and compared how similar each question is to its own repeat against how similar the near-miss pairs get.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Embedding model&lt;/th&gt;
&lt;th&gt;Lowest-scoring repeat&lt;/th&gt;
&lt;th&gt;Most similar near-miss&lt;/th&gt;
&lt;th&gt;Repeats scoring above every near-miss&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;bge-m3&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;0.856&lt;/td&gt;
&lt;td&gt;0.932&lt;/td&gt;
&lt;td&gt;244 of 288&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;gte-large-en-v1.5&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;0.882&lt;/td&gt;
&lt;td&gt;0.958&lt;/td&gt;
&lt;td&gt;258 of 288&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;qwen3-embedding-0.6b&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;0.848&lt;/td&gt;
&lt;td&gt;0.949&lt;/td&gt;
&lt;td&gt;210 of 288&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;multi-qa-mpnet-base-dot-v1&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;0.866&lt;/td&gt;
&lt;td&gt;0.927&lt;/td&gt;
&lt;td&gt;274 of 288&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;e5-large-v2&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;0.945&lt;/td&gt;
&lt;td&gt;0.982&lt;/td&gt;
&lt;td&gt;186 of 288&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;all-mini-lm-l6-v2&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;0.924&lt;/td&gt;
&lt;td&gt;0.991&lt;/td&gt;
&lt;td&gt;225 of 288&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;For every model, most trivial repeats scored higher against their own original than any near-miss pair in the corpus scored against each other. That suggests a threshold set just above your worst near-miss would catch most such repeats without serving a near-miss. We did not replay the repeats through the cache, and a repeat could still score higher against some other question, so treat it as a likely saving rather than a measured one. It is also much closer to an exact-match cache than to the semantic one people have in mind. Case, punctuation and a greeting are exactly what a normalised exact-match key would catch, with no embedding call. Normalisation has risks of its own and we did not measure that comparison, but it is the baseline a semantic cache has to beat for this kind of repeat. And the high threshold only works if you have measured your own near-miss pairs to find where "just above" is.&lt;/p&gt;

&lt;h2&gt;
  
  
  What we could not conclude
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;How often near-misses happen in real traffic.&lt;/strong&gt; This corpus puts every question's near-miss partner in the stream, which is a deliberately difficult case for a cache. The hit rates and wrong-answer counts depend on how repetitive your traffic is and how many near-misses it contains, and only your own logs can say.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Whether every counted wrong answer was actually wrong.&lt;/strong&gt; Hits are scored against our intent labels, not by reading each answer, and the labels are not perfect. "Empty Redis database 2 only" sits in the same intent as "clear only the current database", though it needs one more command. We read the damaging examples in this post by hand; we did not grade all of them.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Other cache designs.&lt;/strong&gt; This is one global cosine threshold over the question alone, with nearest-neighbour lookup. Partitioning the cache by environment or resource, or only caching some kinds of answer, would behave differently, and we did not test them.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Anything beyond ops questions in English.&lt;/strong&gt; 24 pairs, one domain, one language.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;How much of this is the embedding models and how much is the corpus.&lt;/strong&gt; We wrote the paraphrases to vary their wording and the near-miss pairs to share theirs. A corpus with closer paraphrases could give higher hit rates; we did not test one.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Statistical intervals.&lt;/strong&gt; The 20 orderings are replays of one fixed set of questions, not independent samples, so the ranges we report are spread across orderings, not confidence intervals.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Vector database latency.&lt;/strong&gt; The in-memory search measured 1 to 3 ms. A vector store across the network would add its own round trip, which we did not measure.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Whether a wrong answer would have been acted on.&lt;/strong&gt; We counted wrong answers served, not wrong actions taken.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What we would do
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Build your own near-miss set before you pick a threshold.&lt;/strong&gt; Write down the questions your users ask that differ by one word and need different answers. Score them with your embedding model. Your threshold has to sit above the most similar of those pairs, and that number is specific to the model and to your domain.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Treat a threshold as a property of a model.&lt;/strong&gt; Changing the embedding model without re-measuring changes what the cache does, sometimes from mostly right to mostly wrong at the same setting.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Keep answers that act on things out of the cache.&lt;/strong&gt; An explanation of what a readiness probe is can be served twice. A command to run against production should not come from a question that merely looked similar.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Put the key facts in the key.&lt;/strong&gt; The expensive mistakes here were a word apart: staging or production, one database or all of them, pushed or not. If the environment, the resource and the verb are part of the cache key, the similarity search never gets the chance to confuse them.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Measure the whole trip.&lt;/strong&gt; The embedding call is paid on every question, and it makes every miss slower. Count it before deciding the cache saves anything.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;If you add a verifier, price it.&lt;/strong&gt; Checking each hit with a model made the cache correct here, but no faster and dearer than no cache, because the verifier reasoned its way to a one-word answer. Measure it on your own near-miss pairs and count its tokens before you ship it.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The harness, the 288 labelled questions, every embedding and answer, and the scripts that reproduce every number here are in the repo.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://devops-daily.com/posts/semantic-cache-answers-the-wrong-question" rel="noopener noreferrer"&gt;devops-daily.com&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>devops</category>
      <category>ai</category>
      <category>caching</category>
      <category>embeddings</category>
    </item>
    <item>
      <title>Our task categorizer routed 46% of tasks. A $0.04/MTok decision model routes 97%.</title>
      <dc:creator>Marcell Havlik</dc:creator>
      <pubDate>Tue, 22 Sep 2026 09:54:08 +0000</pubDate>
      <link>https://dev.to/todoforai/our-task-categorizer-routed-46-of-tasks-a-004mtok-decision-model-routes-97-7mf</link>
      <guid>https://dev.to/todoforai/our-task-categorizer-routed-46-of-tasks-a-004mtok-decision-model-routes-97-7mf</guid>
      <description>&lt;p&gt;&lt;em&gt;Cosine similarity between a task title and a group description is not a classifier.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Bench, cases, raw verdicts: &lt;code&gt;backend/bench/categorization&lt;/code&gt; in the TODOforAI repo. Total&lt;br&gt;
cost ~$0.50.&lt;/p&gt;
&lt;h2&gt;
  
  
  Setup
&lt;/h2&gt;

&lt;p&gt;TODOforAI boards have groups (&lt;em&gt;SEO&lt;/em&gt;, &lt;em&gt;Paid&lt;/em&gt;, &lt;em&gt;Frontend&lt;/em&gt;…); new tasks get a suggested&lt;br&gt;
group. V1: embed title and group name + description, nearest cosine wins. Embedder&lt;br&gt;
&lt;code&gt;Qwen/Qwen3-Embedding-4B&lt;/code&gt; via DeepInfra, 512 dims, unit-normalized, query instruction&lt;br&gt;
&lt;em&gt;"Given a task, retrieve the workstream whose purpose best matches the task"&lt;/em&gt;. Abstain if&lt;br&gt;
best &amp;lt; 0.50 or margin to runner-up &amp;lt; 0.06.&lt;/p&gt;

&lt;p&gt;100% precision on 18 hand cases. Most of my real board in Unsorted.&lt;/p&gt;
&lt;h2&gt;
  
  
  Judges
&lt;/h2&gt;

&lt;p&gt;Same 138 titles, same 8 groups, same description text, no example tasks — exactly what&lt;br&gt;
production sees. Titles written like real todos: short, half Hungarian, 15 nonsense&lt;br&gt;
(&lt;code&gt;call mom&lt;/code&gt;, &lt;code&gt;asdf&lt;/code&gt;), 5 ambiguous.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;embedding&lt;/strong&gt; — production, unchanged&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;jev&lt;/strong&gt; — &lt;a href="https://vercel.com/ai-gateway/models/jev" rel="noopener noreferrer"&gt;TypeSafe Jev&lt;/a&gt;, non-autoregressive
decision model on Vercel AI Gateway. One &lt;code&gt;choice&lt;/code&gt; question, 8 groups + &lt;code&gt;none&lt;/code&gt;.
$0.04/MTok in, output free.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;sonnet&lt;/strong&gt; — Claude Sonnet 4.6, temp 0, one slug or &lt;code&gt;none&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;opus&lt;/strong&gt; — Claude Opus 5, same prompt. &lt;strong&gt;The truth.&lt;/strong&gt; My labels are one more opinion.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Trap: Opus 5 reasons before answering; &lt;code&gt;max_tokens: 20&lt;/code&gt; looked like abstaining on 88/138.&lt;br&gt;
Give it 400.&lt;/p&gt;
&lt;h2&gt;
  
  
  Numbers
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Judge&lt;/th&gt;
&lt;th&gt;agrees with Opus&lt;/th&gt;
&lt;th&gt;wrong group&lt;/th&gt;
&lt;th&gt;missed&lt;/th&gt;
&lt;th&gt;138 tasks&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;embedding&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;77/138&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;60/112&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;8 s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;jev&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;130/138&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;3/112&lt;/td&gt;
&lt;td&gt;24 s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;sonnet 4.6&lt;/td&gt;
&lt;td&gt;131/138&lt;/td&gt;
&lt;td&gt;7&lt;/td&gt;
&lt;td&gt;0/112&lt;/td&gt;
&lt;td&gt;57 s&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Opus routed 112, called &lt;code&gt;none&lt;/code&gt; on 26 (15 nonsense + 11 vague). Embedding: wrong once,&lt;br&gt;
missed 54%. Jev: 97% routed, 5 wrong. Sonnet's 7 wrong are coin flips with Opus.&lt;br&gt;
On my real board Jev and Opus agree 21/26.&lt;/p&gt;
&lt;h2&gt;
  
  
  Why the embedding abstains
&lt;/h2&gt;

&lt;p&gt;Not the margin:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;                         agree   wrong   missed
margin 0.06  floor 0.50    76      2       60     ← production
margin 0     floor 0.50    79     41       18
margin 0     floor 0       76     62        0
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Drop the gate and Unsorted becomes wrong group. &lt;strong&gt;Top-1 is wrong on ~45% of tasks.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;bun test flaky on CI          frontend 0.540   development 0.537
call mom                      email 0.540      plg 0.525
asdf                          development 0.663  frontend 0.659
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Scores sit in 0.42–0.72; &lt;code&gt;asdf&lt;/code&gt; outscores a real task, so no floor finds nonsense.&lt;br&gt;
Frontend/Development, Paid/SEO, Enterprise/Paid overlap, so real tasks sit inside any&lt;br&gt;
margin. Jev and the LLMs know &lt;em&gt;outcomes&lt;/em&gt;: a CI flake is Development because of what fixing&lt;br&gt;
it achieves. Similarity can't say that.&lt;/p&gt;

&lt;h2&gt;
  
  
  What shipped
&lt;/h2&gt;

&lt;p&gt;Jev routes; embedding is fallback when the gateway is unset or down. Batches of 25,&lt;br&gt;
&lt;code&gt;none&lt;/code&gt; explicit, accept probability ≥ 0.5. ~0.3 s per task.&lt;/p&gt;

&lt;p&gt;Not Sonnet: 57 s vs 24 s and ~100× the price, for a suggestion 400 ms after you stop typing.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Router&lt;/th&gt;
&lt;th&gt;per 1,000 tasks&lt;/th&gt;
&lt;th&gt;per 1M tasks&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;embedding (~15 tok, $0.01/MTok)&lt;/td&gt;
&lt;td&gt;$0.0002&lt;/td&gt;
&lt;td&gt;$0.15&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;strong&gt;jev&lt;/strong&gt; (~260 tok catalog per question, $0.04/MTok in, out free)&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;$0.01&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;$10&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;sonnet 4.6 (~350 in / 10 out)&lt;/td&gt;
&lt;td&gt;$1.20&lt;/td&gt;
&lt;td&gt;$1,200&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;opus 5 (~350 in / ~200 out with reasoning)&lt;/td&gt;
&lt;td&gt;$7&lt;/td&gt;
&lt;td&gt;$7,000&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;List prices, 8 groups. Jev costs ~70× the embedding and routes twice as much.&lt;/p&gt;

&lt;p&gt;Jev's 5 wrong are channel-vs-outcome (&lt;code&gt;sponsor a newsletter, measure signups&lt;/code&gt; → email,&lt;br&gt;
not paid). Confidence is lower when wrong (0.70 vs 0.94), but floor 0.6 trades 2 wrong&lt;br&gt;
for 7 Unsorted. We keep the 5.&lt;/p&gt;

&lt;h2&gt;
  
  
  Takeaways
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Precision alone lies. Print recall against something you trust.&lt;/li&gt;
&lt;li&gt;Score against a strong model, not your own labels.&lt;/li&gt;
&lt;li&gt;Check the reference's output budget. Truncated looks cautious.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;138 cases is small. It's enough to see a 54% recall gap; that's the only claim.&lt;/p&gt;

</description>
      <category>benchmarks</category>
      <category>categorization</category>
      <category>embeddings</category>
      <category>llm</category>
    </item>
    <item>
      <title>Embeddings explained for people who write code</title>
      <dc:creator>VLAD</dc:creator>
      <pubDate>Fri, 18 Sep 2026 22:08:01 +0000</pubDate>
      <link>https://dev.to/vladut02/embeddings-explained-for-people-who-write-code-5aja</link>
      <guid>https://dev.to/vladut02/embeddings-explained-for-people-who-write-code-5aja</guid>
      <description>&lt;p&gt;Type &lt;em&gt;"how do I reset my password"&lt;/em&gt; into a good search box, and it finds a page titled &lt;em&gt;"account recovery."&lt;/em&gt; Zero words in common. Nothing matched on text. So how did it know they mean the same thing?&lt;/p&gt;

&lt;p&gt;Your app turned &lt;strong&gt;meaning into numbers&lt;/strong&gt; — and once meaning is numbers, a computer can measure it. That trick is called an &lt;strong&gt;embedding&lt;/strong&gt;. Get this one idea and half the AI buzzwords — "vector search," "cosine similarity," "vector database" — stop being scary.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Prefer to watch? Full walkthrough with the meaning-space animation:&lt;/p&gt;

&lt;p&gt;  &lt;iframe src="https://www.youtube.com/embed/MfBKyEhz-x8?start=3" width="710" height="399"&gt;
  &lt;/iframe&gt;
&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  An embedding is a list of numbers
&lt;/h2&gt;

&lt;p&gt;Start with the machine itself. You hand it a piece of text — a word, a sentence, a whole paragraph. It hands back a &lt;strong&gt;list of numbers&lt;/strong&gt;. That list &lt;em&gt;is&lt;/em&gt; the embedding. Same text in, same numbers out, every time.&lt;/p&gt;

&lt;p&gt;How long is the list? For a common model — OpenAI's &lt;a href="https://platform.openai.com/docs/guides/embeddings" rel="noopener noreferrer"&gt;&lt;code&gt;text-embedding-3-small&lt;/code&gt;&lt;/a&gt; — it's &lt;strong&gt;1,536&lt;/strong&gt; numbers. That sounds like a lot, until you think of each number as a &lt;strong&gt;coordinate&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Two numbers place a point on a map. Three place it in a room. 1,536 place it in a space you can't picture — but the math works exactly the same as the map.&lt;/p&gt;

&lt;h2&gt;
  
  
  Meaning becomes position
&lt;/h2&gt;

&lt;p&gt;Here's the whole point of that space: the model places text so that &lt;strong&gt;similar meaning lands in a similar spot&lt;/strong&gt;. "cat" and "dog" end up as neighbors. "car" ends up far away.&lt;/p&gt;

&lt;p&gt;Nobody wrote that rule. The model &lt;strong&gt;learned&lt;/strong&gt; it — it read a mountain of text and noticed which words keep the same company. Words used the same way get pushed together; words used differently get pushed apart. This is an old idea from linguistics called the &lt;a href="https://en.wikipedia.org/wiki/Distributional_semantics" rel="noopener noreferrer"&gt;distributional hypothesis&lt;/a&gt;: a word's meaning is shaped by the words it usually appears next to. Meaning, in this space, is just &lt;strong&gt;where you land&lt;/strong&gt; relative to everything else.&lt;/p&gt;

&lt;h2&gt;
  
  
  "Cosine similarity" is just an angle
&lt;/h2&gt;

&lt;p&gt;Now the real question in search: are these two things &lt;em&gt;close&lt;/em&gt;? You embed the question, you embed every document, and you grab the points &lt;strong&gt;nearest&lt;/strong&gt; the question. But "nearest" means one specific thing — and it's simpler than it sounds.&lt;/p&gt;

&lt;p&gt;Draw an arrow from the center of the space out to each point.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Two texts with &lt;strong&gt;similar&lt;/strong&gt; meaning? Their arrows point almost the same way — a &lt;strong&gt;small angle&lt;/strong&gt; between them.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Unrelated&lt;/strong&gt; texts? The arrows point off in different directions — a wide angle.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That angle &lt;em&gt;is&lt;/em&gt; cosine similarity. A value near &lt;strong&gt;1&lt;/strong&gt; means the arrows point the same way (very similar). A value near &lt;strong&gt;0&lt;/strong&gt; means they're unrelated. That's the whole comparison — "cosine similarity" is just measuring the angle between two arrows.&lt;/p&gt;

&lt;h2&gt;
  
  
  In code, it's three lines
&lt;/h2&gt;

&lt;p&gt;Every "AI search" feature you've used is basically this, under a nicer name:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;doc&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="s2"&gt;account recovery steps&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;      &lt;span class="c1"&gt;// → [0.02, -0.91, …] · 1536 numbers&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;query&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="s2"&gt;how do I reset my password?&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;// cosine: 1 = same direction, 0 = unrelated&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;score&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;cosine&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="nx"&gt;doc&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;                   &lt;span class="c1"&gt;// ≈ 0.86 — close&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Embed your text, embed your query, compare them, keep the closest. (Those scores are illustrative — the real point is &lt;em&gt;high = same direction&lt;/em&gt;.)&lt;/p&gt;

&lt;h2&gt;
  
  
  Trap 1: the numbers only make sense inside one model
&lt;/h2&gt;

&lt;p&gt;These coordinates are only meaningful &lt;strong&gt;within a single model&lt;/strong&gt;. A vector from model A and a vector from model B are not comparable — it's gibberish. Same coordinates, different maps.&lt;/p&gt;

&lt;p&gt;So embed your query and your documents with the &lt;strong&gt;exact same model&lt;/strong&gt;, always. Change the model, and you have to re-embed everything you're searching over.&lt;/p&gt;

&lt;h2&gt;
  
  
  Trap 2: embeddings capture meaning, not exact strings
&lt;/h2&gt;

&lt;p&gt;Embeddings are great at meaning — which makes them bad at things that &lt;em&gt;have&lt;/em&gt; no meaning. An error code. A product ID. &lt;code&gt;SKU-4417&lt;/code&gt;. There's nothing to place on the meaning-map; it's just an exact string, and pure vector search fumbles it, because nothing is "close in meaning" to a serial number.&lt;/p&gt;

&lt;p&gt;That's why real systems run &lt;strong&gt;both&lt;/strong&gt;: keyword search to catch exact strings, vector search to catch meaning, and the results merged. If you've built RAG and watched it miss an obvious error code, this is usually why.&lt;/p&gt;

&lt;h2&gt;
  
  
  The takeaway
&lt;/h2&gt;

&lt;p&gt;An embedding, start to finish:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Text becomes a list of numbers.&lt;/li&gt;
&lt;li&gt;The numbers are coordinates in a meaning-space.&lt;/li&gt;
&lt;li&gt;Close in meaning → close in space.&lt;/li&gt;
&lt;li&gt;"Similarity" is just the angle between two arrows.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Once you see it as numbers on a map, the buzzwords fall away — and you can actually &lt;strong&gt;debug&lt;/strong&gt; your search instead of trusting it. When a result looks wrong, you're not staring at magic; you're asking a concrete question about distance on a map.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What's the weirdest match your vector search ever returned — the one that made no sense at all?&lt;/strong&gt; Drop it in the comments — I read them.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;I make &lt;a href="https://www.youtube.com/channel/UCUO8Uo5LsEy1b9eRkrH6JNg" rel="noopener noreferrer"&gt;Vlad's Stack&lt;/a&gt; — how the tools you use every day actually work, for people who write code. Full video walkthrough is above.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>embeddings</category>
      <category>ai</category>
      <category>machinelearning</category>
      <category>vectordatabase</category>
    </item>
  </channel>
</rss>
