<?xml version="1.0" encoding="utf-8"?><feed xmlns="http://www.w3.org/2005/Atom" ><generator uri="https://jekyllrb.com/" version="3.10.0">Jekyll</generator><link href="https://deepanseeralan.com/feed.xml" rel="self" type="application/atom+xml" /><link href="https://deepanseeralan.com/" rel="alternate" type="text/html" /><updated>2026-06-16T02:01:29-04:00</updated><id>https://deepanseeralan.com/feed.xml</id><title type="html">Deepan Seeralan</title><subtitle>Writing about AI pipelines, RAG, vector databases, and applied LLM engineering. Learning in public, with code.</subtitle><author><name>Deepan Seeralan</name></author><entry><title type="html">OKF: Google’s Markdown-Based Knowledge Format for AI Agents</title><link href="https://deepanseeralan.com/tech/google-open-knowledge-format/" rel="alternate" type="text/html" title="OKF: Google’s Markdown-Based Knowledge Format for AI Agents" /><published>2026-06-14T00:00:00-04:00</published><updated>2026-06-14T00:00:00-04:00</updated><id>https://deepanseeralan.com/tech/google-open-knowledge-format</id><content type="html" xml:base="https://deepanseeralan.com/tech/google-open-knowledge-format/"><![CDATA[<p>Two days ago, Google Cloud published a spec called the Open Knowledge Format (OKF). It’s a v0.1 release — early, explicit about that — but the idea behind it is worth understanding now because it points at a real problem that anyone building with LLMs runs into eventually.</p>

<p>The problem: your AI agents don’t have a good place to put what they learn.</p>

<p>RAG pipelines are good at retrieval. They’re not good at accumulation. You ingest documents, chunk them, embed them, and query them — but the pipeline doesn’t build on itself. Every new piece of knowledge has to be shoved into the same undifferentiated vector soup. There’s no structure, no relationships, no concept of “this table depends on that metric.” There’s also no way for agents to write back what they’ve figured out.</p>

<p>OKF is Google’s answer to the question: what if we treated organizational knowledge as a format rather than a platform?</p>

<hr />

<h2 id="the-core-idea">The Core Idea</h2>

<p>OKF is simple to state: a <strong>bundle</strong> is a directory of Markdown files. Each file represents one concept — a database table, a business metric, an API endpoint, a runbook. Files link to each other using standard Markdown links. The whole thing ships as a folder, a tarball, or a git repository.</p>

<p>That’s it. No proprietary database. No vendor SDK. No authentication layer between your agents and the knowledge.</p>

<p>The insight that makes this interesting is the separation between <strong>producers</strong> and <strong>consumers</strong>. Whatever writes OKF bundles — an LLM, a script, a human — doesn’t need to know anything about whatever reads them. The format is the contract. A BigQuery enrichment agent can produce a bundle, and a completely unrelated AI assistant can consume it, without either side knowing about the other.</p>

<p>This is what Andrej Karpathy was gesturing at with his “LLM Wiki” idea: instead of repeatedly retrieving from raw documents, have agents incrementally build and maintain a structured wiki. Knowledge compiles once and stays current. OKF formalizes that pattern.</p>

<hr />

<h2 id="file-format">File Format</h2>

<p>A concept file looks like this:</p>

<div class="language-markdown highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nn">---</span>
<span class="na">type</span><span class="pi">:</span> <span class="s">BigQuery Table</span>
<span class="na">title</span><span class="pi">:</span> <span class="s">orders</span>
<span class="na">description</span><span class="pi">:</span> <span class="s">Source of truth for all e-commerce transactions</span>
<span class="na">resource</span><span class="pi">:</span> <span class="s">https://bigquery.googleapis.com/projects/myproject/datasets/ecommerce/tables/orders</span>
<span class="na">tags</span><span class="pi">:</span> <span class="pi">[</span><span class="nv">ecommerce</span><span class="pi">,</span> <span class="nv">transactions</span><span class="pi">,</span> <span class="nv">revenue</span><span class="pi">]</span>
<span class="na">timestamp</span><span class="pi">:</span> <span class="s">2026-06-12T00:00:00Z</span>
<span class="nn">---</span>

<span class="gu">## Schema</span>

| Column      | Type      | Description                  |
|-------------|-----------|------------------------------|
| order_id    | INTEGER   | Primary key                  |
| customer_id | INTEGER   | FK → <span class="p">[</span><span class="nv">customers</span><span class="p">](</span><span class="sx">/tables/customers.md</span><span class="p">)</span> |
| total_usd   | FLOAT     | Order total before tax       |
| created_at  | TIMESTAMP | UTC creation time            |

<span class="gu">## Purpose</span>

This table feeds the <span class="p">[</span><span class="nv">Revenue metric</span><span class="p">](</span><span class="sx">/metrics/revenue.md</span><span class="p">)</span> and is the primary
source for the daily orders dashboard.

<span class="gu">## Gotchas</span>

<span class="sb">`created_at`</span> is stored in UTC but the reporting layer converts to PST.
Don't join against <span class="sb">`sessions`</span> on this column directly — use the
<span class="sb">`event_date`</span> partition key instead.
</code></pre></div></div>

<p>The only required frontmatter field is <code class="language-plaintext highlighter-rouge">type</code>. Everything else — <code class="language-plaintext highlighter-rouge">title</code>, <code class="language-plaintext highlighter-rouge">description</code>, <code class="language-plaintext highlighter-rouge">resource</code>, <code class="language-plaintext highlighter-rouge">tags</code>, <code class="language-plaintext highlighter-rouge">timestamp</code> — is optional. The Markdown body is completely free-form. You write what’s useful.</p>

<p>Two filenames are reserved:</p>

<ul>
  <li><code class="language-plaintext highlighter-rouge">index.md</code> — a bundle overview / table of contents for progressive disclosure</li>
  <li><code class="language-plaintext highlighter-rouge">log.md</code> — a chronological change history in ISO 8601 format</li>
</ul>

<p>Everything else is up to you.</p>

<hr />

<h2 id="bundle-structure">Bundle Structure</h2>

<p>A typical bundle for a data warehouse might look like:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>knowledge/
├── index.md
├── log.md
├── tables/
│   ├── orders.md
│   ├── customers.md
│   └── sessions.md
├── metrics/
│   ├── revenue.md
│   ├── dau.md
│   └── conversion_rate.md
└── playbooks/
    ├── incident-response.md
    └── oncall-runbook.md
</code></pre></div></div>

<p>The cross-references between files (<code class="language-plaintext highlighter-rouge">/tables/customers.md</code>, <code class="language-plaintext highlighter-rouge">/metrics/revenue.md</code>) create a knowledge graph that both humans and agents can traverse. Navigate it in a text editor, in a git browser, or in one of the reference visualizers Google shipped alongside the spec.</p>

<p>What makes this git-native is that bundles are just directories. You get diffs, blame, branch-based experimentation, and PRs for free. An agent updates <code class="language-plaintext highlighter-rouge">metrics/revenue.md</code> and the change shows up in the PR review like any other file edit.</p>

<hr />

<h2 id="concept-types">Concept Types</h2>

<p>OKF is deliberately unopinionated about what types are valid. The spec defines a few illustrative examples:</p>

<table>
  <thead>
    <tr>
      <th>Type</th>
      <th>Example use</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">BigQuery Table</code></td>
      <td>Data warehouse table documentation</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">Metric</code></td>
      <td>Business metric with calculation and owners</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">API Endpoint</code></td>
      <td>REST endpoint with method, path, auth</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">Playbook</code></td>
      <td>Step-by-step operational runbook</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">Policy</code></td>
      <td>Access control or data governance rules</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">Laravel Model</code></td>
      <td>Application model with relationships</td>
    </tr>
  </tbody>
</table>

<p>You define your own taxonomy. A team building internal tooling might have <code class="language-plaintext highlighter-rouge">type: Slack Workflow</code> or <code class="language-plaintext highlighter-rouge">type: Terraform Module</code>. The spec doesn’t care.</p>

<p>The <code class="language-plaintext highlighter-rouge">type</code> field is the only required field because it’s the minimum needed for tooling to route and categorize concepts — without it, you have no way to tell a metric file from a runbook file programmatically.</p>

<hr />

<h2 id="reference-tools">Reference Tools</h2>

<p>Google shipped three tools alongside the v0.1 spec:</p>

<p><strong>BigQuery enrichment agent</strong> — walks your dataset and auto-generates an OKF bundle from table and view definitions. You point it at a BigQuery project and it drafts concept docs for every table, pulling schema information, partition keys, and row counts into structured files.</p>

<p><strong>Static HTML visualizer</strong> — renders a bundle as an interactive graph. Single self-contained HTML file, no backend required. You can ship the visualizer alongside a bundle in the same tarball and it works offline.</p>

<p><strong>Sample bundles</strong> — reference implementations for GA4 e-commerce, Stack Overflow data, and Bitcoin transaction data. Useful for seeing what a well-structured bundle looks like before you build your own.</p>

<p>These are reference implementations, not the product. The point is to demonstrate that the format is useful without requiring anyone to adopt Google’s specific toolchain.</p>

<hr />

<h2 id="use-cases">Use Cases</h2>

<p><strong>Self-maintaining data dictionaries.</strong> The current state of most data team documentation: stale Confluence pages that were last touched eighteen months ago. An enrichment agent that runs on a schedule, diffs the current schema against the OKF bundle, and opens a PR with updates is genuinely useful. The knowledge doesn’t live in the agent’s context — it lives in files that persist across runs.</p>

<p><strong>Runbooks that agents can read and write.</strong> An on-call agent works through an incident response playbook, executes the steps, and appends a <code class="language-plaintext highlighter-rouge">## 2026-06-12</code> section to the playbook with what it did and what it found. The next time there’s a similar incident, the playbook is richer. This is accumulation, not just retrieval.</p>

<p><strong>Portable client knowledge.</strong> If you’re a consultancy or a vendor, you can ship an OKF bundle alongside your automation that documents what the automation does, what data it touches, and how to debug it. The documentation travels with the code in the repo. When the engagement ends, the client has both.</p>

<p><strong>Compliance audit trails.</strong> The optional <code class="language-plaintext highlighter-rouge">log.md</code> is ISO 8601 timestamped by convention. If you’re writing entries into it as knowledge changes, you get an audit trail of what was known and when without any extra infrastructure. Whether that’s sufficient for actual compliance requirements depends on your context, but it’s a useful starting point.</p>

<p><strong>Agent context bootstrapping.</strong> Instead of shoving your entire knowledge base into a system prompt, you give an agent a bundle path and it reads the specific files it needs. The <code class="language-plaintext highlighter-rouge">index.md</code> provides navigation. The agent traverses relationships to build up just the context it needs for the task at hand.</p>

<hr />

<h2 id="how-it-compares">How It Compares</h2>

<p>It’s worth being clear about what OKF is not, because there are a lot of overlapping things in this space.</p>

<p><strong>OKF vs. RAG.</strong> RAG retrieves chunks from a document corpus. OKF stores structured, curated, linked knowledge. They’re complementary — you might RAG over OKF bundles, but OKF bundles are also useful without any RAG in the pipeline. The difference is authorship: RAG sources are usually documents written for humans; OKF files are often written or maintained by agents specifically for agent consumption.</p>

<p><strong>OKF vs. MCP.</strong> Model Context Protocol connects agents to tools and data sources at runtime. OKF is static knowledge at rest. An MCP server might expose an OKF bundle as a resource — that’s a sensible combination — but they’re solving different problems.</p>

<p><strong>OKF vs. llms.txt.</strong> The <a href="https://llmstxt.org/">llms.txt proposal</a> is a single file convention for mapping a website’s structure. OKF is a multi-file format for representing an organization’s internal knowledge graph. Different scale, different purpose.</p>

<p><strong>OKF vs. Notion/Confluence.</strong> These tools have richer editors, mature ecosystems, and better accessibility for non-technical users. OKF’s advantages are: no authentication needed for agents, git-native versioning, zero vendor lock-in, and a format that doesn’t change on you. If your team already lives in Notion, OKF doesn’t replace it — but you could export a Notion database as an OKF bundle and the agent-facing properties become much simpler.</p>

<p><strong>OKF vs. data catalogs.</strong> Tools like Datahub or Alation are heavyweight: they require dedicated infrastructure, have their own data models, and are mostly read-only for agents. OKF is lightweight enough that a shell script can produce a valid bundle. Whether that simplicity is a feature or a limitation depends on your scale.</p>

<hr />

<h2 id="whats-missing-so-far">What’s Missing (So Far)</h2>

<p>v0.1 means incomplete by design. A few things are notably absent:</p>

<p><strong>Access control.</strong> A bundle is a directory. If an agent can read the directory, it can read everything. For sensitive knowledge (PII policies, security runbooks), you’d need to handle this at the filesystem or repository level, outside the spec.</p>

<p><strong>Conflict resolution.</strong> If two agents write to the same file concurrently, you have a merge conflict like any other. The spec doesn’t define a strategy for this. In practice, you’d probably handle it the same way you handle concurrent git edits — with locks, queuing, or PR-based workflows.</p>

<p><strong>Query semantics.</strong> There’s no defined way to query across a bundle beyond “read the files you need.” For small bundles this is fine. For a bundle with thousands of concept files, you’d want some kind of index — either an LLM-navigable index.md hierarchy or a simple key-value lookup by type and name. The spec doesn’t specify this.</p>

<p><strong>Validation beyond type.</strong> The only required field is <code class="language-plaintext highlighter-rouge">type</code>. There’s a validator for v0.1 compliance, but nothing prevents a concept file from having incorrect schema information or outdated descriptions. Data quality is still a human (or agent) responsibility.</p>

<hr />

<h2 id="why-this-matters-now">Why This Matters Now</h2>

<p>The timing makes sense. The last year has produced solid tooling for agents that act (MCP, tool use, function calling) but relatively little for agents that know and remember. Context windows are larger but still finite. RAG is mature but doesn’t accumulate. Fine-tuning is expensive and static.</p>

<p>A simple, portable, text-based format for persistent structured knowledge fills a gap that wasn’t filled by any of the above. The fact that it’s just Markdown in a folder means the barrier to adoption is nearly zero — if your agents can write files, they can write OKF.</p>

<p>Whether OKF specifically becomes a standard or whether it gets superseded by something better, the pattern it embodies — agents maintaining interlinked knowledge files — is going to be a fixture of how AI systems work. The question of where knowledge lives, who maintains it, and how it’s structured is one of the genuinely interesting open problems in applied AI right now.</p>

<p>OKF is a reasonable opening move.</p>]]></content><author><name>Deepan Seeralan</name></author><category term="Tech" /><category term="ai" /><category term="llm" /><category term="knowledge-management" /><category term="agents" /><category term="markdown" /><summary type="html"><![CDATA[Two days ago, Google Cloud published a spec called the Open Knowledge Format (OKF). It’s a v0.1 release — early, explicit about that — but the idea behind it is worth understanding now because it points at a real problem that anyone building with LLMs runs into eventually.]]></summary></entry><entry><title type="html">Building a Document Q&amp;amp;A Service: From Prototype to Production</title><link href="https://deepanseeralan.com/tech/document-qa-service/" rel="alternate" type="text/html" title="Building a Document Q&amp;amp;A Service: From Prototype to Production" /><published>2025-03-09T00:00:00-05:00</published><updated>2025-03-09T00:00:00-05:00</updated><id>https://deepanseeralan.com/tech/document-qa-service</id><content type="html" xml:base="https://deepanseeralan.com/tech/document-qa-service/"><![CDATA[<p>This is the post I wish I had six months ago when I was trying to take my first RAG prototype and turn it into something I could actually deploy and share with others. The gap between “it works in a Jupyter notebook” and “it works as a service” is where a lot of engineering time disappears.</p>

<p>I’ll walk through building a document Q&amp;A service end to end — ingestion, retrieval, FastAPI serving, and the parts that bit me along the way.</p>

<hr />

<h2 id="what-were-building">What We’re Building</h2>

<p>A service that:</p>
<ol>
  <li>Accepts document uploads (PDF, text, markdown)</li>
  <li>Chunks and embeds them into a vector store</li>
  <li>Exposes an API endpoint where you can ask questions about uploaded documents</li>
  <li>Returns answers with source citations</li>
</ol>

<p>The architecture is straightforward:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Client → FastAPI → RAG Pipeline → LLM
                       ↑
                 Vector Store (Qdrant)
                       ↑
              Document Ingestion Worker
</code></pre></div></div>

<p>The stack: <strong>FastAPI</strong> for the API, <strong>Qdrant</strong> for the vector store (running locally via Docker), <strong>LangChain</strong> for orchestration, <strong>OpenAI</strong> for embeddings and generation.</p>

<hr />

<h2 id="project-structure">Project Structure</h2>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>doc-qa-service/
├── app/
│   ├── __init__.py
│   ├── main.py          # FastAPI application
│   ├── ingest.py        # Document ingestion pipeline
│   ├── retrieval.py     # Query + generation
│   ├── config.py        # Settings
│   └── models.py        # Pydantic models
├── docker-compose.yml   # Qdrant + service
├── Dockerfile
├── requirements.txt
└── README.md
</code></pre></div></div>

<hr />

<h2 id="step-1-configuration">Step 1: Configuration</h2>

<p>Using Pydantic’s <code class="language-plaintext highlighter-rouge">BaseSettings</code> to manage config — this makes switching between dev and prod environments clean.</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># app/config.py
</span><span class="kn">from</span> <span class="nn">pydantic_settings</span> <span class="kn">import</span> <span class="n">BaseSettings</span>

