<?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: Yugandhar Surya</title>
    <description>The latest articles on DEV Community by Yugandhar Surya (@yugandharsurya).</description>
    <link>https://dev.to/yugandharsurya</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%2F493038%2F2643b1f9-013c-4266-87f2-65760f8eea39.png</url>
      <title>DEV Community: Yugandhar Surya</title>
      <link>https://dev.to/yugandharsurya</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/yugandharsurya"/>
    <language>en</language>
    <item>
      <title>Building an Enterprise GenAI Platform on OCI — Part 2: The Data Pipeline Nobody Talks About</title>
      <dc:creator>Yugandhar Surya</dc:creator>
      <pubDate>Thu, 10 Sep 2026 19:54:00 +0000</pubDate>
      <link>https://dev.to/yugandharsurya/building-an-enterprise-genai-platform-on-oci-part-2-the-data-pipeline-nobody-talks-about-a22</link>
      <guid>https://dev.to/yugandharsurya/building-an-enterprise-genai-platform-on-oci-part-2-the-data-pipeline-nobody-talks-about-a22</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;The quality of a RAG system is decided long before the query reaches the LLM.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;I learned that the hard way.&lt;/p&gt;

&lt;p&gt;After designing the architecture in Part 1, my first instinct was to move straight to embeddings.&lt;/p&gt;

&lt;p&gt;After all, embeddings are where RAG starts getting interesting, right?&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Not quite.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Before generating a single vector, I had a much more basic problem to solve.&lt;/p&gt;

&lt;p&gt;Before generating a single vector, I had a much more basic problem:&lt;/p&gt;

&lt;p&gt;*&lt;em&gt;How do you reliably move 70,000+ documents through a pipeline running on constrained infrastructure?&lt;br&gt;
*&lt;/em&gt;&lt;br&gt;
They needed to be collected, stored, cleaned, chunked, processed, and eventually transformed into something a retrieval system could understand.&lt;/p&gt;

&lt;p&gt;My OCI Compute instance wasn't exactly a powerhouse either.&lt;/p&gt;

&lt;p&gt;What initially looked like a scraping problem quickly became a &lt;strong&gt;data-engineering problem.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;And that's where Part 2 begins.&lt;/p&gt;
&lt;h2&gt;
  
  
  The Pipeline Before the AI
&lt;/h2&gt;

&lt;p&gt;There is a tendency when building GenAI applications to start here:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Document → Embedding → Vector DB → LLM

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

&lt;/div&gt;



&lt;p&gt;But... &lt;strong&gt;where did that document come from?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;What happens when there are 70,000 of them?&lt;/p&gt;

&lt;p&gt;What if the process crashes halfway?&lt;/p&gt;

&lt;p&gt;What if you need to reprocess the data without scraping everything again?&lt;/p&gt;

&lt;p&gt;What if your embedding strategy changes tomorrow?&lt;/p&gt;

&lt;p&gt;Suddenly, this isn't an LLM problem.&lt;/p&gt;

&lt;p&gt;It's a &lt;strong&gt;data lifecycle problem.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;So I deliberately separated the pipeline into stages:&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fomr3ist4kzh8mr2wfau9.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fomr3ist4kzh8mr2wfau9.png" alt="RAG Pipeline" width="799" height="258"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Each stage produces an artifact that the next stage can consume.&lt;/p&gt;

&lt;p&gt;That decision became much more important later.&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ftq8xa82g6b6xgg5ufidl.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ftq8xa82g6b6xgg5ufidl.png" alt="Pipeline RAG" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;Stage 1: Getting the Data In&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;The first component was straightforward.&lt;/p&gt;

&lt;p&gt;I needed technical knowledge for the DevOps assistant.&lt;/p&gt;

&lt;p&gt;So I used an OCI Compute instance for scraping and data collection.&lt;/p&gt;

&lt;p&gt;But I made an important decision early:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;The compute instance should process data. It shouldn't become the permanent home of the data.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Why?&lt;br&gt;
Because:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Compute is ephemeral.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Instances can be stopped.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Disks have limits.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Applications change.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Pipelines fail.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The knowledge base needed to survive independently of the machine that created it.&lt;/p&gt;

&lt;p&gt;That made &lt;strong&gt;OCI Object Storage&lt;/strong&gt; the natural persistence layer.&lt;/p&gt;
&lt;h2&gt;
  
  
  &lt;strong&gt;Why Not Just Save Everything Locally?&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;For a prototype, I could have done this:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;scraper/&lt;br&gt;
├── data/&lt;br&gt;
│   ├── file1.json&lt;br&gt;
│   ├── file2.json&lt;br&gt;
│   ├── file3.json&lt;br&gt;
│   └── ...&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;And initially, that feels simpler.&lt;/p&gt;

&lt;p&gt;But now the data is tied to the Compute instance.&lt;/p&gt;

&lt;p&gt;What happens when preprocessing moves to OCI Data Science?&lt;/p&gt;

&lt;p&gt;I would need to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;transfer files&lt;/li&gt;
&lt;li&gt;mount storage&lt;/li&gt;
&lt;li&gt;expose an endpoint&lt;/li&gt;
&lt;li&gt;synchronise directories&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Or...&lt;/p&gt;

&lt;p&gt;I could make both services communicate through durable object storage.&lt;/p&gt;

&lt;p&gt;That is exactly what I did.&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fe3gc0xreospll3vpgcq1.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fe3gc0xreospll3vpgcq1.png" alt="Communication between OCI services" width="800" height="433"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Now Compute and Data Science don't need to know anything about each other.&lt;/p&gt;

