<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/">
  <channel>
    <title>Aman Trivedi — articles</title>
    <link>https://www.amantrivedi.com/articles/</link>
    <atom:link href="https://www.amantrivedi.com/articles/feed.xml" rel="self" type="application/rss+xml"/>
    <description>Technical articles and engineering notes from Aman Trivedi on software, data, AI, systems and debugging.</description>
    <language>en</language>
    <lastBuildDate>Fri, 18 Sep 2026 00:00:00 +0000</lastBuildDate>
    <item>
      <title>Notes on a policy RAG assistant</title>
      <link>https://www.amantrivedi.com/articles/rag-policy-assistant-notes/</link>
      <guid isPermaLink="true">https://www.amantrivedi.com/articles/rag-policy-assistant-notes/</guid>
      <pubDate>Fri, 18 Sep 2026 00:00:00 +0000</pubDate>
      <description>Design notes on building a retrieval-augmented assistant for internal policy: scoping, private documents and awkward tables.</description>
      <category>RAG</category>
      <category>Qdrant</category>
      <category>AWS Bedrock</category>
      <category>FastAPI</category>
      <content:encoded><![CDATA[<p>Notes from building the <a href="/projects/policy-assistant/">internal policy assistant</a>.</p>
<h2 id="the-pipeline">The pipeline</h2>
<p>Detect the question's language, translate if needed, search the vector store, then generate an answer grounded in what was retrieved.</p>
<h2 id="scope-retrieval-by-department">Scope retrieval by department</h2>
<p>Policy chat is scoped by department, so people retrieve from the documents they should see. Filtering at retrieval time is safer than asking the model to ignore what it was handed.</p>
<h2 id="private-documents-are-a-different-mode">Private documents are a different mode</h2>
<p>"My Docs" is separate: uploaded files belong to a session and are purged after a time limit, so private material never mixes with organisation-wide policy.</p>
<h2 id="tables-don-t-survive-extraction">Tables don't survive extraction</h2>
<p>Credit policy is full of eligibility tables with conditional logic across loan products. Automated PDF extraction mangled them, so I restructured those tables by hand before indexing. Slower, but it's the difference between a right answer and a plausible one.</p>]]></content:encoded>
    </item>
    <item>
      <title>Notes on a hardware-style plugin UI in JUCE</title>
      <link>https://www.amantrivedi.com/articles/juce-webview-ui-notes/</link>
      <guid isPermaLink="true">https://www.amantrivedi.com/articles/juce-webview-ui-notes/</guid>
      <pubDate>Fri, 18 Sep 2026 00:00:00 +0000</pubDate>
      <description>Bugs from building a skeuomorphic plugin UI in React inside a JUCE WebView: rotation, transparency and rounding.</description>
      <category>JUCE</category>
      <category>React</category>
      <category>C++</category>
      <category>Audio</category>
      <content:encoded><![CDATA[<p>Notes from the UI side of <a href="/projects/vintage-ml-compressor/">Vintage ML Compressor</a>, where React and TypeScript render inside a JUCE <code>WebBrowserComponent</code>.</p>
<ul>
<li><strong>VU needle rotation.</strong> The needle rotated around the wrong point because of a transform bug. Fix the transform origin first, then the angle math.</li>
<li><strong>White boxes around knobs.</strong> The knob PNGs had no alpha channel. They needed transparent backgrounds (or chroma-keying) to sit on the panel.</li>
<li><strong>Filmstrip drift.</strong> Mapping a value to a filmstrip frame drifted because of frame rounding in the rotation math.</li>
<li><strong>Smaller fixes.</strong> Button aspect ratio and overlapping VU labels.</li>
</ul>
<p>The UI loads and renders. The parameter bridge to C++ isn't built yet.</p>]]></content:encoded>
    </item>
    <item>
      <title>Fixing a slow dashboard without a rewrite</title>
      <link>https://www.amantrivedi.com/articles/postgres-n-plus-one-not-rust/</link>
      <guid isPermaLink="true">https://www.amantrivedi.com/articles/postgres-n-plus-one-not-rust/</guid>
      <pubDate>Fri, 18 Sep 2026 00:00:00 +0000</pubDate>
      <description>A slow dashboard looked like a backend-language problem. It was N+1 queries and missing indexes.</description>
      <category>PostgreSQL</category>
      <category>Performance</category>
      <category>FastAPI</category>
      <content:encoded><![CDATA[<p>The <a href="/projects/operations-dashboard/">operations dashboard</a> was slow enough that rewriting the backend in Rust came up as an option.</p>
<h2 id="measure-before-you-pick-a-language">Measure before you pick a language</h2>
<p>The slowness was I/O-bound, not CPU-bound: the backend spent its time waiting on the database. A faster language does nothing for waiting.</p>
<h2 id="what-it-actually-was">What it actually was</h2>
<ul>
<li><strong>N+1 queries.</strong> One query for a list, then one more per row.</li>
<li><strong>Missing indexes</strong>, including trigram indexes (<code>pg_trgm</code>) for text search.</li>
</ul>
<p>Both are fixed in the database and the query layer. No rewrite needed.</p>
<h2 id="takeaway">Takeaway</h2>
<p>When "the backend is slow", find out whether it is computing or waiting. That single question decided this one.</p>]]></content:encoded>
    </item>
    <item>
      <title>Airflow on one EC2 box</title>
      <link>https://www.amantrivedi.com/articles/airflow-on-one-ec2-box/</link>
      <guid isPermaLink="true">https://www.amantrivedi.com/articles/airflow-on-one-ec2-box/</guid>
      <pubDate>Fri, 18 Sep 2026 00:00:00 +0000</pubDate>
      <description>Notes from running Apache Airflow with LocalExecutor and a Postgres metadata database on a single EC2 instance to schedule PySpark jobs.</description>
      <category>Airflow</category>
      <category>AWS</category>
      <category>PySpark</category>
      <category>EC2</category>
      <content:encoded><![CDATA[<p>I needed something to schedule existing PySpark ETL jobs, and a managed Airflow was more than the job called for. So: one EC2 instance, Airflow with <code>LocalExecutor</code>, and PostgreSQL as the metadata database.</p>
<h2 id="why-localexecutor">Why LocalExecutor</h2>
<p><code>LocalExecutor</code> runs tasks as processes on the same machine, so there's no broker or worker fleet to run. The catch is that it needs a real database for metadata, not SQLite, which is why Postgres is in the picture from day one.</p>
<h2 id="keeping-it-alive">Keeping it alive</h2>
<p>The webserver and scheduler each run as a systemd service, so they start on boot and restart when they die. That is most of the "operations" story on a single box.</p>
<h2 id="what-this-doesn-t-give-you">What this doesn't give you</h2>
<p>One machine is one failure domain, and every task shares its CPU and memory. It's a fine start for existing jobs. It's not where you stop if the workload grows.</p>]]></content:encoded>
    </item>
  </channel>
</rss>