<span class="k">class</span> <span class="nc">Settings</span><span class="p">(</span><span class="n">BaseSettings</span><span class="p">):</span>
    <span class="n">openai_api_key</span><span class="p">:</span> <span class="nb">str</span>
    <span class="n">qdrant_host</span><span class="p">:</span> <span class="nb">str</span> <span class="o">=</span> <span class="s">"localhost"</span>
    <span class="n">qdrant_port</span><span class="p">:</span> <span class="nb">int</span> <span class="o">=</span> <span class="mi">6333</span>
    <span class="n">collection_name</span><span class="p">:</span> <span class="nb">str</span> <span class="o">=</span> <span class="s">"documents"</span>
    <span class="n">embedding_model</span><span class="p">:</span> <span class="nb">str</span> <span class="o">=</span> <span class="s">"text-embedding-3-small"</span>
    <span class="n">llm_model</span><span class="p">:</span> <span class="nb">str</span> <span class="o">=</span> <span class="s">"gpt-4o-mini"</span>
    <span class="n">chunk_size</span><span class="p">:</span> <span class="nb">int</span> <span class="o">=</span> <span class="mi">512</span>
    <span class="n">chunk_overlap</span><span class="p">:</span> <span class="nb">int</span> <span class="o">=</span> <span class="mi">64</span>
    <span class="n">retrieval_k</span><span class="p">:</span> <span class="nb">int</span> <span class="o">=</span> <span class="mi">4</span>

    <span class="k">class</span> <span class="nc">Config</span><span class="p">:</span>
        <span class="n">env_file</span> <span class="o">=</span> <span class="s">".env"</span>

<span class="n">settings</span> <span class="o">=</span> <span class="n">Settings</span><span class="p">()</span>
</code></pre></div></div>

<hr />

<h2 id="step-2-document-ingestion">Step 2: Document Ingestion</h2>

<p>The ingestion module handles loading, chunking, and storing documents. I wanted this to be callable both directly and via the API endpoint.</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># app/ingest.py
</span><span class="kn">import</span> <span class="nn">hashlib</span>
<span class="kn">import</span> <span class="nn">uuid</span>
<span class="kn">from</span> <span class="nn">pathlib</span> <span class="kn">import</span> <span class="n">Path</span>
<span class="kn">from</span> <span class="nn">typing</span> <span class="kn">import</span> <span class="n">Optional</span>

<span class="kn">from</span> <span class="nn">langchain_community.document_loaders</span> <span class="kn">import</span> <span class="p">(</span>
    <span class="n">PyPDFLoader</span><span class="p">,</span>
    <span class="n">TextLoader</span><span class="p">,</span>
    <span class="n">UnstructuredMarkdownLoader</span><span class="p">,</span>
<span class="p">)</span>
<span class="kn">from</span> <span class="nn">langchain.text_splitter</span> <span class="kn">import</span> <span class="n">RecursiveCharacterTextSplitter</span>
<span class="kn">from</span> <span class="nn">langchain_openai</span> <span class="kn">import</span> <span class="n">OpenAIEmbeddings</span>
<span class="kn">from</span> <span class="nn">langchain_qdrant</span> <span class="kn">import</span> <span class="n">QdrantVectorStore</span>
<span class="kn">from</span> <span class="nn">qdrant_client</span> <span class="kn">import</span> <span class="n">QdrantClient</span>
<span class="kn">from</span> <span class="nn">qdrant_client.models</span> <span class="kn">import</span> <span class="n">Distance</span><span class="p">,</span> <span class="n">VectorParams</span>

<span class="kn">from</span> <span class="nn">.config</span> <span class="kn">import</span> <span class="n">settings</span>


<span class="k">def</span> <span class="nf">get_loader</span><span class="p">(</span><span class="n">file_path</span><span class="p">:</span> <span class="nb">str</span><span class="p">):</span>
    <span class="n">ext</span> <span class="o">=</span> <span class="n">Path</span><span class="p">(</span><span class="n">file_path</span><span class="p">).</span><span class="n">suffix</span><span class="p">.</span><span class="n">lower</span><span class="p">()</span>
    <span class="n">loaders</span> <span class="o">=</span> <span class="p">{</span>
        <span class="s">".pdf"</span><span class="p">:</span> <span class="n">PyPDFLoader</span><span class="p">,</span>
        <span class="s">".txt"</span><span class="p">:</span> <span class="n">TextLoader</span><span class="p">,</span>
        <span class="s">".md"</span><span class="p">:</span> <span class="n">UnstructuredMarkdownLoader</span><span class="p">,</span>
    <span class="p">}</span>
    <span class="n">loader_class</span> <span class="o">=</span> <span class="n">loaders</span><span class="p">.</span><span class="n">get</span><span class="p">(</span><span class="n">ext</span><span class="p">)</span>
    <span class="k">if</span> <span class="ow">not</span> <span class="n">loader_class</span><span class="p">:</span>
        <span class="k">raise</span> <span class="nb">ValueError</span><span class="p">(</span><span class="sa">f</span><span class="s">"Unsupported file type: </span><span class="si">{</span><span class="n">ext</span><span class="si">}</span><span class="s">"</span><span class="p">)</span>
    <span class="k">return</span> <span class="n">loader_class</span><span class="p">(</span><span class="n">file_path</span><span class="p">)</span>


<span class="k">def</span> <span class="nf">ensure_collection</span><span class="p">(</span><span class="n">client</span><span class="p">:</span> <span class="n">QdrantClient</span><span class="p">,</span> <span class="n">dimension</span><span class="p">:</span> <span class="nb">int</span> <span class="o">=</span> <span class="mi">1536</span><span class="p">):</span>
    <span class="s">"""Create collection if it doesn't exist."""</span>
    <span class="n">existing</span> <span class="o">=</span> <span class="p">[</span><span class="n">c</span><span class="p">.</span><span class="n">name</span> <span class="k">for</span> <span class="n">c</span> <span class="ow">in</span> <span class="n">client</span><span class="p">.</span><span class="n">get_collections</span><span class="p">().</span><span class="n">collections</span><span class="p">]</span>
    <span class="k">if</span> <span class="n">settings</span><span class="p">.</span><span class="n">collection_name</span> <span class="ow">not</span> <span class="ow">in</span> <span class="n">existing</span><span class="p">:</span>
        <span class="n">client</span><span class="p">.</span><span class="n">create_collection</span><span class="p">(</span>
            <span class="n">collection_name</span><span class="o">=</span><span class="n">settings</span><span class="p">.</span><span class="n">collection_name</span><span class="p">,</span>
            <span class="n">vectors_config</span><span class="o">=</span><span class="n">VectorParams</span><span class="p">(</span><span class="n">size</span><span class="o">=</span><span class="n">dimension</span><span class="p">,</span> <span class="n">distance</span><span class="o">=</span><span class="n">Distance</span><span class="p">.</span><span class="n">COSINE</span><span class="p">),</span>
        <span class="p">)</span>