&lt;p&gt;They only need to understand the &lt;strong&gt;storage contract&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;That's &lt;strong&gt;decoupling&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;And this was one of the first architectural decisions that made the platform considerably easier to evolve.&lt;/p&gt;
&lt;h2&gt;
  
  
  &lt;strong&gt;Streaming Data Instead of Hoarding It&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;There was another constraint.&lt;/p&gt;

&lt;p&gt;The &lt;strong&gt;Compute instance&lt;/strong&gt;, I was using had limited resources.&lt;/p&gt;

&lt;p&gt;Loading thousands of documents into memory before uploading them would have been unnecessary and risky.&lt;/p&gt;

&lt;p&gt;So instead of thinking:&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fek9vqs3dqbkaakvxrsyg.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fek9vqs3dqbkaakvxrsyg.png" alt="Naive way for processing data" width="800" height="105"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I moved toward:&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Flj2qfbk0mtytr5jn89js.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Flj2qfbk0mtytr5jn89js.png" alt="Optimized way to process data" width="799" height="250"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;One object at a time.&lt;/p&gt;

&lt;p&gt;This sounds like a small implementation detail.&lt;/p&gt;

&lt;p&gt;It isn't.&lt;/p&gt;

&lt;p&gt;It changes the memory profile of the ingestion pipeline.&lt;/p&gt;

&lt;p&gt;Instead of memory consumption increasing with dataset size, the worker only needs enough memory for the data currently being processed.&lt;/p&gt;

&lt;p&gt;Conceptually:&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;for&lt;/span&gt; &lt;span class="n"&gt;document&lt;/span&gt; &lt;span class="ow"&gt;in&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;content&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;scrape&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;document&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;cleaned&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;basic_clean&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="nf"&gt;upload_to_object_storage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cleaned&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;del&lt;/span&gt; &lt;span class="n"&gt;content&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The actual implementation evolved, but the principle stayed the same:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Move data through the pipeline instead of accumulating it inside the worker.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;Object Storage Became the Data Backbone&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;Initially, I thought of Object Storage as:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"The place where I'll put my files."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That definition quickly became too simplistic.&lt;/p&gt;

&lt;p&gt;It became the data backbone connecting the different stages of the platform.&lt;/p&gt;

&lt;p&gt;I organised the bucket roughly like this:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;mlops-llm-data/&lt;br&gt;
├── datasets/&lt;br&gt;
├── processed/&lt;br&gt;
├── features/&lt;br&gt;
├── models/&lt;br&gt;
└── logs/&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;Each represented a different stage of the data lifecycle.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Prefix&lt;/th&gt;
&lt;th&gt;Responsibility&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;datasets/&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Raw ingested data&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;processed/&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Cleaned and chunked documents&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;features/&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Generated embedding artifacts&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;models/&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Model and retrieval artifacts&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;logs/&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Pipeline and operational logs&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;docker-build/&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Docker build-related artifacts&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;conda/&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Conda/environment-related artifacts&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;These aren't traditional filesystem directories. But architecturally, they create clear boundaries between stages.&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fzzkuew4nwle7cajth7ut.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fzzkuew4nwle7cajth7ut.png" alt="Object Storage as Data Backbone" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  &lt;strong&gt;Raw Data Should Stay Raw&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;This became one of the most important rules in the pipeline:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Never destroy the original dataset just because your processing logic changed.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Suppose I scraped 70,000 documents.&lt;/p&gt;

&lt;p&gt;Then I cleaned them.&lt;/p&gt;

&lt;p&gt;A week later, I change my cleaning logic.&lt;/p&gt;

&lt;p&gt;If I overwrote the originals, I'd have to scrape everything again.&lt;/p&gt;

&lt;p&gt;Instead: &lt;code&gt;datasets/&lt;/code&gt; -&amp;gt; remained the source of truth.&lt;/p&gt;

&lt;p&gt;And: &lt;code&gt;processed/&lt;/code&gt; -&amp;gt; contained derived data.&lt;/p&gt;

&lt;p&gt;That meant I could rebuild the downstream pipeline without repeating ingestion.&lt;/p&gt;

&lt;p&gt;This is essentially an &lt;strong&gt;immutable raw-data pattern.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;And it gave me something extremely valuable: &lt;strong&gt;reproducibility.&lt;/strong&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  &lt;strong&gt;Then Came Chunking&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;Now the data existed. But an LLM retrieval pipeline doesn't necessarily want entire documents.&lt;br&gt;
Imagine retrieving a 5,000-word article because the answer exists in three sentences somewhere in the middle.&lt;/p&gt;

&lt;p&gt;That creates several problems:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;More irrelevant context&lt;/li&gt;
&lt;li&gt;Higher token consumption&lt;/li&gt;
&lt;li&gt;Larger prompts&lt;/li&gt;
&lt;li&gt;Increased inference latency&lt;/li&gt;
&lt;li&gt;Greater chance of distracting the model&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So documents needed to be divided into smaller semantic units.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Chunks.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Conceptually:&lt;br&gt;
&lt;code&gt;Document&lt;br&gt;
│&lt;br&gt;
├── Chunk 1&lt;br&gt;
├── Chunk 2&lt;br&gt;
├── Chunk 3&lt;br&gt;
├── Chunk 4&lt;br&gt;
└── Chunk 5&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;Those chunks would later become the units used for embedding and retrieval.&lt;/p&gt;

&lt;p&gt;But chunking introduces its own engineering question.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;How big should a chunk be?&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;
  
  
  &lt;strong&gt;Chunk Size Is Not Just a Number&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;Make chunks too large and retrieval becomes noisy.&lt;br&gt;
Make them too small and you destroy context.&lt;/p&gt;

&lt;p&gt;Consider:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Docker containers package applications together with their dependencies, allowing them to run consistently across environments.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;A sensible chunk preserves that idea.&lt;/p&gt;

