<?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: Aayush Chaturvedi</title>
    <description>The latest articles on DEV Community by Aayush Chaturvedi (@4vedi_aayush).</description>
    <link>https://dev.to/4vedi_aayush</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3975798%2Fb4be9428-e982-42a9-a2b4-af7fdb1f6eee.jpg</url>
      <title>DEV Community: Aayush Chaturvedi</title>
      <link>https://dev.to/4vedi_aayush</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/4vedi_aayush"/>
    <language>en</language>
    <item>
      <title>Stop Paying to Re-Embed the Same RAG Data Every Night</title>
      <dc:creator>Aayush Chaturvedi</dc:creator>
      <pubDate>Wed, 10 Jun 2026 07:17:20 +0000</pubDate>
      <link>https://dev.to/4vedi_aayush/stop-paying-to-re-embed-the-same-rag-data-every-night-4d62</link>
      <guid>https://dev.to/4vedi_aayush/stop-paying-to-re-embed-the-same-rag-data-every-night-4d62</guid>
      <description>&lt;p&gt;Right now, a cron job at your company is spending $40 a night to discover that nothing changed.&lt;/p&gt;

&lt;p&gt;You know the one. Every team doing RAG has it: a 200-line PySpark job that reads a Delta table, chunks the rows, calls an embedding API in a &lt;code&gt;pandas_udf&lt;/code&gt;, and upserts the vectors to Qdrant.&lt;/p&gt;

&lt;p&gt;It was written by someone who has since left the company, and nobody has read it since. It runs every night at 2am, and every night it makes the same four quiet mistakes:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;It re-embeds everything, every night.&lt;/strong&gt; All 10M rows, even though ~95% of them didn't change. That's the $40: paid in full to re-confirm that nothing happened (back-of-envelope: ~200 tokens/row at $0.02/1M).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;It has no memory.&lt;/strong&gt; Run it twice, get double the upserts. Kill it mid-run, get partial state and no clean retry.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;It turns model upgrades into a weekend war room.&lt;/strong&gt; OpenAI deprecates &lt;code&gt;ada-002&lt;/code&gt;, you're supposed to move to &lt;code&gt;3-small&lt;/code&gt;, and the official answer is "build a new index and re-embed all of it." So most teams just don't, for a year.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;It can't answer the boring questions.&lt;/strong&gt; Which table cost what last week? A user invoked GDPR, can you prove their vector is actually gone? In most teams: no.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;None of these are hard problems. They're just nobody's job. So they survive, untouched, in that one file at every company, until the bill or the audit makes them someone's job at the worst possible time.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;TL;DR&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;I built &lt;a href="https://github.com/aayush4vedi/drift-spark" rel="noopener noreferrer"&gt;Drift&lt;/a&gt; to turn that one file into three lifecycle functions plus a ledger:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;embed()&lt;/code&gt; - produce vectors, with a memory&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;watch()&lt;/code&gt; - refresh only what changed&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;migrate()&lt;/code&gt; - upgrade embedding models without rebuilding the index, when the adapter passes a recall gate&lt;/li&gt;
&lt;li&gt;the ledger - cost, provenance, and compliance under all three&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Try it: &lt;code&gt;pip install drift-spark&lt;/code&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Drift makes the embedding lifecycle explicit:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Lifecycle stage&lt;/th&gt;
&lt;th&gt;The old script&lt;/th&gt;
&lt;th&gt;Drift&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Produce vectors&lt;/td&gt;
&lt;td&gt;re-embeds everything&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;embed()&lt;/code&gt; dedups and writes idempotently&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Refresh data&lt;/td&gt;
&lt;td&gt;scans the whole table&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;watch()&lt;/code&gt; reads only the Delta Change Data Feed&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Upgrade models&lt;/td&gt;
&lt;td&gt;rebuilds the index&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;migrate()&lt;/code&gt; learns a query-time adapter when it's safe&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Audit runs&lt;/td&gt;
&lt;td&gt;grep the logs and hope&lt;/td&gt;
&lt;td&gt;the &lt;code&gt;ledger&lt;/code&gt; records cost, provenance, and deletes&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Everything below runs with no API key (shadow mode uses deterministic mock vectors), so you can try it for $0.&lt;/p&gt;