<span class="k">def</span> <span class="nf">ingest_document</span><span class="p">(</span><span class="n">file_path</span><span class="p">:</span> <span class="nb">str</span><span class="p">,</span> <span class="n">doc_id</span><span class="p">:</span> <span class="n">Optional</span><span class="p">[</span><span class="nb">str</span><span class="p">]</span> <span class="o">=</span> <span class="bp">None</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="nb">dict</span><span class="p">:</span>
    <span class="s">"""
    Load, chunk, embed, and store a document.
    Returns metadata about the ingestion.
    """</span>
    <span class="k">if</span> <span class="n">doc_id</span> <span class="ow">is</span> <span class="bp">None</span><span class="p">:</span>
        <span class="c1"># Deterministic ID based on file content
</span>        <span class="n">content_hash</span> <span class="o">=</span> <span class="n">hashlib</span><span class="p">.</span><span class="n">sha256</span><span class="p">(</span>
            <span class="n">Path</span><span class="p">(</span><span class="n">file_path</span><span class="p">).</span><span class="n">read_bytes</span><span class="p">()</span>
        <span class="p">).</span><span class="n">hexdigest</span><span class="p">()[:</span><span class="mi">16</span><span class="p">]</span>
        <span class="n">doc_id</span> <span class="o">=</span> <span class="n">content_hash</span>

    <span class="n">loader</span> <span class="o">=</span> <span class="n">get_loader</span><span class="p">(</span><span class="n">file_path</span><span class="p">)</span>
    <span class="n">docs</span> <span class="o">=</span> <span class="n">loader</span><span class="p">.</span><span class="n">load</span><span class="p">()</span>

    <span class="n">splitter</span> <span class="o">=</span> <span class="n">RecursiveCharacterTextSplitter</span><span class="p">(</span>
        <span class="n">chunk_size</span><span class="o">=</span><span class="n">settings</span><span class="p">.</span><span class="n">chunk_size</span><span class="p">,</span>
        <span class="n">chunk_overlap</span><span class="o">=</span><span class="n">settings</span><span class="p">.</span><span class="n">chunk_overlap</span><span class="p">,</span>
    <span class="p">)</span>
    <span class="n">chunks</span> <span class="o">=</span> <span class="n">splitter</span><span class="p">.</span><span class="n">split_documents</span><span class="p">(</span><span class="n">docs</span><span class="p">)</span>

    <span class="c1"># Tag each chunk with the doc_id for later filtering
</span>    <span class="k">for</span> <span class="n">chunk</span> <span class="ow">in</span> <span class="n">chunks</span><span class="p">:</span>
        <span class="n">chunk</span><span class="p">.</span><span class="n">metadata</span><span class="p">[</span><span class="s">"doc_id"</span><span class="p">]</span> <span class="o">=</span> <span class="n">doc_id</span>
        <span class="n">chunk</span><span class="p">.</span><span class="n">metadata</span><span class="p">[</span><span class="s">"source_file"</span><span class="p">]</span> <span class="o">=</span> <span class="n">Path</span><span class="p">(</span><span class="n">file_path</span><span class="p">).</span><span class="n">name</span>

    <span class="n">embeddings</span> <span class="o">=</span> <span class="n">OpenAIEmbeddings</span><span class="p">(</span>
        <span class="n">model</span><span class="o">=</span><span class="n">settings</span><span class="p">.</span><span class="n">embedding_model</span><span class="p">,</span>
        <span class="n">api_key</span><span class="o">=</span><span class="n">settings</span><span class="p">.</span><span class="n">openai_api_key</span><span class="p">,</span>
    <span class="p">)</span>

    <span class="n">client</span> <span class="o">=</span> <span class="n">QdrantClient</span><span class="p">(</span><span class="n">host</span><span class="o">=</span><span class="n">settings</span><span class="p">.</span><span class="n">qdrant_host</span><span class="p">,</span> <span class="n">port</span><span class="o">=</span><span class="n">settings</span><span class="p">.</span><span class="n">qdrant_port</span><span class="p">)</span>
    <span class="n">ensure_collection</span><span class="p">(</span><span class="n">client</span><span class="p">)</span>

    <span class="n">vectorstore</span> <span class="o">=</span> <span class="n">QdrantVectorStore</span><span class="p">(</span>
        <span class="n">client</span><span class="o">=</span><span class="n">client</span><span class="p">,</span>
        <span class="n">collection_name</span><span class="o">=</span><span class="n">settings</span><span class="p">.</span><span class="n">collection_name</span><span class="p">,</span>
        <span class="n">embedding</span><span class="o">=</span><span class="n">embeddings</span><span class="p">,</span>
    <span class="p">)</span>
    <span class="n">vectorstore</span><span class="p">.</span><span class="n">add_documents</span><span class="p">(</span><span class="n">chunks</span><span class="p">)</span>

    <span class="k">return</span> <span class="p">{</span>
        <span class="s">"doc_id"</span><span class="p">:</span> <span class="n">doc_id</span><span class="p">,</span>
        <span class="s">"chunks_stored"</span><span class="p">:</span> <span class="nb">len</span><span class="p">(</span><span class="n">chunks</span><span class="p">),</span>
        <span class="s">"source_file"</span><span class="p">:</span> <span class="n">Path</span><span class="p">(</span><span class="n">file_path</span><span class="p">).</span><span class="n">name</span><span class="p">,</span>
    <span class="p">}</span>
</code></pre></div></div>

<p>A few decisions worth noting:</p>
<ul>
  <li>The deterministic doc_id means re-ingesting the same file is idempotent in terms of tracking, though you’d want deduplication logic in the vector store for production use</li>
  <li>Tagging chunks with <code class="language-plaintext highlighter-rouge">doc_id</code> allows scoped queries later (“only search within this document”)</li>
</ul>

<hr />

<h2 id="step-3-retrieval-and-generation">Step 3: Retrieval and Generation</h2>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># app/retrieval.py
</span><span class="kn">from</span> <span class="nn">typing</span> <span class="kn">import</span> <span class="n">Optional</span>
<span class="kn">from</span> <span class="nn">langchain_openai</span> <span class="kn">import</span> <span class="n">ChatOpenAI</span><span class="p">,</span> <span class="n">OpenAIEmbeddings</span>
<span class="kn">from</span> <span class="nn">langchain_qdrant</span> <span class="kn">import</span> <span class="n">QdrantVectorStore</span>
<span class="kn">from</span> <span class="nn">langchain.chains</span> <span class="kn">import</span> <span class="n">RetrievalQA</span>
<span class="kn">from</span> <span class="nn">langchain.prompts</span> <span class="kn">import</span> <span class="n">PromptTemplate</span>
<span class="kn">from</span> <span class="nn">qdrant_client</span> <span class="kn">import</span> <span class="n">QdrantClient</span>
<span class="kn">from</span> <span class="nn">qdrant_client.models</span> <span class="kn">import</span> <span class="n">Filter</span><span class="p">,</span> <span class="n">FieldCondition</span><span class="p">,</span> <span class="n">MatchValue</span>

<span class="kn">from</span> <span class="nn">.config</span> <span class="kn">import</span> <span class="n">settings</span>


<span class="n">SYSTEM_PROMPT</span> <span class="o">=</span> <span class="s">"""You are a helpful assistant that answers questions based on provided document context.

Rules:
- Answer only based on the context provided
- If the context doesn't contain the answer, say "I don't have enough information in the provided documents to answer this"
- Cite the source document and page number when available
- Be concise but complete

Context:
{context}

Question: {question}

Answer:"""</span>


<span class="k">def</span> <span class="nf">get_vectorstore</span><span class="p">(</span><span class="n">doc_id</span><span class="p">:</span> <span class="n">Optional</span><span class="p">[</span><span class="nb">str</span><span class="p">]</span> <span class="o">=</span> <span class="bp">None</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="n">QdrantVectorStore</span><span class="p">:</span>
    <span class="n">client</span> <span class="o">=</span> <span class="n">QdrantClient</span><span class="p">(</span><span class="n">host</span><span class="o">=</span><span class="n">settings</span><span class="p">.</span><span class="n">qdrant_host</span><span class="p">,</span> <span class="n">port</span><span class="o">=</span><span class="n">settings</span><span class="p">.</span><span class="n">qdrant_port</span><span class="p">)</span>
    <span class="n">embeddings</span> <span class="o">=</span> <span class="n">OpenAIEmbeddings</span><span class="p">(</span>
        <span class="n">model</span><span class="o">=</span><span class="n">settings</span><span class="p">.</span><span class="n">embedding_model</span><span class="p">,</span>
        <span class="n">api_key</span><span class="o">=</span><span class="n">settings</span><span class="p">.</span><span class="n">openai_api_key</span><span class="p">,</span>
    <span class="p">)</span>

    <span class="n">vectorstore</span> <span class="o">=</span> <span class="n">QdrantVectorStore</span><span class="p">(</span>
        <span class="n">client</span><span class="o">=</span><span class="n">client</span><span class="p">,</span>
        <span class="n">collection_name</span><span class="o">=</span><span class="n">settings</span><span class="p">.</span><span class="n">collection_name</span><span class="p">,</span>
        <span class="n">embedding</span><span class="o">=</span><span class="n">embeddings</span><span class="p">,</span>
    <span class="p">)</span>
    <span class="k">return</span> <span class="n">vectorstore</span>


<span class="k">def</span> <span class="nf">answer_question</span><span class="p">(</span><span class="n">question</span><span class="p">:</span> <span class="nb">str</span><span class="p">,</span> <span class="n">doc_id</span><span class="p">:</span> <span class="n">Optional</span><span class="p">[</span><span class="nb">str</span><span class="p">]</span> <span class="o">=</span> <span class="bp">None</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="nb">dict</span><span class="p">:</span>
    <span class="n">vectorstore</span> <span class="o">=</span> <span class="n">get_vectorstore</span><span class="p">()</span>

    <span class="n">retriever_kwargs</span> <span class="o">=</span> <span class="p">{</span><span class="s">"k"</span><span class="p">:</span> <span class="n">settings</span><span class="p">.</span><span class="n">retrieval_k</span><span class="p">}</span>

    <span class="c1"># Optionally scope to a specific document
</span>    <span class="k">if</span> <span class="n">doc_id</span><span class="p">:</span>
        <span class="n">retriever_kwargs</span><span class="p">[</span><span class="s">"filter"</span><span class="p">]</span> <span class="o">=</span> <span class="n">Filter</span><span class="p">(</span>
            <span class="n">must</span><span class="o">=</span><span class="p">[</span>
                <span class="n">FieldCondition</span><span class="p">(</span>
                    <span class="n">key</span><span class="o">=</span><span class="s">"metadata.doc_id"</span><span class="p">,</span>
                    <span class="n">match</span><span class="o">=</span><span class="n">MatchValue</span><span class="p">(</span><span class="n">value</span><span class="o">=</span><span class="n">doc_id</span><span class="p">),</span>
                <span class="p">)</span>
            <span class="p">]</span>
        <span class="p">)</span>

    <span class="n">retriever</span> <span class="o">=</span> <span class="n">vectorstore</span><span class="p">.</span><span class="n">as_retriever</span><span class="p">(</span><span class="n">search_kwargs</span><span class="o">=</span><span class="n">retriever_kwargs</span><span class="p">)</span>

    <span class="n">prompt</span> <span class="o">=</span> <span class="n">PromptTemplate</span><span class="p">(</span>
        <span class="n">template</span><span class="o">=</span><span class="n">SYSTEM_PROMPT</span><span class="p">,</span>
        <span class="n">input_variables</span><span class="o">=</span><span class="p">[</span><span class="s">"context"</span><span class="p">,</span> <span class="s">"question"</span><span class="p">],</span>
    <span class="p">)</span>

    <span class="n">llm</span> <span class="o">=</span> <span class="n">ChatOpenAI</span><span class="p">(</span>
        <span class="n">model</span><span class="o">=</span><span class="n">settings</span><span class="p">.</span><span class="n">llm_model</span><span class="p">,</span>
        <span class="n">temperature</span><span class="o">=</span><span class="mi">0</span><span class="p">,</span>
        <span class="n">api_key</span><span class="o">=</span><span class="n">settings</span><span class="p">.</span><span class="n">openai_api_key</span><span class="p">,</span>
    <span class="p">)</span>

    <span class="n">qa_chain</span> <span class="o">=</span> <span class="n">RetrievalQA</span><span class="p">.</span><span class="n">from_chain_type</span><span class="p">(</span>
        <span class="n">llm</span><span class="o">=</span><span class="n">llm</span><span class="p">,</span>
        <span class="n">chain_type</span><span class="o">=</span><span class="s">"stuff"</span><span class="p">,</span>
        <span class="n">retriever</span><span class="o">=</span><span class="n">retriever</span><span class="p">,</span>
        <span class="n">return_source_documents</span><span class="o">=</span><span class="bp">True</span><span class="p">,</span>
        <span class="n">chain_type_kwargs</span><span class="o">=</span><span class="p">{</span><span class="s">"prompt"</span><span class="p">:</span> <span class="n">prompt</span><span class="p">},</span>
    <span class="p">)</span>

    <span class="n">result</span> <span class="o">=</span> <span class="n">qa_chain</span><span class="p">.</span><span class="n">invoke</span><span class="p">({</span><span class="s">"query"</span><span class="p">:</span> <span class="n">question</span><span class="p">})</span>

    <span class="c1"># Extract source citations
</span>    <span class="n">sources</span> <span class="o">=</span> <span class="p">[]</span>
    <span class="k">for</span> <span class="n">doc</span> <span class="ow">in</span> <span class="n">result</span><span class="p">.</span><span class="n">get</span><span class="p">(</span><span class="s">"source_documents"</span><span class="p">,</span> <span class="p">[]):</span>
        <span class="n">sources</span><span class="p">.</span><span class="n">append</span><span class="p">({</span>
            <span class="s">"file"</span><span class="p">:</span> <span class="n">doc</span><span class="p">.</span><span class="n">metadata</span><span class="p">.</span><span class="n">get</span><span class="p">(</span><span class="s">"source_file"</span><span class="p">,</span> <span class="s">"unknown"</span><span class="p">),</span>
            <span class="s">"page"</span><span class="p">:</span> <span class="n">doc</span><span class="p">.</span><span class="n">metadata</span><span class="p">.</span><span class="n">get</span><span class="p">(</span><span class="s">"page"</span><span class="p">,</span> <span class="bp">None</span><span class="p">),</span>
            <span class="s">"doc_id"</span><span class="p">:</span> <span class="n">doc</span><span class="p">.</span><span class="n">metadata</span><span class="p">.</span><span class="n">get</span><span class="p">(</span><span class="s">"doc_id"</span><span class="p">),</span>
        <span class="p">})</span>

    <span class="c1"># Deduplicate sources
</span>    <span class="n">seen</span> <span class="o">=</span> <span class="nb">set</span><span class="p">()</span>
    <span class="n">unique_sources</span> <span class="o">=</span> <span class="p">[]</span>
    <span class="k">for</span> <span class="n">s</span> <span class="ow">in</span> <span class="n">sources</span><span class="p">:</span>
        <span class="n">key</span> <span class="o">=</span> <span class="p">(</span><span class="n">s</span><span class="p">[</span><span class="s">"file"</span><span class="p">],</span> <span class="n">s</span><span class="p">[</span><span class="s">"page"</span><span class="p">])</span>
        <span class="k">if</span> <span class="n">key</span> <span class="ow">not</span> <span class="ow">in</span> <span class="n">seen</span><span class="p">:</span>
            <span class="n">seen</span><span class="p">.</span><span class="n">add</span><span class="p">(</span><span class="n">key</span><span class="p">)</span>
            <span class="n">unique_sources</span><span class="p">.</span><span class="n">append</span><span class="p">(</span><span class="n">s</span><span class="p">)</span>

    <span class="k">return</span> <span class="p">{</span>
        <span class="s">"answer"</span><span class="p">:</span> <span class="n">result</span><span class="p">[</span><span class="s">"result"</span><span class="p">],</span>
        <span class="s">"sources"</span><span class="p">:</span> <span class="n">unique_sources</span><span class="p">,</span>
    <span class="p">}</span>
</code></pre></div></div>

<hr />

<h2 id="step-4-fastapi-application">Step 4: FastAPI Application</h2>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># app/main.py
</span><span class="kn">import</span> <span class="nn">shutil</span>
<span class="kn">import</span> <span class="nn">tempfile</span>
<span class="kn">from</span> <span class="nn">pathlib</span> <span class="kn">import</span> <span class="n">Path</span>

<span class="kn">from</span> <span class="nn">fastapi</span> <span class="kn">import</span> <span class="n">FastAPI</span><span class="p">,</span> <span class="n">File</span><span class="p">,</span> <span class="n">HTTPException</span><span class="p">,</span> <span class="n">UploadFile</span>
<span class="kn">from</span> <span class="nn">fastapi.middleware.cors</span> <span class="kn">import</span> <span class="n">CORSMiddleware</span>

<span class="kn">from</span> <span class="nn">.ingest</span> <span class="kn">import</span> <span class="n">ingest_document</span>
<span class="kn">from</span> <span class="nn">.models</span> <span class="kn">import</span> <span class="n">QuestionRequest</span><span class="p">,</span> <span class="n">QuestionResponse</span><span class="p">,</span> <span class="n">IngestResponse</span>
<span class="kn">from</span> <span class="nn">.retrieval</span> <span class="kn">import</span> <span class="n">answer_question</span>

<span class="n">app</span> <span class="o">=</span> <span class="n">FastAPI</span><span class="p">(</span>
    <span class="n">title</span><span class="o">=</span><span class="s">"Document Q&amp;A Service"</span><span class="p">,</span>
    <span class="n">description</span><span class="o">=</span><span class="s">"Upload documents and ask questions about them"</span><span class="p">,</span>
    <span class="n">version</span><span class="o">=</span><span class="s">"0.1.0"</span><span class="p">,</span>
<span class="p">)</span>

<span class="n">app</span><span class="p">.</span><span class="n">add_middleware</span><span class="p">(</span>
    <span class="n">CORSMiddleware</span><span class="p">,</span>
    <span class="n">allow_origins</span><span class="o">=</span><span class="p">[</span><span class="s">"*"</span><span class="p">],</span>
    <span class="n">allow_methods</span><span class="o">=</span><span class="p">[</span><span class="s">"*"</span><span class="p">],</span>
    <span class="n">allow_headers</span><span class="o">=</span><span class="p">[</span><span class="s">"*"</span><span class="p">],</span>
<span class="p">)</span>


<span class="o">@</span><span class="n">app</span><span class="p">.</span><span class="n">get</span><span class="p">(</span><span class="s">"/health"</span><span class="p">)</span>
<span class="k">def</span> <span class="nf">health</span><span class="p">():</span>
    <span class="k">return</span> <span class="p">{</span><span class="s">"status"</span><span class="p">:</span> <span class="s">"ok"</span><span class="p">}</span>


<span class="o">@</span><span class="n">app</span><span class="p">.</span><span class="n">post</span><span class="p">(</span><span class="s">"/ingest"</span><span class="p">,</span> <span class="n">response_model</span><span class="o">=</span><span class="n">IngestResponse</span><span class="p">)</span>
<span class="k">async</span> <span class="k">def</span> <span class="nf">ingest</span><span class="p">(</span><span class="nb">file</span><span class="p">:</span> <span class="n">UploadFile</span> <span class="o">=</span> <span class="n">File</span><span class="p">(...)):</span>
    <span class="s">"""Upload and ingest a document (PDF, TXT, or MD)."""</span>
    <span class="n">allowed</span> <span class="o">=</span> <span class="p">{</span><span class="s">".pdf"</span><span class="p">,</span> <span class="s">".txt"</span><span class="p">,</span> <span class="s">".md"</span><span class="p">}</span>
    <span class="n">ext</span> <span class="o">=</span> <span class="n">Path</span><span class="p">(</span><span class="nb">file</span><span class="p">.</span><span class="n">filename</span><span class="p">).</span><span class="n">suffix</span><span class="p">.</span><span class="n">lower</span><span class="p">()</span>
    <span class="k">if</span> <span class="n">ext</span> <span class="ow">not</span> <span class="ow">in</span> <span class="n">allowed</span><span class="p">:</span>
        <span class="k">raise</span> <span class="n">HTTPException</span><span class="p">(</span>
            <span class="n">status_code</span><span class="o">=</span><span class="mi">400</span><span class="p">,</span>
            <span class="n">detail</span><span class="o">=</span><span class="sa">f</span><span class="s">"Unsupported file type '</span><span class="si">{</span><span class="n">ext</span><span class="si">}</span><span class="s">'. Allowed: </span><span class="si">{</span><span class="n">allowed</span><span class="si">}</span><span class="s">"</span>
        <span class="p">)</span>

    <span class="c1"># Write to temp file — the loaders need a file path
</span>    <span class="k">with</span> <span class="n">tempfile</span><span class="p">.</span><span class="n">NamedTemporaryFile</span><span class="p">(</span><span class="n">suffix</span><span class="o">=</span><span class="n">ext</span><span class="p">,</span> <span class="n">delete</span><span class="o">=</span><span class="bp">False</span><span class="p">)</span> <span class="k">as</span> <span class="n">tmp</span><span class="p">:</span>
        <span class="n">shutil</span><span class="p">.</span><span class="n">copyfileobj</span><span class="p">(</span><span class="nb">file</span><span class="p">.</span><span class="nb">file</span><span class="p">,</span> <span class="n">tmp</span><span class="p">)</span>
        <span class="n">tmp_path</span> <span class="o">=</span> <span class="n">tmp</span><span class="p">.</span><span class="n">name</span>

    <span class="k">try</span><span class="p">:</span>
        <span class="n">result</span> <span class="o">=</span> <span class="n">ingest_document</span><span class="p">(</span><span class="n">tmp_path</span><span class="p">)</span>
    <span class="k">finally</span><span class="p">:</span>
        <span class="n">Path</span><span class="p">(</span><span class="n">tmp_path</span><span class="p">).</span><span class="n">unlink</span><span class="p">(</span><span class="n">missing_ok</span><span class="o">=</span><span class="bp">True</span><span class="p">)</span>

    <span class="k">return</span> <span class="n">IngestResponse</span><span class="p">(</span><span class="o">**</span><span class="n">result</span><span class="p">)</span>


<span class="o">@</span><span class="n">app</span><span class="p">.</span><span class="n">post</span><span class="p">(</span><span class="s">"/ask"</span><span class="p">,</span> <span class="n">response_model</span><span class="o">=</span><span class="n">QuestionResponse</span><span class="p">)</span>
<span class="k">async</span> <span class="k">def</span> <span class="nf">ask</span><span class="p">(</span><span class="n">request</span><span class="p">:</span> <span class="n">QuestionRequest</span><span class="p">):</span>
    <span class="s">"""Ask a question about ingested documents."""</span>
    <span class="k">if</span> <span class="ow">not</span> <span class="n">request</span><span class="p">.</span><span class="n">question</span><span class="p">.</span><span class="n">strip</span><span class="p">():</span>
        <span class="k">raise</span> <span class="n">HTTPException</span><span class="p">(</span><span class="n">status_code</span><span class="o">=</span><span class="mi">400</span><span class="p">,</span> <span class="n">detail</span><span class="o">=</span><span class="s">"Question cannot be empty"</span><span class="p">)</span>

    <span class="n">result</span> <span class="o">=</span> <span class="n">answer_question</span><span class="p">(</span>
        <span class="n">question</span><span class="o">=</span><span class="n">request</span><span class="p">.</span><span class="n">question</span><span class="p">,</span>
        <span class="n">doc_id</span><span class="o">=</span><span class="n">request</span><span class="p">.</span><span class="n">doc_id</span><span class="p">,</span>
    <span class="p">)</span>
    <span class="k">return</span> <span class="n">QuestionResponse</span><span class="p">(</span><span class="o">**</span><span class="n">result</span><span class="p">)</span>
</code></pre></div></div>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># app/models.py
</span><span class="kn">from</span> <span class="nn">typing</span> <span class="kn">import</span> <span class="n">List</span><span class="p">,</span> <span class="n">Optional</span>
<span class="kn">from</span> <span class="nn">pydantic</span> <span class="kn">import</span> <span class="n">BaseModel</span>


<span class="k">class</span> <span class="nc">IngestResponse</span><span class="p">(</span><span class="n">BaseModel</span><span class="p">):</span>
    <span class="n">doc_id</span><span class="p">:</span> <span class="nb">str</span>
    <span class="n">chunks_stored</span><span class="p">:</span> <span class="nb">int</span>
    <span class="n">source_file</span><span class="p">:</span> <span class="nb">str</span>


<span class="k">class</span> <span class="nc">QuestionRequest</span><span class="p">(</span><span class="n">BaseModel</span><span class="p">):</span>
    <span class="n">question</span><span class="p">:</span> <span class="nb">str</span>
    <span class="n">doc_id</span><span class="p">:</span> <span class="n">Optional</span><span class="p">[</span><span class="nb">str</span><span class="p">]</span> <span class="o">=</span> <span class="bp">None</span>  <span class="c1"># Scope to a specific document
</span>

<span class="k">class</span> <span class="nc">Source</span><span class="p">(</span><span class="n">BaseModel</span><span class="p">):</span>
    <span class="nb">file</span><span class="p">:</span> <span class="nb">str</span>
    <span class="n">page</span><span class="p">:</span> <span class="n">Optional</span><span class="p">[</span><span class="nb">int</span><span class="p">]</span> <span class="o">=</span> <span class="bp">None</span>
    <span class="n">doc_id</span><span class="p">:</span> <span class="n">Optional</span><span class="p">[</span><span class="nb">str</span><span class="p">]</span> <span class="o">=</span> <span class="bp">None</span>


<span class="k">class</span> <span class="nc">QuestionResponse</span><span class="p">(</span><span class="n">BaseModel</span><span class="p">):</span>
    <span class="n">answer</span><span class="p">:</span> <span class="nb">str</span>
    <span class="n">sources</span><span class="p">:</span> <span class="n">List</span><span class="p">[</span><span class="n">Source</span><span class="p">]</span>
</code></pre></div></div>

<hr />

<h2 id="step-5-running-it">Step 5: Running It</h2>

<p><strong>docker-compose.yml:</strong></p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">services</span><span class="pi">:</span>
  <span class="na">qdrant</span><span class="pi">:</span>
    <span class="na">image</span><span class="pi">:</span> <span class="s">qdrant/qdrant:latest</span>
    <span class="na">ports</span><span class="pi">:</span>
      <span class="pi">-</span> <span class="s2">"</span><span class="s">6333:6333"</span>
      <span class="pi">-</span> <span class="s2">"</span><span class="s">6334:6334"</span>
    <span class="na">volumes</span><span class="pi">:</span>
      <span class="pi">-</span> <span class="s">qdrant_storage:/qdrant/storage</span>

  <span class="na">api</span><span class="pi">:</span>
    <span class="na">build</span><span class="pi">:</span> <span class="s">.</span>
    <span class="na">ports</span><span class="pi">:</span>
      <span class="pi">-</span> <span class="s2">"</span><span class="s">8000:8000"</span>
    <span class="na">environment</span><span class="pi">:</span>
      <span class="pi">-</span> <span class="s">OPENAI_API_KEY=${OPENAI_API_KEY}</span>
      <span class="pi">-</span> <span class="s">QDRANT_HOST=qdrant</span>
    <span class="na">depends_on</span><span class="pi">:</span>
      <span class="pi">-</span> <span class="s">qdrant</span>

<span class="na">volumes</span><span class="pi">:</span>
  <span class="na">qdrant_storage</span><span class="pi">:</span>
</code></pre></div></div>

<p><strong>Dockerfile:</strong></p>

<div class="language-dockerfile highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">FROM</span><span class="s"> python:3.11-slim</span>

<span class="k">WORKDIR</span><span class="s"> /app</span>

<span class="k">COPY</span><span class="s"> requirements.txt .</span>
<span class="k">RUN </span>pip <span class="nb">install</span> <span class="nt">--no-cache-dir</span> <span class="nt">-r</span> requirements.txt

<span class="k">COPY</span><span class="s"> app/ ./app/</span>

<span class="k">CMD</span><span class="s"> ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"]</span>
</code></pre></div></div>

<p><strong>requirements.txt:</strong></p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>fastapi==0.109.0
uvicorn[standard]==0.27.0
langchain==0.1.0
langchain-openai==0.0.5
langchain-qdrant==0.0.1
langchain-community==0.0.15
qdrant-client==1.7.0
pypdf==4.0.1
unstructured==0.12.0
pydantic-settings==2.1.0
python-multipart==0.0.6
</code></pre></div></div>

<p>Start everything with:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">export </span><span class="nv">OPENAI_API_KEY</span><span class="o">=</span><span class="s2">"your-key"</span>
docker-compose up
</code></pre></div></div>

<hr />

<h2 id="testing-the-service">Testing the Service</h2>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># Upload a document</span>
curl <span class="nt">-X</span> POST <span class="s2">"http://localhost:8000/ingest"</span> <span class="se">\</span>
  <span class="nt">-F</span> <span class="s2">"file=@/path/to/your/document.pdf"</span>

<span class="c"># Response:</span>
<span class="c"># {"doc_id": "a3f2b1c4", "chunks_stored": 47, "source_file": "document.pdf"}</span>

<span class="c"># Ask a question</span>
curl <span class="nt">-X</span> POST <span class="s2">"http://localhost:8000/ask"</span> <span class="se">\</span>
  <span class="nt">-H</span> <span class="s2">"Content-Type: application/json"</span> <span class="se">\</span>
  <span class="nt">-d</span> <span class="s1">'{"question": "What are the main conclusions?", "doc_id": "a3f2b1c4"}'</span>

<span class="c"># Response:</span>
<span class="c"># {</span>
<span class="c">#   "answer": "The main conclusions are...",</span>
<span class="c">#   "sources": [{"file": "document.pdf", "page": 12, "doc_id": "a3f2b1c4"}]</span>
<span class="c"># }</span>
</code></pre></div></div>

<p>Interactive API docs at <code class="language-plaintext highlighter-rouge">http://localhost:8000/docs</code> (FastAPI’s built-in Swagger UI).</p>

<hr />

<h2 id="lessons-from-making-this-production-ready">Lessons from Making This Production-Ready</h2>

<p><strong>Mistake 1: Not handling Qdrant connection failures gracefully.</strong>
The Qdrant client will throw on connection refused. Wrap your vectorstore operations in try/except and return appropriate HTTP status codes.</p>

<p><strong>Mistake 2: Blocking the event loop during ingestion.</strong>
<code class="language-plaintext highlighter-rouge">ingest_document</code> is CPU and I/O heavy. For production, move it to a background task or a queue (Celery, ARQ, etc.) and return a job ID immediately.</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="o">@</span><span class="n">app</span><span class="p">.</span><span class="n">post</span><span class="p">(</span><span class="s">"/ingest"</span><span class="p">)</span>
<span class="k">async</span> <span class="k">def</span> <span class="nf">ingest</span><span class="p">(</span><span class="n">background_tasks</span><span class="p">:</span> <span class="n">BackgroundTasks</span><span class="p">,</span> <span class="nb">file</span><span class="p">:</span> <span class="n">UploadFile</span> <span class="o">=</span> <span class="n">File</span><span class="p">(...)):</span>
    <span class="c1"># ... save file ...
</span>    <span class="n">job_id</span> <span class="o">=</span> <span class="nb">str</span><span class="p">(</span><span class="n">uuid</span><span class="p">.</span><span class="n">uuid4</span><span class="p">())</span>
    <span class="n">background_tasks</span><span class="p">.</span><span class="n">add_task</span><span class="p">(</span><span class="n">ingest_document</span><span class="p">,</span> <span class="n">tmp_path</span><span class="p">,</span> <span class="n">job_id</span><span class="p">)</span>
    <span class="k">return</span> <span class="p">{</span><span class="s">"job_id"</span><span class="p">:</span> <span class="n">job_id</span><span class="p">,</span> <span class="s">"status"</span><span class="p">:</span> <span class="s">"processing"</span><span class="p">}</span>
</code></pre></div></div>

<p><strong>Mistake 3: Not deduplicating on re-upload.</strong>
If someone uploads the same document twice, you get duplicate chunks in the vector store which inflate your results. Use the content hash as doc_id and check if it already exists before re-ingesting.</p>

<p><strong>Mistake 4: Trusting the LLM to cite correctly.</strong>
The LLM will sometimes fabricate citations. Don’t ask the LLM to produce source references — collect them from the retrieved chunks yourself (as shown in the <code class="language-plaintext highlighter-rouge">retrieval.py</code> above) and append them to the response separately.</p>

<hr />

<p>The service above is functional but the path from here to something truly production-ready involves proper auth, rate limiting, background job processing, observability, and evaluation. I’ll cover those in subsequent posts.</p>]]></content><author><name>Deepan Seeralan</name></author><category term="Tech" /><category term="ai" /><category term="rag" /><category term="fastapi" /><category term="python" /><category term="learning" /><summary type="html"><![CDATA[This is the post I wish I had six months ago when I was trying to take my first RAG prototype and turn it into something I could actually deploy and share with others. The gap between “it works in a Jupyter notebook” and “it works as a service” is where a lot of engineering time disappears.]]></summary></entry><entry><title type="html">Understanding Snowflake ID and its uses</title><link href="https://deepanseeralan.com/tech/understanding-snowflakeid/" rel="alternate" type="text/html" title="Understanding Snowflake ID and its uses" /><published>2025-01-17T00:00:00-05:00</published><updated>2025-01-17T00:00:00-05:00</updated><id>https://deepanseeralan.com/tech/understanding-snowflakeid</id><content type="html" xml:base="https://deepanseeralan.com/tech/understanding-snowflakeid/"><![CDATA[<p>In distributed systems, ensuring unique and scalable identifiers is critical. While working on a recent problem, I needed to generate unique 64bit numbers across services. Snowflake ID approach fitted that requirement efficiently and reliably. Lets explore the Snowflake ID algorithm, its applications, and provide a Python implementation example. We’ll also cover deploying the application using Docker.</p>

<hr />

<h2 id="what-is-snowflake-id">What is Snowflake ID?</h2>

<p>Snowflake ID is a 64-bit unique identifier originally developed by Twitter. It is highly efficient and scalable, designed to work in distributed systems. The ID consists of the following components:</p>

<ol>
  <li><strong>Timestamp (41 bits)</strong>: Represents the time in milliseconds since a custom epoch.</li>
  <li><strong>Machine ID (10 bits)</strong>: Uniquely identifies the machine or node generating the ID.</li>
  <li><strong>Sequence Number (12 bits)</strong>: Ensures uniqueness for IDs generated within the same millisecond.</li>
</ol>

<hr />

<h2 id="why-use-snowflake-id">Why Use Snowflake ID?</h2>

<ol>
  <li><strong>Scalability</strong>: Works seamlessly in distributed environments.</li>
  <li><strong>Uniqueness</strong>: Combines timestamp, machine ID, and sequence number to ensure no duplicates.</li>
  <li><strong>Efficiency</strong>: Generates IDs in constant time, even under high loads.</li>
  <li><strong>Compactness</strong>: Represents large IDs in a compact 64-bit format.</li>
</ol>

<hr />

<h2 id="snowflake-id-python-implementation">Snowflake ID Python Implementation</h2>

<p>Below is a Python implementation of a Snowflake ID generator and its integration with MongoDB:</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">import</span> <span class="nn">os</span>
<span class="kn">import</span> <span class="nn">time</span>
<span class="kn">from</span> <span class="nn">pymongo</span> <span class="kn">import</span> <span class="n">MongoClient</span>
<span class="kn">from</span> <span class="nn">pymongo.errors</span> <span class="kn">import</span> <span class="n">DuplicateKeyError</span>

<span class="k">class</span> <span class="nc">SnowflakeIDGenerator</span><span class="p">:</span>
    <span class="s">"""
    A class to generate unique Snowflake IDs.

    Attributes:
        epoch (int): The custom epoch timestamp in milliseconds. Default is 1640995200000.
        machine_id (int): The machine ID, derived from the environment variable "MACHINE_ID" and masked with 0x3FF.
        sequence (int): The sequence number for IDs generated within the same millisecond.
        last_timestamp (int): The timestamp of the last generated ID.

    Methods:
        _current_timestamp(): Returns the current timestamp in milliseconds.
        generate_id(): Generates a unique Snowflake ID based on the current timestamp, machine ID, and sequence number.
    """</span>
    <span class="k">def</span> <span class="nf">__init__</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">epoch</span><span class="p">:</span> <span class="nb">int</span> <span class="o">=</span> <span class="mi">1640995200000</span><span class="p">):</span>
        <span class="bp">self</span><span class="p">.</span><span class="n">machine_id</span> <span class="o">=</span> <span class="nb">int</span><span class="p">(</span><span class="n">os</span><span class="p">.</span><span class="n">getenv</span><span class="p">(</span><span class="s">"MACHINE_ID"</span><span class="p">,</span> <span class="s">"0"</span><span class="p">))</span> <span class="o">&amp;</span> <span class="mh">0x3FF</span>
        <span class="bp">self</span><span class="p">.</span><span class="n">epoch</span> <span class="o">=</span> <span class="n">epoch</span>
        <span class="bp">self</span><span class="p">.</span><span class="n">sequence</span> <span class="o">=</span> <span class="mi">0</span>
        <span class="bp">self</span><span class="p">.</span><span class="n">last_timestamp</span> <span class="o">=</span> <span class="o">-</span><span class="mi">1</span>

    <span class="k">def</span> <span class="nf">_current_timestamp</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
        <span class="k">return</span> <span class="nb">int</span><span class="p">(</span><span class="n">time</span><span class="p">.</span><span class="n">time</span><span class="p">()</span> <span class="o">*</span> <span class="mi">1000</span><span class="p">)</span>

    <span class="k">def</span> <span class="nf">generate_id</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
        <span class="n">timestamp</span> <span class="o">=</span> <span class="bp">self</span><span class="p">.</span><span class="n">_current_timestamp</span><span class="p">()</span>

        <span class="k">if</span> <span class="n">timestamp</span> <span class="o">==</span> <span class="bp">self</span><span class="p">.</span><span class="n">last_timestamp</span><span class="p">:</span>
            <span class="bp">self</span><span class="p">.</span><span class="n">sequence</span> <span class="o">=</span> <span class="p">(</span><span class="bp">self</span><span class="p">.</span><span class="n">sequence</span> <span class="o">+</span> <span class="mi">1</span><span class="p">)</span> <span class="o">&amp;</span> <span class="mh">0xFFF</span>
            <span class="k">if</span> <span class="bp">self</span><span class="p">.</span><span class="n">sequence</span> <span class="o">==</span> <span class="mi">0</span><span class="p">:</span>
                <span class="k">while</span> <span class="n">timestamp</span> <span class="o">&lt;=</span> <span class="bp">self</span><span class="p">.</span><span class="n">last_timestamp</span><span class="p">:</span>
                    <span class="n">timestamp</span> <span class="o">=</span> <span class="bp">self</span><span class="p">.</span><span class="n">_current_timestamp</span><span class="p">()</span>
        <span class="k">else</span><span class="p">:</span>
            <span class="bp">self</span><span class="p">.</span><span class="n">sequence</span> <span class="o">=</span> <span class="mi">0</span>

        <span class="bp">self</span><span class="p">.</span><span class="n">last_timestamp</span> <span class="o">=</span> <span class="n">timestamp</span>

        <span class="k">return</span> <span class="p">(</span>
            <span class="p">((</span><span class="n">timestamp</span> <span class="o">-</span> <span class="bp">self</span><span class="p">.</span><span class="n">epoch</span><span class="p">)</span> <span class="o">&lt;&lt;</span> <span class="mi">22</span><span class="p">)</span> <span class="o">|</span>
            <span class="p">(</span><span class="bp">self</span><span class="p">.</span><span class="n">machine_id</span> <span class="o">&lt;&lt;</span> <span class="mi">12</span><span class="p">)</span> <span class="o">|</span>
            <span class="bp">self</span><span class="p">.</span><span class="n">sequence</span>
        <span class="p">)</span>

<span class="c1"># MongoDB connection
</span><span class="n">client</span> <span class="o">=</span> <span class="n">MongoClient</span><span class="p">(</span><span class="n">os</span><span class="p">.</span><span class="n">getenv</span><span class="p">(</span><span class="s">"MONGO_URI"</span><span class="p">,</span> <span class="s">"mongodb://mongo:27017/"</span><span class="p">))</span>
<span class="n">db</span> <span class="o">=</span> <span class="n">client</span><span class="p">[</span><span class="s">"test_database"</span><span class="p">]</span>
<span class="n">collection</span> <span class="o">=</span> <span class="n">db</span><span class="p">[</span><span class="s">"test_collection"</span><span class="p">]</span>

<span class="c1"># Initialize Snowflake Generator
</span><span class="n">generator</span> <span class="o">=</span> <span class="n">SnowflakeIDGenerator</span><span class="p">()</span>

<span class="c1"># Generate and insert IDs
</span><span class="n">batch_size</span> <span class="o">=</span> <span class="nb">int</span><span class="p">(</span><span class="n">os</span><span class="p">.</span><span class="n">getenv</span><span class="p">(</span><span class="s">"BATCH_SIZE"</span><span class="p">,</span> <span class="s">"100000"</span><span class="p">))</span>
<span class="k">for</span> <span class="n">i</span> <span class="ow">in</span> <span class="nb">range</span><span class="p">(</span><span class="n">batch_size</span><span class="p">):</span>
    <span class="n">unique_id</span> <span class="o">=</span> <span class="n">generator</span><span class="p">.</span><span class="n">generate_id</span><span class="p">()</span>
    <span class="n">document</span> <span class="o">=</span> <span class="p">{</span>
        <span class="s">"_id"</span><span class="p">:</span> <span class="n">unique_id</span><span class="p">,</span>
        <span class="s">"pod"</span><span class="p">:</span> <span class="n">os</span><span class="p">.</span><span class="n">getenv</span><span class="p">(</span><span class="s">"HOSTNAME"</span><span class="p">,</span> <span class="s">"unknown"</span><span class="p">),</span>
        <span class="s">"timestamp"</span><span class="p">:</span> <span class="n">time</span><span class="p">.</span><span class="n">time</span><span class="p">()</span>
    <span class="p">}</span>
    <span class="k">try</span><span class="p">:</span>
        <span class="n">collection</span><span class="p">.</span><span class="n">insert_one</span><span class="p">(</span><span class="n">document</span><span class="p">)</span>
    <span class="k">except</span> <span class="n">DuplicateKeyError</span><span class="p">:</span>
        <span class="k">print</span><span class="p">(</span><span class="sa">f</span><span class="s">"Document with _id </span><span class="si">{</span><span class="n">document</span><span class="p">[</span><span class="s">'_id'</span><span class="p">]</span><span class="si">}</span><span class="s"> already exists."</span><span class="p">)</span>

<span class="k">print</span><span class="p">(</span><span class="sa">f</span><span class="s">"Generated and inserted </span><span class="si">{</span><span class="n">batch_size</span><span class="si">}</span><span class="s"> IDs."</span><span class="p">)</span>
</code></pre></div></div>

<p>This application generates unique Snowflake IDs and writes them to a MongoDB database. Each ID is stored in a document along with metadata, such as the hostname and timestamp, enabling scalable storage and retrieval in distributed systems.</p>

<hr />

<h2 id="deploying-the-application-using-docker">Deploying the Application Using Docker</h2>

<p>To simplify deployment, we use Docker and Docker Compose. Below is the <code class="language-plaintext highlighter-rouge">Dockerfile</code> and <code class="language-plaintext highlighter-rouge">docker-compose.yml</code> configuration:</p>

<h3 id="dockerfile">Dockerfile</h3>

<div class="language-dockerfile highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># Use an official Python runtime as the base image</span>
<span class="k">FROM</span><span class="s"> python:3.10-slim</span>

<span class="c"># Set the working directory in the container</span>
<span class="k">WORKDIR</span><span class="s"> /app</span>

<span class="c"># Copy application code</span>
<span class="k">COPY</span><span class="s"> app.py /app</span>

<span class="c"># Install dependencies</span>
<span class="k">RUN </span>pip <span class="nb">install </span>pymongo

<span class="c"># Define environment variables</span>
<span class="k">ENV</span><span class="s"> MONGO_URI=mongodb://mongo:27017/</span>
<span class="k">ENV</span><span class="s"> MACHINE_ID=0</span>

<span class="c"># Run the application</span>
<span class="k">CMD</span><span class="s"> ["python", "app.py"]</span>
</code></pre></div></div>

<h3 id="docker-compose">Docker Compose</h3>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">services</span><span class="pi">:</span>
  <span class="na">mongo</span><span class="pi">:</span>
    <span class="na">image</span><span class="pi">:</span> <span class="s">mongo:latest</span>
    <span class="na">container_name</span><span class="pi">:</span> <span class="s">mongo</span>
    <span class="na">ports</span><span class="pi">:</span>
      <span class="pi">-</span> <span class="s2">"</span><span class="s">27017:27017"</span>
    <span class="na">volumes</span><span class="pi">:</span>
      <span class="pi">-</span> <span class="s">mongo_data:/data/db</span>

  <span class="na">app1</span><span class="pi">:</span>
    <span class="na">image</span><span class="pi">:</span> <span class="s">snowflakeidgen</span>
    <span class="na">build</span><span class="pi">:</span>
      <span class="na">context</span><span class="pi">:</span> <span class="s">.</span>
      <span class="na">dockerfile</span><span class="pi">:</span> <span class="s">Dockerfile</span>
    <span class="na">environment</span><span class="pi">:</span>
      <span class="pi">-</span> <span class="s">MONGO_URI=mongodb://mongo:27017/</span>
      <span class="pi">-</span> <span class="s">MACHINE_ID=1</span>
    <span class="na">depends_on</span><span class="pi">:</span>
      <span class="pi">-</span> <span class="s">mongo</span>

  <span class="na">app2</span><span class="pi">:</span>
    <span class="na">image</span><span class="pi">:</span> <span class="s">snowflakeidgen</span>
    <span class="na">environment</span><span class="pi">:</span>
      <span class="pi">-</span> <span class="s">MONGO_URI=mongodb://mongo:27017/</span>
      <span class="pi">-</span> <span class="s">MACHINE_ID=2</span>
    <span class="na">depends_on</span><span class="pi">:</span>
      <span class="pi">-</span> <span class="s">mongo</span>

  <span class="na">app3</span><span class="pi">:</span>
    <span class="na">image</span><span class="pi">:</span> <span class="s">snowflakeidgen</span>
    <span class="na">environment</span><span class="pi">:</span>
      <span class="pi">-</span> <span class="s">MONGO_URI=mongodb://mongo:27017/</span>
      <span class="pi">-</span> <span class="s">MACHINE_ID=3</span>
    <span class="na">depends_on</span><span class="pi">:</span>
      <span class="pi">-</span> <span class="s">mongo</span>

<span class="na">volumes</span><span class="pi">:</span>
  <span class="na">mongo_data</span><span class="pi">:</span>
</code></pre></div></div>

<hr />

<h2 id="running-the-application">Running the Application</h2>

<ol>
  <li>
    <p><strong>Build the Docker Image</strong>:</p>

    <div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>docker-compose build
</code></pre></div>    </div>
  </li>
  <li>
    <p><strong>Start the Services</strong>:</p>

    <div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>docker-compose up
</code></pre></div>    </div>
  </li>
  <li>
    <p><strong>Verify the Output</strong>:</p>

    <ul>
      <li>The MongoDB container will be accessible at <code class="language-plaintext highlighter-rouge">localhost:27017</code>.</li>
      <li>The application will generate and insert unique Snowflake IDs into MongoDB.</li>
    </ul>
  </li>
</ol>]]></content><author><name>Deepan Seeralan</name></author><category term="Tech" /><category term="learning" /><category term="tools" /><category term="python" /><summary type="html"><![CDATA[In distributed systems, ensuring unique and scalable identifiers is critical. While working on a recent problem, I needed to generate unique 64bit numbers across services. Snowflake ID approach fitted that requirement efficiently and reliably. Lets explore the Snowflake ID algorithm, its applications, and provide a Python implementation example. We’ll also cover deploying the application using Docker.]]></summary></entry><entry><title type="html">Pinecone Goes Serverless: What Actually Changes</title><link href="https://deepanseeralan.com/tech/pinecone-serverless/" rel="alternate" type="text/html" title="Pinecone Goes Serverless: What Actually Changes" /><published>2024-11-10T00:00:00-05:00</published><updated>2024-11-10T00:00:00-05:00</updated><id>https://deepanseeralan.com/tech/pinecone-serverless</id><content type="html" xml:base="https://deepanseeralan.com/tech/pinecone-serverless/"><![CDATA[<p>Pinecone announced their serverless architecture in early 2024, and after using both the pod-based and serverless versions on a few small projects, I have some thoughts on what actually changed — and what didn’t.</p>

