<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Ryan Giggs</title>
    <description>The latest articles on DEV Community by Ryan Giggs (@derrickryangiggs).</description>
    <link>https://dev.to/derrickryangiggs</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F2544697%2F1f321a52-47b1-4034-9cc0-fba8e0b8273c.jpeg</url>
      <title>DEV Community: Ryan Giggs</title>
      <link>https://dev.to/derrickryangiggs</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/derrickryangiggs"/>
    <language>en</language>
    <item>
      <title>Why Your AI Works in a Notebook But Fails in Production — LLM Zoomcamp Module 3</title>
      <dc:creator>Ryan Giggs</dc:creator>
      <pubDate>Sun, 05 Jul 2026 13:58:10 +0000</pubDate>
      <link>https://dev.to/derrickryangiggs/why-your-ai-works-in-a-notebook-but-fails-in-production-llm-zoomcamp-module-3-74b</link>
      <guid>https://dev.to/derrickryangiggs/why-your-ai-works-in-a-notebook-but-fails-in-production-llm-zoomcamp-module-3-74b</guid>
      <description>&lt;p&gt;Every developer has seen it: an AI prototype that impresses in demos but breaks the moment real users touch it. The problem is rarely the model. It's everything around the model, how context is prepared, how tools are called, how failures are handled, how costs are tracked.&lt;/p&gt;

&lt;p&gt;Module 3 of LLM Zoomcamp 2026 addresses this gap head-on. Taught by Will Russell from Kestra, this module shifts the conversation from "how do I call an LLM" to "how do I run AI reliably at scale."&lt;/p&gt;

&lt;p&gt;Here's what I learned and what most LLM tutorials never teach you.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Experiment That Changed How I Think About AI
&lt;/h2&gt;

&lt;p&gt;Try this yourself. Open ChatGPT and ask it to generate a Kestra workflow that loads CSV data into BigQuery. You'll get YAML that looks completely reasonable and doesn't work. Wrong plugin types, outdated parameters, invented task names.&lt;/p&gt;

&lt;p&gt;Now try the same prompt in Kestra's AI Copilot. It generates a flow that runs.&lt;/p&gt;

&lt;p&gt;Same model family. Same prompt. Completely different outcome.&lt;/p&gt;

&lt;p&gt;The reason: Kestra's AI Copilot retrieves current plugin documentation before generating. The model has accurate, up-to-date context. ChatGPT is guessing from training data that's months or years old.&lt;/p&gt;

&lt;p&gt;This is &lt;strong&gt;Context Engineering&lt;/strong&gt;, the discipline of giving a model exactly the right information at the right time. It's more impactful than model size, fine-tuning, or prompt tricks.&lt;/p&gt;




&lt;h2&gt;
  
  
  RAG vs No RAG: Measured, Not Assumed
&lt;/h2&gt;

&lt;p&gt;The module includes a concrete experiment with two pre-built flows:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Without RAG&lt;/strong&gt;, the model invents plausible but fabricated Kestra 1.1 features with full confidence. It describes "Declarative I/O with Variables", "Enhanced Task Groups", and other features that sound real but aren't accurate to that release.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;With RAG&lt;/strong&gt;, the model retrieves the actual Kestra 1.1 release notes and returns real features: the No-Code Dashboard Editor, Multi-Agent AI Systems, Fix with AI, the Human Task for manual approvals, and improved air-gapped environment support.&lt;/p&gt;

&lt;p&gt;One is a liability in production. The other is actually useful.&lt;/p&gt;




&lt;h2&gt;
  
  
  AI Agents: The LLM Takes the Wheel
&lt;/h2&gt;

&lt;p&gt;The second half of the module moves from RAG to agents. The difference:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;RAG pipeline&lt;/strong&gt;: you decide when to search, what to search, how many results to retrieve&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;AI agent&lt;/strong&gt;: the model decides all of that itself&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In Kestra, agents are declared in YAML and the framework handles the agentic loop:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;research_agent&lt;/span&gt;
  &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;io.kestra.plugin.ai.agent.AIAgent&lt;/span&gt;
  &lt;span class="na"&gt;systemMessage&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
    &lt;span class="s"&gt;You are a technical research assistant.&lt;/span&gt;
    &lt;span class="s"&gt;Use your tools to find accurate information before answering.&lt;/span&gt;
  &lt;span class="na"&gt;prompt&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;{{&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;inputs.question&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;}}"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The agent decides whether to call a tool, which tool, with what query, and when it has enough context to stop searching and answer. You get full observability, every tool call, every response, every token logged in the Kestra UI.&lt;/p&gt;




&lt;h2&gt;
  
  
  Tracking What Matters: Token Usage
&lt;/h2&gt;

&lt;p&gt;Most tutorials skip this. The module doesn't.&lt;/p&gt;

&lt;p&gt;Every agent task in Kestra exposes token usage on its output object. The module shows you how to log it explicitly:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;log_usage&lt;/span&gt;
  &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;io.kestra.plugin.core.log.Log&lt;/span&gt;
  &lt;span class="na"&gt;message&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
    &lt;span class="s"&gt;Output tokens: {{ outputs.agent.tokenUsage.outputTokenCount }}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;What I measured across runs:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Short summary: 63 output tokens&lt;/li&gt;
&lt;li&gt;Long summary: 184 output tokens (~3x more)&lt;/li&gt;
&lt;li&gt;3-sentence vs 1-sentence prompt: ~2x more tokens&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Small wording changes in prompts have real cost implications when running thousands of executions. Monitoring from day one prevents surprises.&lt;/p&gt;




&lt;h2&gt;
  
  
  When Agents Are the Wrong Choice
&lt;/h2&gt;

&lt;p&gt;This is the lesson most AI courses skip: &lt;strong&gt;agents are non-deterministic&lt;/strong&gt;. The same input can produce different tool calls, different search strategies, different answers across runs.&lt;/p&gt;

&lt;p&gt;For creative tasks, research, and open-ended workflows that flexibility is a feature.&lt;/p&gt;

&lt;p&gt;For financial reporting, compliance workflows, audit trails, and regulated industries it's a liability. You need deterministic, repeatable, fully auditable pipelines where every step is predictable and logged.&lt;/p&gt;

&lt;p&gt;Kestra supports both. Knowing when to use which is the actual engineering judgment.&lt;/p&gt;




&lt;h2&gt;
  
  
  My Full Solution
&lt;/h2&gt;

&lt;p&gt;All flows and code are open source:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://github.com/Derrick-Ryan-Giggs/llm-zoomcamp-2026" rel="noopener noreferrer"&gt;github.com/Derrick-Ryan-Giggs/llm-zoomcamp-2026&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Take the Course Free
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://github.com/DataTalksClub/llm-zoomcamp/" rel="noopener noreferrer"&gt;github.com/DataTalksClub/llm-zoomcamp&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Module 4 covers Evaluation — how to measure whether your RAG system is actually working. Writing about that next.&lt;/p&gt;

</description>
      <category>rag</category>
      <category>agents</category>
      <category>kestra</category>
      <category>datatalksclub</category>
    </item>
    <item>
      <title>I Built a Hybrid Search Engine From Scratch — Here's What I Learned (LLM Zoomcamp 2026, Module 2)</title>
      <dc:creator>Ryan Giggs</dc:creator>
      <pubDate>Fri, 26 Jun 2026 11:49:10 +0000</pubDate>
      <link>https://dev.to/derrickryangiggs/i-built-a-hybrid-search-engine-from-scratch-heres-what-i-learned-llm-zoomcamp-2026-module-2-3jdj</link>
      <guid>https://dev.to/derrickryangiggs/i-built-a-hybrid-search-engine-from-scratch-heres-what-i-learned-llm-zoomcamp-2026-module-2-3jdj</guid>
      <description>&lt;p&gt;I just completed Module 2 of the &lt;strong&gt;LLM Zoomcamp 2026&lt;/strong&gt; by &lt;a href="https://github.com/DataTalksClub/llm-zoomcamp/" rel="noopener noreferrer"&gt;@DataTalksClub&lt;/a&gt; — and this module completely changed how I think about search.&lt;/p&gt;

&lt;p&gt;Module 1 taught me RAG and agentic pipelines. Module 2 taught me that the search step inside RAG matters far more than I realized — and that keyword search is only half the story.&lt;/p&gt;

&lt;p&gt;Here's everything I built and learned.&lt;/p&gt;




&lt;h2&gt;
  
  
  What Is Vector Search and Why Does It Matter?
&lt;/h2&gt;

&lt;p&gt;Traditional keyword search matches words. If you search for "enroll", it finds documents containing "enroll" — but misses documents about "joining", "signing up", or "registration" even if they mean exactly the same thing.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Vector search matches meaning, not words.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Every piece of text gets converted into a vector — a list of hundreds of numbers that captures its semantic meaning. Similar meanings produce similar vectors, so you can find relevant documents even when they use completely different words.&lt;/p&gt;

&lt;p&gt;This is the foundation of modern AI-powered search, and it's what makes RAG systems actually work at scale.&lt;/p&gt;




&lt;h2&gt;
  
  
  What I Built in Module 2
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. Text Embeddings with a Lightweight ONNX Model
&lt;/h3&gt;

&lt;p&gt;Instead of downloading the full PyTorch + CUDA stack (~2GB), I used a lightweight ONNX runtime embedder — same vectors, 30x smaller installation, runs on any CPU:&lt;br&gt;
&lt;/p&gt;

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

&lt;span class="n"&gt;embedder&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Embedder&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;  &lt;span class="c1"&gt;# loads Xenova/all-MiniLM-L6-v2 via ONNX
&lt;/span&gt;&lt;span class="n"&gt;v&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;embedder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;encode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;How does approximate nearest neighbor search work?&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;  &lt;span class="c1"&gt;# 384 dimensions
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The model produces &lt;strong&gt;384-dimensional vectors&lt;/strong&gt; — each number represents a dimension of meaning in the text.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Vector Search From Scratch with NumPy
&lt;/h3&gt;

&lt;p&gt;Before using any library, I implemented vector search by hand to understand what's happening under the hood:&lt;br&gt;
&lt;/p&gt;

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

&lt;span class="c1"&gt;# cosine similarity — vectors are normalized, so dot product works directly
&lt;/span&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;cosine_similarity&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;dot&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;# score all chunks against a query
&lt;/span&gt;&lt;span class="n"&gt;scores&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;X&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;dot&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="c1"&gt;# X is the matrix of all chunk embeddings
&lt;/span&gt;&lt;span class="n"&gt;best_idx&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;argmax&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;scores&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is exactly what vector databases like Qdrant and pgvector do internally — just much faster at scale using HNSW indexing.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Chunking Long Documents for Better Retrieval
&lt;/h3&gt;

&lt;p&gt;Full pages are too long and dilute the embedding — a match buried deep in a 10,000-character page still pulls in the whole page. The fix is chunking:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;gitsource&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;chunk_documents&lt;/span&gt;
&lt;span class="n"&gt;chunks&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;chunk_documents&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;documents&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;size&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;2000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;step&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;1000&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="c1"&gt;# 72 pages → 295 overlapping chunks
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Overlapping chunks (step &amp;lt; size) ensure sentences at boundaries don't get cut off. After chunking, retrieval becomes far more precise.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Vector Search with minsearch
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;minsearch&lt;/code&gt; now has a &lt;code&gt;VectorSearch&lt;/code&gt; class that wraps the numpy math into a clean interface:&lt;br&gt;
&lt;/p&gt;

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

&lt;span class="n"&gt;vector_index&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;VectorSearch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;keyword_fields&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;filename&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
&lt;span class="n"&gt;vector_index&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;X&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;chunks&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;results&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;vector_index&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;search&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;query_vector&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;num_results&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  5. Comparing Keyword vs Vector Search
&lt;/h3&gt;

&lt;p&gt;For the query &lt;strong&gt;"How do I store vectors in PostgreSQL?"&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Keyword search&lt;/strong&gt; — missed &lt;code&gt;08-pgvector.md&lt;/code&gt; entirely because "pgvector" wasn't in the query&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Vector search&lt;/strong&gt; — ranked &lt;code&gt;08-pgvector.md&lt;/code&gt; first because it understood the semantic connection between "store vectors" and "pgvector"&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is the key insight: vector search finds meaning, keyword search finds words.&lt;/p&gt;

&lt;h3&gt;
  
  
  6. Hybrid Search with Reciprocal Rank Fusion (RRF)
&lt;/h3&gt;

&lt;p&gt;Neither approach is perfect on its own:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Vector search can miss exact terms, names, and rare keywords&lt;/li&gt;
&lt;li&gt;Keyword search misses paraphrases and synonyms&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The solution is &lt;strong&gt;hybrid search&lt;/strong&gt; — run both and merge the results using RRF:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;rrf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;result_lists&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;k&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;60&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;num_results&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;scores&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;
    &lt;span class="n"&gt;docs&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;results&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;result_lists&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;rank&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;doc&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;enumerate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;results&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
            &lt;span class="n"&gt;key&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;doc&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;filename&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;doc&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;start&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
            &lt;span class="n"&gt;scores&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;scores&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;k&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;rank&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="n"&gt;docs&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;doc&lt;/span&gt;
    &lt;span class="n"&gt;ranked&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;sorted&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;scores&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;scores&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;get&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;reverse&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;docs&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;key&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;ranked&lt;/span&gt;&lt;span class="p"&gt;[:&lt;/span&gt;&lt;span class="n"&gt;num_results&lt;/span&gt;&lt;span class="p"&gt;]]&lt;/span&gt;

&lt;span class="n"&gt;results&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;rrf&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="n"&gt;vector_results&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;text_results&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;RRF ignores raw scores (which live on different scales) and only looks at rank position. A document that ranks well in both lists beats one that's only strong in a single list — even if it wasn't first in either.&lt;/p&gt;




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

&lt;p&gt;&lt;strong&gt;1. Embeddings capture meaning, not words.&lt;/strong&gt; "Enroll" and "join" produce similar vectors. "Pizza" and "enrollment" don't. This is what makes semantic search powerful.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Chunking is not optional.&lt;/strong&gt; Full pages dilute embeddings. 2,000-character overlapping chunks dramatically improve retrieval precision and cut LLM input tokens by 3x.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Neither keyword nor vector search is best.&lt;/strong&gt; Use hybrid search (RRF) in production. It consistently outperforms either approach alone.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. ONNX makes embeddings practical anywhere.&lt;/strong&gt; No GPU, no PyTorch, no CUDA. 67MB download, runs on a basic laptop. There's no reason not to use vector search even in constrained environments.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;5. The right search approach depends on your data.&lt;/strong&gt; Vector search wins for semantic queries. Keyword search wins for exact terms (names, codes, IDs). Hybrid wins most of the time — but measure to be sure.&lt;/p&gt;




&lt;h2&gt;
  
  
  My Homework Solution
&lt;/h2&gt;

&lt;p&gt;All my code for Module 2 is open source:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://github.com/Derrick-Ryan-Giggs/llm-zoomcamp-2026" rel="noopener noreferrer"&gt;github.com/Derrick-Ryan-Giggs/llm-zoomcamp-2026&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;It includes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;vector-search.ipynb&lt;/code&gt; — embeddings, Qdrant, and vector RAG pipeline&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Vector Search Homework.ipynb&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Want to Learn Too?
&lt;/h2&gt;

&lt;p&gt;LLM Zoomcamp is &lt;strong&gt;completely free&lt;/strong&gt; — no paywall, no certificate fees.&lt;/p&gt;

&lt;p&gt;Sign up: &lt;a href="https://github.com/DataTalksClub/llm-zoomcamp/" rel="noopener noreferrer"&gt;github.com/DataTalksClub/llm-zoomcamp&lt;/a&gt;&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Are you working through LLM Zoomcamp 2026? Drop a comment — I'd love to compare notes.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>rag</category>
      <category>llm</category>
      <category>vectordatabase</category>
      <category>datatalksclub</category>
    </item>
    <item>
      <title>Cloud Native Architecture Fundamentals: Building for the Modern Era</title>
      <dc:creator>Ryan Giggs</dc:creator>
      <pubDate>Thu, 25 Jun 2026 09:06:34 +0000</pubDate>
      <link>https://dev.to/derrickryangiggs/cloud-native-architecture-fundamentals-building-for-the-modern-era-15lo</link>
      <guid>https://dev.to/derrickryangiggs/cloud-native-architecture-fundamentals-building-for-the-modern-era-15lo</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;Optimize your software for cost, efficiency, reliability, and faster delivery — by combining the right technological and architectural patterns.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Based on cloud native curriculum from the &lt;a href="https://www.linuxfoundation.org" rel="noopener noreferrer"&gt;@Linux Foundation&lt;/a&gt; | CNCF&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  What Is Cloud Native?
&lt;/h2&gt;

&lt;p&gt;The Cloud Native Computing Foundation (CNCF) defines cloud native as a set of practices that empower organizations to develop, build, and deploy workloads in computing environments — public, private, and hybrid cloud — to meet organizational needs at scale in a programmatic and repeatable manner.&lt;/p&gt;

&lt;p&gt;Cloud native systems are characterized by being &lt;strong&gt;loosely coupled&lt;/strong&gt;, interoperating in a manner that is &lt;strong&gt;secure, resilient, manageable, sustainable, and observable&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;In practice, cloud native architectures typically consist of some combination of:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Containers&lt;/li&gt;
&lt;li&gt;Service meshes&lt;/li&gt;
&lt;li&gt;Microservices&lt;/li&gt;
&lt;li&gt;Immutable infrastructure&lt;/li&gt;
&lt;li&gt;Serverless functions&lt;/li&gt;
&lt;li&gt;Declarative APIs&lt;/li&gt;
&lt;li&gt;Multi-tenancy&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;And the ecosystem is thriving. According to the &lt;a href="https://www.cncf.io/announcements/2025/04/01/cncf-research-reveals-how-cloud-native-technology-is-reshaping-global-business-and-innovation/" rel="noopener noreferrer"&gt;CNCF 2024 Annual Survey&lt;/a&gt;, cloud native adoption has reached an all-time high of &lt;strong&gt;89%&lt;/strong&gt; among surveyed organizations, with Kubernetes being used, piloted, or evaluated by &lt;strong&gt;93%&lt;/strong&gt; of those organizations.&lt;/p&gt;




&lt;h2&gt;
  
  
  Monolith vs. Cloud Native: Why the Shift?
&lt;/h2&gt;

&lt;p&gt;Traditional monolithic applications bundle all functionality into a single deployable unit. This works fine at small scale — but as user demand grows and teams expand, a monolith becomes a bottleneck. Every change risks the whole system, scaling requires duplicating everything, and a single bug can bring the entire application down.&lt;/p&gt;

&lt;p&gt;Cloud native architecture addresses this by &lt;strong&gt;breaking the application into smaller, independently deployable units&lt;/strong&gt; — each with a clearly defined scope of function. These are called &lt;strong&gt;microservices&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Instead of one giant app, you could have:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A &lt;strong&gt;UI service&lt;/strong&gt; rendering the frontend&lt;/li&gt;
&lt;li&gt;A &lt;strong&gt;checkout service&lt;/strong&gt; handling payments&lt;/li&gt;
&lt;li&gt;A &lt;strong&gt;notifications service&lt;/strong&gt; managing alerts&lt;/li&gt;
&lt;li&gt;An &lt;strong&gt;inventory service&lt;/strong&gt; tracking stock&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Each microservice communicates with the others over a network, typically via REST APIs or message queues. They can be developed, deployed, scaled, and updated independently — by separate teams, on separate schedules.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Monolith → Microservices&lt;/strong&gt; is not just a technical shift. It's a business transformation that directly impacts how fast you can ship, how resilient your product is, and how efficiently your teams operate.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  Core Technologies in Cloud Native
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Containers
&lt;/h3&gt;

&lt;p&gt;Containers package your application and all its dependencies into a single portable unit. They ensure the app runs consistently across development, staging, and production environments, eliminating the classic "it works on my machine" problem. Docker popularized this model; containerd is now the standard runtime in production Kubernetes clusters.&lt;/p&gt;

&lt;h3&gt;
  
  
  Kubernetes (K8s)
&lt;/h3&gt;

&lt;p&gt;Kubernetes is the de facto orchestration platform for containers. It automates deployment, scaling, and management of containerized applications. Advanced Kubernetes usage, including auto-scaling with the Horizontal Pod Autoscaler (HPA) and Vertical Pod Autoscaler (VPA), has been shown to &lt;a href="https://medium.com/@reiqwan/cloud-native-architecture-patterns-for-2025-building-enterprise-systems-that-scale-7c465142aaa4" rel="noopener noreferrer"&gt;increase deployment efficiency by 55%&lt;/a&gt; according to CNCF data.&lt;/p&gt;

&lt;h3&gt;
  
  
  Service Meshes
&lt;/h3&gt;

&lt;p&gt;A service mesh manages secure, reliable communication between microservices — handling load balancing, retries, circuit breaking, and mutual TLS (mTLS) encryption. Tools like &lt;strong&gt;Istio&lt;/strong&gt; and &lt;strong&gt;Linkerd&lt;/strong&gt; abstract these concerns away from your application code.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;2025 trend watch:&lt;/strong&gt; Service mesh is evolving away from the resource-heavy sidecar-per-pod model. Istio's &lt;strong&gt;ambient mode&lt;/strong&gt; now uses a node-level proxy (ztunnel) instead, dramatically reducing overhead and making mesh adoption more practical for teams previously deterred by operational complexity.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  Serverless
&lt;/h3&gt;

&lt;p&gt;Serverless takes abstraction further — you write code, the cloud provider handles all underlying infrastructure. Functions-as-a-Service (FaaS) platforms like AWS Lambda, Azure Functions, and Google Cloud Functions are ideal for event-driven, unpredictable workloads. Plans for serverless adoption are growing, with the share of companies running no serverless platforms expected to drop significantly through 2025.&lt;/p&gt;

&lt;h3&gt;
  
  
  Declarative APIs &amp;amp; Infrastructure-as-Code (IaC)
&lt;/h3&gt;

&lt;p&gt;Rather than imperatively telling systems &lt;em&gt;how&lt;/em&gt; to do something, you declare &lt;em&gt;what&lt;/em&gt; the desired state should be. Tools like &lt;strong&gt;Terraform&lt;/strong&gt;, &lt;strong&gt;Helm&lt;/strong&gt;, and &lt;strong&gt;Crossplane&lt;/strong&gt; allow teams to version-control infrastructure alongside application code — making environments reproducible and auditable.&lt;/p&gt;




&lt;h2&gt;
  
  
  Key Characteristics of Cloud Native Architecture
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. High-Level Automation
&lt;/h3&gt;

&lt;p&gt;To manage all the moving parts of a cloud native system, automation is critical at every stage — from development to production. CI/CD pipelines (using tools like GitHub Actions, ArgoCD, or Flux) enable fast, reliable delivery and make disaster recovery dramatically easier — if you must rebuild your entire system, your pipeline can do it reproducibly from source.&lt;/p&gt;