&lt;p&gt;But an aggressive split could produce:&lt;/p&gt;

&lt;p&gt;`Chunk 1:&lt;br&gt;
Docker containers package applications together&lt;/p&gt;

&lt;p&gt;Chunk 2:&lt;br&gt;
with their dependencies, allowing them&lt;/p&gt;

&lt;p&gt;Chunk 3:&lt;br&gt;
to run consistently across environments.`&lt;/p&gt;

&lt;p&gt;Each chunk now carries less meaning on its own. That's where &lt;strong&gt;chunk overlap&lt;/strong&gt; helps.&lt;/p&gt;

&lt;p&gt;Instead of:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;AAAA | BBBB | CCCC&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;we can create:&lt;br&gt;
&lt;code&gt;&lt;br&gt;
AAAA&lt;br&gt;
   AABBBB&lt;br&gt;
        BBBCCCC&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;Some information is intentionally repeated across neighbouring chunks.&lt;/p&gt;

&lt;p&gt;That gives the retriever a better chance of preserving concepts that happen to cross chunk boundaries.&lt;/p&gt;

&lt;p&gt;But overlap isn't free.&lt;/p&gt;

&lt;p&gt;More overlap means:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;more chunks -&amp;gt; more embeddings -&amp;gt; larger index -&amp;gt; more storage -&amp;gt; more processing&lt;br&gt;
&lt;/code&gt;&lt;br&gt;
There is no universally perfect chunk size.&lt;/p&gt;

&lt;p&gt;It depends on the documents, embedding model, retrieval strategy, and downstream context window.&lt;/p&gt;

&lt;p&gt;This is a recurring theme in RAG:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Every retrieval optimisation has a cost somewhere else.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;
  
  
  &lt;strong&gt;70,000 Chunks Changed the Problem&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;Eventually, preprocessing was producing roughly 70,000 chunks.&lt;/p&gt;

&lt;p&gt;And that's when another lesson became obvious:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Code that works beautifully for: &lt;code&gt;100 documents&lt;/code&gt; doesn't necessarily behave beautifully for: &lt;code&gt;70,000 documents&lt;/code&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The naive approach would be:&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;chunks&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="nb"&gt;file&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;all_files&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="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;load&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;file&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;

&lt;span class="nf"&gt;process&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;basically says:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"Load everything first. Worry about memory later."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Not ideal.&lt;/p&gt;

&lt;p&gt;Especially under constrained infrastructure. So the next architectural decision was obvious. &lt;strong&gt;Batch processing.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;Batch Processing Instead of "Load Everything"&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;Rather than loading the entire dataset at once, I processed smaller groups:&lt;/p&gt;

&lt;p&gt;70,000 objects&lt;br&gt;
      ↓&lt;br&gt;
┌───────────────┐&lt;br&gt;
│ Batch 1       │&lt;br&gt;
│ 200 objects   │&lt;br&gt;
└───────────────┘&lt;br&gt;
      ↓&lt;br&gt;
   Process&lt;br&gt;
      ↓&lt;br&gt;
   Release&lt;br&gt;
      ↓&lt;br&gt;
┌───────────────┐&lt;br&gt;
│ Batch 2       │&lt;br&gt;
│ 200 objects   │&lt;br&gt;
└───────────────┘&lt;br&gt;
      ↓&lt;br&gt;
   Process&lt;br&gt;
      ↓&lt;br&gt;
   Release&lt;br&gt;
      ↓&lt;br&gt;
     ...&lt;/p&gt;

&lt;p&gt;The exact batch size is tunable.&lt;/p&gt;

&lt;p&gt;The principle is what matters:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Bound the amount of data being processed at any given moment.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;This gives you predictable memory usage and makes larger datasets manageable on relatively modest infrastructure.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;Then a Small Bug Exposed a Bigger Problem&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;This was one of my favourite lessons from the entire pipeline.&lt;/p&gt;

&lt;p&gt;The processed dataset contained roughly:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;70,000 chunks&lt;/strong&gt;&lt;br&gt;
But the next stage reported:&lt;/p&gt;

&lt;p&gt;Total feature files: 1000&lt;br&gt;
Total vectors in index: 999&lt;br&gt;
Training vectors shape: (999, 384)&lt;/p&gt;

&lt;p&gt;Wait.&lt;br&gt;
**&lt;br&gt;
70,000 chunks in.&lt;/p&gt;

&lt;p&gt;1,000 feature files out?**&lt;/p&gt;

&lt;p&gt;Something was wrong.&lt;/p&gt;

&lt;p&gt;And here's the interesting part:&lt;/p&gt;

&lt;p&gt;It wasn't FAISS.&lt;/p&gt;

&lt;p&gt;It wasn't the embedding model.&lt;/p&gt;

&lt;p&gt;It wasn't the AI.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The data pipeline was incomplete.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The pipeline was unintentionally limiting the number of objects being listed from OCI Object Storage.&lt;/p&gt;

&lt;p&gt;A listing operation wasn't traversing the complete collection.&lt;/p&gt;

&lt;p&gt;The result?&lt;/p&gt;

&lt;p&gt;No crash.&lt;/p&gt;

&lt;p&gt;No exception.&lt;/p&gt;

&lt;p&gt;Just incomplete data.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;A pipeline can be technically successful and logically wrong.&lt;/strong&gt;&lt;br&gt;
That's a much scarier failure mode than a simple application crash.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;Pagination: The Bug That Looked Like an AI Problem&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;Cloud APIs commonly paginate large responses.&lt;/p&gt;

&lt;p&gt;Conceptually:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;Request&lt;br&gt;
   ↓&lt;br&gt;