<p>Short version: it’s meaningfully better for prototyping and small workloads. For production at scale, the tradeoffs are more nuanced.</p>

<hr />

<h2 id="background-how-pinecone-used-to-work">Background: How Pinecone Used to Work</h2>

<p>Before serverless, Pinecone used a <strong>pod-based</strong> model. You provisioned dedicated pods (p1, p2, s1) and paid by the hour regardless of whether you were querying or not. A pod running 24/7 costs money whether you have 100 queries a day or 100,000.</p>

<p>This made sense for production workloads with predictable traffic. It made less sense if you were:</p>
<ul>
  <li>Building a prototype that gets used a few times a week</li>
  <li>Running experiments across multiple indexes</li>
  <li>Doing batch jobs that query heavily for an hour then go quiet</li>
</ul>

<p>The minimum viable setup (1 x s1.x1 pod) ran about $70/month. Not huge, but enough to make you think twice about spinning up indexes for exploration.</p>

<hr />

<h2 id="what-serverless-actually-is">What Serverless Actually Is</h2>

<p>Serverless Pinecone decouples storage from compute. Your vectors live in blob storage (AWS S3 under the hood). When a query comes in, Pinecone spins up compute to search across that storage, then bills you for the query itself.</p>

<p>Billing shifts from <strong>time-based</strong> to <strong>usage-based</strong>:</p>
<ul>
  <li>Storage: ~$0.033/GB/month</li>
  <li>Reads: ~$8 per million read units (each query uses multiple read units depending on your index size)</li>
  <li>Writes: ~$2 per million write units</li>