&lt;p&gt;A related practice worth knowing: &lt;strong&gt;GitOps&lt;/strong&gt;, where Git is the single source of truth for both application and infrastructure state. GitOps-based tools like ArgoCD continuously reconcile your running environment against what's declared in a repository, reducing configuration drift and enabling fast, auditable rollbacks.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Self-Healing
&lt;/h3&gt;

&lt;p&gt;Failures happen — this is expected and designed for. Cloud native applications embed &lt;strong&gt;health checks&lt;/strong&gt; that allow orchestration platforms like Kubernetes to automatically detect unhealthy containers and restart or replace them, often without any human intervention. Your uptime doesn't depend on someone being awake at 3am.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Scalability
&lt;/h3&gt;

&lt;p&gt;Cloud native applications are designed to &lt;strong&gt;scale horizontally&lt;/strong&gt; — spinning up more instances of a service under load, rather than upgrading a single server. Kubernetes' HPA can trigger this automatically based on real-time CPU, memory, or custom metrics. This elasticity means you always have the capacity to handle demand, without permanently over-provisioning.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Scaling goes both ways. Scaling &lt;em&gt;down&lt;/em&gt; during low-traffic periods — combined with usage-based pricing from cloud providers — is a major source of cost savings.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  4. Cost Efficiency
&lt;/h3&gt;

&lt;p&gt;Orchestration systems like Kubernetes are excellent at &lt;strong&gt;bin-packing&lt;/strong&gt; — placing workloads efficiently across your compute fleet to minimize waste. Combined with autoscaling and cloud pricing models, cloud native architectures allow you to pay for what you actually use, not what you might need in a worst-case scenario. Tools like &lt;strong&gt;OpenCost&lt;/strong&gt; and &lt;strong&gt;Kepler&lt;/strong&gt; (a CNCF project) now even provide visibility into Kubernetes spend and carbon consumption per workload.&lt;/p&gt;

&lt;h3&gt;
  
  
  5. Ease of Maintenance
&lt;/h3&gt;

&lt;p&gt;Microservices make applications more &lt;strong&gt;portable, testable, and independently deployable&lt;/strong&gt;. Small, well-scoped services are easier to understand, debug, and hand off between teams. This is especially valuable in organizations where multiple product teams need to move at their own pace without stepping on each other.&lt;/p&gt;

&lt;h3&gt;
  
  
  6. Secure by Default
&lt;/h3&gt;

&lt;p&gt;Cloud environments are often shared across multiple teams and workloads, requiring layered security models. Cloud native security operates across several dimensions:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Network policies&lt;/strong&gt; to restrict traffic between services&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;mTLS&lt;/strong&gt; via service meshes to encrypt inter-service communication&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;RBAC&lt;/strong&gt; (Role-Based Access Control) to limit what each workload can access&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Supply chain security&lt;/strong&gt; using tools like Trivy (image scanning) and KubeSec (Kubernetes manifest analysis)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Security zones&lt;/strong&gt; to enforce isolation between tenants or environments&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The CNCF Security Technical Advisory Group continuously develops guidance and tooling to address emerging threats across the cloud native landscape.&lt;/p&gt;




&lt;h2&gt;
  
  
  Observability: The Pillar You Can't Skip
&lt;/h2&gt;

&lt;p&gt;In a distributed, microservices-based architecture, knowing &lt;em&gt;what's happening inside your system&lt;/em&gt; is non-trivial. This is where &lt;strong&gt;observability&lt;/strong&gt; becomes a first-class concern — not something bolted on after problems arise.&lt;/p&gt;

&lt;p&gt;Cloud native observability rests on three pillars:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Metrics&lt;/strong&gt; — quantitative data about system health (CPU, memory, request rates, error rates)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Logs&lt;/strong&gt; — structured records of events within services&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Traces&lt;/strong&gt; — end-to-end tracking of a request as it flows across multiple microservices&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Key tools in this space:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Tool&lt;/th&gt;
&lt;th&gt;Role&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Prometheus&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Metrics collection and alerting&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Grafana&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Visualization dashboards&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Jaeger / Zipkin&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Distributed tracing&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;OpenTelemetry&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Vendor-neutral instrumentation standard&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Fluentd / Loki&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Log aggregation&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;According to the CNCF Q3 2025 State of Cloud Native report, observability tools are now the &lt;strong&gt;second most widely adopted&lt;/strong&gt; cloud native technology after Kubernetes, used by 28% of backend developers.&lt;/p&gt;




&lt;h2&gt;
  
  
  Immutable Infrastructure: Stop Patching, Start Replacing
&lt;/h2&gt;

&lt;p&gt;Traditional operations involved SSH-ing into servers to apply patches, update configs, or fix issues. Cloud native systems take the opposite approach — &lt;strong&gt;immutable infrastructure&lt;/strong&gt; means servers are never modified after deployment. When a change is needed, a new image is built, tested, and deployed, and the old instance is discarded.&lt;/p&gt;

&lt;p&gt;This makes deployments &lt;strong&gt;predictable and repeatable&lt;/strong&gt;, eliminates configuration drift (where production slowly diverges from your declared desired state), and simplifies rollback — you simply redeploy the previous image.&lt;/p&gt;




&lt;h2&gt;
  
  
  Cloud Native vs. Cloud-Based: Know the Difference
&lt;/h2&gt;

&lt;p&gt;These terms are often used interchangeably, but they describe very different things:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;Cloud-Based&lt;/th&gt;
&lt;th&gt;Cloud Native&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Design&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Legacy apps migrated to cloud&lt;/td&gt;
&lt;td&gt;Built from the ground up for cloud&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Scaling&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Often manual or vertical&lt;/td&gt;
&lt;td&gt;Automatic, horizontal&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Deployment&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;May still be monolithic&lt;/td&gt;
&lt;td&gt;Microservices, containers&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Infrastructure&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Often mutable&lt;/td&gt;
&lt;td&gt;Immutable, declarative&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Resilience&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Basic backup/restore&lt;/td&gt;
&lt;td&gt;Self-healing, auto-failover&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h2&gt;
  
  
  What's Next: Cloud Native in the Age of AI
&lt;/h2&gt;

&lt;p&gt;Cloud native is rapidly becoming the default platform for AI/ML workloads too. Kubernetes is emerging as the primary runtime for GPU-accelerated training and inference. Tools like &lt;strong&gt;Kubeflow&lt;/strong&gt;, &lt;strong&gt;Argo Workflows&lt;/strong&gt;, and &lt;strong&gt;Kserve&lt;/strong&gt; bring MLOps discipline to model pipelines. The same principles — declarative configuration, autoscaling, observability, and GitOps — apply directly to AI infrastructure.&lt;/p&gt;

&lt;p&gt;This convergence of cloud native and AI is reshaping how organizations think about infrastructure at scale.&lt;/p&gt;




&lt;h2&gt;
  
  
  Getting Started: A Practical Path
&lt;/h2&gt;

&lt;p&gt;If you're new to cloud native, here's a pragmatic starting point:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Containerize a small, stateless service&lt;/strong&gt; with Docker&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Deploy it on Kubernetes&lt;/strong&gt; (try Minikube or kind locally)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Set up a basic CI/CD pipeline&lt;/strong&gt; with GitHub Actions&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Add observability&lt;/strong&gt; with Prometheus and Grafana&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Explore GitOps&lt;/strong&gt; with ArgoCD or Flux&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Experiment with a service mesh&lt;/strong&gt; (Istio or Linkerd) as you scale out&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The &lt;a href="https://maturitymodel.cncf.io" rel="noopener noreferrer"&gt;CNCF Cloud Native Maturity Model&lt;/a&gt; is an excellent framework for understanding where you are and where to go next.&lt;/p&gt;




&lt;h2&gt;
  
  
  Final Thoughts
&lt;/h2&gt;

&lt;p&gt;Cloud native architecture is not a single technology — it's a philosophy and a set of practices for building software that is resilient, scalable, and efficient by design. It demands a shift in how teams think about infrastructure, deployment, and failure. The payoff: faster delivery, lower costs, and systems that can grow as your users do.&lt;/p&gt;

&lt;p&gt;The ecosystem is maturing rapidly, and now is an excellent time to build these skills — they're increasingly foundational to modern software engineering.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Learnings from the &lt;a href="https://www.linuxfoundation.org" rel="noopener noreferrer"&gt;Linux Foundation&lt;/a&gt;'s cloud native curriculum. The Linux Foundation is the parent organization of CNCF and one of the leading sources of open source and cloud native education globally.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>cloudnative</category>
      <category>devops</category>
      <category>kubernetes</category>
      <category>microservices</category>
    </item>
    <item>
      <title>I Just Built an Agentic RAG System From Scratch — Here's What I Learned (LLM Zoomcamp 2026, Module 1)</title>
      <dc:creator>Ryan Giggs</dc:creator>
      <pubDate>Sun, 21 Jun 2026 12:17:44 +0000</pubDate>
      <link>https://dev.to/derrickryangiggs/i-just-built-an-agentic-rag-system-from-scratch-heres-what-i-learned-llm-zoomcamp-2026-module-5a2c</link>
      <guid>https://dev.to/derrickryangiggs/i-just-built-an-agentic-rag-system-from-scratch-heres-what-i-learned-llm-zoomcamp-2026-module-5a2c</guid>
      <description>&lt;p&gt;I just completed Module 1 of the &lt;strong&gt;LLM Zoomcamp 2026&lt;/strong&gt; by &lt;a href="https://github.com/DataTalksClub/llm-zoomcamp/" rel="noopener noreferrer"&gt;@DataTalksClub&lt;/a&gt; — and honestly, this is the most hands-on AI course I've taken.&lt;/p&gt;

&lt;p&gt;No fluff. No hand-holding. Just real code, real concepts, and a working system by the end.&lt;/p&gt;

&lt;p&gt;Here's everything I learned — and why it matters if you're a data engineer or software developer trying to break into LLM applications.&lt;/p&gt;




&lt;h2&gt;
  
  
  What Is LLM Zoomcamp?
&lt;/h2&gt;

&lt;p&gt;LLM Zoomcamp is a &lt;strong&gt;free, open-source course&lt;/strong&gt; by Alexey Grigorev and DataTalks.Club that teaches you how to build production-ready LLM applications from scratch. No GPU required. No expensive API bills. Just Python, curiosity, and a willingness to build.&lt;/p&gt;

&lt;p&gt;Module 1 is called &lt;strong&gt;Agentic RAG&lt;/strong&gt; — and it covers everything from what an LLM is to building a fully autonomous AI agent that decides when and what to search.&lt;/p&gt;




&lt;h2&gt;
  
  
  What I Built in Module 1
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. A RAG Pipeline From Scratch
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;RAG&lt;/strong&gt; stands for Retrieval-Augmented Generation. The idea is simple: instead of asking an LLM a question and hoping it knows the answer, you first &lt;em&gt;search&lt;/em&gt; a knowledge base for relevant documents, then pass those documents as context to the LLM.&lt;/p&gt;

&lt;p&gt;The result? Grounded, accurate answers instead of hallucinations.&lt;/p&gt;

&lt;p&gt;I built the full pipeline in ~30 lines of Python:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;rag&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;question&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;search_results&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;search&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;question&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;          &lt;span class="c1"&gt;# 1. find relevant docs
&lt;/span&gt;    &lt;span class="n"&gt;prompt&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;build_prompt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;question&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;search_results&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="c1"&gt;# 2. build context prompt
&lt;/span&gt;    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;llm&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;prompt&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;                         &lt;span class="c1"&gt;# 3. generate answer
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Simple. Powerful. The foundation of every production RAG system.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Document Indexing with minsearch
&lt;/h3&gt;

&lt;p&gt;I indexed &lt;strong&gt;1,242 FAQ documents&lt;/strong&gt; from the DataTalks.Club Zoomcamp courses using &lt;code&gt;minsearch&lt;/code&gt; — a lightweight keyword search library built by Alexey himself. It uses the same concepts as Elasticsearch (text fields, keyword fields, boosting, filtering) but runs in pure Python with zero infrastructure.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Document Chunking for Better Retrieval
&lt;/h3&gt;

&lt;p&gt;Long documents hurt retrieval precision. A match deep inside a 10,000-character page still pulls the whole page into the LLM context — wasteful and noisy.&lt;/p&gt;

&lt;p&gt;The fix is &lt;strong&gt;chunking&lt;/strong&gt;: split each document into smaller overlapping pieces and index those instead.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;gitsource&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;chunk_documents&lt;/span&gt;
&lt;span class="n"&gt;chunks&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;chunk_documents&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;documents&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;size&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;2000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;step&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;1000&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The result? &lt;strong&gt;~3× fewer input tokens&lt;/strong&gt; sent to the LLM — smaller, faster, cheaper, and more accurate.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Turning RAG Into an Agent with Function Calling
&lt;/h3&gt;

&lt;p&gt;This is where it gets exciting.&lt;/p&gt;

&lt;p&gt;A standard RAG pipeline is &lt;strong&gt;fixed&lt;/strong&gt;: question → search once → answer. The developer controls the flow.&lt;/p&gt;

&lt;p&gt;An &lt;strong&gt;agentic RAG&lt;/strong&gt; system puts the LLM in charge:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;question → LLM thinks → search? → LLM thinks → search again? → LLM thinks → answer
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The LLM decides:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Whether to search at all&lt;/li&gt;
&lt;li&gt;What to search for&lt;/li&gt;
&lt;li&gt;How many times to search&lt;/li&gt;
&lt;li&gt;When it has enough context to answer&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I implemented this using &lt;strong&gt;function calling&lt;/strong&gt; — giving the LLM a &lt;code&gt;search&lt;/code&gt; tool it can invoke on its own. In one test, the agent autonomously made &lt;strong&gt;3 different searches&lt;/strong&gt; with progressively refined queries before generating a final answer. No hardcoding. No fixed flow.&lt;/p&gt;




&lt;h2&gt;
  
  
  Tech Stack
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;LLM&lt;/strong&gt;: Groq API (llama-3.1-8b-instant) — free tier, blazing fast&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Search&lt;/strong&gt;: minsearch — lightweight keyword search&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Chunking&lt;/strong&gt;: gitsource chunk_documents&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Environment&lt;/strong&gt;: uv + Python 3.12&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Knowledge base&lt;/strong&gt;: DataTalks.Club Zoomcamp lesson pages (72 markdown files, 295 chunks)&lt;/li&gt;
&lt;/ul&gt;




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

&lt;p&gt;&lt;strong&gt;1. RAG is just 3 functions.&lt;/strong&gt; search() + build_prompt() + llm(). Everything else is optimization.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Chunking matters more than you think.&lt;/strong&gt; Going from full documents to 2,000-character chunks reduced input tokens by 3× and improved answer quality significantly.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Agents are just loops.&lt;/strong&gt; The "magic" of agentic AI is literally a while loop that keeps calling the LLM until finish_reason == "stop". Understanding this demystifies 90% of agent frameworks like LangChain and LlamaIndex.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. You don't need OpenAI.&lt;/strong&gt; I ran everything on Groq's free tier using Llama 3.1. The OpenAI-compatible API means zero code changes.&lt;/p&gt;




&lt;h2&gt;
  
  
  My Homework Solution
&lt;/h2&gt;

&lt;p&gt;All my code for Module 1 is open source on GitHub:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://github.com/Derrick-Ryan-Giggs/llm-zoomcamp-2026" rel="noopener noreferrer"&gt;github.com/Derrick-Ryan-Giggs/llm-zoomcamp-2026&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;It includes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;rag-intro.ipynb&lt;/code&gt; — the full RAG pipeline&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;agents.ipynb&lt;/code&gt; — function calling and the agentic loop&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;homework.ipynb&lt;/code&gt; — Module 1 homework solutions&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Want to Learn Too?
&lt;/h2&gt;

&lt;p&gt;LLM Zoomcamp is &lt;strong&gt;completely free&lt;/strong&gt;. No paywall, no certificate fees, just open-source learning.&lt;/p&gt;

&lt;p&gt;Sign up here: &lt;a href="https://github.com/DataTalksClub/llm-zoomcamp/" rel="noopener noreferrer"&gt;github.com/DataTalksClub/llm-zoomcamp&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Module 2 is up next — &lt;strong&gt;Vector Search&lt;/strong&gt;. I'll be writing about that too.&lt;/p&gt;

&lt;p&gt;If you're a data engineer, ML practitioner, or software developer who wants to build real LLM applications — not just call ChatGPT — this course is for you.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Are you taking LLM Zoomcamp 2026? Drop a comment — I'd love to connect and compare notes.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>llm</category>
      <category>ai</category>
      <category>rag</category>
      <category>datatalksclub</category>
    </item>
    <item>
      <title>I Built a Data Lakehouse to Map Kenya's Healthcare Inequality — Here's What I Learned</title>
      <dc:creator>Ryan Giggs</dc:creator>
      <pubDate>Mon, 08 Jun 2026 17:36:51 +0000</pubDate>
      <link>https://dev.to/derrickryangiggs/i-built-a-data-lakehouse-to-map-kenyas-healthcare-inequality-heres-what-i-learned-31b</link>
      <guid>https://dev.to/derrickryangiggs/i-built-a-data-lakehouse-to-map-kenyas-healthcare-inequality-heres-what-i-learned-31b</guid>
      <description>&lt;p&gt;A child born in Westlands, Nairobi has access to 7.23 health facilities per 10,000 people.&lt;/p&gt;

&lt;p&gt;A child born in Embakasi North, also in Nairobi, has 0.62.&lt;/p&gt;

&lt;p&gt;Same city. Same county. One-tenth the access.&lt;/p&gt;

&lt;p&gt;That number did not come from a government report. It did not come from a consultant's PowerPoint. It came from a pipeline I built from scratch, joining three public datasets that nobody had systematically connected before — the Ministry of Health facility registry, the KNBS 2019 Census, and HDX county boundary GeoJSON.&lt;/p&gt;

&lt;p&gt;This is the story of how I built it, why I made the architecture choices I made, and what I would do differently.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Problem
&lt;/h2&gt;

&lt;p&gt;Kenya has 47 counties and over 20,000 registered health facilities. The Ministry of Health publishes facility data through the Kenya Master Health Facility Registry (KMHFR). The Kenya National Bureau of Statistics publishes county population data. The Humanitarian Data Exchange publishes county boundary GeoJSON for choropleth mapping.&lt;/p&gt;

&lt;p&gt;Three datasets. Three different sources. All public. None of them joined.&lt;/p&gt;

&lt;p&gt;Without joining them, you cannot answer the most basic questions:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Which counties are most underserved relative to their population?&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Which counties have no maternity facilities? No ART centres? No TB clinics?&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;How many facilities would Bungoma need to reach the national baseline of 3 per 10,000 people?&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Has the facility landscape changed over time — are counties gaining or losing services?&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The data to answer all of this exists. It just has not been assembled. That is the problem this pipeline solves.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Stack
&lt;/h2&gt;

&lt;p&gt;Here is what I built with, and more importantly, why:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Infrastructure as Code: OpenTofu&lt;/strong&gt; — Provisions two MinIO buckets before the stack starts. OpenTofu is the community-maintained open-source fork of Terraform following HashiCorp's BSL license change.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Orchestration: Apache Airflow (CeleryExecutor + Redis)&lt;/strong&gt; — Monthly DAG: three ingestion tasks run in parallel, a barrier gate waits for all three to complete, then dbt runs.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Data Lake: MinIO&lt;/strong&gt; — S3-compatible object storage running in Docker. Raw NDJSON files land here first, partitioned by year/month/day.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Table Format: Apache Iceberg&lt;/strong&gt; — ACID transactions, schema evolution, and time travel on top of files. The SCD2 snapshot tracks which facilities gained or lost maternity/ART/TB services month over month.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Catalog: Iceberg REST Catalog&lt;/strong&gt; — SQLite-backed, persists across Docker restarts via a named volume.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Query Engine: Trino 480&lt;/strong&gt; — Federated SQL over Iceberg tables stored in MinIO.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Transformations: dbt Core + dbt-trino&lt;/strong&gt; — Staging models, four mart models, one SCD2 snapshot, 30 passing data quality tests, one seed file.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Dashboards: Apache Superset 5.0.0&lt;/strong&gt; — Eight charts on one dashboard.&lt;/p&gt;

&lt;p&gt;Everything runs in Docker Compose on my HP EliteBook 840. Zero cloud spend.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Architecture Tradeoff I Need To Be Honest About
&lt;/h2&gt;

&lt;p&gt;Here is where I am going to say something that might surprise you.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;This stack is overkill for 20,000 rows of data.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Trino is a distributed query engine built for petabyte-scale federated queries across dozens of data sources. My biggest table has 20,391 rows. DuckDB — a single embedded analytics database that runs in a Python process — would query that in milliseconds without any infrastructure at all.&lt;/p&gt;

&lt;p&gt;CeleryExecutor with Redis is designed for Airflow deployments with many concurrent tasks across multiple workers. I have three DAGs that run once a month. LocalExecutor handles that just fine with zero additional services.&lt;/p&gt;

&lt;p&gt;The Iceberg REST catalog backed by SQLite is, genuinely, the worst of both worlds at this scale — you get all of Iceberg's operational complexity without most of its benefits.&lt;/p&gt;

&lt;p&gt;So why did I build it this way?&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Because this is the architecture I would run at scale — and I needed to prove I can operate it.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;If this pipeline were ingesting facility data from all 54 African Union member states, with real-time updates from DHIS2, and serving 200 analysts querying simultaneously — you would need exactly this stack. Trino for federated querying. CeleryExecutor so ingestion tasks do not block dashboard queries. Iceberg for time travel and schema evolution as the data model matures.&lt;/p&gt;

&lt;p&gt;The data volume is small. The architecture patterns are production-grade. I built it this way intentionally, because the goal of a portfolio project is not just to answer a data question — it is to demonstrate that you can operate the systems that answer data questions at companies where the stakes are real.&lt;/p&gt;

&lt;p&gt;When I sit in a data engineering interview and someone asks "have you worked with Trino and Iceberg?" the answer is yes, in anger, debugging catalog persistence issues and permission conflicts at 11pm. That is worth more than a hypothetical answer.&lt;/p&gt;




&lt;h2&gt;
  
  
  What I Actually Found
&lt;/h2&gt;

&lt;p&gt;After all of it was running, here is what the data showed:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;National level:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Bungoma is Kenya's most underserved county: 1.88 facilities per 10,000 people, serving 1.67 million people&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Samburu is the only county with a critical TB gap — just 1 TB facility for 310,327 people&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Every county in Kenya has at least one emergency facility — that surprised me&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Mandera has the highest raw ratio (6.26 per 10k) — but that is sparse population density, not good healthcare access. Ratios lie without context.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Nairobi sub-county drill-down:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Starehe (CBD): 7.23 facilities per 10,000 people&lt;/li&gt;
&lt;li&gt;Embakasi North: 0.62 facilities per 10,000 people&lt;/li&gt;
&lt;li&gt;The most underserved sub-county has eleven times fewer facilities per capita than the most served, within the same city&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  The Dashboard
&lt;/h2&gt;