&lt;p&gt;The upgrade stage has the trick I find genuinely cool, so I've saved it for the middle. But the parts you'd use every day are the first two.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Credit:&lt;/strong&gt; the zero-reindex migration below is inspired by a &lt;a href="https://arxiv.org/abs/2509.23471" rel="noopener noreferrer"&gt;2025 EMNLP paper&lt;/a&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The rest of this post takes the three in turn, in the order a pipeline meets them, each with the code you'd actually run and the numbers behind the claim.&lt;/p&gt;




&lt;h2&gt;
  
  
  1. &lt;code&gt;embed()&lt;/code&gt; - produce, with a memory
&lt;/h2&gt;

&lt;p&gt;The declarative replacement for the hand-rolled loop. On every run it:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;dedups across runs (an MD5 per text, scoped to the &lt;code&gt;(model, sink)&lt;/code&gt; pair)&lt;/li&gt;
&lt;li&gt;batches the calls and backs off on 429s&lt;/li&gt;
&lt;li&gt;writes idempotent point IDs&lt;/li&gt;
&lt;li&gt;records what the run cost
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;drift&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;embed&lt;/span&gt;

&lt;span class="n"&gt;run&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="n"&gt;df&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;df&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;text_col&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;body&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;openai/text-embedding-3-small&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;sink&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;qdrant://localhost:6333/docs&lt;/span&gt;&lt;span class="sh"&gt;"&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="n"&gt;run&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="c1"&gt;# EmbedRun(n_rows_processed=10000, n_rows_deduped=0, cost_usd=0.0038, ...)
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Run it again on the same data:&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="n"&gt;run2&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="n"&gt;df&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;df&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;text_col&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;body&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;openai/text-embedding-3-small&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
             &lt;span class="n"&gt;sink&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;qdrant://localhost:6333/docs&lt;/span&gt;&lt;span class="sh"&gt;"&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="n"&gt;run2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="c1"&gt;# EmbedRun(n_rows_processed=10000, n_rows_deduped=10000, cost_usd=0.0, ...)
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The cost drops from &lt;code&gt;$0.0038&lt;/code&gt; to &lt;strong&gt;&lt;code&gt;$0.0&lt;/code&gt;&lt;/strong&gt;: zero API calls, zero upserts, because nothing changed and Drift knows it. That's the difference between the nightly job paying ~$40 once and paying it again every night for nothing.&lt;/p&gt;

&lt;p&gt;No API key handy? &lt;code&gt;shadow_mode=True&lt;/code&gt; does the same with deterministic mock vectors, so dedup and lineage are testable in CI for $0.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. &lt;code&gt;watch()&lt;/code&gt; - refresh only what changed
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;embed()&lt;/code&gt; still reads the whole table. The function you actually want on a schedule is &lt;code&gt;watch()&lt;/code&gt;, which reads the Delta &lt;strong&gt;Change Data Feed&lt;/strong&gt; from the last checkpoint and re-embeds &lt;em&gt;only the rows that changed&lt;/em&gt;:&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;from&lt;/span&gt; &lt;span class="n"&gt;drift&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;watch&lt;/span&gt;

&lt;span class="n"&gt;run&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;watch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;source_table&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;catalog.docs&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;text_col&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;body&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;sink&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;qdrant://localhost:6333/docs&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;openai/text-embedding-3-small&lt;/span&gt;&lt;span class="sh"&gt;"&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="n"&gt;run&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="c1"&gt;# WatchRun(n_inserted=31200, n_updated=18800, n_deleted=412, ...)
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Under the hood it's five lines of Delta:&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="n"&gt;cdf&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;spark&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;read&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;format&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;delta&lt;/span&gt;&lt;span class="sh"&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;option&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;readChangeFeed&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;true&lt;/span&gt;&lt;span class="sh"&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;option&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;startingVersion&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;since_version&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
       &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;table&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;source_table&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The change feed sorts itself into three paths:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;inserts and updates go through &lt;code&gt;embed()&lt;/code&gt;, so they inherit dedup and cost tracking for free&lt;/li&gt;
&lt;li&gt;deletes remove the matching vector by its deterministic ID&lt;/li&gt;
&lt;li&gt;the new Delta version is written back to the ledger, so the next run resumes exactly where this one stopped&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;On a 10M-row table with 5% daily churn, that's 500K rows touched instead of 10M, in both API spend and Spark compute.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. &lt;code&gt;migrate()&lt;/code&gt; - upgrade without re-indexing (the cool part)
&lt;/h2&gt;