</ul>

<p>The free tier is genuinely useful now — 2GB of storage and a monthly allowance of read/write units. That’s enough for real experimentation.</p>

<hr />

<h2 id="setting-up-a-serverless-index">Setting Up a Serverless Index</h2>

<p>The code change is small. The key difference is in the <code class="language-plaintext highlighter-rouge">spec</code> parameter when creating an index:</p>

<p><strong>Before (pod-based):</strong></p>
<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">from</span> <span class="nn">pinecone</span> <span class="kn">import</span> <span class="n">Pinecone</span><span class="p">,</span> <span class="n">PodSpec</span>

<span class="n">pc</span> <span class="o">=</span> <span class="n">Pinecone</span><span class="p">(</span><span class="n">api_key</span><span class="o">=</span><span class="s">"your-api-key"</span><span class="p">)</span>

<span class="n">pc</span><span class="p">.</span><span class="n">create_index</span><span class="p">(</span>
    <span class="n">name</span><span class="o">=</span><span class="s">"my-index"</span><span class="p">,</span>
    <span class="n">dimension</span><span class="o">=</span><span class="mi">1536</span><span class="p">,</span>
    <span class="n">metric</span><span class="o">=</span><span class="s">"cosine"</span><span class="p">,</span>
    <span class="n">spec</span><span class="o">=</span><span class="n">PodSpec</span><span class="p">(</span>
        <span class="n">environment</span><span class="o">=</span><span class="s">"gcp-starter"</span><span class="p">,</span>
        <span class="n">pod_type</span><span class="o">=</span><span class="s">"p1.x1"</span><span class="p">,</span>
        <span class="n">pods</span><span class="o">=</span><span class="mi">1</span>
    <span class="p">)</span>
<span class="p">)</span>
</code></pre></div></div>

<p><strong>After (serverless):</strong></p>
<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">from</span> <span class="nn">pinecone</span> <span class="kn">import</span> <span class="n">Pinecone</span><span class="p">,</span> <span class="n">ServerlessSpec</span>

<span class="n">pc</span> <span class="o">=</span> <span class="n">Pinecone</span><span class="p">(</span><span class="n">api_key</span><span class="o">=</span><span class="s">"your-api-key"</span><span class="p">)</span>

<span class="n">pc</span><span class="p">.</span><span class="n">create_index</span><span class="p">(</span>
    <span class="n">name</span><span class="o">=</span><span class="s">"my-index"</span><span class="p">,</span>
    <span class="n">dimension</span><span class="o">=</span><span class="mi">1536</span><span class="p">,</span>
    <span class="n">metric</span><span class="o">=</span><span class="s">"cosine"</span><span class="p">,</span>
    <span class="n">spec</span><span class="o">=</span><span class="n">ServerlessSpec</span><span class="p">(</span>
        <span class="n">cloud</span><span class="o">=</span><span class="s">"aws"</span><span class="p">,</span>
        <span class="n">region</span><span class="o">=</span><span class="s">"us-east-1"</span>
    <span class="p">)</span>
<span class="p">)</span>
</code></pre></div></div>

<p>That’s it. The index operations (upsert, query, delete) are identical.</p>

<hr />

<h2 id="before-vs-after-a-real-comparison">Before vs After: A Real Comparison</h2>

<p>I ran the same workload — a small document Q&amp;A system with ~50K vectors — against both setups. Here’s what I observed:</p>

<h3 id="setup-experience">Setup Experience</h3>

<p><strong>Pod-based</strong>: Creating an index took 2–5 minutes while the pod initialized. You’d see a “Initializing” state in the dashboard. Frustrating when you just want to test something quickly.</p>

<p><strong>Serverless</strong>: Index creation is near-instant. Available in under 10 seconds in my tests. This sounds minor but it substantially changes the experimentation loop.</p>

<h3 id="query-latency">Query Latency</h3>

<p>This is where serverless has a genuine tradeoff. Cold start behavior is noticeable:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Pod-based (p1.x1, warm):   8–15ms average query latency
Serverless (warm):         15–30ms average query latency
Serverless (cold start):   200–800ms first query after idle period
</code></pre></div></div>

<p>For a RAG application where you’re chaining LLM calls anyway, the extra 15ms on warm queries doesn’t matter. The cold start does matter if your application has spiky traffic.</p>

<p>Pinecone has improved this significantly over 2024 — cold starts are faster than when serverless first launched.</p>

<h3 id="cost-at-different-scales">Cost at Different Scales</h3>

<p>I ran some rough estimates for a document Q&amp;A workload:</p>

<table>
  <thead>
    <tr>
      <th>Scale</th>
      <th>Pod-based (s1.x1)</th>
      <th>Serverless</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>100 queries/day</td>
      <td>~$25/month</td>
      <td>~$1/month</td>
    </tr>
    <tr>
      <td>1,000 queries/day</td>
      <td>~$25/month</td>
      <td>~$5/month</td>
    </tr>
    <tr>
      <td>10,000 queries/day</td>
      <td>~$25/month</td>
      <td>~$30/month</td>
    </tr>
    <tr>
      <td>50,000 queries/day</td>
      <td>~$50/month</td>
      <td>~$120/month</td>
    </tr>
  </tbody>
</table>

<p>The crossover point is somewhere around 10–15K queries/day depending on your index size and query complexity. Below that, serverless wins on cost. Above that, it depends heavily on your traffic pattern.</p>

<hr />

<h2 id="migrating-an-existing-index">Migrating an Existing Index</h2>

<p>There’s no in-place migration — you create a new serverless index and reindex your data. With LangChain this is straightforward:</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">from</span> <span class="nn">pinecone</span> <span class="kn">import</span> <span class="n">Pinecone</span><span class="p">,</span> <span class="n">ServerlessSpec</span>
<span class="kn">from</span> <span class="nn">langchain_pinecone</span> <span class="kn">import</span> <span class="n">PineconeVectorStore</span>
<span class="kn">from</span> <span class="nn">langchain_openai</span> <span class="kn">import</span> <span class="n">OpenAIEmbeddings</span>

<span class="n">pc</span> <span class="o">=</span> <span class="n">Pinecone</span><span class="p">(</span><span class="n">api_key</span><span class="o">=</span><span class="s">"your-api-key"</span><span class="p">)</span>

<span class="c1"># Create new serverless index
</span><span class="n">pc</span><span class="p">.</span><span class="n">create_index</span><span class="p">(</span>
    <span class="n">name</span><span class="o">=</span><span class="s">"my-index-serverless"</span><span class="p">,</span>
    <span class="n">dimension</span><span class="o">=</span><span class="mi">1536</span><span class="p">,</span>
    <span class="n">metric</span><span class="o">=</span><span class="s">"cosine"</span><span class="p">,</span>
    <span class="n">spec</span><span class="o">=</span><span class="n">ServerlessSpec</span><span class="p">(</span><span class="n">cloud</span><span class="o">=</span><span class="s">"aws"</span><span class="p">,</span> <span class="n">region</span><span class="o">=</span><span class="s">"us-east-1"</span><span class="p">)</span>
<span class="p">)</span>

<span class="c1"># Re-embed and upsert your documents
</span><span class="n">embeddings</span> <span class="o">=</span> <span class="n">OpenAIEmbeddings</span><span class="p">(</span><span class="n">model</span><span class="o">=</span><span class="s">"text-embedding-3-small"</span><span class="p">)</span>
<span class="n">vectorstore</span> <span class="o">=</span> <span class="n">PineconeVectorStore</span><span class="p">.</span><span class="n">from_documents</span><span class="p">(</span>
    <span class="n">documents</span><span class="o">=</span><span class="n">your_chunks</span><span class="p">,</span>
    <span class="n">embedding</span><span class="o">=</span><span class="n">embeddings</span><span class="p">,</span>
    <span class="n">index_name</span><span class="o">=</span><span class="s">"my-index-serverless"</span>
<span class="p">)</span>
</code></pre></div></div>

<p>If your source documents are still available (they should be — don’t rely on the vector store as your source of truth), this is a straightforward operation. The re-embedding cost is real though — factor that in if you have millions of vectors.</p>

<hr />

<h2 id="what-id-use-now">What I’d Use Now</h2>

<p>For new projects in late 2024, my default is serverless unless I have a concrete reason for the pod-based model. The reasons to stick with pods:</p>

<ol>
  <li>Consistent sub-20ms latency with no cold starts</li>
  <li>You’re already paying for the pod and don’t want to migrate</li>
  <li>You need features that are pod-only (metadata filtering at very large scale behaves differently)</li>
</ol>

<p>For everything else — prototypes, low-to-medium traffic production apps, experiments — serverless is the better default. The free tier is enough to build real things, and the pay-per-use model means you’re not bleeding money on idle resources.</p>

<p>The user experience around indexing being fast now is actually the biggest win in day-to-day use. Faster feedback loop matters more than I expected.</p>

<hr />

<h2 id="quick-reference">Quick Reference</h2>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># Full working example - serverless index with LangChain
</span><span class="kn">import</span> <span class="nn">os</span>
<span class="kn">from</span> <span class="nn">pinecone</span> <span class="kn">import</span> <span class="n">Pinecone</span><span class="p">,</span> <span class="n">ServerlessSpec</span>
<span class="kn">from</span> <span class="nn">langchain_pinecone</span> <span class="kn">import</span> <span class="n">PineconeVectorStore</span>
<span class="kn">from</span> <span class="nn">langchain_openai</span> <span class="kn">import</span> <span class="n">OpenAIEmbeddings</span><span class="p">,</span> <span class="n">ChatOpenAI</span>
<span class="kn">from</span> <span class="nn">langchain.chains</span> <span class="kn">import</span> <span class="n">RetrievalQA</span>

<span class="n">pc</span> <span class="o">=</span> <span class="n">Pinecone</span><span class="p">(</span><span class="n">api_key</span><span class="o">=</span><span class="n">os</span><span class="p">.</span><span class="n">environ</span><span class="p">[</span><span class="s">"PINECONE_API_KEY"</span><span class="p">])</span>

<span class="n">index_name</span> <span class="o">=</span> <span class="s">"rag-demo"</span>

<span class="c1"># Create if it doesn't exist
</span><span class="k">if</span> <span class="n">index_name</span> <span class="ow">not</span> <span class="ow">in</span> <span class="n">pc</span><span class="p">.</span><span class="n">list_indexes</span><span class="p">().</span><span class="n">names</span><span class="p">():</span>
    <span class="n">pc</span><span class="p">.</span><span class="n">create_index</span><span class="p">(</span>
        <span class="n">name</span><span class="o">=</span><span class="n">index_name</span><span class="p">,</span>
        <span class="n">dimension</span><span class="o">=</span><span class="mi">1536</span><span class="p">,</span>
        <span class="n">metric</span><span class="o">=</span><span class="s">"cosine"</span><span class="p">,</span>
        <span class="n">spec</span><span class="o">=</span><span class="n">ServerlessSpec</span><span class="p">(</span><span class="n">cloud</span><span class="o">=</span><span class="s">"aws"</span><span class="p">,</span> <span class="n">region</span><span class="o">=</span><span class="s">"us-east-1"</span><span class="p">)</span>
    <span class="p">)</span>

<span class="c1"># Use with LangChain
</span><span class="n">embeddings</span> <span class="o">=</span> <span class="n">OpenAIEmbeddings</span><span class="p">(</span><span class="n">model</span><span class="o">=</span><span class="s">"text-embedding-3-small"</span><span class="p">)</span>
<span class="n">vectorstore</span> <span class="o">=</span> <span class="n">PineconeVectorStore</span><span class="p">(</span>
    <span class="n">index_name</span><span class="o">=</span><span class="n">index_name</span><span class="p">,</span>
    <span class="n">embedding</span><span class="o">=</span><span class="n">embeddings</span>
<span class="p">)</span>

<span class="n">retriever</span> <span class="o">=</span> <span class="n">vectorstore</span><span class="p">.</span><span class="n">as_retriever</span><span class="p">(</span><span class="n">search_kwargs</span><span class="o">=</span><span class="p">{</span><span class="s">"k"</span><span class="p">:</span> <span class="mi">4</span><span class="p">})</span>
<span class="n">llm</span> <span class="o">=</span> <span class="n">ChatOpenAI</span><span class="p">(</span><span class="n">model</span><span class="o">=</span><span class="s">"gpt-4o-mini"</span><span class="p">,</span> <span class="n">temperature</span><span class="o">=</span><span class="mi">0</span><span class="p">)</span>

<span class="n">qa</span> <span class="o">=</span> <span class="n">RetrievalQA</span><span class="p">.</span><span class="n">from_chain_type</span><span class="p">(</span>
    <span class="n">llm</span><span class="o">=</span><span class="n">llm</span><span class="p">,</span>
    <span class="n">retriever</span><span class="o">=</span><span class="n">retriever</span>
<span class="p">)</span>

<span class="n">result</span> <span class="o">=</span> <span class="n">qa</span><span class="p">.</span><span class="n">invoke</span><span class="p">({</span><span class="s">"query"</span><span class="p">:</span> <span class="s">"Your question here"</span><span class="p">})</span>
<span class="k">print</span><span class="p">(</span><span class="n">result</span><span class="p">[</span><span class="s">"result"</span><span class="p">])</span>
</code></pre></div></div>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>pip <span class="nb">install </span>pinecone langchain langchain-pinecone langchain-openai
</code></pre></div></div>]]></content><author><name>Deepan Seeralan</name></author><category term="Tech" /><category term="ai" /><category term="vector-database" /><category term="pinecone" /><category term="rag" /><category term="learning" /><summary type="html"><![CDATA[Pinecone announced their serverless architecture in early 2024, and after using both the pod-based and serverless versions on a few small projects, I have some thoughts on what actually changed — and what didn’t.]]></summary></entry><entry><title type="html">Building Blocks of a RAG Pipeline</title><link href="https://deepanseeralan.com/tech/rag-building-blocks/" rel="alternate" type="text/html" title="Building Blocks of a RAG Pipeline" /><published>2024-09-15T00:00:00-04:00</published><updated>2024-09-15T00:00:00-04:00</updated><id>https://deepanseeralan.com/tech/rag-building-blocks</id><content type="html" xml:base="https://deepanseeralan.com/tech/rag-building-blocks/"><![CDATA[<p>Over the past year I’ve watched “RAG” go from a niche research acronym to something that comes up in every engineering discussion involving LLMs. If you’ve been meaning to understand what it actually is — not the hand-wavy version, but the mechanics — this post is for you.</p>