&lt;p&gt;Eight charts. One story. Here is what each one shows.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Chart 1 — Facilities per 10,000 People by County&lt;/strong&gt;&lt;/p&gt;

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

&lt;p&gt;The primary national scorecard. Every bar is one of Kenya's 47 counties. The spike you see around Lamu reflects its small population relative to its facility count — a reminder that raw ratios must be read alongside absolute population figures. The counties sitting below 2.0 on the left — Trans Nzoia, Kakamega — are large-population counties being severely underserved in absolute terms. The Y axis is facilities per 10,000 people. The X axis is sorted alphabetically; the ranked view lives in the next chart.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Chart 2 — Underserved Counties Ranking&lt;/strong&gt;&lt;/p&gt;

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

&lt;p&gt;All 47 counties ranked from most to least underserved. The severity score column is highlighted red for scores of 3 (critical) and pink for scores of 2 — Bungoma and Samburu both carry severity score 3. The &lt;code&gt;facilities_needed_to_baseline&lt;/code&gt; column shows how many additional facilities each county would need just to reach a minimum of 3 per 10,000 people. Bungoma needs 21. The &lt;code&gt;tb_gap_flag&lt;/code&gt; column confirms Samburu as the only county in Kenya with a critical TB service gap. This table is the most action-oriented output in the entire pipeline — it is a prioritised to-do list for the Ministry of Health.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Chart 3 — Maternity Coverage by County&lt;/strong&gt;&lt;/p&gt;

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

&lt;p&gt;Maternity facilities per 100,000 people across all 47 counties. The spike at Lamu (~52) is the small-population effect again. More meaningful are the counties between 10 and 17 on the left side — these are where maternal healthcare access is most constrained on a population-adjusted basis. Kenya's maternal mortality remains high in underserved counties, and this chart makes that structural gap visible and quantified for the first time in a reproducible pipeline.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Chart 4 — Service Coverage Rates by County&lt;/strong&gt;&lt;/p&gt;

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

&lt;p&gt;Four services compared side by side for every county: TB (blue), maternity (dark red), ART/HIV treatment (orange-red), and emergency care (amber). The tall blue TB bars dominate because TB diagnosis points are integrated into a wider range of facility types nationally. The very short amber emergency bars across most counties are a quiet crisis in themselves — emergency care coverage is thin across the country. This is the most information-dense chart in the dashboard and rewards close examination county by county.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Chart 5 — Nairobi Sub-County Facility Density&lt;/strong&gt;&lt;/p&gt;

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

&lt;p&gt;This is the chart that stops people. Embakasi North: 0.62 facilities per 10,000 people. Starehe: 7.23. Same city. The bar on the far right (Starehe/Westlands cluster) towers over everything else. Embakasi North, the short bar third from the left, has 291,760 people and only 18 health facilities. This chart is the most visceral data visualisation in the project — it shows that healthcare inequality is not just a rural versus urban story, but an intra-city story playing out at sub-county level inside Nairobi.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Chart 6 — Nairobi Sub-County Service Gaps&lt;/strong&gt;&lt;/p&gt;

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

&lt;p&gt;The full service breakdown for all 17 Nairobi sub-counties, sorted by facilities per 10k ascending — most underserved first. The table shows 2024 projected population (2019 census + 2.3% annual KNBS growth rate) alongside raw counts of maternity, ART, and emergency facilities per sub-county. What stands out is that every sub-county shows no_maternity_flag = false and no_emergency_flag = false. Nairobi's inequality is not a service absence crisis — it is a density crisis. Every sub-county has some maternity and emergency facilities. Not nearly enough of them.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Chart 7 — Total Counties Mapped&lt;/strong&gt;&lt;/p&gt;

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

&lt;p&gt;A single bold number: 47. All of Kenya's counties accounted for. This is not just cosmetic — a custom dbt test (&lt;code&gt;assert_county_count_equals_47&lt;/code&gt;) enforces this in code. If any pipeline run produces fewer than 47 counties due to data quality issues, join failures, or ingestion gaps, the test fails and the pipeline stops before writing bad data to the mart tables.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Chart 8 — Kenya Facility Density Map&lt;/strong&gt;&lt;/p&gt;

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

&lt;p&gt;A full choropleth of all 47 Kenya counties, colour-coded by facility density. Light yellow is least served (0.9–1.8 per 10k). Dark maroon is most served (4.4–5.3 per 10k). The dense cluster of small coloured counties in the southwest reflects the high-population Rift Valley and central counties. The large pale northern counties — Turkana, Marsabit, Wajir, Mandera — are sparse in both population and facilities. Building this map required significant engineering: Superset's built-in Kenya map only has the pre-2013 provincial boundaries (8 provinces, not 47 counties), so I built a custom virtual dataset wrapping each county's raw GeoJSON geometry as a full Feature object to make deck.gl render it correctly.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Engineering Problems That Nearly Broke Me
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;The Superset logout loop&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Both Airflow and Superset were logging me out within 20 seconds of login. The root causes turned out to be three separate things simultaneously: multiple Airflow gunicorn workers not sharing session state, a Superset 5.0 regression breaking database session persistence, and Flask generating a random SECRET_KEY on every container restart — invalidating every session cookie on every &lt;code&gt;docker compose restart&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Fix: &lt;code&gt;AIRFLOW__WEBSERVER__WORKERS=1&lt;/code&gt;, a fixed SECRET_KEY in &lt;code&gt;.env&lt;/code&gt; from &lt;code&gt;openssl rand -hex 32&lt;/code&gt;, and a &lt;code&gt;before_request&lt;/code&gt; Flask hook forcing &lt;code&gt;session.permanent = True&lt;/code&gt; on every request.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The Iceberg catalog disappearing on restart&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Every restart wiped all Trino schema and table knowledge. The REST catalog was using in-memory SQLite by default.&lt;/p&gt;

&lt;p&gt;Fix: &lt;code&gt;CATALOG_URI=jdbc:sqlite:/catalog/iceberg_catalog.db&lt;/code&gt; with a named Docker volume. Two config lines. One hour of debugging.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The dbt target/ permission conflict&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The VS Code dbt extension creates &lt;code&gt;dbt/target/&lt;/code&gt; with host user ownership (UID 1000). Airflow runs as UID 50000. Every &lt;code&gt;dbt run&lt;/code&gt; failed with Permission denied.&lt;/p&gt;

&lt;p&gt;Fix: declare &lt;code&gt;dbt-target&lt;/code&gt; as a named Docker volume mounted over the bind-mounted &lt;code&gt;./dbt&lt;/code&gt; directory. Named volumes are owned by the Docker daemon, not the host user.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The KMHFR API redirect&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Original pipeline hit &lt;code&gt;kmhfl.health.go.ke&lt;/code&gt;. That domain 301-redirects to &lt;code&gt;kmhfr.health.go.ke&lt;/code&gt;, which returned just the URL path as plain text — not JSON. Three weeks of failed ingestion before finding the correct public API endpoint: &lt;code&gt;api.kmhfr.health.go.ke/api/public/facilities/&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The Superset Country Map with 8 old provinces&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Superset's built-in Kenya GeoJSON has the pre-2013 provincial boundaries — 8 provinces, not 47 counties. No ISO code mapping fixes this. The solution was switching to deck.gl Polygon with a custom SQL virtual dataset wrapping each county's geometry as a GeoJSON Feature object, with explicit CASE mapping for three county name mismatches across datasets.&lt;/p&gt;




&lt;h2&gt;
  
  
  What I Would Do Differently
&lt;/h2&gt;

&lt;p&gt;Use DuckDB instead of Trino for a project of this scale. Use LocalExecutor for Airflow. Start with the data model before the infrastructure.&lt;/p&gt;

&lt;p&gt;The data model is what creates value. The infrastructure is just plumbing. I spent more time debugging the plumbing than thinking about what the data should actually say.&lt;/p&gt;




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

&lt;p&gt;The pipeline runs. The dashboards are live. The data tells a real story.&lt;/p&gt;

&lt;p&gt;The next step is getting it to the people who can act on it: Ministry of Health Kenya, UNICEF Kenya, Amref Health Africa, the Council of Governors, Code for Kenya. The data is public. The pipeline is open source. Anyone can run it.&lt;/p&gt;

&lt;p&gt;If you work at any of those organisations and want to talk about this data — reach out.&lt;/p&gt;

&lt;p&gt;Github : &lt;a href="https://github.com/Derrick-Ryan-Giggs/kenya-health-pipeline" rel="noopener noreferrer"&gt;github.com/Derrick-Ryan-Giggs/kenya-health-pipeline&lt;/a&gt;&lt;/p&gt;

</description>
      <category>dataengineering</category>
      <category>opensource</category>
      <category>python</category>
      <category>healthtech</category>
    </item>
    <item>
      <title>I Built a Real-Time Crypto Analytics Pipeline for $0.01/Month — Here's the Full Architecture</title>
      <dc:creator>Ryan Giggs</dc:creator>
      <pubDate>Mon, 27 Apr 2026 08:39:51 +0000</pubDate>
      <link>https://dev.to/derrickryangiggs/i-built-a-real-time-crypto-analytics-pipeline-for-001month-heres-the-full-architecture-pfl</link>
      <guid>https://dev.to/derrickryangiggs/i-built-a-real-time-crypto-analytics-pipeline-for-001month-heres-the-full-architecture-pfl</guid>
      <description>&lt;p&gt;&lt;em&gt;How I combined Apache Flink, Redpanda, Airflow, dbt Cloud, and Grafana to track Bitcoin, Ethereum, Solana, BNB, and Cardano in real time — all running on Google Cloud for less than a cup of coffee per month.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;If you've been learning data engineering, you've probably built pipelines that move CSV files from A to B. Nothing wrong with that — but real data engineering interviews are starting to ask a different question: &lt;strong&gt;Can you handle data that never stops arriving?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;This article walks through CoinPulse, a project I built from scratch to answer that question. It's a hybrid streaming and batch crypto analytics pipeline that:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Consumes live trade events from Binance's WebSocket feed at ~1 tick per second per coin&lt;/li&gt;
&lt;li&gt;Processes them with Apache Flink using 1-minute tumbling windows&lt;/li&gt;
&lt;li&gt;Enriches the streaming data daily with CoinGecko market metadata via Airflow&lt;/li&gt;
&lt;li&gt;Transforms everything with dbt Cloud&lt;/li&gt;
&lt;li&gt;Serves a live, auto-refreshing Grafana dashboard — publicly accessible&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Total cloud cost: &lt;strong&gt;~$0.01/month.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Here's how I built it.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Problem I Was Solving
&lt;/h2&gt;

&lt;p&gt;Crypto market data has two distinct tempos:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Real-time (milliseconds to seconds):&lt;/strong&gt; Price ticks from exchanges. You need stream processing here — you can't batch-load ticks after the fact and call it "live." The data volume is too high to dump raw into a warehouse row by row.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Daily (slow-moving context):&lt;/strong&gt; Market cap, trading volume, rankings, OHLC candlesticks. These change once a day. You don't need Flink for this — a simple scheduled API call works fine.&lt;/p&gt;

&lt;p&gt;The interesting engineering challenge is: &lt;strong&gt;how do you join these two tempos cheaply and reliably?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The naive answer — stream everything into BigQuery using the Streaming Insert API — costs money at scale. The smarter answer is what I built: stream to GCS first, batch-load to BigQuery for free, and join the two lanes in dbt.&lt;/p&gt;

&lt;h2&gt;
  
  
  Architecture Overview
&lt;/h2&gt;

&lt;p&gt;Before diving in, here's the full picture:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;STREAMING LANE
Binance WebSocket → Python Producer → Redpanda → PyFlink → GCS → BigQuery
                    (real-time ticks)  (Kafka)  (windows)  (JSONL) (free load)

BATCH LANE  
CoinGecko API → Airflow DAG (7 tasks) → GCS → BigQuery
                (daily @ 06:00 UTC)    (Parquet)  (free load)

TRANSFORMATION LAYER
BigQuery → dbt Cloud → crypto_staging.* → crypto_mart.*
           (daily @ 07:00 UTC)

VISUALIZATION
BigQuery → Grafana Cloud → 6 panels, auto-refresh 30s
           (BigQuery plugin, free)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Everything runs locally in Docker except the data warehouse (BigQuery) and transformation/visualization layers (dbt Cloud + Grafana Cloud). This is the "local-to-cloud" pattern — keep compute on your machine, use GCP only for storage and analytics.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Streaming Lane: Binance → Redpanda → PyFlink → GCS
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Why Binance Instead of CoinCap?
&lt;/h3&gt;

&lt;p&gt;I originally planned to use CoinCap's WebSocket feed. Halfway through the build, I discovered CoinCap now requires an API key even for the free tier. Binance's WebSocket, however, is completely open — no account, no key, no rate limits on the trade stream.&lt;/p&gt;

&lt;p&gt;The URL pattern is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;wss://stream.binance.com:9443/stream?streams=btcusdt@trade/ethusdt@trade/solusdt@trade/bnbusdt@trade/adausdt@trade
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each message looks like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"stream"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"btcusdt@trade"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"data"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"s"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"BTCUSDT"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"p"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"76034.12"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"q"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"0.00150000"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"T"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1776875894886&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  The Python Producer
&lt;/h3&gt;

&lt;p&gt;The producer is a Docker container running &lt;code&gt;websockets&lt;/code&gt; + &lt;code&gt;confluent-kafka&lt;/code&gt;. It connects to Binance, maps Binance pair names to readable coin names (&lt;code&gt;BTCUSDT → bitcoin&lt;/code&gt;), and publishes JSON messages to a Redpanda topic:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;message&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;symbol&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;bitcoin&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;price_usd&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;76034.12&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;event_timestamp&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;2026-04-23T18:31:34+00:00&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="n"&gt;producer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;produce&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;topic&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;crypto-prices&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;bitcoin&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;dumps&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Why Redpanda Instead of Kafka?
&lt;/h3&gt;

&lt;p&gt;Kafka requires Zookeeper. In a local Docker setup, that's three containers just for the message broker (Zookeeper, Kafka broker, Schema Registry if you want it). Redpanda is Kafka-API compatible — meaning PyFlink's Kafka connector works with it unchanged — but runs as a &lt;strong&gt;single binary, no Zookeeper&lt;/strong&gt;. It starts in under 3 seconds and uses ~512MB RAM instead of Kafka's 2-3GB.&lt;/p&gt;

&lt;h3&gt;
  
  
  The PyFlink Job
&lt;/h3&gt;

&lt;p&gt;The Flink job does the heavy lifting. It consumes from Redpanda, applies a &lt;strong&gt;1-minute tumbling event-time window&lt;/strong&gt; per coin symbol, and computes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;avg_price&lt;/code&gt; — average price within the window&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;min_price&lt;/code&gt; / &lt;code&gt;max_price&lt;/code&gt; — price range&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;price_stddev&lt;/code&gt; — standard deviation (volatility proxy)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;open_price&lt;/code&gt; / &lt;code&gt;close_price&lt;/code&gt; — first and last tick&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;record_count&lt;/code&gt; — number of ticks received&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Event-time windows (as opposed to processing-time) are important here. They use the timestamp embedded in the Binance message, so late-arriving messages (within a 10-second watermark) land in the correct window rather than the current one.&lt;/p&gt;

&lt;p&gt;The output of each completed window is a JSONL file written to GCS:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;gs://coinpulse-data-lake/streaming/2026/04/23/18/stream_bitcoin_20260423_183134.jsonl
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A scheduled BigQuery Load Job (free) then loads these files into &lt;code&gt;crypto_raw.stream_prices&lt;/code&gt;, partitioned by HOUR on &lt;code&gt;event_timestamp&lt;/code&gt; and clustered by &lt;code&gt;symbol&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The key insight here:&lt;/strong&gt; I never use BigQuery's Streaming Insert API, which charges ~$0.01 per 200MB. Load Jobs are completely free. The data is 1 minute old in the warehouse instead of 0 seconds old — but for a dashboard refreshing every 30 seconds, nobody notices.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Batch Lane: CoinGecko → Airflow → GCS → BigQuery
&lt;/h2&gt;

&lt;h3&gt;
  
  
  The Airflow DAG
&lt;/h3&gt;

&lt;p&gt;The batch pipeline runs as a 7-task Airflow DAG at 06:00 UTC daily with two parallel branches:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;fetch_coingecko → transform_to_parquet → upload_to_gcs → load_to_bigquery
fetch_ohlc      → upload_ohlc_to_gcs   → load_ohlc_to_bigquery
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Markets branch&lt;/strong&gt; hits CoinGecko's &lt;code&gt;/coins/markets&lt;/code&gt; endpoint for all 5 coins in a single call, getting current price, market cap, 24h volume, price change percentage, rank, and fully diluted valuation.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;OHLC branch&lt;/strong&gt; hits &lt;code&gt;/coins/{id}/ohlc?days=1&lt;/code&gt; for each coin, getting candlestick arrays &lt;code&gt;[timestamp, open, high, low, close]&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Both branches serialize data to &lt;strong&gt;typed Parquet&lt;/strong&gt; (using explicit schema definitions, not autodetect — more on why this matters below) and upload to GCS before loading to BigQuery.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Schema War (And How I Won It)
&lt;/h3&gt;

&lt;p&gt;This took me longer than I'd like to admit. The root cause: PyArrow (used by Pandas &lt;code&gt;.to_parquet()&lt;/code&gt;) infers Python &lt;code&gt;datetime&lt;/code&gt; objects as &lt;code&gt;TIMESTAMP&lt;/code&gt; in Parquet metadata. But when BigQuery's autodetect reads a &lt;code&gt;datetime.isoformat()&lt;/code&gt; string, it creates the column as &lt;code&gt;STRING&lt;/code&gt;. When the same column arrives as a proper &lt;code&gt;datetime&lt;/code&gt; object in the next run, BigQuery rejects the load because &lt;code&gt;TIMESTAMP&lt;/code&gt; ≠ &lt;code&gt;STRING&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The permanent fix: define explicit schemas in the BigQuery Load Job config and force all string columns to &lt;code&gt;str&lt;/code&gt; dtype in Pandas before writing Parquet:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;df&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;snapshot_date&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;df&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;snapshot_date&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;astype&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;df&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ingested_at&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;   &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;df&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ingested_at&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;astype&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;job_config&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;bigquery&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;LoadJobConfig&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;source_format&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;bigquery&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;SourceFormat&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;PARQUET&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;schema&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;MARKETS_SCHEMA&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  &lt;span class="c1"&gt;# explicit, no autodetect
&lt;/span&gt;    &lt;span class="n"&gt;write_disposition&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;bigquery&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;WriteDisposition&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;WRITE_APPEND&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Once I stopped fighting autodetect and defined schemas explicitly, the pipeline ran clean for weeks.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Transformation Layer: dbt Cloud
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Why dbt Cloud Over dbt Core
&lt;/h3&gt;

&lt;p&gt;I already had Redpanda, Flink, Airflow, and PostgreSQL running locally — approximately 13GB RAM. Adding a dbt Core Docker container would push that higher for no functional gain. dbt Cloud's free developer plan gives a managed scheduler, visual lineage graph, and test runner at zero cost and zero local RAM.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Model Structure
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Staging layer&lt;/strong&gt; (views, zero storage cost):&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;stg_stream_prices&lt;/code&gt; — cleans and type-casts streaming data, filters nulls and zero prices&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;stg_coingecko_markets&lt;/code&gt; — adds &lt;code&gt;market_cap_category&lt;/code&gt; (large/mid/small/micro cap) derived column&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;stg_ohlc_candles&lt;/code&gt; — computes &lt;code&gt;candle_range&lt;/code&gt;, &lt;code&gt;candle_change_pct&lt;/code&gt;, and &lt;code&gt;candle_direction&lt;/code&gt; (bullish/bearish)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Mart layer&lt;/strong&gt; (incremental tables, insert_overwrite):&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;mart_crypto_prices&lt;/code&gt; — joins streaming aggregations with latest daily market metadata&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;mart_volatility&lt;/code&gt; — computes hourly &lt;code&gt;composite_volatility_score&lt;/code&gt; combining streaming stddev with OHLC candle metrics&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The mart models use &lt;code&gt;insert_overwrite&lt;/code&gt; with hour-level partitioning and an incremental filter that scans only the last 2 hours of streaming data per run. This means dbt never full-scans the streaming table — it touches only the partitions it needs to update.&lt;/p&gt;

&lt;h3&gt;
  
  
  The dbt Deploy Job
&lt;/h3&gt;

&lt;p&gt;A scheduled dbt Cloud production environment job runs daily at &lt;strong&gt;07:00 UTC&lt;/strong&gt; — one hour after Airflow — ensuring mart tables always have fresh data before the dashboard refreshes.&lt;/p&gt;

&lt;p&gt;After some initial failures (a common dbt Fusion syntax issue with &lt;code&gt;accepted_values&lt;/code&gt; requiring &lt;code&gt;arguments:&lt;/code&gt; nesting), the deploy job has run cleanly every day since April 19, 2026. Each run completes in under 45 seconds.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Dashboard: Grafana Cloud
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Why Grafana Over Looker Studio
&lt;/h3&gt;

&lt;p&gt;Looker Studio is the natural choice for BigQuery — it has a native connector and no setup friction. But it has one fatal flaw for streaming analytics: &lt;strong&gt;it doesn't auto-refresh&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Grafana Cloud's free tier includes the BigQuery plugin (one-click install, no credit card), supports 30-second auto-refresh, has proper time series panels with logarithmic Y-axis (essential when plotting BTC at $76K alongside ADA at $0.25 on the same chart), and produces significantly more polished visualizations.&lt;/p&gt;

&lt;p&gt;My prior projects (Sovereign Debt Observatory, Tech Ecosystem Observatory) both used Looker Studio, so using Grafana here also demonstrates range across tooling.&lt;/p&gt;

&lt;h3&gt;
  
  
  The 6 Dashboard Panels
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Panel 1 — Live Crypto Prices (Streaming)&lt;/strong&gt;&lt;br&gt;
A treemap showing the latest price for each coin from &lt;code&gt;crypto_raw.stream_prices&lt;/code&gt;, colour-coded green/red by price direction. BTC at $76K, ETH at $2.3K, SOL at $86, BNB at $630, ADA at $0.249.&lt;/p&gt;

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

&lt;p&gt;&lt;strong&gt;Panel 2 — Price Trend Over Time (Streaming)&lt;/strong&gt;&lt;br&gt;
A time series chart with logarithmic Y-axis showing &lt;code&gt;avg_price&lt;/code&gt; per 1-minute window for all coins. The log scale handles the 300,000x price spread between BTC and ADA.&lt;/p&gt;

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