Objects 1–1000&lt;br&gt;
   ↓&lt;br&gt;
Next Page Token&lt;br&gt;
   ↓&lt;br&gt;
Objects 1001–2000&lt;br&gt;
   ↓&lt;br&gt;
Next Page Token&lt;br&gt;
   ↓&lt;br&gt;
...&lt;br&gt;
   ↓&lt;br&gt;
All Objects&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;If you forget pagination, your code can still run perfectly.&lt;/p&gt;

&lt;p&gt;No exception.&lt;/p&gt;

&lt;p&gt;No crash.&lt;/p&gt;

&lt;p&gt;No obvious error.&lt;/p&gt;

&lt;p&gt;It simply processes an incomplete dataset.&lt;/p&gt;

&lt;p&gt;After fixing pagination, the downstream stages could finally see the complete collection.&lt;/p&gt;

&lt;p&gt;That changed how I thought about validation.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Counts Became a Data Quality Check&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;From that point onward, counts became a basic sanity check.&lt;/p&gt;

&lt;p&gt;At every stage:&lt;/p&gt;

&lt;p&gt;How many records entered?&lt;br&gt;
How many succeeded?&lt;br&gt;
How many failed?&lt;br&gt;
How many were skipped?&lt;br&gt;
How many artifacts were produced?&lt;/p&gt;

&lt;p&gt;If ingestion produces:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;70,000 documents ** but embedding generation sees: 1,000 chunks, something is clearly **wrong&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;This is a simple form of &lt;strong&gt;data observability.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;You don't need a huge monitoring platform to start.&lt;/p&gt;

&lt;p&gt;Sometimes a few carefully placed counters can save hours of debugging.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;Why OCI Data Science Entered the Architecture&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;As processing became heavier, running everything on Compute became increasingly uncomfortable.&lt;/p&gt;

&lt;p&gt;I could have simply increased the Compute shape.&lt;/p&gt;

&lt;p&gt;Instead, I asked:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Does this workload actually belong on the same machine?&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The answer was no.&lt;/p&gt;

&lt;p&gt;Scraping and preprocessing have different resource characteristics.&lt;/p&gt;

&lt;p&gt;So heavier processing moved toward OCI Data Science notebook sessions.&lt;/p&gt;

&lt;p&gt;The responsibilities became:&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F2zww8v0wiihvoqchsx9k.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F2zww8v0wiihvoqchsx9k.png" alt="ingestion_processing_object_storage" width="800" height="424"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Compute collected the data.&lt;/p&gt;

&lt;p&gt;Object Storage persisted it.&lt;/p&gt;

&lt;p&gt;Data Science transformed it.&lt;/p&gt;

&lt;p&gt;Again:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;separation of concerns.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;IAM Became Part of the AI Architecture&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;Once multiple OCI services started communicating, authentication became part of the design.&lt;/p&gt;

&lt;p&gt;Instead of putting credentials inside configuration files, the notebook used &lt;strong&gt;OCI Resource Principals.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Conceptually:&lt;/p&gt;

&lt;p&gt;OCI resource has an identity&lt;br&gt;
            +&lt;br&gt;
IAM defines permissions&lt;/p&gt;

&lt;p&gt;This changes the model from:&lt;/p&gt;

&lt;p&gt;Application possesses credentials&lt;/p&gt;

&lt;p&gt;to:&lt;/p&gt;

&lt;p&gt;Cloud resource has an identity&lt;br&gt;
            +&lt;br&gt;
IAM controls what it can access&lt;/p&gt;

&lt;p&gt;That's a much better foundation for cloud-native workloads.&lt;/p&gt;

&lt;p&gt;And it reinforces another principle: &lt;strong&gt;Least Privilege Access.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;RAG may sound like an AI problem.&lt;/p&gt;

&lt;p&gt;But once the application touches Object Storage, Data Science, Model Deployment, or Container Registry, &lt;strong&gt;identity becomes part of the AI architecture too.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;What the Pipeline Looks Like Now&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;At this point, the data flow looked like this:&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ftmd0c7if2vl20y3zmuu8.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ftmd0c7if2vl20y3zmuu8.png" alt="full_vertical_ingestion_to_embedding_pipeline" width="800" height="871"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Notice what's missing.&lt;br&gt;
The LLM.&lt;/p&gt;

&lt;p&gt;And that's intentional.&lt;/p&gt;

&lt;p&gt;Before generating a single response, we've already had to solve:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Data ingestion&lt;/li&gt;
&lt;li&gt;Persistence&lt;/li&gt;
&lt;li&gt;Decoupling&lt;/li&gt;
&lt;li&gt;Raw-data management&lt;/li&gt;
&lt;li&gt;Chunking&lt;/li&gt;
&lt;li&gt;Batch processing&lt;/li&gt;
&lt;li&gt;Pagination&lt;/li&gt;
&lt;li&gt;Data validation&lt;/li&gt;
&lt;li&gt;Resource isolation&lt;/li&gt;
&lt;li&gt;IAM&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That's the point.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;The Bigger Lesson&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;Building RAG made me appreciate something that's easy to forget in the GenAI hype cycle:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;RAG is as much a data-engineering problem as it is an AI problem.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The LLM only sees what the retrieval pipeline gives it.&lt;/p&gt;

&lt;p&gt;The retrieval pipeline only searches what was indexed.&lt;/p&gt;

&lt;p&gt;The index only contains what was embedded.&lt;/p&gt;

&lt;p&gt;Embeddings only represent what was processed.&lt;/p&gt;

&lt;p&gt;And processing can only operate on what ingestion successfully collected.&lt;/p&gt;