<p>I want to use this as a foundation post for a series I’m planning on AI Pipelines and applied LLM work. A lot of what I’ll write going forward builds on these ideas, so it’s worth getting the basics right.</p>

<hr />

<h2 id="why-rag-exists">Why RAG Exists</h2>

<p>Large language models have a knowledge cutoff. Ask GPT-4 about something that happened last month and it either hallucinates or tells you it doesn’t know. More practically, ask it about your company’s internal documentation and it definitely doesn’t know.</p>

<p>The naive fix is fine-tuning — train the model on your data. The problems: fine-tuning is expensive, slow, and static. Every time your data changes you need to re-train.</p>

<p><strong>Retrieval-Augmented Generation</strong> (RAG) is the other approach. Instead of baking knowledge into the model weights, you retrieve relevant context at query time and feed it to the model as part of the prompt. The model’s job becomes: “given this context, answer this question.”</p>

<p>This is powerful because:</p>
<ul>
  <li>Your knowledge base can be updated without touching the model</li>
  <li>You can trace where the answer came from (the retrieved chunks)</li>
  <li>It works with any LLM — even smaller, cheaper models improve dramatically with good context</li>
</ul>

<hr />

<h2 id="the-five-components">The Five Components</h2>

<p>A RAG pipeline has five logical pieces. Let me walk through each one.</p>

<h3 id="1-document-ingestion">1. Document Ingestion</h3>

<p>Before you can retrieve anything, you need to get your documents into a format the pipeline can use. This typically means:</p>

<ul>
  <li><strong>Loading</strong>: Reading PDFs, web pages, markdown files, database exports, etc.</li>
  <li><strong>Chunking</strong>: Splitting long documents into smaller pieces</li>
  <li><strong>Metadata extraction</strong>: Keeping track of source, date, author</li>
</ul>

<p>The chunking step is more art than science. Too small and individual chunks lose context. Too large and you’re paying for tokens you don’t need and diluting relevance.</p>

<p>A simple recursive character splitter is a reasonable starting point:</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">from</span> <span class="nn">langchain.text_splitter</span> <span class="kn">import</span> <span class="n">RecursiveCharacterTextSplitter</span>

<span class="n">splitter</span> <span class="o">=</span> <span class="n">RecursiveCharacterTextSplitter</span><span class="p">(</span>
    <span class="n">chunk_size</span><span class="o">=</span><span class="mi">512</span><span class="p">,</span>
    <span class="n">chunk_overlap</span><span class="o">=</span><span class="mi">64</span><span class="p">,</span>
    <span class="n">separators</span><span class="o">=</span><span class="p">[</span><span class="s">"</span><span class="se">\n\n</span><span class="s">"</span><span class="p">,</span> <span class="s">"</span><span class="se">\n</span><span class="s">"</span><span class="p">,</span> <span class="s">". "</span><span class="p">,</span> <span class="s">" "</span><span class="p">,</span> <span class="s">""</span><span class="p">]</span>
<span class="p">)</span>

<span class="n">chunks</span> <span class="o">=</span> <span class="n">splitter</span><span class="p">.</span><span class="n">split_documents</span><span class="p">(</span><span class="n">docs</span><span class="p">)</span>
</code></pre></div></div>

<p>The <code class="language-plaintext highlighter-rouge">chunk_overlap</code> matters — it ensures that ideas that span chunk boundaries aren’t silently cut off.</p>

<h3 id="2-embedding">2. Embedding</h3>

<p>Each chunk gets converted into a dense vector — a list of floating point numbers that represents the semantic meaning of the text. Semantically similar text ends up close together in this vector space.</p>

<p>The embedding model does this translation. Common choices in late 2024:</p>

<ul>
  <li><strong>OpenAI <code class="language-plaintext highlighter-rouge">text-embedding-3-small</code></strong> — cheap, good quality, 1536 dimensions</li>
  <li><strong>OpenAI <code class="language-plaintext highlighter-rouge">text-embedding-3-large</code></strong> — higher quality, 3072 dimensions</li>
  <li><strong><code class="language-plaintext highlighter-rouge">sentence-transformers/all-MiniLM-L6-v2</code></strong> — local, fast, smaller (384 dims)</li>
  <li><strong>Cohere embed-v3</strong> — strong multilingual support</li>
</ul>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">from</span> <span class="nn">langchain_openai</span> <span class="kn">import</span> <span class="n">OpenAIEmbeddings</span>

<span class="n">embeddings</span> <span class="o">=</span> <span class="n">OpenAIEmbeddings</span><span class="p">(</span><span class="n">model</span><span class="o">=</span><span class="s">"text-embedding-3-small"</span><span class="p">)</span>

<span class="c1"># Embed a single string
</span><span class="n">vector</span> <span class="o">=</span> <span class="n">embeddings</span><span class="p">.</span><span class="n">embed_query</span><span class="p">(</span><span class="s">"What is a transformer architecture?"</span><span class="p">)</span>
<span class="k">print</span><span class="p">(</span><span class="nb">len</span><span class="p">(</span><span class="n">vector</span><span class="p">))</span>  <span class="c1"># 1536
</span></code></pre></div></div>

<h3 id="3-vector-store">3. Vector Store</h3>

<p>The vectors need to live somewhere searchable. A vector store indexes embeddings so you can find the nearest neighbors to a query vector quickly — usually using HNSW (Hierarchical Navigable Small World) or FAISS under the hood.</p>

<p>Options range from in-memory (good for prototyping) to fully managed cloud databases:</p>

<table>
  <thead>
    <tr>
      <th>Store</th>
      <th>Good for</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>ChromaDB</td>
      <td>Local development, small datasets</td>
    </tr>
    <tr>
      <td>FAISS</td>
      <td>In-process, large-scale offline search</td>
    </tr>
    <tr>
      <td>Qdrant</td>
      <td>Self-hosted or cloud, production use</td>
    </tr>
    <tr>
      <td>Pinecone</td>
      <td>Fully managed, serverless option</td>
    </tr>
    <tr>
      <td>pgvector</td>
      <td>You already have Postgres</td>
    </tr>
  </tbody>
</table>

<p>For a quick local setup with ChromaDB:</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">from</span> <span class="nn">langchain_chroma</span> <span class="kn">import</span> <span class="n">Chroma</span>
<span class="kn">from</span> <span class="nn">langchain_openai</span> <span class="kn">import</span> <span class="n">OpenAIEmbeddings</span>

<span class="n">vectorstore</span> <span class="o">=</span> <span class="n">Chroma</span><span class="p">.</span><span class="n">from_documents</span><span class="p">(</span>
    <span class="n">documents</span><span class="o">=</span><span class="n">chunks</span><span class="p">,</span>
    <span class="n">embedding</span><span class="o">=</span><span class="n">OpenAIEmbeddings</span><span class="p">(</span><span class="n">model</span><span class="o">=</span><span class="s">"text-embedding-3-small"</span><span class="p">),</span>
    <span class="n">persist_directory</span><span class="o">=</span><span class="s">"./chroma_db"</span>
<span class="p">)</span>
</code></pre></div></div>

<h3 id="4-retrieval">4. Retrieval</h3>

<p>At query time, the user’s question gets embedded using the same model, and you find the <code class="language-plaintext highlighter-rouge">k</code> most similar chunks in the vector store.</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">query</span> <span class="o">=</span> <span class="s">"How does attention mechanism work in transformers?"</span>
<span class="n">results</span> <span class="o">=</span> <span class="n">vectorstore</span><span class="p">.</span><span class="n">similarity_search</span><span class="p">(</span><span class="n">query</span><span class="p">,</span> <span class="n">k</span><span class="o">=</span><span class="mi">4</span><span class="p">)</span>

<span class="k">for</span> <span class="n">doc</span> <span class="ow">in</span> <span class="n">results</span><span class="p">:</span>
    <span class="k">print</span><span class="p">(</span><span class="n">doc</span><span class="p">.</span><span class="n">metadata</span><span class="p">)</span>
    <span class="k">print</span><span class="p">(</span><span class="n">doc</span><span class="p">.</span><span class="n">page_content</span><span class="p">[:</span><span class="mi">200</span><span class="p">])</span>
    <span class="k">print</span><span class="p">(</span><span class="s">"---"</span><span class="p">)</span>
</code></pre></div></div>

<p>This is where a lot of the tuning happens in practice. Raw cosine similarity isn’t always what you want. You might want:</p>
<ul>
  <li><strong>MMR (Maximal Marginal Relevance)</strong> — balance relevance with diversity so you don’t retrieve 4 chunks that all say the same thing</li>
  <li><strong>Hybrid search</strong> — combine dense (semantic) with sparse (keyword/BM25) retrieval</li>
  <li><strong>Reranking</strong> — use a second model to re-score the retrieved chunks</li>
</ul>

<h3 id="5-generation">5. Generation</h3>

<p>The retrieved chunks get assembled into a prompt along with the user’s question, and sent to an LLM:</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">from</span> <span class="nn">langchain_openai</span> <span class="kn">import</span> <span class="n">ChatOpenAI</span>
<span class="kn">from</span> <span class="nn">langchain.chains</span> <span class="kn">import</span> <span class="n">RetrievalQA</span>
<span class="kn">from</span> <span class="nn">langchain.prompts</span> <span class="kn">import</span> <span class="n">PromptTemplate</span>

<span class="n">template</span> <span class="o">=</span> <span class="s">"""Use the following context to answer the question.
If you don't know the answer based on the context, say so — don't make things up.

Context:
{context}

Question: {question}

Answer:"""</span>

<span class="n">prompt</span> <span class="o">=</span> <span class="n">PromptTemplate</span><span class="p">(</span>
    <span class="n">input_variables</span><span class="o">=</span><span class="p">[</span><span class="s">"context"</span><span class="p">,</span> <span class="s">"question"</span><span class="p">],</span>
    <span class="n">template</span><span class="o">=</span><span class="n">template</span>
<span class="p">)</span>

<span class="n">llm</span> <span class="o">=</span> <span class="n">ChatOpenAI</span><span class="p">(</span><span class="n">model</span><span class="o">=</span><span class="s">"gpt-4o-mini"</span><span class="p">,</span> <span class="n">temperature</span><span class="o">=</span><span class="mi">0</span><span class="p">)</span>

<span class="n">qa_chain</span> <span class="o">=</span> <span class="n">RetrievalQA</span><span class="p">.</span><span class="n">from_chain_type</span><span class="p">(</span>
    <span class="n">llm</span><span class="o">=</span><span class="n">llm</span><span class="p">,</span>
    <span class="n">chain_type</span><span class="o">=</span><span class="s">"stuff"</span><span class="p">,</span>
    <span class="n">retriever</span><span class="o">=</span><span class="n">vectorstore</span><span class="p">.</span><span class="n">as_retriever</span><span class="p">(</span><span class="n">search_kwargs</span><span class="o">=</span><span class="p">{</span><span class="s">"k"</span><span class="p">:</span> <span class="mi">4</span><span class="p">}),</span>
    <span class="n">chain_type_kwargs</span><span class="o">=</span><span class="p">{</span><span class="s">"prompt"</span><span class="p">:</span> <span class="n">prompt</span><span class="p">}</span>
<span class="p">)</span>

<span class="n">response</span> <span class="o">=</span> <span class="n">qa_chain</span><span class="p">.</span><span class="n">invoke</span><span class="p">({</span><span class="s">"query"</span><span class="p">:</span> <span class="s">"How does attention work?"</span><span class="p">})</span>
<span class="k">print</span><span class="p">(</span><span class="n">response</span><span class="p">[</span><span class="s">"result"</span><span class="p">])</span>
</code></pre></div></div>

<hr />

<h2 id="putting-it-all-together">Putting It All Together</h2>

<p>Here’s a minimal end-to-end pipeline — load some documents, embed them, store them, and query:</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">import</span> <span class="nn">os</span>
<span class="kn">from</span> <span class="nn">langchain_community.document_loaders</span> <span class="kn">import</span> <span class="n">DirectoryLoader</span><span class="p">,</span> <span class="n">TextLoader</span>
<span class="kn">from</span> <span class="nn">langchain.text_splitter</span> <span class="kn">import</span> <span class="n">RecursiveCharacterTextSplitter</span>
<span class="kn">from</span> <span class="nn">langchain_openai</span> <span class="kn">import</span> <span class="n">OpenAIEmbeddings</span><span class="p">,</span> <span class="n">ChatOpenAI</span>
<span class="kn">from</span> <span class="nn">langchain_chroma</span> <span class="kn">import</span> <span class="n">Chroma</span>
<span class="kn">from</span> <span class="nn">langchain.chains</span> <span class="kn">import</span> <span class="n">RetrievalQA</span>
<span class="kn">from</span> <span class="nn">langchain.prompts</span> <span class="kn">import</span> <span class="n">PromptTemplate</span>

<span class="c1"># 1. Load documents
</span><span class="n">loader</span> <span class="o">=</span> <span class="n">DirectoryLoader</span><span class="p">(</span><span class="s">"./docs"</span><span class="p">,</span> <span class="n">glob</span><span class="o">=</span><span class="s">"**/*.md"</span><span class="p">,</span> <span class="n">loader_cls</span><span class="o">=</span><span class="n">TextLoader</span><span class="p">)</span>
<span class="n">docs</span> <span class="o">=</span> <span class="n">loader</span><span class="p">.</span><span class="n">load</span><span class="p">()</span>
<span class="k">print</span><span class="p">(</span><span class="sa">f</span><span class="s">"Loaded </span><span class="si">{</span><span class="nb">len</span><span class="p">(</span><span class="n">docs</span><span class="p">)</span><span class="si">}</span><span class="s"> documents"</span><span class="p">)</span>

<span class="c1"># 2. Chunk
</span><span class="n">splitter</span> <span class="o">=</span> <span class="n">RecursiveCharacterTextSplitter</span><span class="p">(</span><span class="n">chunk_size</span><span class="o">=</span><span class="mi">512</span><span class="p">,</span> <span class="n">chunk_overlap</span><span class="o">=</span><span class="mi">64</span><span class="p">)</span>
<span class="n">chunks</span> <span class="o">=</span> <span class="n">splitter</span><span class="p">.</span><span class="n">split_documents</span><span class="p">(</span><span class="n">docs</span><span class="p">)</span>
<span class="k">print</span><span class="p">(</span><span class="sa">f</span><span class="s">"Split into </span><span class="si">{</span><span class="nb">len</span><span class="p">(</span><span class="n">chunks</span><span class="p">)</span><span class="si">}</span><span class="s"> chunks"</span><span class="p">)</span>

<span class="c1"># 3. Embed + store
</span><span class="n">embeddings</span> <span class="o">=</span> <span class="n">OpenAIEmbeddings</span><span class="p">(</span><span class="n">model</span><span class="o">=</span><span class="s">"text-embedding-3-small"</span><span class="p">)</span>
<span class="n">vectorstore</span> <span class="o">=</span> <span class="n">Chroma</span><span class="p">.</span><span class="n">from_documents</span><span class="p">(</span>
    <span class="n">documents</span><span class="o">=</span><span class="n">chunks</span><span class="p">,</span>
    <span class="n">embedding</span><span class="o">=</span><span class="n">embeddings</span><span class="p">,</span>
    <span class="n">persist_directory</span><span class="o">=</span><span class="s">"./chroma_db"</span>
<span class="p">)</span>

<span class="c1"># 4. Build retrieval chain
</span><span class="n">template</span> <span class="o">=</span> <span class="s">"""Answer based on the context below. Say "I don't know" if the context
doesn't contain the answer.

Context: {context}
Question: {question}
Answer:"""</span>

<span class="n">qa</span> <span class="o">=</span> <span class="n">RetrievalQA</span><span class="p">.</span><span class="n">from_chain_type</span><span class="p">(</span>
    <span class="n">llm</span><span class="o">=</span><span class="n">ChatOpenAI</span><span class="p">(</span><span class="n">model</span><span class="o">=</span><span class="s">"gpt-4o-mini"</span><span class="p">,</span> <span class="n">temperature</span><span class="o">=</span><span class="mi">0</span><span class="p">),</span>
    <span class="n">chain_type</span><span class="o">=</span><span class="s">"stuff"</span><span class="p">,</span>
    <span class="n">retriever</span><span class="o">=</span><span class="n">vectorstore</span><span class="p">.</span><span class="n">as_retriever</span><span class="p">(</span><span class="n">search_kwargs</span><span class="o">=</span><span class="p">{</span><span class="s">"k"</span><span class="p">:</span> <span class="mi">4</span><span class="p">}),</span>
    <span class="n">chain_type_kwargs</span><span class="o">=</span><span class="p">{</span>
        <span class="s">"prompt"</span><span class="p">:</span> <span class="n">PromptTemplate</span><span class="p">(</span>
            <span class="n">template</span><span class="o">=</span><span class="n">template</span><span class="p">,</span>
            <span class="n">input_variables</span><span class="o">=</span><span class="p">[</span><span class="s">"context"</span><span class="p">,</span> <span class="s">"question"</span><span class="p">]</span>
        <span class="p">)</span>
    <span class="p">}</span>
<span class="p">)</span>

<span class="c1"># 5. Query
</span><span class="k">while</span> <span class="bp">True</span><span class="p">:</span>
    <span class="n">question</span> <span class="o">=</span> <span class="nb">input</span><span class="p">(</span><span class="s">"</span><span class="se">\n</span><span class="s">Ask a question (or 'quit'): "</span><span class="p">)</span>
    <span class="k">if</span> <span class="n">question</span><span class="p">.</span><span class="n">lower</span><span class="p">()</span> <span class="o">==</span> <span class="s">"quit"</span><span class="p">:</span>
        <span class="k">break</span>
    <span class="n">result</span> <span class="o">=</span> <span class="n">qa</span><span class="p">.</span><span class="n">invoke</span><span class="p">({</span><span class="s">"query"</span><span class="p">:</span> <span class="n">question</span><span class="p">})</span>
    <span class="k">print</span><span class="p">(</span><span class="sa">f</span><span class="s">"</span><span class="se">\n</span><span class="si">{</span><span class="n">result</span><span class="p">[</span><span class="s">'result'</span><span class="p">]</span><span class="si">}</span><span class="s">"</span><span class="p">)</span>