&lt;p&gt;&lt;strong&gt;Panel 3 — Hourly Volatility Score (Streaming)&lt;/strong&gt;&lt;br&gt;
A gradient bar chart (green → red) showing the &lt;code&gt;composite_volatility_score&lt;/code&gt; per coin from &lt;code&gt;mart_volatility&lt;/code&gt;. Cardano leads at 0.566 (most volatile relative to its price), Binance Coin lowest at 0.264.&lt;/p&gt;

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

&lt;p&gt;&lt;strong&gt;Panel 4 — Market Cap Rankings (Batch)&lt;/strong&gt;&lt;br&gt;
BTC dominates at $1.57T, ETH at $289B. The scale difference is immediately visible and gives context to the streaming data.&lt;/p&gt;

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

&lt;p&gt;&lt;strong&gt;Panel 5 — Daily OHLC Candles (Batch)&lt;/strong&gt;&lt;br&gt;
A table panel showing raw candle data from &lt;code&gt;coingecko_ohlc_candles&lt;/code&gt; with &lt;code&gt;candle_direction&lt;/code&gt; colour-coded — bearish rows highlighted in red.&lt;/p&gt;

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

&lt;p&gt;&lt;strong&gt;Panel 6 — 24h Price Change % (Batch)&lt;/strong&gt;&lt;br&gt;
ADA leads at +0.563%, ETH is the only negative at -0.132%.&lt;/p&gt;

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

&lt;p&gt;&lt;strong&gt;Public URL:&lt;/strong&gt; &lt;a href="https://derrickryangiggs.grafana.net/public-dashboards/81560968e15140f08f65b52d78a4b252" rel="noopener noreferrer"&gt;https://derrickryangiggs.grafana.net/public-dashboards/81560968e15140f08f65b52d78a4b252&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Infrastructure: Terraform for Everything
&lt;/h2&gt;

&lt;p&gt;All GCP resources are defined in Terraform — nothing clicked in the console. Running &lt;code&gt;terraform apply&lt;/code&gt; provisions:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;GCP project with billing account linked&lt;/li&gt;
&lt;li&gt;Required APIs (BigQuery, BigQuery Storage, Cloud Storage, Resource Manager)&lt;/li&gt;
&lt;li&gt;Service account with least-privilege IAM roles (BQ Data Editor, BQ Job User, GCS Object Admin)&lt;/li&gt;
&lt;li&gt;GCS bucket with 90-day lifecycle rule&lt;/li&gt;
&lt;li&gt;BigQuery datasets and tables with explicit schemas, partitioning, and clustering&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Nothing is hardcoded.&lt;/strong&gt; All values flow from &lt;code&gt;.env&lt;/code&gt; via &lt;code&gt;TF_VAR_&lt;/code&gt; prefixed variables. This is non-negotiable — hardcoded project IDs in &lt;code&gt;.tf&lt;/code&gt; files are a code review failure waiting to happen.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Cost Breakdown (For Real)
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Service&lt;/th&gt;
&lt;th&gt;Details&lt;/th&gt;
&lt;th&gt;Monthly Cost&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Redpanda&lt;/td&gt;
&lt;td&gt;Local Docker&lt;/td&gt;
&lt;td&gt;$0.00&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;PyFlink&lt;/td&gt;
&lt;td&gt;Local Docker&lt;/td&gt;
&lt;td&gt;$0.00&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Apache Airflow&lt;/td&gt;
&lt;td&gt;Local Docker&lt;/td&gt;
&lt;td&gt;$0.00&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Google Cloud Storage&lt;/td&gt;
&lt;td&gt;~200MB of Parquet + JSONL&lt;/td&gt;
&lt;td&gt;~$0.01&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;BigQuery&lt;/td&gt;
&lt;td&gt;&amp;lt;1GB data, Load Jobs only&lt;/td&gt;
&lt;td&gt;~$0.00&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;dbt Cloud&lt;/td&gt;
&lt;td&gt;Developer plan&lt;/td&gt;
&lt;td&gt;$0.00&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Grafana Cloud&lt;/td&gt;
&lt;td&gt;Free tier&lt;/td&gt;
&lt;td&gt;$0.00&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Total&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;~$0.01/month&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The $0.01 is literally the GCS storage bill. Everything else is free.&lt;/p&gt;

&lt;p&gt;The three decisions that make this possible:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Use GCS + Load Jobs instead of BigQuery Streaming Inserts.&lt;/strong&gt; Streaming Inserts charge per row. Load Jobs are free. The trade-off is 1-minute data freshness instead of real-time — acceptable for a dashboard refreshing every 30 seconds.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Run compute locally.&lt;/strong&gt; Cloud Composer (managed Airflow) starts at $300/month. Dataproc (managed Flink) is similarly expensive. Running on your own machine costs nothing except electricity.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Use free tiers everywhere else.&lt;/strong&gt; dbt Cloud developer plan, Grafana Cloud free tier, CoinGecko Demo API free tier, Binance WebSocket (no auth required). Every external service in this pipeline has a genuinely free tier sufficient for this workload.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Hard-Won Lessons
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;1. Flink's &lt;code&gt;WindowFunction.apply()&lt;/code&gt; signature changed in 2.2.0&lt;/strong&gt;&lt;br&gt;
In PyFlink 1.18, &lt;code&gt;apply(self, key, window, inputs, collector)&lt;/code&gt; uses a collector argument. In 2.2.0, the signature is &lt;code&gt;apply(self, key, window, inputs)&lt;/code&gt; and you use &lt;code&gt;yield&lt;/code&gt; instead. This isn't documented prominently. I lost an afternoon to this.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Read env vars inside functions, not at module import time&lt;/strong&gt;&lt;br&gt;
My Airflow DAG originally read &lt;code&gt;GCS_BUCKET_NAME = os.environ["GCS_BUCKET_NAME"]&lt;/code&gt; at the module level. When Airflow's scheduler reloads the module after a container restart, &lt;code&gt;GCS_BUCKET_NAME&lt;/code&gt; is empty — the env var isn't available at import time in the scheduler process. Moving all &lt;code&gt;os.environ&lt;/code&gt; reads inside the task functions fixed this permanently.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Never use autodetect for BigQuery Load Jobs if you care about schema stability&lt;/strong&gt;&lt;br&gt;
&lt;code&gt;autodetect=True&lt;/code&gt; infers schema from the first file it sees. If that file has a &lt;code&gt;datetime&lt;/code&gt; object, BQ creates a &lt;code&gt;TIMESTAMP&lt;/code&gt; column. If the next file has an &lt;code&gt;.isoformat()&lt;/code&gt; string, BQ rejects it as a &lt;code&gt;STRING&lt;/code&gt; / &lt;code&gt;TIMESTAMP&lt;/code&gt; mismatch. Define explicit schemas in the &lt;code&gt;LoadJobConfig&lt;/code&gt; and cast DataFrame columns explicitly before writing Parquet.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. The Flink TaskManager won't connect if &lt;code&gt;jobmanager.rpc.address&lt;/code&gt; in &lt;code&gt;config.yaml&lt;/code&gt; doesn't match your Docker container name&lt;/strong&gt;&lt;br&gt;
The baked-in &lt;code&gt;config.yaml&lt;/code&gt; in the PyFlink image has &lt;code&gt;jobmanager.rpc.address: jobmanager&lt;/code&gt;. My container is named &lt;code&gt;coinpulse-flink-jobmanager&lt;/code&gt;. The &lt;code&gt;FLINK_PROPERTIES&lt;/code&gt; env var doesn't always override &lt;code&gt;config.yaml&lt;/code&gt;. The fix: add a &lt;code&gt;sed&lt;/code&gt; command in the Dockerfile to rewrite the address in &lt;code&gt;config.yaml&lt;/code&gt; directly.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;5. Redpanda's &lt;code&gt;rpk topic create&lt;/code&gt; command arguments must be on one line&lt;/strong&gt;&lt;br&gt;
Multi-line bash in Docker Compose &lt;code&gt;entrypoint&lt;/code&gt; with &lt;code&gt;&amp;gt;&lt;/code&gt; YAML block scalar doesn't handle line continuations the way you'd expect. The &lt;code&gt;--brokers&lt;/code&gt;, &lt;code&gt;--partitions&lt;/code&gt;, and &lt;code&gt;--replicas&lt;/code&gt; flags ended up being interpreted as separate commands. Fix: use &lt;code&gt;["bash", "-c", "single long command string"]&lt;/code&gt; format instead.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I'd Add Next (The "Circuit Breaker" Enhancements)
&lt;/h2&gt;

&lt;p&gt;After sharing the project, I got feedback suggesting three production-grade improvements. Here's my assessment:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. dbt-expectations data contracts&lt;/strong&gt;&lt;br&gt;
Add guardrails so the pipeline fails loudly if bad data arrives — negative prices, null coin symbols, impossible price spikes. In dbt, this is two lines per test in your schema YAML. The &lt;code&gt;dbt-expectations&lt;/code&gt; package brings &lt;code&gt;expect_column_values_to_be_between&lt;/code&gt; and similar assertions. Highly recommended, zero cost.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. BigQuery ML ARIMA_PLUS price forecasting&lt;/strong&gt;&lt;br&gt;
Train a time series model directly in BigQuery SQL to forecast the next 7 days of prices per coin. The first 10GB of &lt;code&gt;CREATE MODEL&lt;/code&gt; data processed monthly is free — both projects' datasets are well under that threshold, making this effectively free. It turns the dashboard from "here's what happened" into "here's what might happen."&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. GitHub Actions CI/CD for Terraform&lt;/strong&gt;&lt;br&gt;
A &lt;code&gt;terraform plan&lt;/code&gt; on every PR to the repo, ensuring infrastructure changes are reviewed before applying. Standard practice in production environments, takes about 20 lines of GitHub Actions YAML to implement.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Full Stack, Summarised
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Layer&lt;/th&gt;
&lt;th&gt;Tool&lt;/th&gt;
&lt;th&gt;Why&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;IaC&lt;/td&gt;
&lt;td&gt;Terraform&lt;/td&gt;
&lt;td&gt;Reproducible, version-controlled infrastructure&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Message Broker&lt;/td&gt;
&lt;td&gt;Redpanda&lt;/td&gt;
&lt;td&gt;Kafka-compatible, no Zookeeper, runs in one container&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Stream Processing&lt;/td&gt;
&lt;td&gt;PyFlink 2.2.0&lt;/td&gt;
&lt;td&gt;Stateful windowed aggregations, event-time semantics&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Orchestration&lt;/td&gt;
&lt;td&gt;Apache Airflow&lt;/td&gt;
&lt;td&gt;Full end-to-end DAG, retry logic, monitoring&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Object Storage&lt;/td&gt;
&lt;td&gt;GCS&lt;/td&gt;
&lt;td&gt;Staging layer between compute and warehouse&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Data Warehouse&lt;/td&gt;
&lt;td&gt;BigQuery&lt;/td&gt;
&lt;td&gt;Serverless, free Load Jobs, columnar, partitioned&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Transformations&lt;/td&gt;
&lt;td&gt;dbt Cloud&lt;/td&gt;
&lt;td&gt;Incremental models, lineage graph, scheduled runs&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Visualization&lt;/td&gt;
&lt;td&gt;Grafana Cloud&lt;/td&gt;
&lt;td&gt;Auto-refresh, BigQuery plugin free on all tiers&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Try It Yourself
&lt;/h2&gt;

&lt;p&gt;The full project is open source:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;GitHub:&lt;/strong&gt; &lt;a href="https://github.com/Derrick-Ryan-Giggs/coinpulse" rel="noopener noreferrer"&gt;https://github.com/Derrick-Ryan-Giggs/coinpulse&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Live Dashboard:&lt;/strong&gt; &lt;a href="https://derrickryangiggs.grafana.net/public-dashboards/81560968e15140f08f65b52d78a4b252" rel="noopener noreferrer"&gt;https://derrickryangiggs.grafana.net/public-dashboards/81560968e15140f08f65b52d78a4b252&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The README has step-by-step reproduction instructions — clone, fill in &lt;code&gt;.env&lt;/code&gt;, run &lt;code&gt;terraform apply&lt;/code&gt;, start the Docker stacks, and you have a running pipeline within about 20 minutes. All you need is a GCP account (free tier works), a CoinGecko Demo API key (free, no card), dbt Cloud account (free developer plan), and Grafana Cloud account (free forever).&lt;/p&gt;

&lt;h2&gt;
  
  
  What This Project Taught Me
&lt;/h2&gt;

&lt;p&gt;Building this reinforced something I suspected but didn't fully appreciate: &lt;strong&gt;the hardest problems in data engineering aren't the algorithms — they're the plumbing.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Getting PyFlink to connect to a Redpanda broker with the right container hostname was a day of debugging. Getting the Airflow DAG to read env vars correctly on scheduled runs (not just manual triggers) was another. Getting BigQuery to accept the same schema consistently across runs required understanding how PyArrow serialises Python types to Parquet column metadata.&lt;/p&gt;

&lt;p&gt;None of these problems are glamorous. But solving them is what separates a "pipeline that worked once on my laptop" from a "pipeline that runs reliably every day."&lt;/p&gt;

&lt;p&gt;That's what CoinPulse is — a pipeline that runs reliably every day, for $0.01/month.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;If you found this useful or have questions about any part of the implementation, feel free to reach out. I write regularly about data engineering, cloud infrastructure, and GCP on &lt;a href="https://medium.com/@derrickryangiggs" rel="noopener noreferrer"&gt;Medium&lt;/a&gt;, &lt;a href="https://dev.to/derrickryangiggs"&gt;Dev.to&lt;/a&gt;, and &lt;a href="https://ryan-giggs.hashnode.dev" rel="noopener noreferrer"&gt;Hashnode&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>python</category>
      <category>dataengineering</category>
      <category>gcp</category>
      <category>kafka</category>
    </item>
    <item>
      <title>Oracle GoldenGate 23ai: Powering Distributed AI with Real-Time Data Replication</title>
      <dc:creator>Ryan Giggs</dc:creator>
      <pubDate>Sat, 18 Apr 2026 11:10:46 +0000</pubDate>
      <link>https://dev.to/derrickryangiggs/oracle-goldengate-23ai-powering-distributed-ai-with-real-time-data-replication-12a1</link>
      <guid>https://dev.to/derrickryangiggs/oracle-goldengate-23ai-powering-distributed-ai-with-real-time-data-replication-12a1</guid>
      <description>&lt;p&gt;The database world has always needed a reliable bridge between operational systems and analytical or AI workloads. For decades, &lt;strong&gt;Oracle GoldenGate&lt;/strong&gt; has served that role — silently moving terabytes of transactional changes across databases, clouds, and continents with minimal latency and near-zero impact on source systems. With the release of &lt;strong&gt;GoldenGate 23ai&lt;/strong&gt; in 2024, that bridge now carries a new payload: &lt;strong&gt;AI vector embeddings&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;This post covers GoldenGate's core use cases, the Microservices Architecture that underpins modern deployments, and the powerful new distributed AI capabilities that make GoldenGate 23ai a critical component of enterprise GenAI pipelines.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Is Oracle GoldenGate?
&lt;/h2&gt;

&lt;p&gt;At its heart, GoldenGate is a &lt;strong&gt;Change Data Capture (CDC)&lt;/strong&gt; and real-time replication engine. Rather than querying the source database directly — which would impose load — GoldenGate reads the database's &lt;strong&gt;redo/transaction logs&lt;/strong&gt;, captures only the changed deltas (inserts, updates, deletes), and delivers them to one or more target systems with sub-second latency.&lt;/p&gt;

&lt;p&gt;This log-based approach makes GoldenGate extremely low-impact on production systems while supporting highly demanding replication topologies across heterogeneous environments: Oracle to PostgreSQL, Oracle to Kafka, MySQL to BigQuery, and many more.&lt;/p&gt;

&lt;h2&gt;
  
  
  Common GoldenGate Use Cases
&lt;/h2&gt;

&lt;p&gt;GoldenGate's flexibility makes it applicable across a broad range of enterprise scenarios:&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Multi-Active, High Availability, and Cross-Region Deployments
&lt;/h3&gt;

&lt;p&gt;GoldenGate enables &lt;strong&gt;active-active (bidirectional) replication&lt;/strong&gt;, where multiple database instances in different regions accept both reads and writes simultaneously. Built-in conflict detection and resolution ensures data consistency across sites. This architecture powers mission-critical applications that require zero planned downtime and regional failover capabilities — with GoldenGate's &lt;strong&gt;ExaPortMon&lt;/strong&gt;-style awareness ensuring continuous replication even under network disruptions.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Data Offloading and Data Hub
&lt;/h3&gt;

&lt;p&gt;Rather than running analytical queries against production OLTP databases, GoldenGate streams &lt;strong&gt;only the changed data&lt;/strong&gt; to a dedicated analytics database, data warehouse, or data lake in real time. This eliminates the traditional "nightly CSV batch" pattern and gives analysts access to data that is seconds, not hours, behind production.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Migrations and Upgrades
&lt;/h3&gt;

&lt;p&gt;GoldenGate is the gold standard for &lt;strong&gt;zero-downtime database migrations&lt;/strong&gt; — moving from on-premises to cloud, upgrading Oracle Database versions, or switching from Oracle to PostgreSQL. The approach: synchronize old and new systems in parallel using GoldenGate, then cut over the application connection once replication lag reaches zero. Even migrations of hundreds of terabytes are reduced to a switchover window measured in minutes.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Analytics Data Feeds
&lt;/h3&gt;

&lt;p&gt;GoldenGate feeds real-time change streams into analytics platforms — data warehouses, Apache Kafka topics, Oracle Streaming Service, Confluent Cloud, and cloud object stores. This underpins event-driven architectures and real-time dashboards that would be impossible with batch-based ETL.&lt;/p&gt;

&lt;h3&gt;
  
  
  5. Stream Analytics
&lt;/h3&gt;

&lt;p&gt;With &lt;strong&gt;GoldenGate Stream Analytics&lt;/strong&gt;, change events flowing through GoldenGate can be enriched, filtered, and analyzed in motion — before they land in a target system. Integrations with &lt;strong&gt;Oracle Machine Learning (OML)&lt;/strong&gt; and &lt;strong&gt;ONNX (Open Neural Network Exchange)&lt;/strong&gt; enable actionable AI/ML directly from streaming pipelines, turning raw CDC events into predictive signals.&lt;/p&gt;

&lt;h2&gt;
  
  
  GoldenGate Microservices Architecture
&lt;/h2&gt;

&lt;p&gt;When Oracle introduced the &lt;strong&gt;Microservices Architecture (MA)&lt;/strong&gt; in &lt;strong&gt;GoldenGate 12.3 (August 2017)&lt;/strong&gt;, it represented a fundamental modernization of how GoldenGate is deployed, managed, and integrated. The classic command-line-driven architecture (GGSCI) was complemented by a fully API-driven, web-based platform.&lt;/p&gt;

&lt;h3&gt;
  
  
  Five Core Components
&lt;/h3&gt;

&lt;p&gt;The Microservices Architecture is built around five independently manageable services:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Service Manager&lt;/strong&gt; — acts as a watchdog, managing and monitoring all other services within a deployment&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Administration Server&lt;/strong&gt; — the central control entity for managing Extract, Replicat, and all replication processes&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Distribution Server&lt;/strong&gt; — a high-performance data distribution agent that routes trail files to one or more targets over HTTPS/WebSockets&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Receiver Server&lt;/strong&gt; — handles all incoming trail files from remote Distribution Servers&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Performance Metrics Server&lt;/strong&gt; — collects and exposes real-time performance data for all GoldenGate processes via REST API or JSON&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Key Benefits of Microservices Architecture
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;REST API-driven management&lt;/strong&gt;: Every GoldenGate operation — starting a replicat, checking lag, configuring a pipeline — is exposed via a standardized REST API, enabling automation with Python, Terraform, and CI/CD pipelines.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Renewed web UI in 23ai&lt;/strong&gt;: GoldenGate 23ai ships with a &lt;strong&gt;completely redesigned graphical interface&lt;/strong&gt;, with more logical, guided assistant steps for setting up integrations — a marked improvement over the previous look and feel.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;TLS encryption and OAuth 2.0 authentication&lt;/strong&gt; out of the box, with integration into external identity providers.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Simpler patching and upgrades&lt;/strong&gt;: Each microservice can be updated with minimal disruption. Upgrading from GoldenGate 21c to 23ai, for example, requires only downloading the new software and updating the &lt;code&gt;OGG_HOME&lt;/code&gt; directory parameters.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Deployment flexibility&lt;/strong&gt;: Runs on-premises, as a fully managed service in OCI (OCI GoldenGate), or on third-party clouds including AWS, Azure, and GCP.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  GoldenGate Version Upgrade Path
&lt;/h3&gt;

&lt;p&gt;For teams still running older GoldenGate versions, the supported upgrade sequence through the modern microservices era is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;12.1 → 12.2 → 12.3 (Microservices introduced) → 18c → 19c → 21c → 23ai → 26ai (next LTS)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note&lt;/strong&gt;: Classic Architecture support for Oracle Database is deprecated in 23ai. All new deployments should use Microservices Architecture. GoldenGate &lt;strong&gt;26ai&lt;/strong&gt; has been announced as the next Long-Term Support (LTS) release, with further AI and Data Mesh enhancements planned.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Distributed AI Processing with Vector Replication
&lt;/h2&gt;

&lt;p&gt;This is where GoldenGate 23ai breaks genuinely new ground. The release adds first-class support for the &lt;strong&gt;Oracle Database 23ai &lt;code&gt;VECTOR&lt;/code&gt; data type&lt;/strong&gt; — the foundational building block of AI Vector Search and RAG (Retrieval-Augmented Generation) pipelines.&lt;/p&gt;

&lt;h3&gt;
  
  
  What Is a Vector Embedding?
&lt;/h3&gt;

&lt;p&gt;When AI models process documents, images, or other unstructured data, they convert that content into &lt;strong&gt;vector embeddings&lt;/strong&gt; — high-dimensional numerical representations that encode semantic meaning. These vectors are stored in vector databases and searched using similarity algorithms (cosine distance, dot product, Euclidean distance) to retrieve contextually relevant content.&lt;/p&gt;

&lt;p&gt;Keeping vector stores synchronized with the operational databases that hold the source data is a significant engineering challenge — and exactly what GoldenGate 23ai solves.&lt;/p&gt;

&lt;h3&gt;
  
  
  Key Vector Replication Capabilities
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Migrate Vectors into Oracle Vector Database Without Downtime&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;GoldenGate can migrate vector embeddings stored in one database into Oracle Database 23ai (or Oracle AI Database 26ai) without any service interruption. This enables seamless transitions from on-premises environments to OCI, or between cloud providers, while keeping AI-powered applications running continuously.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Replicate and Consolidate Vector Changes in Real Time&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;As source data changes — a customer record is updated, a product description is revised — GoldenGate captures those changes and propagates updated vectors to all target vector stores in real time. This ensures that RAG pipelines are always querying fresh, consistent embeddings rather than stale snapshots.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Heterogeneous Vector Replication&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;GoldenGate 23ai can replicate vectors both homogeneously (Oracle to Oracle) and heterogeneously across different vector stores, provided the &lt;strong&gt;embedding algorithm, type, and dimension are consistent&lt;/strong&gt; across source and target. If the source and target use different embedding algorithms, GoldenGate can replicate the &lt;strong&gt;raw source data&lt;/strong&gt; instead, allowing the target system to re-vectorize it with its own model.&lt;/p&gt;