&lt;p&gt;So the dependency chain is:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;Data Quality -&amp;gt; Chunk Quality -&amp;gt; Embedding Quality -&amp;gt; Retrieval Quality -&amp;gt; Context Quality -&amp;gt; LLM Response Quality&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;A failure near the beginning propagates through everything downstream.&lt;/p&gt;

&lt;p&gt;A bigger model won't fix missing data.&lt;/p&gt;

&lt;p&gt;Prompt engineering won't fix a dataset accidentally truncated at 1,000 objects.&lt;/p&gt;

&lt;p&gt;And a re-ranker can't rank documents that never entered the index.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;Engineering Takeaways&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;This stage changed how I approached the rest of the project.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Treat Object Storage as an architectural boundary&lt;/strong&gt;&lt;br&gt;
&lt;em&gt;It keeps ingestion, processing, and downstream workloads decoupled.&lt;/em&gt;&lt;br&gt;
&lt;strong&gt;2. Keep raw data immutable&lt;/strong&gt;&lt;br&gt;
&lt;em&gt;Processing strategies will change. Your original data shouldn't disappear with them.&lt;/em&gt;&lt;br&gt;
&lt;strong&gt;3. Design for bounded memory&lt;/strong&gt;&lt;br&gt;
&lt;em&gt;Batching becomes increasingly important as datasets grow.&lt;/em&gt;&lt;br&gt;
&lt;strong&gt;4. Never assume an API returned everything&lt;/strong&gt;&lt;br&gt;
&lt;em&gt;Pagination bugs can silently create incomplete ML datasets.&lt;/em&gt;&lt;br&gt;
&lt;strong&gt;5. Validate every stage&lt;/strong&gt;&lt;br&gt;
&lt;em&gt;Counts, failures, skips, and artifact counts are simple but powerful observability signals.&lt;/em&gt;&lt;br&gt;
&lt;strong&gt;6. Separate workloads by responsibility&lt;/strong&gt;&lt;br&gt;
&lt;em&gt;Scraping, preprocessing, embedding generation, and inference don't necessarily belong on the same infrastructure.&lt;/em&gt;&lt;br&gt;
&lt;strong&gt;7. Treat cloud identity as part of application architecture&lt;/strong&gt;&lt;br&gt;
&lt;em&gt;Resource Principals and IAM matter just as much as your Python code once services start communicating.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  And Now We Can Finally Talk About Vectors
&lt;/h2&gt;

&lt;p&gt;We started with raw technical documents.&lt;/p&gt;

&lt;p&gt;We now have cleaned, chunked, reproducible data sitting in Object Storage.&lt;/p&gt;

&lt;p&gt;Roughly &lt;strong&gt;70,000 chunks&lt;/strong&gt; are waiting.&lt;/p&gt;

&lt;p&gt;But there's one problem.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;FAISS can't search text.&lt;/strong&gt;&lt;br&gt;
It searches &lt;strong&gt;vectors&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;So somehow this:&lt;/p&gt;

&lt;p&gt;"How does Kubernetes service discovery work?"&lt;/p&gt;

&lt;p&gt;needs to become something like:&lt;/p&gt;

&lt;p&gt;[0.018, -0.042, 0.091, ..., 0.027]&lt;/p&gt;

&lt;p&gt;And documents discussing Kubernetes networking need to end up close to that query in vector space.&lt;/p&gt;

&lt;p&gt;That's where things get considerably more interesting.&lt;/p&gt;

&lt;p&gt;Because in &lt;strong&gt;&lt;em&gt;Part 3&lt;/em&gt;&lt;/strong&gt;, we're going from:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Words → Numbers → Meaning → Search&lt;br&gt;
We'll look at:&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;*&lt;em&gt;Embeddings. Sentence Transformers. 384-dimensional vectors. Similarity search. FAISS. IVF indexes. Centroids. nlist. nprobe.&lt;br&gt;
*&lt;/em&gt;&lt;br&gt;
And why FAISS told me that 999 training vectors weren't enough.&lt;/p&gt;

&lt;p&gt;That warning ended up teaching me more about vector search than simply getting the code to run ever could.&lt;/p&gt;

&lt;p&gt;If you'd like to follow my work or connect, you can find me here:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://www.linkedin.com/in/yugandharsurya/" rel="noopener noreferrer"&gt;LinkedIn&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/surya7765/" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://x.com/YugandharSurya4" rel="noopener noreferrer"&gt;Twitter/X&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>ai</category>
      <category>devops</category>
      <category>oci</category>
      <category>rag</category>
    </item>
    <item>
      <title>Building an Enterprise GenAI Platform on OCI — Part 1: Designing the Architecture</title>
      <dc:creator>Yugandhar Surya</dc:creator>
      <pubDate>Fri, 17 Jul 2026 16:45:34 +0000</pubDate>
      <link>https://dev.to/yugandharsurya/building-an-enterprise-genai-platform-on-oci-part-1-designing-the-architecture-1ebc</link>
      <guid>https://dev.to/yugandharsurya/building-an-enterprise-genai-platform-on-oci-part-1-designing-the-architecture-1ebc</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;Every production system starts with a diagram, not code.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;When I started this project, I thought I would spend most of my time choosing and configuring an LLM.&lt;/p&gt;

&lt;p&gt;Instead, I spent days thinking about something else.&lt;/p&gt;