&lt;p&gt;Here's the belief almost everyone holds that turns out to be mostly false: changing your embedding model means re-embedding every vector you own. For a billion-vector store that's roughly &lt;strong&gt;$6,000 and ~28 hours&lt;/strong&gt;, with a stale index the whole time.&lt;/p&gt;

&lt;p&gt;It turns out two embedding models mostly disagree about &lt;em&gt;orientation&lt;/em&gt;, not &lt;em&gt;meaning&lt;/em&gt;: their vector spaces are very nearly rotations of each other. Find the rotation &lt;code&gt;R&lt;/code&gt; and apply it at query time, and the old index never moves. This is the &lt;a href="https://en.wikipedia.org/wiki/Orthogonal_Procrustes_problem" rel="noopener noreferrer"&gt;Orthogonal Procrustes problem&lt;/a&gt;, closed-form since 1966, no gradient descent and no GPU:&lt;/p&gt;

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

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Why it's safe:&lt;/strong&gt; an orthogonal matrix preserves cosine similarity exactly.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;cos(u, v) == cos(Ru, Rv)&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;So the nearest-neighbour structure your index depends on survives the rotation untouched.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The whole upgrade then ships as one ~9 MB &lt;code&gt;.npy&lt;/code&gt; file.&lt;/p&gt;

&lt;h3&gt;
  
  
  The guardrail matters more than the trick
&lt;/h3&gt;

&lt;p&gt;And, loudly: it doesn't always work. Some model pairs are architecturally incompatible. GloVe to MPNet tops out around 71.5% recall, which is useless.&lt;/p&gt;

&lt;p&gt;So &lt;code&gt;migrate(strategy="drift-adapter")&lt;/code&gt; measures Adapted Recall Rate and refuses to hand you an adapter below 0.97. Below that, it raises:&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="n"&gt;AdapterQualityError&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;and tells you to fall back to the dull-but-safe path:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;dual-write -&amp;gt; re-embed into a fresh collection -&amp;gt; run both -&amp;gt; cut over
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The trick has a guardrail, and the guardrail is the actually-responsible engineering. Full derivation: &lt;a href="https://github.com/aayush4vedi/drift-spark/blob/main/docs/drift-adapter-math.md" rel="noopener noreferrer"&gt;docs/drift-adapter-math.md&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Credit where it's due: the method isn't mine. The rotation comes from &lt;a href="https://arxiv.org/abs/2509.23471" rel="noopener noreferrer"&gt;a 2025 EMNLP paper&lt;/a&gt;; what I added is the implementation and the recall guardrail that decides when to trust it.&lt;/p&gt;

&lt;h2&gt;
  
  
  The ledger - cost, provenance, compliance
&lt;/h2&gt;

&lt;p&gt;Under all of the above is a local SQLite ledger that answers the boring questions:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;what every run cost&lt;/li&gt;
&lt;li&gt;which row and model produced each vector
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;drift.ledger&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Ledger&lt;/span&gt;
&lt;span class="n"&gt;ledger&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Ledger&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="n"&gt;ledger&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;cost_by_model&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;          &lt;span class="c1"&gt;# [{'model': 'openai/text-embedding-3-small', 'cost_usd': 4.27}]
&lt;/span&gt;&lt;span class="n"&gt;ledger&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;provenance&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;3f2a1b...&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="c1"&gt;# the GDPR-delete audit answer: which row, which model, what cost
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's all four pieces. Here's how they fit together in one run:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F9grubuhk461o4mn9tbqk.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F9grubuhk461o4mn9tbqk.png" alt="Drift architecture: change-data-feed sources flow through watch() and embed() into Qdrant or pgvector, with a SQLite lineage ledger underneath and migrate() reading it to upgrade models" width="800" height="708"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;&lt;code&gt;watch()&lt;/code&gt; reads changed rows from the source, &lt;code&gt;embed()&lt;/code&gt; produces vectors into Qdrant or pgvector, the ledger records every run, and &lt;code&gt;migrate()&lt;/code&gt; reads the ledger to decide what to re-embed. Iceberg and Postgres sources are planned.&lt;/em&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  "Isn't this just &amp;lt;X&amp;gt;?"
&lt;/h2&gt;