&lt;p&gt;Support extends beyond Oracle: GoldenGate 23ai also supports capture and delivery of the &lt;strong&gt;pgvector extension&lt;/strong&gt; for PostgreSQL and its derivatives, as well as the &lt;strong&gt;VECTOR datatype for MySQL 9.0+&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Multi-Cloud, Multi-Active Oracle Vector Database&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;GoldenGate enables &lt;strong&gt;active-active vector database architectures&lt;/strong&gt; spanning multiple cloud regions or providers. AI applications in different regions can write to and read from local vector stores while GoldenGate keeps all instances synchronized — enabling both low-latency AI inference and high availability for vector workloads.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Stream Changes to Search Engines&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;GoldenGate can stream vector and document changes in real time to &lt;strong&gt;Elasticsearch or OpenSearch compatible search indexes&lt;/strong&gt;, enabling hybrid semantic + keyword search architectures without manual synchronization pipelines.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;GoldenGate Data Streams: Pub/Sub for Database Events&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;A major new capability in GoldenGate 23ai is the &lt;strong&gt;Data Streams service&lt;/strong&gt;, which provides simplified, event-driven access to database change records via &lt;strong&gt;AsyncAPI over WebSocket connections&lt;/strong&gt;. This enables developers to subscribe to database change events — including vector updates — in a pub/sub pattern, without needing to manage trail files or configure traditional Replicat processes. Combined with &lt;strong&gt;Oracle Database 23ai's JSON Relational Duality views&lt;/strong&gt;, this means document-side changes can be captured and propagated as structured events in real time.&lt;/p&gt;

&lt;h2&gt;
  
  
  Generative AI with Your Own Business Data
&lt;/h2&gt;

&lt;p&gt;GoldenGate 23ai directly enables three patterns for building AI applications on private enterprise data:&lt;/p&gt;

&lt;h3&gt;
  
  
  1. From-Scratch LLMs (Custom Foundation Models)
&lt;/h3&gt;

&lt;p&gt;Organizations with truly unique proprietary datasets — medical records, legal case files, financial transaction histories — may opt to train custom Large Language Models from the ground up on that data. GoldenGate ensures the training datasets fed into these models are continuously fresh and complete, sourced in real time from operational databases rather than periodic exports.&lt;/p&gt;

&lt;p&gt;This is an advanced, resource-intensive approach suited to well-resourced organizations with data assets that are sufficiently distinctive to justify the investment.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Fine-Tuned LLMs (Domain Adaptation)
&lt;/h3&gt;

&lt;p&gt;A more practical approach for most enterprises: take a pre-trained foundational LLM (such as Llama, Mistral, or an OCI Generative AI model) and &lt;strong&gt;fine-tune it on a private domain dataset&lt;/strong&gt;. GoldenGate replicates the relevant operational data into a training pipeline, keeping the fine-tuned model aligned with current business reality as data evolves over time.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. RAG (Retrieval-Augmented Generation) — The Most Common Pattern
&lt;/h3&gt;

&lt;p&gt;RAG is currently the dominant architecture for enterprise GenAI. Instead of baking private knowledge into model weights (which requires expensive retraining), RAG retrieves relevant documents at query time and injects them into the LLM's prompt as context.&lt;/p&gt;

&lt;p&gt;GoldenGate 23ai is purpose-built to power better RAG pipelines:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Keeps vector stores current&lt;/strong&gt;: As source data changes, GoldenGate propagates those changes to the vector index within seconds — not hours.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Enables a consolidated vector hub&lt;/strong&gt;: Rather than maintaining separate, potentially inconsistent vector stores for each application, GoldenGate can consolidate vectors from multiple source databases into a single Oracle AI Database vector hub.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Reduces hallucination risk&lt;/strong&gt;: Stale vector indexes are one of the primary causes of LLM hallucinations in RAG systems. Fresh data = more accurate, grounded responses.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Supports heterogeneous sources&lt;/strong&gt;: GoldenGate can pull data from Oracle, PostgreSQL, MySQL, SQL Server, DB2, and more — vectorize it, and deliver it to Oracle AI Database, all in a unified pipeline.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Why GoldenGate 23ai for AI: The Summary
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Capability&lt;/th&gt;
&lt;th&gt;Impact&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Real-time vector replication&lt;/td&gt;
&lt;td&gt;RAG pipelines query fresh embeddings, not stale snapshots&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Zero-downtime vector migration&lt;/td&gt;
&lt;td&gt;Move AI workloads to cloud without service interruption&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Multi-cloud active-active vectors&lt;/td&gt;
&lt;td&gt;Low-latency AI inference globally with high availability&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Heterogeneous source support&lt;/td&gt;
&lt;td&gt;Unify vectors from Oracle, PostgreSQL, MySQL, and more&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Data Streams (AsyncAPI)&lt;/td&gt;
&lt;td&gt;Event-driven AI pipelines without complex trail file management&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Stream to search engines&lt;/td&gt;
&lt;td&gt;Elasticsearch/OpenSearch integration for hybrid search&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Fine-tuning data pipelines&lt;/td&gt;
&lt;td&gt;Keep domain-adapted LLMs current with live business data&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The underlying thesis is the same one Oracle argues for Exadata AI Storage: &lt;strong&gt;bring AI to the data, not data to the AI&lt;/strong&gt;. GoldenGate 23ai extends this philosophy to the replication layer — ensuring that wherever your AI application runs, it has access to consistent, real-time data from your authoritative operational systems.&lt;/p&gt;

&lt;p&gt;As enterprise AI moves from experimentation to production, the quality of data pipelines will increasingly determine the quality of AI outcomes. GoldenGate 23ai is Oracle's answer to that challenge.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Are you using GoldenGate for AI pipelines or RAG architectures? What challenges have you run into keeping vector stores synchronized? Share in the comments below.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>oraclegoldengate</category>
      <category>dataengineering</category>
      <category>generativeai</category>
      <category>datareplication</category>
    </item>
    <item>
      <title>Building the Sovereign Debt Observatory: An End-to-End ELT Pipeline on World Bank Debt Data for Low and Middle-Income Countries</title>
      <dc:creator>Ryan Giggs</dc:creator>
      <pubDate>Thu, 09 Apr 2026 10:02:52 +0000</pubDate>
      <link>https://dev.to/derrickryangiggs/building-the-sovereign-debt-observatory-an-end-to-end-elt-pipeline-on-world-bank-debt-data-for-low-4988</link>
      <guid>https://dev.to/derrickryangiggs/building-the-sovereign-debt-observatory-an-end-to-end-elt-pipeline-on-world-bank-debt-data-for-low-4988</guid>
      <description>&lt;h2&gt;
  
  
  Introduction
&lt;/h2&gt;

&lt;p&gt;Global sovereign debt is one of the most consequential datasets in existence. It shapes foreign policy, determines credit ratings, drives IMF bailout decisions, and affects the daily lives of billions of people in developing countries. The World Bank publishes this data openly — 130+ countries, 27 years of history, updated quarterly — yet there is no ready-made analytical layer on top of it.&lt;/p&gt;

&lt;p&gt;If you want to answer a question like "which African countries have the highest ratio of private nonguaranteed debt to total external debt, and how has that changed since 2010?", you have to manually download Excel files from multiple World Bank portals, clean inconsistent column names, handle missing values, and stitch everything together in a spreadsheet. Every time the data updates, you do it again.&lt;/p&gt;

&lt;p&gt;That is the problem this project solves.&lt;/p&gt;

&lt;p&gt;The Sovereign Debt Observatory is an end-to-end ELT pipeline that ingests World Bank external debt data, lands it in a cloud data lake, transforms it in BigQuery using dbt Cloud, orchestrates everything quarterly with Apache Airflow, and surfaces the answers in a Looker Studio dashboard.&lt;/p&gt;

&lt;p&gt;This article walks through every layer of the pipeline — the architecture decisions, the technical challenges, and how I solved them.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Five Questions This Pipeline Answers
&lt;/h2&gt;

&lt;p&gt;Before writing a single line of code, I defined the analytical questions the pipeline needed to answer. This kept every decision grounded in purpose rather than technology for its own sake.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;How is gross external debt distributed across public, publicly guaranteed, private nonguaranteed, and multilateral sectors per country?&lt;/li&gt;
&lt;li&gt;Which countries carry the highest short-term external debt exposure and how has that changed since 2010?&lt;/li&gt;
&lt;li&gt;What share of external debt is foreign-currency denominated and where is that ratio worsening?&lt;/li&gt;
&lt;li&gt;How has regional external debt stock evolved from 1998 to 2025 across Africa, Latin America, East Asia, South Asia, Europe and Central Asia, and the Middle East?&lt;/li&gt;
&lt;li&gt;Which countries face the heaviest debt service pressure relative to their total debt position?&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Architecture Overview
&lt;/h2&gt;

&lt;p&gt;The pipeline follows a modern ELT pattern — extract and load first, transform inside the warehouse.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;World Bank API (IDS source 2)
        |
        v
PySpark job (Docker container)
        |
        v
Google Cloud Storage — raw Parquet, partitioned by extracted_date
        |
        v
BigQuery external tables (raw dataset)
        |
        v
dbt Cloud — staging views + mart tables
        |
        v
Looker Studio dashboard
        |
Orchestrated by Apache Airflow on Docker Compose
Infrastructure provisioned by Terraform
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Why ELT and not ETL?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;In ETL, transformation happens before loading — your Spark job does the cleaning, aggregation, and business logic before writing to the warehouse. In ELT, raw data lands untransformed and the warehouse does all the heavy lifting.&lt;/p&gt;

&lt;p&gt;For this project, ELT is the right choice for three reasons. First, raw data is preserved in GCS indefinitely — if analytical requirements change six months from now, I just write a new dbt model without re-running the ingestion layer. Second, BigQuery is optimized for analytical SQL transformations at scale — it is far better at this than PySpark running in a Docker container on a local machine. Third, dbt gives us version-controlled, tested, documented transformations that are readable by anyone with SQL knowledge.&lt;/p&gt;

&lt;p&gt;PySpark's job in this pipeline is purely mechanical: hit the API, paginate, write Parquet. No business logic. No aggregations. Pure extract and load.&lt;/p&gt;

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

&lt;h3&gt;
  
  
  International Debt Statistics (IDS) — World Bank source 2
&lt;/h3&gt;

&lt;p&gt;The IDS database is the flagship World Bank debt dataset. It covers external debt stocks and flows for low and middle income countries, with annual data going back to 1998. I access it through the &lt;code&gt;wbgapi&lt;/code&gt; Python library, which wraps the World Bank Indicators API v2.&lt;/p&gt;

&lt;p&gt;I ingest nine series:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Series Code&lt;/th&gt;
&lt;th&gt;Description&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;DT.DOD.DECT.CD&lt;/td&gt;
&lt;td&gt;Total external debt stocks&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;DT.DOD.DLXF.CD&lt;/td&gt;
&lt;td&gt;Long-term external debt&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;DT.DOD.DPNG.CD&lt;/td&gt;
&lt;td&gt;Private nonguaranteed debt&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;DT.DOD.MIBR.CD&lt;/td&gt;
&lt;td&gt;PPG IBRD loans&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;DT.DOD.DPPG.CD&lt;/td&gt;
&lt;td&gt;Public and publicly guaranteed debt&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;DT.DOD.DIMF.CD&lt;/td&gt;
&lt;td&gt;IMF credit&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;DT.DOD.PVLX.CD&lt;/td&gt;
&lt;td&gt;Present value of external debt&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;DT.DOD.MWBG.CD&lt;/td&gt;
&lt;td&gt;IBRD loans and IDA credits&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;DT.DOD.MIDA.CD&lt;/td&gt;
&lt;td&gt;PPG IDA loans&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;One important lesson from building this: the World Bank has multiple API source databases, and not all of them support the standard &lt;code&gt;wbgapi&lt;/code&gt; query format. Sources 22 (QEDS SDDS), 23 (QEDS GDDS), and 54 (JEDH) all return JSON decode errors when queried programmatically — they use a separate DataBank backend. Only source 2 (IDS) reliably supports the Indicators API. This cost me several hours of debugging.&lt;/p&gt;

&lt;h3&gt;
  
  
  Quarterly External Debt Statistics SDDS (QEDS)
&lt;/h3&gt;

&lt;p&gt;QEDS provides quarterly debt payment schedule data broken down by sector and maturity. Unlike IDS, QEDS does not support programmatic API access in the standard format. The World Bank provides bulk Excel downloads for each supplementary table instead.&lt;/p&gt;

&lt;p&gt;I download five Excel files directly:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Table 1.5 — Net external debt position by sector&lt;/li&gt;
&lt;li&gt;Table 3 — Debt service payment schedule by sector&lt;/li&gt;
&lt;li&gt;Table 3.2 — Debt service by sector and instrument&lt;/li&gt;
&lt;li&gt;Table 2.1 — Foreign currency and domestic currency debt&lt;/li&gt;
&lt;li&gt;Table 1.6 — Reconciliation of positions and flows&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Infrastructure as Code with Terraform
&lt;/h2&gt;

&lt;p&gt;Every GCP resource in this project is provisioned by Terraform. The three core resources are a GCS bucket for the data lake and three BigQuery datasets — raw, staging, and mart.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="nx"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"google_storage_bucket"&lt;/span&gt; &lt;span class="s2"&gt;"data_lake"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;name&lt;/span&gt;          &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;var&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;gcs_bucket_name&lt;/span&gt;
  &lt;span class="nx"&gt;location&lt;/span&gt;      &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;var&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;region&lt;/span&gt;
  &lt;span class="nx"&gt;force_destroy&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;

  &lt;span class="nx"&gt;lifecycle_rule&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;action&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;type&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"Delete"&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="nx"&gt;condition&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;age&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;90&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="nx"&gt;versioning&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;enabled&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nx"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"google_bigquery_dataset"&lt;/span&gt; &lt;span class="s2"&gt;"raw"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;dataset_id&lt;/span&gt;                 &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"raw"&lt;/span&gt;
  &lt;span class="nx"&gt;location&lt;/span&gt;                   &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;var&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;location&lt;/span&gt;
  &lt;span class="nx"&gt;delete_contents_on_destroy&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The 90-day lifecycle rule on GCS automatically deletes old partitions, keeping storage costs near zero. The entire GCP footprint for this project costs less than $0.05 per month — BigQuery's free tier covers 1 TB of queries and 10 GB of storage, which is far more than this dataset requires.&lt;/p&gt;

&lt;p&gt;A &lt;code&gt;setup_gcp.sh&lt;/code&gt; script handles the one-time bootstrap — creating the GCP project, enabling APIs, creating the service account, and granting IAM roles. The billing account ID is passed as an environment variable so it never appears in version-controlled files.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;BILLING_ACCOUNT&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;your-billing-id bash scripts/setup_gcp.sh
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Ingestion Layer — PySpark on Docker
&lt;/h2&gt;

&lt;h3&gt;
  
  
  JEDH / IDS Ingestion
&lt;/h3&gt;

&lt;p&gt;The IDS ingestion script uses &lt;code&gt;wbgapi&lt;/code&gt; to fetch all nine series across all available countries from 1998 to 2025. The API returns data in wide format — one row per country per series, with year columns as separate fields. PySpark writes this to GCS as Snappy-compressed Parquet, partitioned by extraction date.&lt;/p&gt;

&lt;p&gt;One critical issue I hit: the World Bank API returns year columns as bare integers (&lt;code&gt;1998&lt;/code&gt;, &lt;code&gt;1999&lt;/code&gt;, etc.). BigQuery rejects column names that start with numbers. The fix was to prefix all year columns with &lt;code&gt;year_&lt;/code&gt; before writing:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;combined&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;columns&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;year_&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;col&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nf"&gt;str&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;col&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;isdigit&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="n"&gt;col&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;col&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;combined&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;columns&lt;/span&gt;
&lt;span class="p"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This produces clean column names like &lt;code&gt;year_1998&lt;/code&gt;, &lt;code&gt;year_1999&lt;/code&gt; that BigQuery accepts without complaint.&lt;/p&gt;

&lt;h3&gt;
  
  
  QEDS Ingestion
&lt;/h3&gt;

&lt;p&gt;The QEDS ingestion downloads five Excel files from World Bank DataBank, reads all sheets from each file using &lt;code&gt;pandas.read_excel&lt;/code&gt;, and concatenates them into a single DataFrame.&lt;/p&gt;

&lt;p&gt;The Excel files have messy column names — spaces, brackets, special characters. A &lt;code&gt;clean_column_name&lt;/code&gt; function normalizes everything:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;clean_column_name&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;col&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;col&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;str&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;col&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;col&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;re&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sub&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;r&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;\s*\[.*?\]&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;''&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;col&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="c1"&gt;# remove [YR2021Q4] suffixes
&lt;/span&gt;    &lt;span class="n"&gt;col&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;col&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;strip&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;col&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;re&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sub&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;r&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;[^a-zA-Z0-9_]&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;_&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;col&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;col&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;re&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sub&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;r&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;_+&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;_&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;col&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;col&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;col&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;strip&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;_&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;col&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;col&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;isdigit&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
        &lt;span class="n"&gt;col&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;q_&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;col&lt;/span&gt;  &lt;span class="c1"&gt;# prefix quarter columns: q_2021q4
&lt;/span&gt;    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;col&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;lower&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I also hit an OOM error trying to write the QEDS data through PySpark — 76,835 rows with 90+ columns across 214 sheets was too much for the JVM heap in the Docker container. The fix was to bypass Spark entirely for QEDS and write directly to GCS using the &lt;code&gt;google-cloud-storage&lt;/code&gt; Python client:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="nb"&gt;buffer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;io&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;BytesIO&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="n"&gt;df&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;to_parquet&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;buffer&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;index&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;False&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;engine&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;pyarrow&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nb"&gt;buffer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;seek&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;client&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;storage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Client&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="n"&gt;bucket&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;bucket&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;GCS_BUCKET&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;blob&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;bucket&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;blob&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;output_path&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;blob&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;upload_from_file&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;buffer&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;content_type&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;application/octet-stream&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No JVM, no Spark executor, no OOM. The lesson: use the right tool for the job. PySpark is excellent for large distributed datasets. For a 76K-row DataFrame from an Excel file, plain pandas and the GCS Python client is simpler and more reliable.&lt;/p&gt;

&lt;h3&gt;
  
  
  Docker Image
&lt;/h3&gt;

&lt;p&gt;The ingestion image is built on &lt;code&gt;eclipse-temurin:17-jdk-jammy&lt;/code&gt; rather than a plain Python image. This is because PySpark requires Java, and the &lt;code&gt;python:3.11-slim&lt;/code&gt; base image uses Debian Trixie which does not carry &lt;code&gt;openjdk-17-jdk&lt;/code&gt; in its default repositories. The Temurin image ships Java 17 out of the box, which is exactly what Spark 3.5.1 needs.&lt;/p&gt;

&lt;p&gt;The GCS connector JAR is downloaded at build time:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight docker"&gt;&lt;code&gt;&lt;span class="k"&gt;RUN &lt;/span&gt;curl &lt;span class="nt"&gt;--progress-bar&lt;/span&gt; &lt;span class="nt"&gt;-L&lt;/span&gt; &lt;span class="se"&gt;\
&lt;/span&gt;    https://storage.googleapis.com/hadoop-lib/gcs/gcs-connector-hadoop3-latest.jar &lt;span class="se"&gt;\
&lt;/span&gt;    &lt;span class="nt"&gt;-o&lt;/span&gt; &lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;SPARK_HOME&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;/jars/gcs-connector-hadoop3-latest.jar
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Credentials are never baked into the image. They are mounted at runtime as a volume:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker run &lt;span class="nt"&gt;--rm&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-v&lt;/span&gt; /path/to/key.json:/app/credentials/key.json &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-e&lt;/span&gt; &lt;span class="nv"&gt;GOOGLE_APPLICATION_CREDENTIALS&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;/app/credentials/key.json &lt;span class="se"&gt;\&lt;/span&gt;
  sovereign-debt-ingestion:v1 python3 extract_jedh.py
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Orchestration with Apache Airflow
&lt;/h2&gt;

&lt;p&gt;Airflow runs on Docker Compose using the official &lt;code&gt;apache/airflow:2.9.2&lt;/code&gt; image. The stack includes a Celery executor with Redis as the message broker and Postgres as the metadata database.&lt;/p&gt;

&lt;p&gt;The DAG runs quarterly — on the 1st of January, April, July, and October at 06:00 UTC:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;schedule_interval&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;0 6 1 1,4,7,10 *&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Task flow:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;extract_load_jedh&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;extract_load_qeds&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Both tasks use the &lt;code&gt;DockerOperator&lt;/code&gt; to spin up the ingestion container on the host machine, which means Airflow itself does not need PySpark, Java, or any data dependencies — it just tells Docker to run the job.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The Docker socket problem&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;DockerOperator&lt;/code&gt; communicates with the host Docker daemon through the Unix socket at &lt;code&gt;/var/run/docker.sock&lt;/code&gt;. This requires two things: the socket must be mounted into the Airflow worker container, and the worker must have permission to use it.&lt;/p&gt;

&lt;p&gt;The socket mount goes in &lt;code&gt;docker-compose.yml&lt;/code&gt; under the common volumes section:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;/var/run/docker.sock:/var/run/docker.sock&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The permission fix requires adding the Docker group ID to the Airflow worker. On my machine the Docker group ID is 984:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;group_add&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;984"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Without the group add, the worker can see the socket but gets &lt;code&gt;PermissionError(13, 'Permission denied')&lt;/code&gt;. This is a common Airflow + Docker-in-Docker gotcha that took several debugging sessions to resolve.&lt;/p&gt;

&lt;h2&gt;
  
  
  Transformation with dbt Cloud
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Staging Layer
&lt;/h3&gt;

&lt;p&gt;The staging models are materialized as views — no storage cost, no latency, just a SQL lens over the raw external tables.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;stg_jedh&lt;/code&gt; does the heavy lifting: it unpivots the wide year-column format into long format using BigQuery's &lt;code&gt;UNPIVOT&lt;/code&gt; operator:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="n"&gt;unpivot&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;value&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="nb"&gt;year&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;year_1998&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;year_1999&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;year_2000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;...,&lt;/span&gt; &lt;span class="n"&gt;year_2025&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then extracts the year integer from the column name:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;cast&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;year&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'year_'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;''&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;int64&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nb"&gt;year&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And adds a human-readable series description via a CASE statement, turning &lt;code&gt;DT.DOD.DECT.CD&lt;/code&gt; into &lt;code&gt;Total external debt stocks&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The result is a clean long-format table: one row per country per series per year.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;stg_qeds&lt;/code&gt; is simpler — it selects the clean columns, uses &lt;code&gt;SAFE_CAST&lt;/code&gt; to convert quarter values to float64, and filters out null countries and series codes. &lt;code&gt;SAFE_CAST&lt;/code&gt; is preferable to &lt;code&gt;CAST&lt;/code&gt; here because it returns NULL on failure rather than throwing an error, which is the right behaviour for messy Excel data.&lt;/p&gt;