</code></pre></div></div>

<hr />

<h2 id="what-this-doesnt-cover-yet">What This Doesn’t Cover (Yet)</h2>

<p>This is the happy path. Production RAG introduces a lot of complexity I want to address in separate posts:</p>

<ul>
  <li><strong>Evaluation</strong> — how do you know if your RAG is actually good? RAGAS, LlamaIndex eval frameworks</li>
  <li><strong>Chunking strategies</strong> — semantic chunking, parent-child chunking, document hierarchy</li>
  <li><strong>Hybrid search</strong> — combining dense and sparse retrieval</li>
  <li><strong>Reranking</strong> — Cohere Rerank, cross-encoder models</li>
  <li><strong>Agentic retrieval</strong> — query decomposition, iterative retrieval</li>
</ul>

<p>The pipeline I’ve shown here is functional but naive. It works well enough to prototype on. The gap between this prototype and a production system is where most of the interesting engineering lives, and that’s what I’ll be exploring in upcoming posts.</p>

<hr />

<h2 id="prerequisites">Prerequisites</h2>

<p>If you want to run the code above:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>pip <span class="nb">install </span>langchain langchain-openai langchain-chroma chromadb
<span class="nb">export </span><span class="nv">OPENAI_API_KEY</span><span class="o">=</span><span class="s2">"your-key-here"</span>
</code></pre></div></div>

<p>Put some markdown or text files in a <code class="language-plaintext highlighter-rouge">./docs</code> directory and you’re off.</p>]]></content><author><name>Deepan Seeralan</name></author><category term="Tech" /><category term="ai" /><category term="rag" /><category term="python" /><category term="langchain" /><category term="learning" /><summary type="html"><![CDATA[Over the past year I’ve watched “RAG” go from a niche research acronym to something that comes up in every engineering discussion involving LLMs. If you’ve been meaning to understand what it actually is — not the hand-wavy version, but the mechanics — this post is for you.]]></summary></entry><entry><title type="html">Tweeting my notes with Google Cloud Functions</title><link href="https://deepanseeralan.com/tech/tweet-my-notes-with-google-cloud-functions/" rel="alternate" type="text/html" title="Tweeting my notes with Google Cloud Functions" /><published>2023-06-06T00:00:00-04:00</published><updated>2023-06-06T00:00:00-04:00</updated><id>https://deepanseeralan.com/tech/tweet-my-notes-with-google-cloud-functions</id><content type="html" xml:base="https://deepanseeralan.com/tech/tweet-my-notes-with-google-cloud-functions/"><![CDATA[<p>A habit that I developed in recent times is to take a short note about the things I learn at work and in my personal explorations. Something like a flash card note, in a plain text, that is easy to take, remember and revisit. There are plenty of great flash card apps, note taking apps for mobile, web and desktop. No doubt about that. I wanted to reduce the friction and distractions in making a note, so things that require me to switch to my phone or different apps on my desktop can possibly distract me too much from my workflow. I just wanted some structure to the notes I take, so started off with a json file (like <a href="https://gist.github.com/deepns/909db3c52c319412a014a6025f676f05">this</a>). These are just plain text notes, with probably a few hundred entries. And, I could very well use my favorite VS code to maintain those notes, since VS Code is where I spent most of my time with. Days went by, my notes grew in number. While looking for ways I can make the learning better, I thought of bringing my notes more prominent in the places I frequent.</p>

<p>Twitter is one of the main sources of media consumption. So why not bring my notes into my twitter feed periodically so I get to look at them more often? That gave birth to a weekend project - Making a twitter bot to read my notes and tweet a note based on my interests. I tagged each note as I took them, so it was easier to choose the ones that mattered to me at that time. It also gave me a choice to tune my bot in such a way that certain notes get tweeted with higher probability. After some fun time with Google Cloud Run in the past, I was reading about Cloud Functions. It fitted perfectly into the use case I was working on. I can have the bot run in a schedule through Cloud Scheduler. The number of invocations, CPU time and network egress for my use case were very minimal and fell well under the generous <a href="https://cloud.google.com/functions/pricing#free_tier">free limits</a>. Putting all together, I came up with something like below.</p>

<figure>
<img src="https://deepanseeralan.com/assets/images/for-posts/tweet-my-notes.jpg" />
</figure>

<p>A little bit about the individual components:</p>

<ul>
  <li><strong>NotesBot</strong>
    <ul>
      <li>An event driven Cloud Function in Python, to post a note into one or more tweets</li>
      <li>Gets a random note from the Note Service</li>
      <li>Processes the note and post them using Tweepy and Twitter v2 API</li>
      <li>Set to be invoked when a message is posted to the connected pub-sub</li>
      <li>Tweets are posted <a href="https://twitter.com/deeptechnotes">@deeptechnotes</a> handle</li>
    </ul>
  </li>
  <li>A Cloud Scheduler job to post a message to the pub-sub on a cadence (e.g. every 3 hours)</li>
  <li><strong>Note Service</strong>
    <ul>
      <li>A HTTP Cloud function in Go to serve a note from my collection. This could have been done with Python/Flask too, went with Go just for some fun as I was learning Go in the recent times. To keep things simple, didn’t add any API spec when I started off. Planning to add that soon.</li>
      <li>Supports different paths to return a random note or multiple notes based on tags specified in the query</li>
      <li>Having this as a separate service makes it convenient to pull the notes from other possible clients (e.g. a vscode extension to display my notes in a notification pop-up, chrome extension) in future</li>
      <li>The service is available <a href="https://mynotesapp-nxxo6p55tq-uc.a.run.app">here</a>. Some sample queries:</li>
    </ul>
  </li>
</ul>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="go">➜  ~ curl -s "https://mynotesapp-nxxo6p55tq-uc.a.run.app/" | jq                        
{                                                                              
  "id": 9,                                                                     
  "description": "shell - sum numbers from stdin",                                                                                                             
  "tags": [
    "shell",
    "commands",                                                                                                                                                
    "tools"
  ],
  "contents": [
</span><span class="gp">    "#</span><span class="w"> </span>Ways to <span class="nb">sum </span>up numbers from stdin or from piped output from previous commands<span class="s2">",
</span><span class="gp">    "#</span><span class="w"> </span><span class="s2">Using paste command"</span>,
<span class="gp">    "$</span><span class="w"> </span><span class="nb">echo </span>somefile | <span class="nb">paste</span> <span class="nt">-s</span> <span class="nt">-d</span>+ | bc<span class="s2">",
</span><span class="gp">    "$</span><span class="w"> </span><span class="s2">cat nums | paste -sd+ - | bc"</span>,
<span class="gp">    "#</span><span class="w"> </span>using awk.. substitute <span class="nv">$1</span> with the actual column number<span class="s2">",
</span><span class="gp">    "$</span><span class="w"> </span><span class="s2">cat nums | awk '{sum += </span><span class="nv">$1</span><span class="s2">} END {print sum}'"</span>
<span class="go">  ]
}

➜  ~ curl -s "https://mynotesapp-nxxo6p55tq-uc.a.run.app/notes?tags=vim&amp;limit=1" | jq
[
  {
    "id": 65,
    "description": "vim - show relative and absolute file path",
    "tags": [
      "vim",
      "programming",
      "editor",
      "shortcuts"
    ],
    "contents": [
      "Ctrl+G - shows the relative path of the current file",
      "{n}Ctrl+G - shows the relative path of the nth file in the buffer",
      "1Ctrl+G - shows the absolute path of the current file"
    ]
  }
]
</span></code></pre></div></div>

<p>It would be interesting to have an vscode extension too, that displays my notes in a pop-up.  May be for another weekend.</p>]]></content><author><name>Deepan Seeralan</name></author><category term="Tech" /><category term="programming" /><category term="gcp" /><category term="cloud" /><category term="go" /><category term="python" /><summary type="html"><![CDATA[A habit that I developed in recent times is to take a short note about the things I learn at work and in my personal explorations. Something like a flash card note, in a plain text, that is easy to take, remember and revisit. There are plenty of great flash card apps, note taking apps for mobile, web and desktop. No doubt about that. I wanted to reduce the friction and distractions in making a note, so things that require me to switch to my phone or different apps on my desktop can possibly distract me too much from my workflow. I just wanted some structure to the notes I take, so started off with a json file (like this). These are just plain text notes, with probably a few hundred entries. And, I could very well use my favorite VS code to maintain those notes, since VS Code is where I spent most of my time with. Days went by, my notes grew in number. While looking for ways I can make the learning better, I thought of bringing my notes more prominent in the places I frequent.]]></summary></entry><entry><title type="html">Serializing data with protobuf and json</title><link href="https://deepanseeralan.com/tech/serializing-data-with-protobuf-json/" rel="alternate" type="text/html" title="Serializing data with protobuf and json" /><published>2023-03-28T00:00:00-04:00</published><updated>2023-03-28T00:00:00-04:00</updated><id>https://deepanseeralan.com/tech/serializing-data-with-protobuf-json</id><content type="html" xml:base="https://deepanseeralan.com/tech/serializing-data-with-protobuf-json/"><![CDATA[<p>I have been reading about gRPC and protobuf in the recent times, exploring <a href="https://protobuf.dev/programming-guides/proto3/">protocol buffers</a> and <a href="https://grpc.io/docs/what-is-grpc/core-concepts/">grpc concepts</a>. The quick start tutorials provided for different languages were pretty good to start off with. After running through the HelloWorld example and another simple service, I was curious to see where protobuf stands tall and where it stands short, when compared to other data interchange mechanisms like json, xml etc. To begin with,</p>

<ul>
  <li>
    <p><strong>Schema Definition</strong>: Protocol Buffers require a schema definition that defines the structure of the message, which is then used to generate code in various languages. On the other hand, JSON does not require a schema definition, and its structure can vary widely depending on the implementation. Makes it super easy to write and test simple cases. No overhead of compiling the schema definition.</p>
  </li>
  <li>
    <p><strong>Language Support</strong>: Protocol Buffers provide first-class support for multiple languages, including Java, C++, Python, and Go, and support for additional languages can be added through extensions. JSON, on the other hand, has ubiquitous support across all languages.</p>
  </li>
  <li>
    <p><strong>Data Types</strong>: Protocol Buffers support a smaller set of data types than JSON, including strings, numbers, booleans, enums, and arrays. JSON supports a wider range of data types, including null, objects, and custom data types.</p>
  </li>
  <li>
    <p><strong>Parsing Overhead</strong>: JSON requires a parser to be used to parse the data, which can have a performance overhead. Protocol Buffers do not require a parser, and the data can be directly deserialized into objects.</p>
  </li>
  <li>
    <p><strong>Readability</strong>: JSON is more human-readable and easier to understand than Protocol Buffers, making it easier to debug and develop applications that consume JSON data.</p>
  </li>
</ul>

<p>Defined a simple protobuf definition with some <a href="https://protobuf.dev/programming-guides/proto3/#scalar">scalar types</a>.</p>

<div class="language-proto highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">syntax</span> <span class="o">=</span> <span class="s">"proto3"</span><span class="p">;</span>
<span class="k">option</span> <span class="na">go_package</span> <span class="o">=</span> <span class="s">"github.com/deepns/codegym/go/learning/grpc/echo"</span><span class="p">;</span>

<span class="kd">message</span> <span class="nc">EchoRequestWithCount</span> <span class="p">{</span>
    <span class="kt">string</span> <span class="kd">message</span> <span class="o">=</span> <span class="mi">1</span><span class="p">;</span>
    <span class="kt">int32</span> <span class="na">count</span> <span class="o">=</span> <span class="mi">2</span><span class="p">;</span>  
<span class="p">}</span>
</code></pre></div></div>

<p>Using Go as the language choice, compiled the above message with a protoc compiler. It created a struct type, with some unexported fields and with the message fields exported.</p>

<div class="language-go highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">type</span> <span class="n">EchoRequestWithCount</span> <span class="k">struct</span> <span class="p">{</span>
	<span class="n">state</span>         <span class="n">protoimpl</span><span class="o">.</span><span class="n">MessageState</span>
	<span class="n">sizeCache</span>     <span class="n">protoimpl</span><span class="o">.</span><span class="n">SizeCache</span>
	<span class="n">unknownFields</span> <span class="n">protoimpl</span><span class="o">.</span><span class="n">UnknownFields</span>

	<span class="n">Message</span> <span class="kt">string</span> <span class="s">`protobuf:"bytes,1,opt,name=message,proto3" json:"message,omitempty"`</span>
	<span class="n">Count</span>   <span class="kt">int32</span>  <span class="s">`protobuf:"varint,2,opt,name=count,proto3" json:"count,omitempty"`</span>
<span class="p">}</span>
</code></pre></div></div>

<p>Lets see how the encoded type loos like.</p>

<div class="language-go highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">import</span> <span class="p">(</span>
    <span class="s">"fmt"</span>
    <span class="n">pb</span> <span class="s">"github.com/deepns/codegym/go/learning/grpc/echo/echo"</span>
    <span class="s">"github.com/golang/protobuf/proto"</span>
<span class="p">)</span>

<span class="k">func</span> <span class="n">main</span><span class="p">()</span> <span class="p">{</span>
	<span class="n">echoRequest</span> <span class="o">:=</span> <span class="n">pb</span><span class="o">.</span><span class="n">EchoRequestWithCount</span><span class="p">{</span>
		<span class="n">Message</span><span class="o">:</span> <span class="s">"Woof!"</span><span class="p">,</span>
		<span class="n">Count</span><span class="o">:</span>   <span class="m">100</span><span class="p">,</span>
	<span class="p">}</span>
	<span class="n">echoRequestBinary</span><span class="p">,</span> <span class="n">_</span> <span class="o">:=</span> <span class="n">proto</span><span class="o">.</span><span class="n">Marshal</span><span class="p">(</span><span class="o">&amp;</span><span class="n">echoRequest</span><span class="p">)</span>
	<span class="n">fmt</span><span class="o">.</span><span class="n">Println</span><span class="p">(</span><span class="s">"echoRequestBinary:"</span><span class="p">,</span> <span class="n">echoRequestBinary</span><span class="p">)</span>
<span class="p">}</span>
</code></pre></div></div>

<p>The output came as:</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="go">echoRequestBinary: [10 5 87 111 111 102 33 16 100]
</span></code></pre></div></div>

<p>I guess <code class="language-plaintext highlighter-rouge">10</code> corresponds to <code class="language-plaintext highlighter-rouge">byte</code>, followed by length of the string (which is 5 in this case), then type of int32, determined by <code class="language-plaintext highlighter-rouge">16</code> followed by the actual value. For larger integers, the encoding scheme seems different.</p>

<p>To explore further, defined another message one with more types.</p>

<div class="language-proto highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">syntax</span> <span class="o">=</span> <span class="s">"proto3"</span><span class="p">;</span>

<span class="k">option</span> <span class="na">go_package</span> <span class="o">=</span> <span class="s">"github.com/deepns/codegym/go/learning/grpc/protobuf/books"</span><span class="p">;</span>

<span class="c1">// Book attributes defined with scalar fields</span>
<span class="kd">message</span> <span class="nc">Book</span> <span class="p">{</span>
    <span class="kt">string</span> <span class="na">title</span> <span class="o">=</span> <span class="mi">1</span><span class="p">;</span>
    <span class="kt">uint32</span> <span class="na">year</span> <span class="o">=</span> <span class="mi">2</span><span class="p">;</span>
    <span class="kt">double</span> <span class="na">price</span> <span class="o">=</span> <span class="mi">3</span><span class="p">;</span>
    <span class="kt">bool</span> <span class="na">is_released</span> <span class="o">=</span> <span class="mi">4</span><span class="p">;</span>
    <span class="n">BookGenre</span> <span class="na">genre</span> <span class="o">=</span> <span class="mi">5</span><span class="p">;</span>
<span class="p">}</span>

<span class="kd">enum</span> <span class="n">BookGenre</span> <span class="p">{</span>
    <span class="na">FICTION</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span>
    <span class="na">THRILLER</span> <span class="o">=</span> <span class="mi">1</span><span class="p">;</span>
    <span class="na">MEMOIR</span> <span class="o">=</span> <span class="mi">2</span><span class="p">;</span>
<span class="p">}</span>

<span class="kd">message</span> <span class="nc">Shelf</span> <span class="p">{</span> 
    <span class="k">repeated</span> <span class="n">Book</span> <span class="na">books_to_read</span> <span class="o">=</span> <span class="mi">1</span><span class="p">;</span>
    <span class="k">repeated</span> <span class="n">Book</span> <span class="na">books_read</span> <span class="o">=</span> <span class="mi">2</span><span class="p">;</span>
<span class="p">}</span>
</code></pre></div></div>

<p>Then tried to create some objects for the above types and serialize them into a file.</p>

<div class="language-go highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">package</span> <span class="n">main</span>

<span class="k">import</span> <span class="p">(</span>
	<span class="s">"encoding/json"</span>
	<span class="s">"log"</span>

	<span class="n">pb</span> <span class="s">"github.com/deepns/codegym/go/learning/grpc/protobuf/books"</span>
	<span class="s">"google.golang.org/protobuf/proto"</span>
<span class="p">)</span>