&lt;p&gt;⚠️ New here? This is Part 1 of the series. If you'd like the backstory on why I built this platform, the mistakes I made, and the lessons from my first production RAG system, read &lt;strong&gt;Part 0&lt;/strong&gt; first:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://dev.to/surya7765/building-an-enterprise-genai-platform-on-oci-lessons-from-my-first-production-rag-system-963"&gt;Building an Enterprise GenAI Platform on OCI – Lessons from My First Production RAG System&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Then come back here for the architecture deep dive.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The architecture.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The moment I stopped thinking about the model and started thinking about the system, everything changed. A production GenAI application needs far more than an LLM, it needs reliable data pipelines, efficient retrieval, scalable infrastructure, and secure deployment. So before writing any code, I asked myself: &lt;strong&gt;What exactly am I building?&lt;/strong&gt; A chatbot, a RAG application, or an enterprise GenAI platform?&lt;/p&gt;

&lt;h3&gt;
  
  
  Thinking Beyond a Chatbot
&lt;/h3&gt;

&lt;p&gt;Most RAG tutorials follow roughly the same flow.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Documents -&amp;gt; Embeddings -&amp;gt; Vector Database -&amp;gt; LLM -&amp;gt; Answer&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;For learning, that &lt;strong&gt;architecture&lt;/strong&gt; works just fine. But &lt;em&gt;production&lt;/em&gt; isn't just about retrieving documents and calling an LLM, it's about building a system that can ingest data continuously, scale reliably, and evolve over time. That's when I realized I wasn't designing a chatbot; &lt;strong&gt;I was designing a platform.&lt;/strong&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  The Architecture
&lt;/h2&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fve3683wxyqb9ngshgtvv.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fve3683wxyqb9ngshgtvv.png" alt="Figure 1 – High-level architecture of the Enterprise GenAI Platform on OCI." width="800" height="533"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The architecture naturally separated into two independent pipelines.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Offline Pipeline&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;This pipeline prepares knowledge before any user sends a query.&lt;/p&gt;

&lt;p&gt;Its responsibility is to transform raw information into something an LLM can retrieve efficiently.&lt;/p&gt;

&lt;p&gt;The flow looks like this:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;External Sources -&amp;gt; OCI Compute -&amp;gt; Object Storage -&amp;gt; Cleaning &amp;amp; Chunking -&amp;gt; Embedding Generation -&amp;gt; FAISS Index -&amp;gt; Artifacts&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;These jobs are compute intensive but don't need to run for every request.&lt;/p&gt;

&lt;p&gt;Keeping them separate allows the knowledge base to evolve independently of the inference service.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Online Pipeline&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Once the data is prepared, the online pipeline takes over.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;User -&amp;gt; Oracle APEX -&amp;gt; FastAPI -&amp;gt; Query Embedding -&amp;gt; FAISS Retrieval -&amp;gt; Prompt Construction -&amp;gt; LLM -&amp;gt; Streaming Response&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Unlike the offline pipeline, every millisecond matters here.&lt;/p&gt;

&lt;p&gt;The goal isn't just to generate an answer.&lt;/p&gt;

&lt;p&gt;The goal is to generate the right answer quickly.&lt;/p&gt;

&lt;p&gt;That changes how every component is designed.&lt;/p&gt;




&lt;h3&gt;
  
  
  Why Split the Platform?
&lt;/h3&gt;

&lt;p&gt;One question I kept asking myself was:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;Why not put everything inside a single application?&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Because different workloads have different requirements.&lt;/p&gt;

&lt;p&gt;The ingestion pipeline may process thousands of documents in batches.&lt;/p&gt;

&lt;p&gt;The inference service should respond within seconds.&lt;/p&gt;

&lt;p&gt;Trying to solve both problems inside one application makes scaling harder and deployments riskier.&lt;/p&gt;

&lt;p&gt;Separating the platform into independent services means each layer can evolve without affecting the others.&lt;/p&gt;




&lt;h2&gt;
  
  
  Designing Around Responsibilities
&lt;/h2&gt;

&lt;p&gt;Once I stopped thinking about frameworks and started thinking about responsibilities, the architecture almost designed itself. Every component had one job: ingest data, process it, retrieve knowledge, generate responses, or serve the application. That's how the platform naturally evolved into six layers.&lt;/p&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;Responsibility&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Data Ingestion&lt;/td&gt;
&lt;td&gt;Collect raw knowledge&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Storage&lt;/td&gt;
&lt;td&gt;Persist datasets, embeddings, indexes, and artifacts&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Processing&lt;/td&gt;
&lt;td&gt;Clean, normalize, and chunk documents&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Retrieval&lt;/td&gt;
&lt;td&gt;Search relevant knowledge&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Inference&lt;/td&gt;
&lt;td&gt;Generate grounded responses&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Application&lt;/td&gt;
&lt;td&gt;Provide the enterprise user interface&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Notice that none of these layers are tied to a specific technology and that's intentional. Good architecture should outlive the tools used to implement it. FastAPI can be replaced by another framework, FAISS by a managed vector database, or TinyLlama by a different LLM. The technologies may evolve, but the responsibilities of each layer remain the same.&lt;/p&gt;




&lt;h2&gt;
  
  
  Architecture Principles
&lt;/h2&gt;

&lt;p&gt;While designing the platform, I kept returning to a small set of principles.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Modular – Every layer should have a single responsibility.&lt;/li&gt;
&lt;li&gt;Cloud-native – Managed services where they make sense.&lt;/li&gt;
&lt;li&gt;Scalable – Independent scaling for ingestion and inference.&lt;/li&gt;
&lt;li&gt;Observable – Health, metrics, and logging built in.&lt;/li&gt;
&lt;li&gt;Secure – Least-privilege access between services.&lt;/li&gt;
&lt;li&gt;Cost aware – Optimize for learning without unnecessary infrastructure.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These principles influenced every decision that followed.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why OCI?
&lt;/h2&gt;

&lt;p&gt;One of the goals of this project was to explore how far a production-oriented GenAI platform could be built using Oracle Cloud Infrastructure.&lt;/p&gt;