&lt;h3&gt;
  
  
  Mart Layer
&lt;/h3&gt;

&lt;p&gt;The mart models are materialized as tables with partitioning and clustering for query efficiency.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;mart_debt_stocks&lt;/code&gt; is partitioned by year and clustered by &lt;code&gt;country_code&lt;/code&gt; and &lt;code&gt;series_code&lt;/code&gt;. It enriches the staging data with YoY percentage change and debt as a percentage of total external debt per country per year — both computed using window functions:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="n"&gt;lag&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;debt_value_usd&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;over&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="k"&gt;partition&lt;/span&gt; &lt;span class="k"&gt;by&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;country_code&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;series_code&lt;/span&gt;
    &lt;span class="k"&gt;order&lt;/span&gt; &lt;span class="k"&gt;by&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nb"&gt;year&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;prev_year_value&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;

&lt;span class="n"&gt;safe_divide&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;debt_value_usd&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;lag&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;debt_value_usd&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;over&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="k"&gt;partition&lt;/span&gt; &lt;span class="k"&gt;by&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;country_code&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;series_code&lt;/span&gt;
        &lt;span class="k"&gt;order&lt;/span&gt; &lt;span class="k"&gt;by&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nb"&gt;year&lt;/span&gt;
    &lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="n"&gt;lag&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;debt_value_usd&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;over&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="k"&gt;partition&lt;/span&gt; &lt;span class="k"&gt;by&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;country_code&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;series_code&lt;/span&gt;
        &lt;span class="k"&gt;order&lt;/span&gt; &lt;span class="k"&gt;by&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nb"&gt;year&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;yoy_change_pct&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;mart_regional_debt&lt;/code&gt; assigns countries to six World Bank regions using a CASE statement on ISO3 country codes, then aggregates total, average, max, and min debt stocks per region per series per year. This powers the regional trajectory time series on the dashboard.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;mart_debt_service&lt;/code&gt; computes total annual debt payments and average quarterly payments per country from the QEDS payment schedule data, enabling debt service pressure analysis.&lt;/p&gt;

&lt;h3&gt;
  
  
  dbt Tests
&lt;/h3&gt;

&lt;p&gt;All models have &lt;code&gt;not_null&lt;/code&gt; tests on primary dimension columns. Running &lt;code&gt;dbt test&lt;/code&gt; after every model change ensures data quality is enforced at the transformation layer rather than discovered downstream in the dashboard.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Dashboard
&lt;/h2&gt;

&lt;p&gt;The Looker Studio dashboard has two pages, both connected directly to the BigQuery mart tables.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Page 1 — Global Debt Overview&lt;/strong&gt; answers Q4 at a glance. A time series chart using &lt;code&gt;mart_regional_debt&lt;/code&gt; shows six regional debt trajectories from 1998 to 2024. Africa's trajectory is notably steeper post-2010. A bar chart shows the top 20 countries by total external debt for the selected year. A scorecard shows total global external debt.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Page 2 — Country Deep-Dive&lt;/strong&gt; answers Q1, Q2, and Q5. A stacked bar chart shows debt composition by sector over time for any selected country. A line chart shows short-term vulnerability trends. A table sorted by total 2021 payments shows which countries face the most acute debt service pressure.&lt;/p&gt;

&lt;p&gt;Live dashboard: &lt;a href="https://lookerstudio.google.com/reporting/7fc18e9e-a5c6-4616-b920-b5b4bddf2264" rel="noopener noreferrer"&gt;https://lookerstudio.google.com/reporting/7fc18e9e-a5c6-4616-b920-b5b4bddf2264&lt;/a&gt; &lt;/p&gt;

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

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

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

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

&lt;h2&gt;
  
  
  Key Technical Lessons
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;1. Know which World Bank API sources support programmatic access.&lt;/strong&gt; Only source 2 (IDS) reliably supports the Indicators API v2 via &lt;code&gt;wbgapi&lt;/code&gt;. Sources 22, 23, and 54 use a different DataBank backend and return JSON decode errors. For QEDS data, use the bulk Excel downloads instead.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. BigQuery rejects column names starting with numbers.&lt;/strong&gt; Prefix them before writing Parquet. A simple list comprehension handles this: &lt;code&gt;f"year_{col}" if str(col).isdigit() else col&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Use the right tool for the data size.&lt;/strong&gt; PySpark is excellent for large datasets but overkill for a 76K-row Excel file. The GCS Python client with pandas and PyArrow is simpler, faster, and doesn't OOM.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. Docker-in-Docker with Airflow requires explicit socket mounting and group permissions.&lt;/strong&gt; Mount &lt;code&gt;/var/run/docker.sock&lt;/code&gt; and add the Docker group ID to the worker container. Without the group add, you get a silent permission error.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;5. ELT separates concerns cleanly.&lt;/strong&gt; When the analytical questions evolved during development, I only needed to update dbt models — never the ingestion layer. This separation is the most valuable architectural decision in the project.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I Would Do Differently
&lt;/h2&gt;

&lt;p&gt;If I were building this again, I would use the World Bank DataBank bulk download API to get historical QEDS time series data instead of point-in-time Excel files. The current QEDS data only has a handful of quarters because the Excel files are snapshots of the latest publication. A proper time series would require downloading and archiving each quarterly release.&lt;/p&gt;

&lt;p&gt;I would also add a &lt;code&gt;load_to_bigquery&lt;/code&gt; step that loads Parquet directly into native BigQuery tables rather than using external tables. External tables work well but they require manually updating the source URI list each time a new partition is added. A native table with partitioning handles this automatically.&lt;/p&gt;

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

&lt;p&gt;The Sovereign Debt Observatory took the World Bank's raw debt data from scattered Excel files and API endpoints to a fully automated, tested, and documented analytical pipeline. Every component is reproducible — Terraform provisions the infrastructure, Docker packages the ingestion environment, Airflow schedules the runs, and dbt documents and tests the transformations.&lt;/p&gt;

&lt;p&gt;The full source code is on GitHub: &lt;a href="https://github.com/Derrick-Ryan-Giggs/sovereign-debt-observatory" rel="noopener noreferrer"&gt;https://github.com/Derrick-Ryan-Giggs/sovereign-debt-observatory&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;If you have questions about any part of the implementation, drop them in the comments. I am happy to go deeper on any layer.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Ryan Derrick Giggs is a data engineering practitioner and technical writer based in Nairobi, Kenya. He is currently completing the DataTalksClub Data Engineering Zoomcamp 2026.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;LinkedIn: &lt;a href="https://linkedin.com/in/ryan-giggs-a19330265" rel="noopener noreferrer"&gt;https://linkedin.com/in/ryan-giggs-a19330265&lt;/a&gt;&lt;/em&gt;&lt;br&gt;
&lt;em&gt;GitHub: &lt;a href="https://github.com/Derrick-Ryan-Giggs" rel="noopener noreferrer"&gt;https://github.com/Derrick-Ryan-Giggs&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>dataengineering</category>
      <category>gcp</category>
      <category>dbt</category>
      <category>worldbank</category>
    </item>
    <item>
      <title>Exadata AI Storage: A New Era of AI-Powered Database Infrastructure</title>
      <dc:creator>Ryan Giggs</dc:creator>
      <pubDate>Mon, 06 Apr 2026 07:41:04 +0000</pubDate>
      <link>https://dev.to/derrickryangiggs/exadata-ai-storage-a-new-era-of-ai-powered-database-infrastructure-12mb</link>
      <guid>https://dev.to/derrickryangiggs/exadata-ai-storage-a-new-era-of-ai-powered-database-infrastructure-12mb</guid>
      <description>&lt;p&gt;Oracle's Exadata platform has always been synonymous with extreme database performance. But with the release of &lt;strong&gt;Exadata System Software 24ai and 25ai&lt;/strong&gt;, alongside the debut of &lt;strong&gt;Oracle Exadata X11M&lt;/strong&gt; in January 2025, Oracle has taken a decisive step into the AI era. Exadata is no longer just a high-performance OLTP and analytics machine — it is now purpose-built to accelerate AI vector search, in-database machine learning, and mixed enterprise workloads, all on a single converged platform.&lt;/p&gt;

&lt;p&gt;This post breaks down the key new features across software, infrastructure, high availability, monitoring, and security — and explains why they matter.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Is Exadata AI Storage?
&lt;/h2&gt;

&lt;p&gt;At its core, Exadata AI Storage refers to Oracle's strategy of pushing intelligence deeper into the storage layer. Rather than offloading AI computation to separate, purpose-built vector databases or GPUs, Oracle brings AI operations — particularly &lt;strong&gt;AI Vector Search&lt;/strong&gt; — directly to the storage servers themselves. This means vector index builds, similarity searches, and distance calculations happen closer to where data lives, dramatically reducing the data movement that kills performance in distributed architectures.&lt;/p&gt;

&lt;p&gt;The result? Key vector search operations running up to &lt;strong&gt;30x faster&lt;/strong&gt; with Exadata System Software 24ai, and further accelerated on X11M with in-memory vector index (HNSW) scans running &lt;strong&gt;up to 43% faster&lt;/strong&gt; on database servers and &lt;strong&gt;up to 55% faster&lt;/strong&gt; on storage servers compared to the previous generation.&lt;/p&gt;

&lt;h2&gt;
  
  
  Key New Software Features
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. AI Smart Scan
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;AI Smart Scan&lt;/strong&gt; is an extension of Exadata's legendary Smart Scan technology, now purpose-built for AI workloads. It offloads compute-intensive &lt;strong&gt;AI Vector Search&lt;/strong&gt; operations — including vector index builds and similarity queries — directly to Exadata's intelligent storage servers. This eliminates the need to ship raw data up to the database tier for processing.&lt;/p&gt;

&lt;p&gt;Critically, AI Smart Scan enables thousands of concurrent AI vector searches in multi-user environments. This is a significant differentiator for enterprise RAG (Retrieval Augmented Generation) pipelines and AI applications that need to serve many users simultaneously, not just batch processes.&lt;/p&gt;

&lt;p&gt;With the latest release (Exadata System Software 25ai / 25.1), &lt;strong&gt;Adaptive Top-K Filtering&lt;/strong&gt; further extends this: each storage server maintains a running Top-K result set, reducing data returned to the database servers by up to &lt;strong&gt;4.7x&lt;/strong&gt;. Similarly, &lt;code&gt;VECTOR_DISTANCE()&lt;/code&gt; calculations are now projected from storage, delivering up to &lt;strong&gt;4.6x faster&lt;/strong&gt; distance-based queries.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Exadata RDMA Memory (XRMEM)
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;XRMEM&lt;/strong&gt; replaces the persistent memory (Intel Optane PMem) that earlier generations used, adapting to changes in the memory vendor landscape. It is built on DDR5 DRAM and accessed via &lt;strong&gt;RDMA (Remote Direct Memory Access) over Converged Ethernet (RoCE)&lt;/strong&gt;, bypassing the OS, I/O, and network software stacks entirely.&lt;/p&gt;

&lt;p&gt;The practical benefit: ultra-low read latency as low as &lt;strong&gt;14 microseconds&lt;/strong&gt; — a 21% improvement over prior generations — with scan throughput of up to &lt;strong&gt;500 GB/s&lt;/strong&gt; from XRMEM alone. Each Exadata X11M Extreme Flash Storage Server contains &lt;strong&gt;1.25 TB of XRMEM&lt;/strong&gt;, which sits as an acceleration tier in front of the Smart Flash Cache.&lt;/p&gt;

&lt;p&gt;XRMEM is particularly impactful for OLTP workloads that require sub-20-microsecond response times, and it now also accelerates AI vector index reads transparently.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. On-Storage Processing
&lt;/h3&gt;

&lt;p&gt;This is the foundational principle that makes all the above possible. Exadata storage servers are not passive disk arrays — they are intelligent compute nodes in their own right. SQL filtering, column projection, decompression, encryption/decryption, bloom filters, and now AI vector operations all execute &lt;strong&gt;on the storage servers&lt;/strong&gt;, not the database servers.&lt;/p&gt;

&lt;p&gt;This dramatically reduces the volume of data sent over the internal network and processed by database-tier CPUs. For analytics workloads, this "push-down" processing model is why Exadata consistently delivers 10–100x better throughput than general-purpose storage.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. In-Memory Columnar Speeds for JSON Queries
&lt;/h3&gt;

&lt;p&gt;Exadata's &lt;strong&gt;In-Memory Columnar Cache&lt;/strong&gt; on storage servers (also called Columnar Cache) stores a columnar representation of row-oriented data directly in flash cache. When queries access this data, they get the performance of columnar analytics without requiring data to be reformatted or migrated.&lt;/p&gt;

&lt;p&gt;With &lt;strong&gt;Oracle Database 23ai&lt;/strong&gt; — which is required to unlock the full Exadata Exascale feature set — JSON documents stored natively benefit from this columnar acceleration. Oracle Database 23ai's &lt;strong&gt;JSON Relational Duality&lt;/strong&gt; views, which expose the same data as both JSON and relational tables simultaneously, can be queried at columnar memory speeds on Exadata, collapsing the performance gap between document and relational workloads.&lt;/p&gt;

&lt;h3&gt;
  
  
  5. Transparent Cross-Tier Scan
&lt;/h3&gt;

&lt;p&gt;Exadata's multi-tier storage hierarchy — XRMEM → Smart Flash Cache → disk — is managed &lt;strong&gt;automatically and transparently&lt;/strong&gt;. When a Smart Scan or AI Smart Scan runs, Exadata intelligently sources data from whichever tier contains it, combining reads from memory, flash, and disk in parallel.&lt;/p&gt;

&lt;p&gt;This means administrators and developers never need to manually partition hot vs. cold data or tune tier placement. The system continuously tracks access patterns and moves data to the appropriate tier based on usage, keeping the hottest data in XRMEM and the next hottest in flash. The database never sees these tiers explicitly — it simply issues a SQL or vector query and receives results.&lt;/p&gt;

&lt;h3&gt;
  
  
  6. Caching Enhancements
&lt;/h3&gt;

&lt;p&gt;Several caching improvements ship with the latest Exadata System Software releases:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Automatic KEEP Object Load into Exadata Flash Cache&lt;/strong&gt;: Objects tagged with the &lt;code&gt;KEEP&lt;/code&gt; storage clause in Oracle Database are automatically and proactively loaded into Exadata Smart Flash Cache — even before they are first accessed — ensuring zero cold-start latency for critical tables and indexes.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Write Back Flash Cache&lt;/strong&gt;: Database block writes are cached in flash, eliminating disk I/O bottlenecks for large OLTP and batch workloads.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cell-to-Cell Rebalance preserving XRMEM and Flash Cache&lt;/strong&gt;: When data rebalances across storage servers (due to maintenance or failure), both the XRMEM and flash cache contents are also rebalanced, preserving performance levels rather than causing a cold-cache performance dip.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  7. Columnar Smart Scan at Memory Speed
&lt;/h3&gt;

&lt;p&gt;When Oracle Database In-Memory is enabled, Exadata automatically stores data in &lt;strong&gt;columnar format within Flash Cache and XRMEM&lt;/strong&gt; if it will improve query performance. This brings columnar analytics performance — historically associated only with in-database memory (DRAM on database servers) — to storage-resident data, enabling analytics at memory speeds even when datasets exceed what fits in database-server DRAM.&lt;/p&gt;

&lt;p&gt;A single Exadata X11M rack can deliver up to &lt;strong&gt;100 GB/s of flash throughput&lt;/strong&gt; and &lt;strong&gt;500 GB/s from XRMEM&lt;/strong&gt; for the hottest data, far exceeding what traditional storage arrays can achieve even with flash added.&lt;/p&gt;

&lt;h3&gt;
  
  
  8. Exadata Cache Observability
&lt;/h3&gt;

&lt;p&gt;Exadata System Software &lt;strong&gt;24.1 introduced &lt;code&gt;ecstat&lt;/code&gt;&lt;/strong&gt; (Exadata Storage Cache Statistics), a real-time utility that provides per-storage-server statistics on Smart Flash Cache usage, XRMEM hits, and I/O performance. This was a long-standing gap — DBAs previously had to rely on AWR snapshots to understand cache behavior.&lt;/p&gt;

&lt;p&gt;In &lt;strong&gt;Exadata System Software 25.2&lt;/strong&gt;, this was extended with &lt;strong&gt;&lt;code&gt;CellSQLStat&lt;/code&gt;&lt;/strong&gt;, which provides real-time, per-storage-server insights into active Smart Scan operations: CPU and memory usage, Storage Index and Columnar Cache I/O savings, flash and XRMEM hit rates, scan rates, and more. Both &lt;code&gt;ecstat&lt;/code&gt; and &lt;code&gt;CellSQLStat&lt;/code&gt; data are automatically included in ExaWatcher collections, making them available for historical analysis.&lt;/p&gt;

&lt;h2&gt;
  
  
  Infrastructure Improvements
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Increased Number of Virtual Machines
&lt;/h3&gt;

&lt;p&gt;With &lt;strong&gt;Exadata Exascale&lt;/strong&gt; (the new intelligent storage architecture introduced in 2024 and available on X11M), the limit on Virtual Machine clusters per database server increases dramatically. Traditional Exadata with ASM supported 4, 8, or 12 VMs per database server. Exascale raises this ceiling to &lt;strong&gt;50 VMs per database server&lt;/strong&gt;, enabling far greater consolidation of Oracle Database workloads on a single Exadata system without sacrificing isolation or performance.&lt;/p&gt;

&lt;p&gt;Exascale also centralizes VM storage in the shared Exascale storage pool rather than on individual database servers, increasing flexibility and simplifying management.&lt;/p&gt;

&lt;h3&gt;
  
  
  Secure Boot for KVM Virtual Machines
&lt;/h3&gt;

&lt;p&gt;Exadata System Software now supports &lt;strong&gt;Secure Boot for KVM guest VMs&lt;/strong&gt;, ensuring that only cryptographically signed and trusted OS images can boot on Exadata database servers. This closes a significant attack vector in virtualized deployments and aligns Exadata's on-premises security posture with cloud-native security standards. It complements existing features like Trusted Partitions for Oracle Linux Virtualization.&lt;/p&gt;

&lt;h2&gt;
  
  
  High Availability and Network Resilience
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Improved RoCE Network Resilience: ExaPortMon
&lt;/h3&gt;

&lt;p&gt;Every Exadata database and storage server connects to the internal network via &lt;strong&gt;dual 100 GbE RoCE ports&lt;/strong&gt; for an aggregate of 200 Gbps. If a RoCE leaf switch port becomes stalled — appearing online but unable to pass traffic — it can cause cluster instability without triggering a clean failure.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;ExaPortMon&lt;/strong&gt; (introduced in Exadata System Software 24ai) solves this. It continuously monitors both RoCE ports on every server. When it detects a stalled port, it &lt;strong&gt;automatically migrates the IP address to the healthy port&lt;/strong&gt;, keeping network traffic flowing and preventing outages. When the stalled port recovers, ExaPortMon automatically returns the IP address to its original port. No manual intervention required.&lt;/p&gt;

&lt;h3&gt;
  
  
  Enhanced RoCE Network Security: Exadata Secure RDMA Fabric Isolation
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Exadata Secure RDMA Fabric Isolation (Secure Fabric)&lt;/strong&gt; provides strict network isolation between VM clusters sharing the same physical Exadata infrastructure. It prevents database servers in one VM cluster from communicating with those in another over the RoCE fabric, eliminating lateral movement risk in consolidated and multi-tenant deployments.&lt;/p&gt;

&lt;p&gt;Starting with Exadata System Software 25.1, &lt;strong&gt;Secure Fabric is automatically selected by default&lt;/strong&gt; for all new on-premises deployments with X8M and newer hardware — bringing on-premises deployments into alignment with cloud deployments, which have always used Secure Fabric.&lt;/p&gt;

&lt;h2&gt;
  
  
  Monitoring and Management
&lt;/h2&gt;

&lt;h3&gt;
  
  
  AWR &amp;amp; SQL Monitor Enhancements
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Oracle Automatic Workload Repository (AWR)&lt;/strong&gt; on Exadata is enhanced with &lt;strong&gt;Exadata-specific storage server metrics&lt;/strong&gt; alongside standard Oracle wait event data. AWR now collects and reports on XRMEM, Flash Cache, and HDD device performance, enabling DBAs to correlate database wait events with storage-tier behavior in a single report.&lt;/p&gt;

&lt;p&gt;SQL Monitor is similarly enhanced, providing end-to-end visibility into query execution that includes storage offload statistics, Smart Scan I/O savings, and flash cache hit rates — all tied to the specific SQL statement being analyzed.&lt;/p&gt;

&lt;h3&gt;
  
  
  JSON API for Management Server
&lt;/h3&gt;

&lt;p&gt;Exadata's Management Server (MS) now exposes a &lt;strong&gt;JSON REST API&lt;/strong&gt;, enabling programmatic access to Exadata management and monitoring functions. This is a significant modernization of Exadata's management interface, making it easier to integrate Exadata health metrics, alerts, and configuration into modern observability stacks (Grafana, custom dashboards, CI/CD pipelines) without relying solely on traditional CLI tools like &lt;code&gt;cellcli&lt;/code&gt; or Enterprise Manager.&lt;/p&gt;

&lt;h2&gt;
  
  
  Security Enhancements
&lt;/h2&gt;

&lt;h3&gt;
  
  
  SNMP v3 Security
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Exadata System Software 24.1&lt;/strong&gt; introduced mandatory SNMP security improvements across all storage servers and database servers. The key changes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;SNMP v3 is now the recommended and encouraged standard&lt;/strong&gt;, supporting SHA-256, SHA-384, and SHA-512 authentication protocols for strong authentication and encryption.&lt;/li&gt;
&lt;li&gt;All SNMP subscriber definitions now &lt;strong&gt;require the connection type to be explicitly specified&lt;/strong&gt; — administrators can no longer leave it ambiguous.&lt;/li&gt;
&lt;li&gt;SNMP v1 remains available but triggers an explicit warning discouraging its use.&lt;/li&gt;
&lt;li&gt;Default community strings like &lt;code&gt;public&lt;/code&gt; and &lt;code&gt;private&lt;/code&gt; are &lt;strong&gt;actively discouraged by the system&lt;/strong&gt;, prompting administrators to set strong, unique values.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This tightens Exadata's management plane security, closing a common vulnerability in enterprise infrastructure where SNMP v1 with default community strings is still widely used.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why This All Matters: The Convergence Thesis
&lt;/h2&gt;