<span class="k">func</span> <span class="n">main</span><span class="p">()</span> <span class="p">{</span>
    <span class="n">shelf</span> <span class="o">:=</span> <span class="o">&amp;</span><span class="n">pb</span><span class="o">.</span><span class="n">Shelf</span><span class="p">{</span>
		<span class="n">BooksToRead</span><span class="o">:</span> <span class="p">[]</span><span class="o">*</span><span class="n">pb</span><span class="o">.</span><span class="n">Book</span><span class="p">{</span>
			<span class="p">{</span>
				<span class="n">Title</span><span class="o">:</span>      <span class="s">"Sapiens: A Brief History of Humankind"</span><span class="p">,</span>
				<span class="n">Year</span><span class="o">:</span>       <span class="m">2015</span><span class="p">,</span>
				<span class="n">Price</span><span class="o">:</span>      <span class="m">12.99</span><span class="p">,</span>
				<span class="n">IsReleased</span><span class="o">:</span> <span class="no">true</span><span class="p">,</span>
				<span class="n">Genre</span><span class="o">:</span>      <span class="n">pb</span><span class="o">.</span><span class="n">BookGenre_MEMOIR</span><span class="p">,</span>
			<span class="p">},</span>
			<span class="p">{</span>
				<span class="n">Title</span><span class="o">:</span>      <span class="s">"The Water Dancer"</span><span class="p">,</span>
				<span class="n">Year</span><span class="o">:</span>       <span class="m">2019</span><span class="p">,</span>
				<span class="n">Price</span><span class="o">:</span>      <span class="m">11.99</span><span class="p">,</span>
				<span class="n">IsReleased</span><span class="o">:</span> <span class="no">true</span><span class="p">,</span>
				<span class="n">Genre</span><span class="o">:</span>      <span class="n">pb</span><span class="o">.</span><span class="n">BookGenre_FICTION</span><span class="p">,</span>
			<span class="p">},</span>
		<span class="p">},</span>
		<span class="n">BooksRead</span><span class="o">:</span> <span class="p">[]</span><span class="o">*</span><span class="n">pb</span><span class="o">.</span><span class="n">Book</span><span class="p">{</span>
			<span class="p">{</span>
				<span class="n">Title</span><span class="o">:</span>      <span class="s">"The Underground Railroad"</span><span class="p">,</span>
				<span class="n">Year</span><span class="o">:</span>       <span class="m">2016</span><span class="p">,</span>
				<span class="n">Price</span><span class="o">:</span>      <span class="m">9.99</span><span class="p">,</span>
				<span class="n">IsReleased</span><span class="o">:</span> <span class="no">true</span><span class="p">,</span>
				<span class="n">Genre</span><span class="o">:</span>      <span class="n">pb</span><span class="o">.</span><span class="n">BookGenre_FICTION</span><span class="p">,</span>
			<span class="p">},</span>
			<span class="p">{</span>
				<span class="n">Title</span><span class="o">:</span>      <span class="s">"The Power of Now: A Guide to Spiritual Enlightenment"</span><span class="p">,</span>
				<span class="n">Year</span><span class="o">:</span>       <span class="m">1997</span><span class="p">,</span>
				<span class="n">Price</span><span class="o">:</span>      <span class="m">7.99</span><span class="p">,</span>
				<span class="n">IsReleased</span><span class="o">:</span> <span class="no">true</span><span class="p">,</span>
				<span class="n">Genre</span><span class="o">:</span>      <span class="n">pb</span><span class="o">.</span><span class="n">BookGenre_MEMOIR</span><span class="p">,</span>
			<span class="p">},</span>
		<span class="p">},</span>
	<span class="p">}</span>

	<span class="c">// Marshal the "shelf" instance into binary format</span>
	<span class="n">shelfData</span><span class="p">,</span> <span class="n">err</span> <span class="o">:=</span> <span class="n">proto</span><span class="o">.</span><span class="n">Marshal</span><span class="p">(</span><span class="n">shelf</span><span class="p">)</span>
	<span class="k">if</span> <span class="n">err</span> <span class="o">!=</span> <span class="no">nil</span> <span class="p">{</span>
		<span class="n">log</span><span class="o">.</span><span class="n">Fatalln</span><span class="p">(</span><span class="s">"Error marshaling shelf data"</span><span class="p">,</span> <span class="n">err</span><span class="p">)</span>
	<span class="p">}</span>

    <span class="c">// Marshal the "shelf" instance into json format</span>
	<span class="c">// Since json encoding is defined in the struct itself, protobuf struct</span>
	<span class="c">// can be readily marshaled to json.</span>
	<span class="n">shelfDataJson</span><span class="p">,</span> <span class="n">err</span> <span class="o">:=</span> <span class="n">json</span><span class="o">.</span><span class="n">Marshal</span><span class="p">(</span><span class="n">shelf</span><span class="p">)</span>
	<span class="k">if</span> <span class="n">err</span> <span class="o">!=</span> <span class="no">nil</span> <span class="p">{</span>
		<span class="n">log</span><span class="o">.</span><span class="n">Fatalln</span><span class="p">(</span><span class="s">"Error marshaling shelf data to json"</span><span class="p">,</span> <span class="n">err</span><span class="p">)</span>
	<span class="p">}</span>

    <span class="n">log</span><span class="o">.</span><span class="n">Println</span><span class="p">(</span><span class="s">"Bytes written in pb format:"</span><span class="p">,</span> <span class="nb">len</span><span class="p">(</span><span class="n">shelfData</span><span class="p">))</span>
	<span class="n">log</span><span class="o">.</span><span class="n">Println</span><span class="p">(</span><span class="s">"Bytes written in json format:"</span><span class="p">,</span> <span class="nb">len</span><span class="p">(</span><span class="n">shelfDataJson</span><span class="p">))</span>
</code></pre></div></div>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="go">2023/03/30 18:30:38 Bytes written in pb format: 205
2023/03/30 18:30:38 Bytes written in json format: 413
</span></code></pre></div></div>

<p>Protobuf bytes took about 205 bytes whereas the json based data bytes took about 413 bytes in this case, ~50% reduction. This is of course highly simplified example, as this could vary depending on the length of the keys and values. These savings on data size and transfer do provide significant values in high performance applications.</p>]]></content><author><name>Deepan Seeralan</name></author><category term="Tech" /><category term="go" /><category term="programming" /><category term="protobuf" /><category term="json" /><summary type="html"><![CDATA[I have been reading about gRPC and protobuf in the recent times, exploring protocol buffers and grpc concepts. The quick start tutorials provided for different languages were pretty good to start off with. After running through the HelloWorld example and another simple service, I was curious to see where protobuf stands tall and where it stands short, when compared to other data interchange mechanisms like json, xml etc. To begin with,]]></summary></entry><entry><title type="html">Exploring Zookeeper C Client</title><link href="https://deepanseeralan.com/tech/exploring-zookeeper-c-client/" rel="alternate" type="text/html" title="Exploring Zookeeper C Client" /><published>2023-03-24T00:00:00-04:00</published><updated>2023-03-24T00:00:00-04:00</updated><id>https://deepanseeralan.com/tech/exploring-zookeeper-c-client</id><content type="html" xml:base="https://deepanseeralan.com/tech/exploring-zookeeper-c-client/"><![CDATA[<p>A short while ago I happened to work on Zookeeper related feature that required the use of Zookeeper C Client library to interact with the ensemble. We ran into many stability issues on the client side, so had to dig deeper to diagnose those issues and fix. Unfortunately ZK C client did’t have rich documentation and code comments, so navigating the code was somewhat challenging at the beginning.</p>

<p>The library provides a set of functions for creating and managing ZooKeeper sessions, creating and deleting nodes in the namespace, setting and getting node data, setting watches on nodes to receive notifications of changes, and performing various other operations such as listing child nodes, checking node existence, and setting access control lists.</p>

<p>The code is organized into several modules, including a main client module (zookeeper.c), threading adaptors (st_adaptor.c and mt_adaptor.c), hashtable (zk_hashtable.c) and various utility modules for data serialization, buffer management, logging, and error handling.</p>

<p>Functions in <code class="language-plaintext highlighter-rouge">zookeeper.c</code> handles communication with the ensemble using ZooKeeper’s wire protocol and manages the client’s session state, including connection management, session timeouts, and error handling. It also supports SSL/TLS connections to ZooKeeper servers through the use of OpenSSL.</p>

<p>A short rundown of the main functions defined in zookeeper.c:</p>

<ul>
  <li>
    <p><strong>zookeeper_init()</strong>: called to initialize the ZooKeeper client library. It takes a set of connection parameters, such as the host and port of the ZooKeeper ensemble, a timeout value, and a callback function to handle connection state changes. The function returns a zhandle_t handle, which is used to identify the client’s session.</p>
  </li>
  <li>
    <p><strong>zookeeper_close()</strong>: called to close the client’s session with the ZooKeeper ensemble. It takes a zhandle_t handle as input and returns 0 on success.</p>
  </li>
  <li>
    <p><strong>zookeeper_create()</strong>: used to create a new node in the ZooKeeper namespace. It takes a zhandle_t handle, the path of the node to create, the initial data for the node, a set of flags to specify node creation options, and a callback function to handle completion of the operation. The function returns the path of the newly created node on success.</p>
  </li>
  <li>
    <p><strong>zookeeper_delete()</strong>: used to delete a node from the ZooKeeper namespace. It takes a zhandle_t handle and the path of the node to delete, along with a version number that must match the current version of the node. The function returns 0 on success.</p>
  </li>
  <li>
    <p><strong>zookeeper_set()</strong>: used to set the data associated with a node in the ZooKeeper namespace. It takes a zhandle_t handle, the path of the node to set, the new data for the node, and a version number that must match the current version of the node. The function returns the version number of the newly set data on success.</p>
  </li>
  <li>
    <p><strong>zookeeper_get()</strong>: used to get the data associated with a node in the ZooKeeper namespace. It takes a zhandle_t handle, the path of the node to get, a callback function to handle completion of the operation, and a set of flags to specify node retrieval options.</p>
  </li>
  <li>
    <p><strong>zookeeper_wget()</strong>: similar to zookeeper_get(), but it also registers a watch on the node to receive notifications of changes to the node’s data.</p>
  </li>
  <li>
    <p><strong>zookeeper_exists()</strong>: used to check if a node exists in the ZooKeeper namespace. It takes a zhandle_t handle, the path of the node to check, a callback function to handle completion of the operation, and a set of flags to specify node existence check options.</p>
  </li>
  <li>
    <p><strong>zookeeper_wexists()</strong>: similar to zookeeper_exists(), but it also registers a watch on the node to receive notifications of changes to the node’s existence status.</p>
  </li>
</ul>

<p><strong>mt_adaptor.c</strong> provides a multithreaded adaptor layer to allow client applications to use the ZooKeeper library in a multithreaded environment. The module uses a global lock to protect access to the ZooKeeper library’s internal state, and all calls to the library are made through the adaptor functions provided by mt_adaptor.c.</p>

<p><strong>zk_hashtable.c</strong> provides a hashtable implementation for storing key-value pairs and uses chaining to handle collisions. Each entry in the hashtable is a struct hash_node, which contains a key-value pair and a pointer to the next node in the chain. Data typically stored in this table are <code class="language-plaintext highlighter-rouge">zhandle_t</code>, <code class="language-plaintext highlighter-rouge">znode_t</code> <code class="language-plaintext highlighter-rouge">watcher_registration_t</code> and <code class="language-plaintext highlighter-rouge">watcher_registration_t</code>.</p>

<p>I forked the repo to explore the code and <a href="https://github.com/deepns/zookeeper/blob/addl-comments/">added detailed comments</a> about the code flow, session management, error handling etc. (just for learning reference, not meant to be production grade) so I can always refer to in case of doubts. Full diffs of the change is available <a href="https://github.com/apache/zookeeper/compare/master...deepns:zookeeper:addl-comments">here</a>.</p>]]></content><author><name>Deepan Seeralan</name></author><category term="Tech" /><category term="zookeeper" /><category term="c" /><category term="programming" /><category term="learning" /><summary type="html"><![CDATA[A short while ago I happened to work on Zookeeper related feature that required the use of Zookeeper C Client library to interact with the ensemble. We ran into many stability issues on the client side, so had to dig deeper to diagnose those issues and fix. Unfortunately ZK C client did’t have rich documentation and code comments, so navigating the code was somewhat challenging at the beginning.]]></summary></entry><entry><title type="html">Using ChatGPT in my everyday workflow</title><link href="https://deepanseeralan.com/tech/using-chatgpt-in-everyday-workflow/" rel="alternate" type="text/html" title="Using ChatGPT in my everyday workflow" /><published>2023-03-11T00:00:00-05:00</published><updated>2023-03-11T00:00:00-05:00</updated><id>https://deepanseeralan.com/tech/using-chatgpt-in-everyday-workflow</id><content type="html" xml:base="https://deepanseeralan.com/tech/using-chatgpt-in-everyday-workflow/"><![CDATA[<p>A few months ago, ChatGPT was made generally available and like many others, I was in awe of its technology, power, and most importantly, its simplicity of use. After the initial fun of exploring it, I didn’t use it much. But slowly, the usage of ChatGPT started to climb up and I now find it so useful in a myriad of use cases. In this post, I will share some of the ways I use ChatGPT in my daily life and why I love it.</p>

<p><strong>Writing Code</strong>: ChatGPT has been incredibly helpful in writing small Bash and Python scripts and tools. All I have to do is provide the requirements and it spits out beautiful code solving the given problem. The clarity of the answer depends on the clarity of the question, which is why I appreciate how it forces me to think and explain the requirements in simple terms. I also use it to write starter code, class definitions, templates and unit tests.</p>

<p><strong>Summarizing Code:</strong> I often use ChatGPT to summarize long lines of code. This makes it easier for me to understand and troubleshoot, especially when dealing with complex codebases. I do this only on opensource code though.</p>

<p><strong>Writing queries, commands</strong>: Generating jq queries for complex data can be time-consuming, but ChatGPT can generate them quickly and accurately. I also find it useful to write shell commands that otherwise required me to read the man pages and find the options and syntax by myself.</p>

<p><strong>Generating Test Data</strong>: ChatGPT can create test data in formats like JSON and YAML, making it a great tool for manual data entry.</p>

<p><strong>Proofreading Content</strong>: ChatGPT can help me check content for language, grammar, and correctness. This is incredibly helpful, especially when I need to write professional emails, feedback, or reviews. (p.s. this post also went through this exercise 😄)</p>

<p><strong>Creative Writing</strong>: I also use ChatGPT to make up stories with different characters and themes to tell to my kid.</p>

<p>I briefly tried the new Bing with ChatGPT and Neeva, but I found that the user experience and result correctness weren’t as good as ChatGPT.</p>

<p>I don’t know much about the AI technology behind ChatGPT, but I am super optimistic about the future powered by tools like ChatGPT. I believe that ChatGPT has the potential to revolutionize the way we work and communicate. It simplifies tasks, improves efficiency, and challenges us to think more clearly and communicate more effectively. ChatGPT has become an essential tool in my daily life, and I’m so excited to see how it will continue to evolve in the future.</p>]]></content><author><name>Deepan Seeralan</name></author><category term="Tech" /><category term="programming" /><category term="productivity" /><category term="tools" /><summary type="html"><![CDATA[A few months ago, ChatGPT was made generally available and like many others, I was in awe of its technology, power, and most importantly, its simplicity of use. After the initial fun of exploring it, I didn’t use it much. But slowly, the usage of ChatGPT started to climb up and I now find it so useful in a myriad of use cases. In this post, I will share some of the ways I use ChatGPT in my daily life and why I love it.]]></summary></entry><entry><title type="html">Updating google analytics tag from universal to analytics-4 in minimal mistakes</title><link href="https://deepanseeralan.com/tech/google-universal-analytics-to-google-analytics-4/" rel="alternate" type="text/html" title="Updating google analytics tag from universal to analytics-4 in minimal mistakes" /><published>2023-03-02T00:00:00-05:00</published><updated>2023-03-02T00:00:00-05:00</updated><id>https://deepanseeralan.com/tech/google-universal-analytics-to-google-analytics-4</id><content type="html" xml:base="https://deepanseeralan.com/tech/google-universal-analytics-to-google-analytics-4/"><![CDATA[<p>I have been using Google Universal analytics tag with my blog for a while. Google has been pushing the users to move to Google Analytics 4 from the current Universal Analytics for almost a year. Admittedly, I use it in a very basic ways just to monitor the page visits, so didn’t bother much to do the required update. I had been using <code class="language-plaintext highlighter-rouge">google-universal</code> as the provider in the jekyll config.</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">analytics</span><span class="pi">:</span>
  <span class="na">provider</span><span class="pi">:</span> <span class="s2">"</span><span class="s">google-universal"</span>
  <span class="na">google</span><span class="pi">:</span>
    <span class="na">tracking_id</span><span class="pi">:</span> <span class="s2">"</span><span class="s">UA-908945612-1"</span>
</code></pre></div></div>

<p>To move to GA-4, I had to</p>

<ul>
  <li>update the provider to <strong>google-gtag</strong></li>
  <li>get the measurement ID from the data stream (as explained <a href="https://support.google.com/analytics/answer/9539598#find-G-ID">here</a>)</li>
</ul>

<p><img src="https://storage.googleapis.com/support-kms-prod/4vzOnPW93ZjrGTZKfeIJYHXXPmpfCmc0UMHy" alt="get-measurement-id" /></p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">analytics</span><span class="pi">:</span>
  <span class="na">provider</span><span class="pi">:</span> <span class="s2">"</span><span class="s">google-gtag"</span>
  <span class="na">google</span><span class="pi">:</span>
    <span class="na">tracking_id</span><span class="pi">:</span> <span class="s2">"</span><span class="s">G-TYVCJUF74I"</span>
</code></pre></div></div>

<p>Post update, data from the universal analytics tag didn’t seem to be carried over. GA-4 property shows only the data from the new traffic. The home page for GA-4 property does look better than the old one though, with cleaner UI, personalized dashboard and customized insights and recommendations.</p>]]></content><author><name>Deepan Seeralan</name></author><category term="[&quot;Tech&quot;]" /><category term="google" /><category term="learning" /><category term="github" /><summary type="html"><![CDATA[I have been using Google Universal analytics tag with my blog for a while. Google has been pushing the users to move to Google Analytics 4 from the current Universal Analytics for almost a year. Admittedly, I use it in a very basic ways just to monitor the page visits, so didn’t bother much to do the required update. I had been using google-universal as the provider in the jekyll config.]]></summary></entry></feed>