&lt;p&gt;OCI offered everything I needed for this stage of the project:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Compute for data collection&lt;/li&gt;
&lt;li&gt;Object Storage for the data lake&lt;/li&gt;
&lt;li&gt;Data Science for experimentation&lt;/li&gt;
&lt;li&gt;Container Registry for deployment&lt;/li&gt;
&lt;li&gt;IAM for secure access&lt;/li&gt;
&lt;li&gt;Oracle APEX for the enterprise application layer&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Rather than introducing every service at once, each will appear naturally as the project evolves throughout this series.&lt;/p&gt;

&lt;h2&gt;
  
  
  One Lesson I Learned Early
&lt;/h2&gt;

&lt;p&gt;I started this project thinking the hardest part would be choosing the right LLM and refining prompts. Instead, I found myself spending far more time designing how data moved through the system. That was the moment I realized a great GenAI application isn't defined by its model, it's defined by its architecture.&lt;/p&gt;




&lt;h3&gt;
  
  
  What's Next?
&lt;/h3&gt;

&lt;p&gt;Now that the blueprint is complete, it's time to start building.&lt;/p&gt;

&lt;p&gt;In &lt;strong&gt;Part 2&lt;/strong&gt;, we'll implement the first stage of the platform:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Collecting data with OCI Compute&lt;/li&gt;
&lt;li&gt;Streaming it directly into Object Storage&lt;/li&gt;
&lt;li&gt;Designing the data lake&lt;/li&gt;
&lt;li&gt;Preparing thousands of documents for downstream processing&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Because before we can build a great retrieval system, we first need great data.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>oci</category>
      <category>oracle</category>
      <category>systemdesign</category>
    </item>
    <item>
      <title>Building an Enterprise GenAI Platform on OCI — Lessons from My First Production RAG System</title>
      <dc:creator>Yugandhar Surya</dc:creator>
      <pubDate>Wed, 08 Jul 2026 13:28:51 +0000</pubDate>
      <link>https://dev.to/yugandharsurya/building-an-enterprise-genai-platform-on-oci-lessons-from-my-first-production-rag-system-963</link>
      <guid>https://dev.to/yugandharsurya/building-an-enterprise-genai-platform-on-oci-lessons-from-my-first-production-rag-system-963</guid>
      <description>&lt;p&gt;&lt;strong&gt;I thought building a RAG application would be straightforward.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Scrape the data. Generate embeddings. Build a FAISS index. Connect an LLM. Expose an API.&lt;/p&gt;

&lt;p&gt;That's what most tutorials make it look like.&lt;/p&gt;

&lt;p&gt;Reality was very different.&lt;br&gt;
Every optimization solved one problem and uncovered another.&lt;/p&gt;

&lt;p&gt;Even after switching to a lightweight language model, every response still took several seconds.&lt;br&gt;
That's when I realized the bottleneck wasn't the LLM. It was the architecture itself.&lt;/p&gt;

&lt;p&gt;That realization completely changed the direction of this project. Instead of building just another chatbot, I decided to design a production-oriented GenAI platform on Oracle Cloud Infrastructure (OCI), focusing on data engineering, retrieval quality, inference optimization, deployment, observability, and LLMOps.&lt;/p&gt;

&lt;p&gt;Over the past few weeks, I built this platform while exploring Oracle Cloud Infrastructure, documenting every architectural decision, optimization, and failure along the way.&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fxoazoemiq5rh1avxi9h0.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fxoazoemiq5rh1avxi9h0.png" alt="Architectural diagram of the GenAI platform on OCI showing data ingestion and inference layers" width="800" height="350"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  Some Problems I Didn't Expect
&lt;/h2&gt;

&lt;p&gt;What started as a simple RAG prototype quickly turned into an engineering exercise.&lt;/p&gt;

&lt;p&gt;Along the way, I encountered challenges I hadn't anticipated:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Large Docker images and slow deployments&lt;/li&gt;
&lt;li&gt;Infrastructure limitations on OCI Free Tier&lt;/li&gt;
&lt;li&gt;Container architecture mismatches (ARM vs. AMD)&lt;/li&gt;
&lt;li&gt;Retrieval and inference latency bottlenecks&lt;/li&gt;
&lt;li&gt;IAM and networking complexities during deployment&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Every challenge forced a new architectural decision and those decisions shaped this entire series.&lt;/p&gt;
&lt;h2&gt;
  
  
  Why Another RAG Series?
&lt;/h2&gt;

&lt;p&gt;If you've searched for RAG tutorials, you've probably noticed a common pattern.&lt;/p&gt;

&lt;p&gt;Most articles demonstrate how to connect an LLM to a vector database and answer questions from a PDF. While that's a great way to understand the basics, it leaves out many of the challenges you'll encounter when building a real-world system.&lt;/p&gt;

&lt;p&gt;Along the way, I found myself asking questions like:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Where should the &lt;em&gt;data actually live?&lt;/em&gt;
&lt;/li&gt;
&lt;li&gt;How do you keep it synchronized?&lt;/li&gt;
&lt;li&gt;Why is retrieval slower than expected?&lt;/li&gt;
&lt;li&gt;Should you use FAISS or a managed vector database?&lt;/li&gt;
&lt;li&gt;How do you deploy and scale inference?&lt;/li&gt;
&lt;li&gt;What happens when &lt;strong&gt;multiple users&lt;/strong&gt; hit the API simultaneously?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Those were exactly the questions I started asking while building this project.&lt;/p&gt;
&lt;h2&gt;
  
  
  The Goal
&lt;/h2&gt;

&lt;p&gt;I wanted to understand why production systems look so different from GitHub demos.&lt;/p&gt;