&lt;p&gt;The strategic bet Oracle is making with Exadata AI Storage is one of &lt;strong&gt;convergence over fragmentation&lt;/strong&gt;. The enterprise AI market has seen an explosion of purpose-built vector databases (Pinecone, Weaviate, Qdrant, Milvus, Chroma), and many organizations are building RAG pipelines that shuffle data between separate systems: a relational database for operational data, a vector database for embeddings, an object store for documents.&lt;/p&gt;

&lt;p&gt;Exadata offers a fundamentally different architecture: &lt;strong&gt;bring the AI to the data, not the data to the AI&lt;/strong&gt;. With Oracle AI Database 23ai (now succeeded by Oracle AI Database 26ai as the long-term support release), all of this runs in a single converged engine — relational queries, vector similarity search, JSON document queries, graph traversals, and full-text search — executed as optimized SQL on Exadata hardware. Advanced AI features including AI Vector Search are included at no additional charge.&lt;/p&gt;

&lt;p&gt;And with &lt;strong&gt;Exadata Exascale&lt;/strong&gt; reducing the entry cost for Exadata Database Service by up to 95% and enabling organizations to start with as little as 300 GB of storage, the platform is no longer exclusively for Fortune 500 database estates. It is increasingly accessible to organizations of any size that need to build AI applications on enterprise-grade, governed, transactionally consistent data.&lt;/p&gt;

&lt;h2&gt;
  
  
  Summary Table
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Category&lt;/th&gt;
&lt;th&gt;Key Feature&lt;/th&gt;
&lt;th&gt;Benefit&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;AI Workloads&lt;/td&gt;
&lt;td&gt;AI Smart Scan + Adaptive Top-K&lt;/td&gt;
&lt;td&gt;Up to 30x faster vector search; 4.7x less data to DB servers&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Memory&lt;/td&gt;
&lt;td&gt;XRMEM (DDR5 + RDMA)&lt;/td&gt;
&lt;td&gt;14µs read latency; 500 GB/s scan throughput&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Caching&lt;/td&gt;
&lt;td&gt;Auto KEEP Load, Write Back, Columnar Cache&lt;/td&gt;
&lt;td&gt;Zero cold-start for critical objects; analytics at memory speed&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Observability&lt;/td&gt;
&lt;td&gt;ecstat + CellSQLStat&lt;/td&gt;
&lt;td&gt;Real-time per-cell Smart Scan and cache monitoring&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Infrastructure&lt;/td&gt;
&lt;td&gt;Exascale VM limit increase&lt;/td&gt;
&lt;td&gt;Up to 50 VMs per DB server (up from 12)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Security&lt;/td&gt;
&lt;td&gt;Secure Fabric default on-prem&lt;/td&gt;
&lt;td&gt;Automatic lateral-movement isolation for VM clusters&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Network HA&lt;/td&gt;
&lt;td&gt;ExaPortMon&lt;/td&gt;
&lt;td&gt;Auto-failover between RoCE ports; no manual intervention&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Security&lt;/td&gt;
&lt;td&gt;SNMP v3 enforcement&lt;/td&gt;
&lt;td&gt;SHA-512 auth; eliminates default community string risk&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Management&lt;/td&gt;
&lt;td&gt;JSON API for Management Server&lt;/td&gt;
&lt;td&gt;Programmatic integration with modern observability stacks&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Exadata AI Storage represents Oracle's clearest articulation yet of its "converged data" strategy: a single platform that handles OLTP, analytics, and AI workloads without requiring organizations to build and manage a fragmented ecosystem of specialized tools. With Exadata System Software 25ai, the X11M generation, and the Exascale architecture now generally available across OCI, multicloud (AWS, Azure, Google Cloud), and on-premises, there has never been a better time to evaluate what Exadata can do for your AI application stack.&lt;/p&gt;

&lt;p&gt;The numbers speak for themselves — but the architecture is the real story.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Have you worked with Exadata AI Storage or Oracle Database 23ai/26ai AI Vector Search? Share your experience in the comments.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>oracle</category>
      <category>exadata</category>
      <category>databaseengineering</category>
      <category>aiinfrastructure</category>
    </item>
    <item>
      <title>Tech Ecosystem Observatory: How I Built a Cloud-Native Data Pipeline to Track Global Tech Layoffs vs YC Startup Activity</title>
      <dc:creator>Ryan Giggs</dc:creator>
      <pubDate>Mon, 30 Mar 2026 07:44:40 +0000</pubDate>
      <link>https://dev.to/derrickryangiggs/tech-ecosystem-observatory-how-i-built-a-cloud-native-data-pipeline-to-track-global-tech-layoffs-5gbe</link>
      <guid>https://dev.to/derrickryangiggs/tech-ecosystem-observatory-how-i-built-a-cloud-native-data-pipeline-to-track-global-tech-layoffs-5gbe</guid>
      <description>&lt;p&gt;Just completed my DEZ Zoomcamp 2026 capstone project — the Tech Ecosystem Observatory&lt;/p&gt;

&lt;p&gt;Built a full cloud-native batch data pipeline from scratch that answers: which industries are shedding the most jobs, and how does that correlate with YC startup activity?&lt;/p&gt;

&lt;p&gt;Here's what the pipeline looks like end to end:&lt;/p&gt;

&lt;p&gt;✅ Terraform — provisioned GCS bucket and BigQuery datasets as infrastructure as code&lt;/p&gt;

&lt;p&gt;✅ Docker — containerized all ingestion scripts into a portable image&lt;/p&gt;

&lt;p&gt;✅ Kestra — orchestrated a 4-task batch DAG running weekly every Monday 6AM UTC&lt;/p&gt;

&lt;p&gt;✅ Google Cloud Storage — raw JSONL data lake storing layoffs and YC company data&lt;/p&gt;

&lt;p&gt;✅ BigQuery — partitioned by date (monthly) and clustered by industry/country for optimized queries&lt;/p&gt;

&lt;p&gt;✅ dbt Cloud — built staging views and mart tables (mart_monthly_layoffs + mart_tech_ecosystem)&lt;/p&gt;

&lt;p&gt;✅ Looker Studio — 2-page interactive dashboard with layoff trends, geo maps, ecosystem stress ratios&lt;/p&gt;

&lt;p&gt;📊 Data: 4,317 layoff events (2023–2024) + 5,690 YC-backed companies&lt;/p&gt;

&lt;p&gt;🔗 Live dashboard: &lt;a href="https://lookerstudio.google.com/reporting/b1620cae-97cb-4911-82b8-dd0c46ee8acb" rel="noopener noreferrer"&gt;https://lookerstudio.google.com/reporting/b1620cae-97cb-4911-82b8-dd0c46ee8acb&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;💻 GitHub: &lt;a href="https://github.com/Derrick-Ryan-Giggs/tech-ecosystem-observatory" rel="noopener noreferrer"&gt;https://github.com/Derrick-Ryan-Giggs/tech-ecosystem-observatory&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Huge thanks to @DataTalksClub and Alexey Grigorev for building and maintaining this incredible free course. If you're serious about data engineering, this is where you start 👇&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/DataTalksClub/data-engineering-zoomcamp/" rel="noopener noreferrer"&gt;https://github.com/DataTalksClub/data-engineering-zoomcamp/&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Who else is building data pipelines? Drop your projects below 👇&lt;/p&gt;

</description>
      <category>dataengineering</category>
      <category>gcp</category>
      <category>bigquery</category>
      <category>dbt</category>
    </item>
    <item>
      <title>Streaming with PyFlink and Redpanda</title>
      <dc:creator>Ryan Giggs</dc:creator>
      <pubDate>Mon, 16 Mar 2026 13:34:19 +0000</pubDate>
      <link>https://dev.to/derrickryangiggs/streaming-with-pyflink-and-redpanda-4n4c</link>
      <guid>https://dev.to/derrickryangiggs/streaming-with-pyflink-and-redpanda-4n4c</guid>
      <description>&lt;p&gt;Week 7 of Data Engineering Zoomcamp by @DataTalksClub complete&lt;/p&gt;

&lt;p&gt;Just finished Module 7 - Streaming with PyFlink. Learned how to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Set up Redpanda as a Kafka replacement&lt;/li&gt;
&lt;li&gt;Build Kafka producers and consumers in Python&lt;/li&gt;
&lt;li&gt;Create tumbling and session windows in Flink&lt;/li&gt;
&lt;li&gt;Analyze real-time taxi trip data with stream processing&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Here's my homework solution: &lt;a href="https://github.com/Derrick-Ryan-Giggs/Streaming-Homework" rel="noopener noreferrer"&gt;https://github.com/Derrick-Ryan-Giggs/Streaming-Homework&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;You can sign up here: &lt;a href="https://github.com/DataTalksClub/data-engineering-zoomcamp/" rel="noopener noreferrer"&gt;https://github.com/DataTalksClub/data-engineering-zoomcamp/&lt;/a&gt;&lt;/p&gt;

</description>
      <category>dataengineering</category>
      <category>apacheflink</category>
      <category>kafka</category>
      <category>python</category>
    </item>
    <item>
      <title>Oracle Database 23ai: Vector Similarity Search - Exact, Approximate, and Multi-Vector Strategies</title>
      <dc:creator>Ryan Giggs</dc:creator>
      <pubDate>Mon, 16 Mar 2026 08:57:31 +0000</pubDate>
      <link>https://dev.to/derrickryangiggs/oracle-database-23ai-vector-similarity-search-exact-approximate-and-multi-vector-strategies-3fci</link>
      <guid>https://dev.to/derrickryangiggs/oracle-database-23ai-vector-similarity-search-exact-approximate-and-multi-vector-strategies-3fci</guid>
      <description>&lt;p&gt;Oracle Database 23ai's AI Vector Search provides multiple strategies for finding similar vectors, each with different trade-offs between accuracy, speed, and resource usage. Understanding when to use exact search, approximate search, or multi-vector search—and knowing the essential vector functions—is crucial for building high-performance semantic search applications.&lt;/p&gt;

&lt;h2&gt;
  
  
  Understanding Similarity Search Types
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. Exact Similarity Search (Flat Search)
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Exact similarity search calculates a query vector's distance to all other vectors&lt;/strong&gt;. It's also called flat search or exhaustive search because every vector in the dataset is compared.&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;Gives the most accurate results&lt;/li&gt;
&lt;li&gt;Perfect search quality (100% recall)&lt;/li&gt;
&lt;li&gt;Involves potentially significant time as dataset grows&lt;/li&gt;
&lt;li&gt;No indexes required&lt;/li&gt;
&lt;li&gt;Suitable for small to medium datasets (thousands to hundreds of thousands of vectors)&lt;/li&gt;
&lt;/ul&gt;

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

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; 
  &lt;span class="n"&gt;product_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;product_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;VECTOR_DISTANCE&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;embedding&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;query_vector&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;COSINE&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;similarity&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;products&lt;/span&gt;
&lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;similarity&lt;/span&gt;
&lt;span class="k"&gt;FETCH&lt;/span&gt; &lt;span class="k"&gt;FIRST&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt; &lt;span class="k"&gt;ROWS&lt;/span&gt; &lt;span class="k"&gt;ONLY&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;When to Use:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Small datasets where performance is acceptable&lt;/li&gt;
&lt;li&gt;When perfect accuracy is required&lt;/li&gt;
&lt;li&gt;Development and testing phases&lt;/li&gt;
&lt;li&gt;Benchmarking approximate search results&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  2. Approximate Similarity Search
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Approximate similarity search uses vector indexes&lt;/strong&gt; to dramatically speed up searches with minimal accuracy loss. Instead of checking every vector, it leverages specialized data structures to narrow the search space.&lt;/p&gt;

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

&lt;p&gt;&lt;strong&gt;You must enable vector pool in the SGA&lt;/strong&gt; for HNSW indexes (in-memory neighbor graph indexes).&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;ALTER&lt;/span&gt; &lt;span class="k"&gt;SYSTEM&lt;/span&gt; &lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="n"&gt;vector_memory_size&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;800&lt;/span&gt;&lt;span class="n"&gt;M&lt;/span&gt; &lt;span class="k"&gt;SCOPE&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;SPFILE&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="c1"&gt;-- Restart required&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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

&lt;ul&gt;
&lt;li&gt;Can be more efficient but less accurate than exact search&lt;/li&gt;
&lt;li&gt;Uses target accuracy setting (typically 90-99%)&lt;/li&gt;
&lt;li&gt;Requires vector indexes (HNSW or IVF)&lt;/li&gt;
&lt;li&gt;Scales to millions or billions of vectors&lt;/li&gt;
&lt;li&gt;Typical accuracy: 95%+ with proper configuration&lt;/li&gt;
&lt;/ul&gt;

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

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- Using FETCH APPROXIMATE&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; 
  &lt;span class="n"&gt;product_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;product_name&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;products&lt;/span&gt;
&lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;embedding&lt;/span&gt; &lt;span class="k"&gt;less&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="k"&gt;than&lt;/span&gt; &lt;span class="k"&gt;equals&lt;/span&gt; &lt;span class="n"&gt;greater&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="k"&gt;than&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;query_vector&lt;/span&gt;
&lt;span class="k"&gt;FETCH&lt;/span&gt; &lt;span class="n"&gt;APPROXIMATE&lt;/span&gt; &lt;span class="k"&gt;FIRST&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt; &lt;span class="k"&gt;ROWS&lt;/span&gt; &lt;span class="k"&gt;ONLY&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Performance Comparison:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;According to Oracle benchmarks, exact search on 50,000 vectors took 1.50 seconds versus 0.47 seconds with HNSW index—over 3x faster with the same top-10 results.&lt;/p&gt;

&lt;h3&gt;
  
  
  Vector Index Types
&lt;/h3&gt;

&lt;p&gt;Oracle AI Vector Search supports two main types of vector indexes:&lt;/p&gt;

&lt;h4&gt;
  
  
  HNSW (Hierarchical Navigable Small World)
&lt;/h4&gt;

&lt;p&gt;&lt;strong&gt;In-Memory Neighbor Graph&lt;/strong&gt; vector index that creates a navigable graph structure for ultra-fast similarity search.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Creating HNSW Index:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="n"&gt;VECTOR&lt;/span&gt; &lt;span class="k"&gt;INDEX&lt;/span&gt; &lt;span class="n"&gt;products_hnsw_idx&lt;/span&gt;
&lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;products&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;embedding&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;ORGANIZATION&lt;/span&gt; &lt;span class="n"&gt;NEIGHBOR&lt;/span&gt; &lt;span class="n"&gt;GRAPH&lt;/span&gt;
&lt;span class="n"&gt;DISTANCE&lt;/span&gt; &lt;span class="n"&gt;COSINE&lt;/span&gt;
&lt;span class="k"&gt;WITH&lt;/span&gt; &lt;span class="n"&gt;TARGET&lt;/span&gt; &lt;span class="n"&gt;ACCURACY&lt;/span&gt; &lt;span class="mi"&gt;95&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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

&lt;ul&gt;
&lt;li&gt;Fully built in-memory in the vector pool&lt;/li&gt;
&lt;li&gt;Extremely fast queries (sub-100ms)&lt;/li&gt;
&lt;li&gt;Requires substantial memory&lt;/li&gt;
&lt;li&gt;No DML operations allowed after creation&lt;/li&gt;
&lt;li&gt;Not available in RAC environments&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Memory Calculation:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Formula: &lt;code&gt;1.3 times number_of_vectors times number_of_dimensions times dimension_byte_size&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;Example for 1M vectors, 768 dimensions, FLOAT32 (4 bytes):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;1.3 × 1,000,000 × 768 × 4 = 3.99 GB
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  IVF (Inverted File Flat)
&lt;/h4&gt;

&lt;p&gt;&lt;strong&gt;Neighbor Partition&lt;/strong&gt; vector index built on disk with blocks cached in the buffer cache.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Creating IVF Index:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="n"&gt;VECTOR&lt;/span&gt; &lt;span class="k"&gt;INDEX&lt;/span&gt; &lt;span class="n"&gt;products_ivf_idx&lt;/span&gt;
&lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;products&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;embedding&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;ORGANIZATION&lt;/span&gt; &lt;span class="n"&gt;NEIGHBOR&lt;/span&gt; &lt;span class="n"&gt;PARTITIONS&lt;/span&gt;
&lt;span class="n"&gt;DISTANCE&lt;/span&gt; &lt;span class="n"&gt;COSINE&lt;/span&gt;
&lt;span class="k"&gt;WITH&lt;/span&gt; &lt;span class="n"&gt;TARGET&lt;/span&gt; &lt;span class="n"&gt;ACCURACY&lt;/span&gt; &lt;span class="mi"&gt;95&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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

&lt;ul&gt;
&lt;li&gt;Disk-based, blocks cached in buffer cache&lt;/li&gt;
&lt;li&gt;More scalable for very large datasets&lt;/li&gt;
&lt;li&gt;Supports DML operations (may require periodic rebuild)&lt;/li&gt;
&lt;li&gt;Works in RAC environments&lt;/li&gt;
&lt;li&gt;Slightly slower than HNSW but still very fast&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;How IVF Works:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The index partitions vectors into clusters based on similarity. By default, the number of partitions equals the square root of the dataset size. During search, only relevant clusters are examined.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Multi-Vector Similarity Search
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Multi-vector similarity search is usually used for multi-document search&lt;/strong&gt; where documents are split into chunks, and chunks are embedded individually into vectors.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Use Cases:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Long documents split into paragraphs or sections&lt;/li&gt;
&lt;li&gt;Product catalogs with multiple descriptions&lt;/li&gt;
&lt;li&gt;Research papers chunked for semantic search&lt;/li&gt;
&lt;li&gt;Multi-modal data (text + images)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;How It Works:&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Documents are split into chunks&lt;/li&gt;
&lt;li&gt;Each chunk is embedded individually into a separate vector&lt;/li&gt;
&lt;li&gt;Chunks are stored with partition keys linking them to parent documents&lt;/li&gt;
&lt;li&gt;Search retrieves top chunks across all documents&lt;/li&gt;
&lt;li&gt;Results can be aggregated by document&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Uses partitions&lt;/strong&gt; to organize chunks belonging to the same document.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Table Structure Example:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;document_chunks&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="n"&gt;doc_id&lt;/span&gt; &lt;span class="n"&gt;NUMBER&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;chunk_id&lt;/span&gt; &lt;span class="n"&gt;NUMBER&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;chunk_text&lt;/span&gt; &lt;span class="k"&gt;CLOB&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;embedding&lt;/span&gt; &lt;span class="n"&gt;VECTOR&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;768&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;FLOAT32&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="k"&gt;PRIMARY&lt;/span&gt; &lt;span class="k"&gt;KEY&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;doc_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;chunk_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;PARTITION&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="k"&gt;RANGE&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;doc_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="k"&gt;PARTITION&lt;/span&gt; &lt;span class="n"&gt;p1&lt;/span&gt; &lt;span class="k"&gt;VALUES&lt;/span&gt; &lt;span class="k"&gt;LESS&lt;/span&gt; &lt;span class="k"&gt;THAN&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1000&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="k"&gt;PARTITION&lt;/span&gt; &lt;span class="n"&gt;p2&lt;/span&gt; &lt;span class="k"&gt;VALUES&lt;/span&gt; &lt;span class="k"&gt;LESS&lt;/span&gt; &lt;span class="k"&gt;THAN&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2000&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="k"&gt;PARTITION&lt;/span&gt; &lt;span class="n"&gt;p3&lt;/span&gt; &lt;span class="k"&gt;VALUES&lt;/span&gt; &lt;span class="k"&gt;LESS&lt;/span&gt; &lt;span class="k"&gt;THAN&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;MAXVALUE&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Multi-Vector Search Query:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- Find top chunks across all documents&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; 
  &lt;span class="n"&gt;doc_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;chunk_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;chunk_text&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;VECTOR_DISTANCE&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;embedding&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;query_vector&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;COSINE&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;score&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;document_chunks&lt;/span&gt;
&lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;score&lt;/span&gt;
&lt;span class="k"&gt;FETCH&lt;/span&gt; &lt;span class="n"&gt;APPROXIMATE&lt;/span&gt; &lt;span class="k"&gt;FIRST&lt;/span&gt; &lt;span class="mi"&gt;20&lt;/span&gt; &lt;span class="k"&gt;ROWS&lt;/span&gt; &lt;span class="k"&gt;ONLY&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Aggregating by Document:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- Get top documents based on best chunk match&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; 
  &lt;span class="n"&gt;doc_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="k"&gt;MIN&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;VECTOR_DISTANCE&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;embedding&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;query_vector&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;COSINE&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;best_score&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;document_chunks&lt;/span&gt;
&lt;span class="k"&gt;GROUP&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;doc_id&lt;/span&gt;
&lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;best_score&lt;/span&gt;
&lt;span class="k"&gt;FETCH&lt;/span&gt; &lt;span class="k"&gt;FIRST&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt; &lt;span class="k"&gt;ROWS&lt;/span&gt; &lt;span class="k"&gt;ONLY&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Narrowing Search Results
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Attribute Filtering with WHERE Clause
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Use the WHERE clause to filter results based on metadata or business attributes. You are not limited by the use of the ORDER BY clause.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;This powerful combination enables hybrid search: semantic similarity (vector search) plus exact filters (traditional SQL).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Example: Filter by Category and Date&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; 
  &lt;span class="n"&gt;product_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;product_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;category&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;COSINE_DISTANCE&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;embedding&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;query_vector&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;similarity&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;products&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;category&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'Electronics'&lt;/span&gt;
  &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;launch_date&lt;/span&gt; &lt;span class="n"&gt;greater&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="k"&gt;than&lt;/span&gt; &lt;span class="nb"&gt;DATE&lt;/span&gt; &lt;span class="s1"&gt;'2024-01-01'&lt;/span&gt;
&lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;embedding&lt;/span&gt; &lt;span class="k"&gt;less&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="k"&gt;than&lt;/span&gt; &lt;span class="k"&gt;equals&lt;/span&gt; &lt;span class="n"&gt;greater&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="k"&gt;than&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;query_vector&lt;/span&gt;
&lt;span class="k"&gt;FETCH&lt;/span&gt; &lt;span class="n"&gt;APPROXIMATE&lt;/span&gt; &lt;span class="k"&gt;FIRST&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt; &lt;span class="k"&gt;ROWS&lt;/span&gt; &lt;span class="k"&gt;ONLY&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Example: Price Range Filter&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; 
  &lt;span class="n"&gt;product_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;product_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;price&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;L2_DISTANCE&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;embedding&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;query_vector&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;distance&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;products&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;price&lt;/span&gt; &lt;span class="k"&gt;BETWEEN&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt; &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="mi"&gt;500&lt;/span&gt;
  &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;in_stock&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'Y'&lt;/span&gt;
&lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;embedding&lt;/span&gt; &lt;span class="k"&gt;less&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="k"&gt;than&lt;/span&gt; &lt;span class="n"&gt;hyphen&lt;/span&gt; &lt;span class="n"&gt;greater&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="k"&gt;than&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;query_vector&lt;/span&gt;
&lt;span class="k"&gt;FETCH&lt;/span&gt; &lt;span class="n"&gt;APPROXIMATE&lt;/span&gt; &lt;span class="k"&gt;FIRST&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt; &lt;span class="k"&gt;ROWS&lt;/span&gt; &lt;span class="k"&gt;ONLY&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Best Practices for Filtering:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Apply filters in WHERE clause before vector search&lt;/li&gt;
&lt;li&gt;Use indexed columns for better performance&lt;/li&gt;
&lt;li&gt;Combine multiple conditions as needed&lt;/li&gt;
&lt;li&gt;Leverage partition pruning for partitioned tables&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Testing Other Distance Functions
&lt;/h2&gt;

&lt;p&gt;Oracle provides shorthand distance functions that simplify syntax and improve code readability.&lt;/p&gt;

&lt;h3&gt;
  
  
  Distance Function Equivalents
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;L1_DISTANCE(v1, v2)&lt;/strong&gt; is similar to Manhattan distance&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;L1_DISTANCE&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="n"&gt;VECTOR&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'[1, 2, 3]'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="n"&gt;VECTOR&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'[4, 5, 6]'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;manhattan_dist&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;DUAL&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;L2_DISTANCE(v1, v2)&lt;/strong&gt; is similar to Euclidean distance&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;L2_DISTANCE&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="n"&gt;VECTOR&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'[3, 0]'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="n"&gt;VECTOR&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'[0, 4]'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;euclidean_dist&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;DUAL&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="c1"&gt;-- Result: 5.0&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;COSINE_DISTANCE&lt;/strong&gt; is the same as cosine&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;COSINE_DISTANCE&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="n"&gt;VECTOR&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'[1, 0]'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="n"&gt;VECTOR&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'[1, 1]'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;cosine_dist&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;DUAL&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;INNER_PRODUCT(v1, v2)&lt;/strong&gt; is the same as dot product&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;INNER_PRODUCT&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="n"&gt;VECTOR&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'[2, 3]'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="n"&gt;VECTOR&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'[4, 5]'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;dot_product&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;DUAL&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="c1"&gt;-- Result: 2*4 + 3*5 = 23&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Note on DOT vs INNER_PRODUCT:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;VECTOR_DISTANCE&lt;/code&gt; function with DOT metric returns the &lt;strong&gt;negated&lt;/strong&gt; inner product, while the &lt;code&gt;INNER_PRODUCT&lt;/code&gt; function returns the actual dot product.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- These are NOT equivalent:&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;VECTOR_DISTANCE&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;v1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;v2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;DOT&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="k"&gt;table&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;     &lt;span class="c1"&gt;-- Returns -1 * dot_product&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;INNER_PRODUCT&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;v1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;v2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="k"&gt;table&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;             &lt;span class="c1"&gt;-- Returns dot_product&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Other Essential Vector Functions
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. Vector Constructors
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;TO_VECTOR()&lt;/strong&gt; converts a string or a character large object (CLOB) to a vector&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- From string&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;TO_VECTOR&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'[1.5, 2.3, 4.1]'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;vec&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;DUAL&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;-- From CLOB&lt;/span&gt;
&lt;span class="k"&gt;DECLARE&lt;/span&gt;
  &lt;span class="n"&gt;my_clob&lt;/span&gt; &lt;span class="k"&gt;CLOB&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'[0.1, 0.2, 0.3, 0.4]'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="n"&gt;my_vector&lt;/span&gt; &lt;span class="n"&gt;VECTOR&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;BEGIN&lt;/span&gt;
  &lt;span class="n"&gt;my_vector&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;TO_VECTOR&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;my_clob&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;END&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="o"&gt;/&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;TO_VECTOR also takes another vector as input, adjusts its format, and returns the adjusted vector as output.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;VECTOR()&lt;/strong&gt; converts a string or CLOB into a vector&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;VECTOR&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'[1, 2, 3]'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;FLOAT32&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;vec&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;DUAL&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;TO_VECTOR and VECTOR are synonymous&lt;/strong&gt;—they perform the same function.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Vector Serializer
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;VECTOR_SERIALIZE()&lt;/strong&gt; converts a vector into a string or a CLOB&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;VECTOR_SERIALIZE&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;embedding&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;vec_string&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;products&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;product_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;-- Result: '[0.12, 0.45, 0.78, ...]'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Use Cases:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Exporting vectors for external processing&lt;/li&gt;
&lt;li&gt;Debugging and inspection&lt;/li&gt;
&lt;li&gt;Logging and auditing&lt;/li&gt;
&lt;li&gt;Integration with non-Oracle systems&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  3. Vector Norm
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;VECTOR_NORM()&lt;/strong&gt; returns the Euclidean norm of a vector—the distance from the origin to the vector.&lt;/p&gt;

&lt;p&gt;Also known as magnitude or length, it's calculated as the square root of the sum of squared components.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Formula:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;norm = square_root(x1-squared + x2-squared + ... + xn-squared)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;VECTOR_NORM&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;TO_VECTOR&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'[3, 4]'&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;magnitude&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;DUAL&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="c1"&gt;-- Result: 5.0 (because square_root(9+16) = 5)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Practical Use: Normalizing Vectors&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- Normalize vector to unit length&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; 
  &lt;span class="n"&gt;embedding&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="n"&gt;VECTOR_NORM&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;embedding&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;normalized_vector&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;products&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;product_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Normalized vectors have a magnitude of 1.0, which is required for meaningful dot product similarity comparisons.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Vector Dimension Count
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;VECTOR_DIMENSION_COUNT()&lt;/strong&gt; returns the number of dimensions of a vector&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;VECTOR_DIMENSION_COUNT&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;embedding&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;dimensions&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;products&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;product_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;-- Result: 768 (for BERT-style embeddings)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Use Cases:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Validating embedding dimensions&lt;/li&gt;
&lt;li&gt;Debugging dimension mismatches&lt;/li&gt;
&lt;li&gt;Dynamic schema inspection&lt;/li&gt;
&lt;li&gt;Migration validation&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  5. Vector Dimension Format
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;VECTOR_DIMENSION_FORMAT()&lt;/strong&gt; returns the storage format of the vector&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;VECTOR_DIMENSION_FORMAT&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;embedding&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;format&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;products&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;product_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;-- Possible results: 'INT8', 'FLOAT32', 'FLOAT64', 'BINARY'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Use Cases:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Schema documentation&lt;/li&gt;
&lt;li&gt;Storage optimization analysis&lt;/li&gt;
&lt;li&gt;Migration planning&lt;/li&gt;
&lt;li&gt;Model compatibility verification&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Complete Working Example
&lt;/h2&gt;

&lt;p&gt;Here's a comprehensive example demonstrating all three search types:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- Create table&lt;/span&gt;
&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;research_papers&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="n"&gt;paper_id&lt;/span&gt; &lt;span class="n"&gt;NUMBER&lt;/span&gt; &lt;span class="k"&gt;PRIMARY&lt;/span&gt; &lt;span class="k"&gt;KEY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;title&lt;/span&gt; &lt;span class="n"&gt;VARCHAR2&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;500&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="n"&gt;abstract&lt;/span&gt; &lt;span class="k"&gt;CLOB&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;category&lt;/span&gt; &lt;span class="n"&gt;VARCHAR2&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="n"&gt;publish_date&lt;/span&gt; &lt;span class="nb"&gt;DATE&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;embedding&lt;/span&gt; &lt;span class="n"&gt;VECTOR&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;768&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;FLOAT32&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;-- Insert sample data&lt;/span&gt;
&lt;span class="k"&gt;INSERT&lt;/span&gt; &lt;span class="k"&gt;INTO&lt;/span&gt; &lt;span class="n"&gt;research_papers&lt;/span&gt; &lt;span class="k"&gt;VALUES&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="s1"&gt;'Advances in Vector Search'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="s1"&gt;'This paper explores efficient algorithms...'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="s1"&gt;'Computer Science'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nb"&gt;DATE&lt;/span&gt; &lt;span class="s1"&gt;'2024-06-15'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;VECTOR&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'[0.1, 0.2, ...]'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;768&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;FLOAT32&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;COMMIT&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;-- 1. EXACT SIMILARITY SEARCH&lt;/span&gt;
&lt;span class="c1"&gt;-- Most accurate, slower for large datasets&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; 
  &lt;span class="n"&gt;paper_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;title&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;COSINE_DISTANCE&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;embedding&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;query_vector&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;similarity&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;research_papers&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;category&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'Computer Science'&lt;/span&gt;
&lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;similarity&lt;/span&gt;
&lt;span class="k"&gt;FETCH&lt;/span&gt; &lt;span class="k"&gt;FIRST&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt; &lt;span class="k"&gt;ROWS&lt;/span&gt; &lt;span class="k"&gt;ONLY&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;-- 2. CREATE VECTOR INDEX for approximate search&lt;/span&gt;
&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="n"&gt;VECTOR&lt;/span&gt; &lt;span class="k"&gt;INDEX&lt;/span&gt; &lt;span class="n"&gt;papers_hnsw_idx&lt;/span&gt;
&lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;research_papers&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;embedding&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;ORGANIZATION&lt;/span&gt; &lt;span class="n"&gt;NEIGHBOR&lt;/span&gt; &lt;span class="n"&gt;GRAPH&lt;/span&gt;
&lt;span class="n"&gt;DISTANCE&lt;/span&gt; &lt;span class="n"&gt;COSINE&lt;/span&gt;
&lt;span class="k"&gt;WITH&lt;/span&gt; &lt;span class="n"&gt;TARGET&lt;/span&gt; &lt;span class="n"&gt;ACCURACY&lt;/span&gt; &lt;span class="mi"&gt;95&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;-- 3. APPROXIMATE SIMILARITY SEARCH&lt;/span&gt;
&lt;span class="c1"&gt;-- Faster, 95%+ accuracy&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; 
  &lt;span class="n"&gt;paper_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;title&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;publish_date&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;research_papers&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;category&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'Computer Science'&lt;/span&gt;
  &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;publish_date&lt;/span&gt; &lt;span class="n"&gt;greater&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="k"&gt;than&lt;/span&gt; &lt;span class="nb"&gt;DATE&lt;/span&gt; &lt;span class="s1"&gt;'2024-01-01'&lt;/span&gt;
&lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;embedding&lt;/span&gt; &lt;span class="k"&gt;less&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="k"&gt;than&lt;/span&gt; &lt;span class="k"&gt;equals&lt;/span&gt; &lt;span class="n"&gt;greater&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="k"&gt;than&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;query_vector&lt;/span&gt;
&lt;span class="k"&gt;FETCH&lt;/span&gt; &lt;span class="n"&gt;APPROXIMATE&lt;/span&gt; &lt;span class="k"&gt;FIRST&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt; &lt;span class="k"&gt;ROWS&lt;/span&gt; &lt;span class="k"&gt;ONLY&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;-- 4. MULTI-VECTOR SEARCH (for chunked documents)&lt;/span&gt;
&lt;span class="c1"&gt;-- Create chunked version&lt;/span&gt;
&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;paper_chunks&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="n"&gt;paper_id&lt;/span&gt; &lt;span class="n"&gt;NUMBER&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;chunk_id&lt;/span&gt; &lt;span class="n"&gt;NUMBER&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;chunk_text&lt;/span&gt; &lt;span class="k"&gt;CLOB&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;embedding&lt;/span&gt; &lt;span class="n"&gt;VECTOR&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;768&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;FLOAT32&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="k"&gt;PRIMARY&lt;/span&gt; &lt;span class="k"&gt;KEY&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;paper_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;chunk_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;-- Find best matching chunks&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; 
  &lt;span class="n"&gt;paper_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;chunk_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;L2_DISTANCE&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;embedding&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;query_vector&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;distance&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;paper_chunks&lt;/span&gt;
&lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;distance&lt;/span&gt;
&lt;span class="k"&gt;FETCH&lt;/span&gt; &lt;span class="n"&gt;APPROXIMATE&lt;/span&gt; &lt;span class="k"&gt;FIRST&lt;/span&gt; &lt;span class="mi"&gt;20&lt;/span&gt; &lt;span class="k"&gt;ROWS&lt;/span&gt; &lt;span class="k"&gt;ONLY&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;-- Aggregate to document level&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; 
  &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;paper_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;title&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="k"&gt;MIN&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;COSINE_DISTANCE&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;embedding&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;query_vector&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;best_match_score&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;research_papers&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;
&lt;span class="k"&gt;JOIN&lt;/span&gt; &lt;span class="n"&gt;paper_chunks&lt;/span&gt; &lt;span class="k"&gt;c&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;paper_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;paper_id&lt;/span&gt;
&lt;span class="k"&gt;GROUP&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;paper_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;title&lt;/span&gt;
&lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;best_match_score&lt;/span&gt;
&lt;span class="k"&gt;FETCH&lt;/span&gt; &lt;span class="k"&gt;FIRST&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt; &lt;span class="k"&gt;ROWS&lt;/span&gt; &lt;span class="k"&gt;ONLY&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Best Practices
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. Choose the Right Search Strategy
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Exact search&lt;/strong&gt;: Less than 100K vectors, need perfect accuracy&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Approximate with HNSW&lt;/strong&gt;: 100K to 10M vectors, need sub-100ms latency, have memory available&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Approximate with IVF&lt;/strong&gt;: 10M+ vectors, limited memory, can tolerate slightly higher latency&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Multi-vector&lt;/strong&gt;: Document chunks, multi-modal data, detailed granularity needed&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  2. Optimize Index Configuration
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- For high accuracy (slower but better results)&lt;/span&gt;
&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="n"&gt;VECTOR&lt;/span&gt; &lt;span class="k"&gt;INDEX&lt;/span&gt; &lt;span class="n"&gt;idx1&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="k"&gt;table&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;embedding&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;ORGANIZATION&lt;/span&gt; &lt;span class="n"&gt;NEIGHBOR&lt;/span&gt; &lt;span class="n"&gt;GRAPH&lt;/span&gt;
&lt;span class="n"&gt;DISTANCE&lt;/span&gt; &lt;span class="n"&gt;COSINE&lt;/span&gt;
&lt;span class="k"&gt;WITH&lt;/span&gt; &lt;span class="n"&gt;TARGET&lt;/span&gt; &lt;span class="n"&gt;ACCURACY&lt;/span&gt; &lt;span class="mi"&gt;99&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;-- For speed (faster but slightly lower accuracy)&lt;/span&gt;
&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="n"&gt;VECTOR&lt;/span&gt; &lt;span class="k"&gt;INDEX&lt;/span&gt; &lt;span class="n"&gt;idx2&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="k"&gt;table&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;embedding&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;ORGANIZATION&lt;/span&gt; &lt;span class="n"&gt;NEIGHBOR&lt;/span&gt; &lt;span class="n"&gt;GRAPH&lt;/span&gt;
&lt;span class="n"&gt;DISTANCE&lt;/span&gt; &lt;span class="n"&gt;COSINE&lt;/span&gt;
&lt;span class="k"&gt;WITH&lt;/span&gt; &lt;span class="n"&gt;TARGET&lt;/span&gt; &lt;span class="n"&gt;ACCURACY&lt;/span&gt; &lt;span class="mi"&gt;90&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  3. Use Attribute Filtering Effectively
&lt;/h3&gt;

&lt;p&gt;Combine vector similarity with traditional filters to narrow results:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- Efficient: Filter first, then search&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;products&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;price&lt;/span&gt; &lt;span class="k"&gt;less&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="k"&gt;than&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;              &lt;span class="c1"&gt;-- Traditional filter&lt;/span&gt;
  &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;category&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'Electronics'&lt;/span&gt;         &lt;span class="c1"&gt;-- Traditional filter&lt;/span&gt;
&lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;embedding&lt;/span&gt; &lt;span class="k"&gt;less&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="k"&gt;than&lt;/span&gt; &lt;span class="k"&gt;equals&lt;/span&gt; &lt;span class="n"&gt;greater&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="k"&gt;than&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;query_vector&lt;/span&gt;  &lt;span class="c1"&gt;-- Vector search&lt;/span&gt;
&lt;span class="k"&gt;FETCH&lt;/span&gt; &lt;span class="n"&gt;APPROXIMATE&lt;/span&gt; &lt;span class="k"&gt;FIRST&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt; &lt;span class="k"&gt;ROWS&lt;/span&gt; &lt;span class="k"&gt;ONLY&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  4. Monitor and Tune Performance
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- Check if index is being used&lt;/span&gt;
&lt;span class="k"&gt;EXPLAIN&lt;/span&gt; &lt;span class="n"&gt;PLAN&lt;/span&gt; &lt;span class="k"&gt;FOR&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;products&lt;/span&gt;
&lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;embedding&lt;/span&gt; &lt;span class="k"&gt;less&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="k"&gt;than&lt;/span&gt; &lt;span class="k"&gt;equals&lt;/span&gt; &lt;span class="n"&gt;greater&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="k"&gt;than&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;query_vector&lt;/span&gt;
&lt;span class="k"&gt;FETCH&lt;/span&gt; &lt;span class="n"&gt;APPROXIMATE&lt;/span&gt; &lt;span class="k"&gt;FIRST&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt; &lt;span class="k"&gt;ROWS&lt;/span&gt; &lt;span class="k"&gt;ONLY&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;DBMS_XPLAN&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;DISPLAY&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  5. Handle Dimension Mismatches
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- Validate dimensions before comparison&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; 
  &lt;span class="n"&gt;VECTOR_DIMENSION_COUNT&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;embedding&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;dims&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;VECTOR_DIMENSION_FORMAT&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;embedding&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;format&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;products&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;VECTOR_DIMENSION_COUNT&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;embedding&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="mi"&gt;768&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Common Pitfalls and Solutions
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Pitfall 1: Forgetting to Enable Vector Pool
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Problem:&lt;/strong&gt; HNSW index creation fails with memory error&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Solution:&lt;/strong&gt; Configure vector pool before creating HNSW indexes&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;ALTER&lt;/span&gt; &lt;span class="k"&gt;SYSTEM&lt;/span&gt; &lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="n"&gt;vector_memory_size&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="k"&gt;G&lt;/span&gt; &lt;span class="k"&gt;SCOPE&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;SPFILE&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="c1"&gt;-- Restart database&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Pitfall 2: Using Wrong Index Type
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Problem:&lt;/strong&gt; Slow performance despite having an index&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Solution:&lt;/strong&gt; Match your workload to index type:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;HNSW for read-heavy, memory-available scenarios&lt;/li&gt;
&lt;li&gt;IVF for write-heavy or memory-constrained scenarios&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Pitfall 3: Metric Mismatch
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Problem:&lt;/strong&gt; Index not being used despite proper syntax&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Solution:&lt;/strong&gt; Ensure query metric matches index metric&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- Index uses COSINE&lt;/span&gt;
&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="n"&gt;VECTOR&lt;/span&gt; &lt;span class="k"&gt;INDEX&lt;/span&gt; &lt;span class="n"&gt;idx&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="k"&gt;table&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;embedding&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;DISTANCE&lt;/span&gt; &lt;span class="n"&gt;COSINE&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;-- Query must also use COSINE&lt;/span&gt;
&lt;span class="c1"&gt;-- Correct: Uses less-than equals greater-than (COSINE operator)&lt;/span&gt;
&lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;embedding&lt;/span&gt; &lt;span class="k"&gt;less&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="k"&gt;than&lt;/span&gt; &lt;span class="k"&gt;equals&lt;/span&gt; &lt;span class="n"&gt;greater&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="k"&gt;than&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;query_vector&lt;/span&gt;

&lt;span class="c1"&gt;-- Wrong: Uses less-than hyphen greater-than (EUCLIDEAN operator)&lt;/span&gt;
&lt;span class="c1"&gt;-- This will NOT use the index!&lt;/span&gt;
&lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;embedding&lt;/span&gt; &lt;span class="k"&gt;less&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="k"&gt;than&lt;/span&gt; &lt;span class="n"&gt;hyphen&lt;/span&gt; &lt;span class="n"&gt;greater&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="k"&gt;than&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;query_vector&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Pitfall 4: Not Using FETCH APPROXIMATE
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Problem:&lt;/strong&gt; Query is slow despite having vector index&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Solution:&lt;/strong&gt; Use &lt;code&gt;FETCH APPROXIMATE&lt;/code&gt; to enable index usage&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- Without APPROXIMATE - performs exact search&lt;/span&gt;
&lt;span class="k"&gt;FETCH&lt;/span&gt; &lt;span class="k"&gt;FIRST&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt; &lt;span class="k"&gt;ROWS&lt;/span&gt; &lt;span class="k"&gt;ONLY&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;-- With APPROXIMATE - uses index&lt;/span&gt;
&lt;span class="k"&gt;FETCH&lt;/span&gt; &lt;span class="n"&gt;APPROXIMATE&lt;/span&gt; &lt;span class="k"&gt;FIRST&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt; &lt;span class="k"&gt;ROWS&lt;/span&gt; &lt;span class="k"&gt;ONLY&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Oracle Database 23ai provides a comprehensive vector search framework with multiple strategies to fit different use cases:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Search Strategies:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Exact&lt;/strong&gt;: Perfect accuracy, suitable for smaller datasets&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Approximate&lt;/strong&gt;: 95%+ accuracy with dramatic speed improvements using HNSW or IVF indexes&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Multi-vector&lt;/strong&gt;: Chunk-level granularity for document search&lt;/li&gt;
&lt;/ul&gt;

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

&lt;ul&gt;
&lt;li&gt;Enable vector pool for HNSW indexes: &lt;code&gt;vector_memory_size&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Use FETCH APPROXIMATE for index-accelerated queries&lt;/li&gt;
&lt;li&gt;Match distance metrics between index and query&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Essential Functions:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Constructors&lt;/strong&gt;: TO_VECTOR(), VECTOR()&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Serializer&lt;/strong&gt;: VECTOR_SERIALIZE()&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Utilities&lt;/strong&gt;: VECTOR_NORM(), VECTOR_DIMENSION_COUNT(), VECTOR_DIMENSION_FORMAT()&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Distance Functions&lt;/strong&gt;: L1_DISTANCE(), L2_DISTANCE(), COSINE_DISTANCE(), INNER_PRODUCT()&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Best Practices:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Choose search strategy based on dataset size and accuracy requirements&lt;/li&gt;
&lt;li&gt;Combine vector search with WHERE clause filters for powerful hybrid queries&lt;/li&gt;
&lt;li&gt;Monitor index usage with EXPLAIN PLAN&lt;/li&gt;
&lt;li&gt;Normalize vectors when using dot product similarity&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;By understanding these search strategies and vector functions, you can build high-performance semantic search applications that scale from thousands to billions of vectors while maintaining excellent accuracy and query speed.&lt;/p&gt;

</description>
      <category>oracleaidatabase</category>
      <category>vectorsearch</category>
      <category>approximatenearestneighbor</category>
    </item>
  </channel>
</rss>