&lt;p&gt;People will ask if this already exists. Not quite. Two of these are adjacent; one is a building block.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Databricks' &lt;strong&gt;Mosaic AI Vector Search&lt;/strong&gt; is the closest. It syncs an index continuously and well, but it's Databricks-only and stops at sync. No model migration, no cost visibility.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Daft&lt;/strong&gt; is an embedding-capable runtime, an engine rather than a manager, so refresh, cost, and migration aren't its job. Drift could one day sit on top of it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;qdrant-spark&lt;/strong&gt; is just the sink driver, the last few lines of the old script. Drift could wrap it.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The adversarial, where-Drift-loses version is in &lt;a href="https://github.com/aayush4vedi/drift-spark/blob/main/docs/competitors.md" rel="noopener noreferrer"&gt;docs/competitors.md&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Known limits
&lt;/h2&gt;

&lt;p&gt;Context first: Drift is a two-week, open-source proof of concept, and the groundwork for something larger. The edges worth knowing before you lean on it:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The dollar figures are estimates, not benchmarks.&lt;/strong&gt; They assume particular table sizes and OpenAI's current pricing, which is hardcoded. Check them against &lt;a href="https://platform.openai.com/docs/pricing" rel="noopener noreferrer"&gt;your own numbers&lt;/a&gt; before they go anywhere that matters.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;migrate()&lt;/code&gt; implements Phases 1-3 of the paper, not the whole thing.&lt;/strong&gt; You get the adapter training and the recall gate, but the transparent query-time client (and the MLP variant) aren't built yet, so for now you apply the rotation to queries yourself.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;embed()&lt;/code&gt; doesn't scale past a few million rows yet.&lt;/strong&gt; It pulls the text back to the Spark driver, so it's fine to ~2M rows, not a distributed path.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A few rough edges are real.&lt;/strong&gt; pgvector is write-only, model migration is Qdrant-only, and the SQLite ledger isn't safe under concurrent Spark executors.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;None of this is fatal to what Drift does today.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try it in 60 seconds
&lt;/h2&gt;

&lt;p&gt;No API key needed - &lt;code&gt;shadow_mode=True&lt;/code&gt; uses deterministic mock vectors at zero cost.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pip &lt;span class="nb"&gt;install &lt;/span&gt;drift-spark
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;pyspark.sql&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;SparkSession&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;drift&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;embed&lt;/span&gt;

&lt;span class="n"&gt;spark&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;SparkSession&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;master&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;local[*]&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;getOrCreate&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="n"&gt;df&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;spark&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createDataFrame&lt;/span&gt;&lt;span class="p"&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;id&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;1&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;body&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;Customer reports login issue after password reset.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&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;id&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;2&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;body&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;Invoice for Q1 shows wrong billing address.&lt;/span&gt;&lt;span class="sh"&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;print&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="n"&gt;df&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;df&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;text_col&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;body&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;openai/text-embedding-3-small&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;sink&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;qdrant://localhost:6333/demo&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;shadow_mode&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;GitHub (start here): &lt;a href="https://github.com/aayush4vedi/drift-spark" rel="noopener noreferrer"&gt;https://github.com/aayush4vedi/drift-spark&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Install: &lt;code&gt;pip install drift-spark&lt;/code&gt; (on &lt;a href="https://pypi.org/project/drift-spark/" rel="noopener noreferrer"&gt;PyPI&lt;/a&gt;)&lt;/li&gt;
&lt;li&gt;The paper: &lt;a href="https://arxiv.org/abs/2509.23471" rel="noopener noreferrer"&gt;https://arxiv.org/abs/2509.23471&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What I'd genuinely like feedback on
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;What would stop you from using this?&lt;/strong&gt; If you already run a nightly embedding job, I'd like the honest blocker: the hard Spark dependency, the Qdrant-only sink, something else?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Which backend would make this usable for you?&lt;/strong&gt; &lt;code&gt;watch()&lt;/code&gt; is Delta and Qdrant only today, but a pluggable source and sink is the next thing I'm building. Iceberg, Postgres, LanceDB, Pinecone, Weaviate?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Is zero-reindex migration worth a breaking change?&lt;/strong&gt; Making it seamless means a query-time client you'd swap into your search code. Worth it, or is plain re-embed-and-cut-over good enough?&lt;/li&gt;
&lt;/ol&gt;




&lt;p&gt;"Not all those who wander are lost, but drifting embeddings usually are."&lt;br&gt;- J.R.R. Tolkien&lt;br&gt;(adjusted for production)&lt;/p&gt;

</description>
      <category>rag</category>
      <category>ai</category>
      <category>spark</category>
      <category>showdev</category>
    </item>
  </channel>
</rss>