&lt;p&gt;The objective wasn't simply to make an LLM answer questions, it was to understand the complete lifecycle of a production GenAI application.&lt;/p&gt;

&lt;p&gt;That meant learning how to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Build scalable data pipelines&lt;/li&gt;
&lt;li&gt;Design an efficient retrieval layer&lt;/li&gt;
&lt;li&gt;Optimize inference latency&lt;/li&gt;
&lt;li&gt;Containerize and deploy services&lt;/li&gt;
&lt;li&gt;Integrate with enterprise applications&lt;/li&gt;
&lt;li&gt;Apply &lt;strong&gt;MLOps&lt;/strong&gt; and &lt;strong&gt;LLMOps&lt;/strong&gt; principles&lt;/li&gt;
&lt;li&gt;Keep operational costs under control&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;
  
  
  Why Oracle Cloud Infrastructure(OCI)?
&lt;/h2&gt;

&lt;p&gt;Most tutorials use AWS or Azure.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;I intentionally chose Oracle Cloud Infrastructure (OCI) because I wanted to explore its AI ecosystem and understand how enterprise GenAI applications could be built using Oracle's cloud services.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The platform combines OCI Compute for ingestion, Object Storage as a data lake, Data Science for experimentation, Model Deployment for inference, and Oracle APEX as the application layer.&lt;/p&gt;

&lt;p&gt;Choosing OCI also introduced a unique set of challenges around resource limits, deployment strategies, container architectures, and cloud-native design decisions many of which I'll cover throughout this series.&lt;/p&gt;
&lt;h2&gt;
  
  
  What We'll Build
&lt;/h2&gt;

&lt;p&gt;By the end of this series, we'll have built an enterprise-oriented RAG platform capable of:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Automated data ingestion&lt;/li&gt;
&lt;li&gt;Cloud-native data lake&lt;/li&gt;
&lt;li&gt;Semantic search using embeddings&lt;/li&gt;
&lt;li&gt;Hybrid Retrieval (FAISS + BM25)&lt;/li&gt;
&lt;li&gt;Cross-Encoder Re-ranking&lt;/li&gt;
&lt;li&gt;Prompt Engineering&lt;/li&gt;
&lt;li&gt;Streaming responses&lt;/li&gt;
&lt;li&gt;Guardrails&lt;/li&gt;
&lt;li&gt;Dockerized deployment&lt;/li&gt;
&lt;li&gt;OCI Model Deployment&lt;/li&gt;
&lt;li&gt;Oracle APEX integration&lt;/li&gt;
&lt;li&gt;CI/CD pipeline&lt;/li&gt;
&lt;li&gt;LLMOps best practices&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;More importantly, we'll discuss &lt;em&gt;why&lt;/em&gt; each architectural decision was made, along with the trade-offs involved.&lt;/p&gt;

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

&lt;blockquote&gt;
&lt;p&gt;This isn't another "build a chatbot in 15 minutes" tutorial. It's an engineering journal documenting the architectural decisions, trade-offs, failures, and lessons learned while building an enterprise GenAI platform on OCI.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;If you're looking for a copy-and-paste RAG tutorial, this series probably isn't for you.&lt;/p&gt;

&lt;p&gt;But if you're interested in understanding how enterprise GenAI systems are designed from data ingestion and retrieval to deployment, observability, and optimization. I hope you'll find this journey worthwhile.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Enterprise AI systems aren't built by choosing the largest model.&lt;br&gt;
They're built through hundreds of engineering decisions.&lt;br&gt;
This series is about those decisions.&lt;br&gt;
See you in Part 1.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;
  
  
  Series Roadmap
&lt;/h2&gt;

&lt;p&gt;&lt;/p&gt;
  📚 This Series (10 Parts)
  &lt;ol&gt;
&lt;li&gt;Architecture &amp;amp; Planning&lt;/li&gt;
&lt;li&gt;Data Engineering Pipeline on OCI&lt;/li&gt;
&lt;li&gt;Embeddings &amp;amp; Vector Search&lt;/li&gt;
&lt;li&gt;Hybrid Retrieval and Re-ranking&lt;/li&gt;
&lt;li&gt;FastAPI Inference Pipeline&lt;/li&gt;
&lt;li&gt;Docker &amp;amp; OCI Model Deployment&lt;/li&gt;
&lt;li&gt;Oracle APEX Integration&lt;/li&gt;
&lt;li&gt;LLMOps &amp;amp; CI/CD&lt;/li&gt;
&lt;li&gt;Performance Optimization&lt;/li&gt;
&lt;li&gt;Lessons Learned &amp;amp; Production Considerations&lt;/li&gt;
&lt;/ol&gt;



&lt;p&gt;&lt;/p&gt;




&lt;p&gt;Before we dive into the next article, I'd love to hear from you:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Have you built a RAG application beyond a simple proof of concept?&lt;/li&gt;
&lt;li&gt;What was the biggest challenge you encountered—retrieval quality, latency, deployment, or something else?&lt;/li&gt;
&lt;li&gt;Are you using OCI, AWS, Azure, or another cloud platform for GenAI workloads?&lt;/li&gt;
&lt;li&gt;Is there a specific topic you'd like me to cover in more detail as this series progresses?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Feel free to share your experiences in the comments. I'm always interested in learning how others are approaching these problems.&lt;/p&gt;

&lt;p&gt;If you'd like to follow my work or connect, you can find me here:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://www.linkedin.com/in/yugandharsurya/" rel="noopener noreferrer"&gt;LinkedIn&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/surya7765/" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://x.com/YugandharSurya4" rel="noopener noreferrer"&gt;Twitter/X&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>architecture</category>
      <category>rag</category>
      <category>ai</category>
    </item>
  </channel>
</rss>
