<rss xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title>SQLite-Vec - Tag - Tracy Atteberry</title><link>https://tracyatteberry.com/tags/sqlite-vec/</link><description>SQLite-Vec - Tag - Tracy Atteberry</description><generator>Hugo -- gohugo.io</generator><language>en-us</language><managingEditor>tracy@magicbydesign.com (Tracy Atteberry)</managingEditor><webMaster>tracy@magicbydesign.com (Tracy Atteberry)</webMaster><lastBuildDate>Fri, 14 Nov 2025 00:00:00 +0000</lastBuildDate><image><url>https://tracyatteberry.com/images/feed-icon.jpg</url><title>SQLite-Vec - Tag - Tracy Atteberry</title><link>https://tracyatteberry.com/tags/sqlite-vec/</link></image><atom:link href="https://tracyatteberry.com/tags/sqlite-vec/" rel="self" type="application/rss+xml"/><item><title>Building a Document Q&amp;A System with Rails 8, SQLite-vec, and OpenAI</title><link>https://tracyatteberry.com/posts/ragtime/</link><pubDate>Fri, 14 Nov 2025 00:00:00 +0000</pubDate><author>Tracy Atteberry</author><guid>https://tracyatteberry.com/posts/ragtime/</guid><description><![CDATA[<div class="featured-image">
                <img src="https://tracyatteberry.com/posts/ragtime/password_access.png" referrerpolicy="no-referrer">
            </div><h1 id="building-a-document-qa-system-with-rails-8-sqlite-vec-and-openai">Building a document Q&amp;A system with Rails 8, SQLite-vec, and OpenAI</h1>
<p>Every company wants to plug AI into their proprietary data. But here&rsquo;s the
thing—it&rsquo;s not just about calling an API. The real challenge is building
systems that actually work reliably and are maintainable in the real world.</p>
<p>So I built Ragtime. Think of it as my playground for figuring out how to build
modern AI applications the right way. It&rsquo;s a document Q&amp;A system where you can
upload PDFs, Word docs, text files, and Markdown, then ask questions and get
answers with actual source citations.</p>
<p>What I really wanted to show wasn&rsquo;t just that I can build an AI app—anyone can
do that these days. I wanted to demonstrate how a senior engineer thinks about
building these systems from the ground up, making smart trade-offs and avoiding
common pitfalls.</p>
<figure style="margin-top: 15px">
  
  <figcaption>Password protected demo login screen</figcaption>
</figure>
<h2 id="architecture-overview">Architecture overview</h2>
<p>So what does Ragtime actually look like under the hood? At its core, it&rsquo;s a RAG
(Retrieval-Augmented Generation) system—fancy talk for &ldquo;find relevant material,
then use it to answer questions.&rdquo;</p>
<div class="mermaid" id="id-1"></div>
<p>Here&rsquo;s how it works: you upload a document, the system pulls out the text,
chops it into smart chunks, turns those chunks into mathematical vectors (more
on that in a bit), and then when you ask a question, it finds the most relevant
chunks and uses them to generate an answer with citations. Simple, right? Well,
there are some fun challenges along the way.</p>
<h3 id="why-this-architecture">Why this architecture?</h3>
<p>Let me tell you why I picked this stack. Rails 8 just clicked for the backend
because it&rsquo;s got some really nice improvements for modern apps—better API
support out of the box, Solid Queue built right in (no more juggling separate
worker processes), and some solid performance tweaks&ndash;especially for SQLite.
Going API-only gave me this clean separation between frontend and backend,
which makes everything easier to maintain and reason about.</p>
<p>For the frontend, Vue.js 3 with the Composition API just feels right for chat
interfaces. You get way better state management than server-rendered options,
and the component-based architecture makes complex UI stuff like real-time chat
and interactive citations so much easier to build.</p>
<h2 id="the-vector-storage-decision-sqlite--sqlite-vec">The vector storage decision: SQLite + sqlite-vec</h2>
<p>This was probably the biggest technical decision I had to make—how to store all
those vector embeddings. I looked at three options:</p>
<ol>
<li><strong>PostgreSQL + pgvector</strong>: This is what everybody uses in production</li>
<li><strong>Dedicated vector databases</strong> (like Pinecone or Weaviate): The fancy specialized solutions</li>
<li><strong>SQLite + sqlite-vec</strong>: The simple, &ldquo;just make it work&rdquo; approach</li>
</ol>
<p>I went with SQLite + sqlite-vec, and I have to be honest—it was way more
complicated than I expected. Here&rsquo;s my thinking: when you&rsquo;re trying to show
engineering competence, deployment simplicity matters more than theoretical
scalability. But sqlite-vec turned out to be a significant engineering challenge
in its own right.</p>
<p>The extension doesn&rsquo;t just work out of the box. I had to:</p>
<ul>
<li>Bootstrap it manually in the docker-entrypoint for production</li>
<li>Load it programmatically in code rather than declaring it in database.yml</li>
<li>Add special verification in health check endpoints because if it fails to load,
the entire RAG functionality breaks</li>
<li>Turn off transactional fixtures in tests and create custom test support code</li>
</ul>
<p>While SQLite gives you that single-file, zero-dependency promise, getting
sqlite-vec to work reliably required significant effort. This really highlights a
key engineering principle: sometimes the &ldquo;simple&rdquo; choice brings its own complex
challenges that you need to account for.</p>
<h2 id="document-processing-pipeline">Document processing pipeline</h2>
<p>Building a document processing pipeline that actually works reliably meant
solving some pretty fun challenges:</p>
<h3 id="challenge-1-getting-text-out-of-different-file-types">Challenge 1: Getting text out of different file types</h3>
<p>First problem: documents come in all shapes and sizes. PDFs, Word docs, plain
text, Markdown—each one needs its own special handling trick. I ended up
building a <code>TextExtractor</code> service that&rsquo;s basically a Swiss Army knife for file
formats. It knows how to handle each type, and when things go wrong (which they
always do), it fails gracefully and tells you what happened.</p>
<div class="code-block code-line-numbers" style="counter-reset: code-block 0">
    <div class="code-header language-ruby">
        <span class="code-title"><i class="arrow fas fa-chevron-right fa-fw" aria-hidden="true"></i></span>
        <span class="ellipses"><i class="fas fa-ellipsis-h fa-fw" aria-hidden="true"></i></span>
        <span class="copy" title="Copy to clipboard"><i class="far fa-copy fa-fw" aria-hidden="true"></i></span>
    </div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ruby" data-lang="ruby"><span class="line"><span class="cl"><span class="c1"># app/services/document_processing/text_extractor.rb</span>
</span></span><span class="line"><span class="cl"><span class="k">class</span> <span class="nc">DocumentProcessing</span><span class="o">::</span><span class="no">TextExtractor</span>
</span></span><span class="line"><span class="cl">  <span class="k">def</span> <span class="nc">self</span><span class="o">.</span><span class="nf">extract</span><span class="p">(</span><span class="n">file</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="k">case</span> <span class="n">file</span><span class="o">.</span><span class="n">content_type</span>
</span></span><span class="line"><span class="cl">    <span class="k">when</span> <span class="s1">&#39;application/pdf&#39;</span>
</span></span><span class="line"><span class="cl">      <span class="no">PdfReader</span><span class="o">.</span><span class="n">new</span><span class="p">(</span><span class="n">file</span><span class="p">)</span><span class="o">.</span><span class="n">extract_text</span>
</span></span><span class="line"><span class="cl">    <span class="k">when</span> <span class="s1">&#39;application/vnd.openxmlformats-officedocument.wordprocessingml.document&#39;</span>
</span></span><span class="line"><span class="cl">      <span class="no">DocxReader</span><span class="o">.</span><span class="n">new</span><span class="p">(</span><span class="n">file</span><span class="p">)</span><span class="o">.</span><span class="n">extract_text</span>
</span></span><span class="line"><span class="cl">    <span class="k">when</span> <span class="s1">&#39;text/plain&#39;</span><span class="p">,</span> <span class="s1">&#39;text/markdown&#39;</span>
</span></span><span class="line"><span class="cl">      <span class="n">file</span><span class="o">.</span><span class="n">download</span>
</span></span><span class="line"><span class="cl">    <span class="k">else</span>
</span></span><span class="line"><span class="cl">      <span class="k">raise</span> <span class="s2">&#34;Unsupported file type: </span><span class="si">#{</span><span class="n">file</span><span class="o">.</span><span class="n">content_type</span><span class="si">}</span><span class="s2">&#34;</span>
</span></span><span class="line"><span class="cl">    <span class="k">end</span>
</span></span><span class="line"><span class="cl">  <span class="k">rescue</span> <span class="o">=&gt;</span> <span class="n">error</span>
</span></span><span class="line"><span class="cl">    <span class="no">Rails</span><span class="o">.</span><span class="n">logger</span><span class="o">.</span><span class="n">error</span> <span class="s2">&#34;Text extraction failed: </span><span class="si">#{</span><span class="n">error</span><span class="o">.</span><span class="n">message</span><span class="si">}</span><span class="s2">&#34;</span>
</span></span><span class="line"><span class="cl">    <span class="k">raise</span> <span class="no">DocumentProcessing</span><span class="o">::</span><span class="no">ExtractionError</span><span class="p">,</span> <span class="s2">&#34;Failed to extract text from document&#34;</span>
</span></span><span class="line"><span class="cl">  <span class="k">end</span>
</span></span><span class="line"><span class="cl"><span class="k">end</span></span></span></code></pre></div></div>
<h3 id="challenge-2-splitting-text-intelligently">Challenge 2: Splitting text intelligently</h3>
<p>Getting the chunking right is crucial for RAG systems. The key challenge is
maintaining context—chunks need to be small enough to be relevant but large
enough to retain meaning.</p>
<p>I implemented a <code>TextChunker</code> that creates 800-token chunks with 200 tokens of
overlap, which is a well-established pattern in RAG systems. The overlap
ensures related context spans multiple chunks, and the system respects
paragraph boundaries to maintain semantic coherence.</p>
<p>The overlap is particularly important—it keeps related ideas connected across
chunk boundaries. I used tiktoken_ruby for accurate token counting because
different models count tokens differently, and that accuracy significantly
affects chunk quality.</p>
<h3 id="challenge-3-implementing-vector-similarity-search">Challenge 3: Implementing vector similarity search</h3>
<p>The <code>ChunkRetriever</code> handles the core vector search functionality. It generates
embeddings for queries and uses sqlite-vec&rsquo;s virtual tables to find the most
similar chunks. The challenge here was tuning the similarity threshold—too low
and you get irrelevant results, too high and you get no results at all.</p>
<div class="code-block code-line-numbers" style="counter-reset: code-block 0">
    <div class="code-header language-ruby">
        <span class="code-title"><i class="arrow fas fa-chevron-right fa-fw" aria-hidden="true"></i></span>
        <span class="ellipses"><i class="fas fa-ellipsis-h fa-fw" aria-hidden="true"></i></span>
        <span class="copy" title="Copy to clipboard"><i class="far fa-copy fa-fw" aria-hidden="true"></i></span>
    </div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ruby" data-lang="ruby"><span class="line"><span class="cl"><span class="c1"># app/services/rag/chunk_retriever.rb</span>
</span></span><span class="line"><span class="cl"><span class="k">class</span> <span class="nc">Rag</span><span class="o">::</span><span class="no">ChunkRetriever</span>
</span></span><span class="line"><span class="cl">  <span class="no">DEFAULT_THRESHOLD</span> <span class="o">=</span> <span class="mi">1</span><span class="o">.</span><span class="mi">2</span> <span class="c1"># L2 distance, tuned for quality results</span>
</span></span><span class="line"><span class="cl">  <span class="no">DEFAULT_LIMIT</span> <span class="o">=</span> <span class="mi">5</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">  <span class="k">def</span> <span class="nf">initialize</span><span class="p">(</span><span class="ss">query</span><span class="p">:,</span> <span class="ss">threshold</span><span class="p">:</span> <span class="no">DEFAULT_THRESHOLD</span><span class="p">,</span> <span class="ss">limit</span><span class="p">:</span> <span class="no">DEFAULT_LIMIT</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="vi">@query</span> <span class="o">=</span> <span class="n">query</span>
</span></span><span class="line"><span class="cl">    <span class="vi">@threshold</span> <span class="o">=</span> <span class="n">threshold</span>
</span></span><span class="line"><span class="cl">    <span class="vi">@limit</span> <span class="o">=</span> <span class="n">limit</span>
</span></span><span class="line"><span class="cl">  <span class="k">end</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">  <span class="k">def</span> <span class="nf">call</span>
</span></span><span class="line"><span class="cl">    <span class="c1"># Generate embedding for query</span>
</span></span><span class="line"><span class="cl">    <span class="n">query_embedding</span> <span class="o">=</span> <span class="no">EmbeddingGenerator</span><span class="o">.</span><span class="n">generate</span><span class="p">(</span><span class="vi">@query</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="c1"># Vector similarity search using sqlite-vec</span>
</span></span><span class="line"><span class="cl">    <span class="n">chunks</span> <span class="o">=</span> <span class="n">execute_vector_search</span><span class="p">(</span><span class="n">query_embedding</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="c1"># Convert to domain objects with metadata</span>
</span></span><span class="line"><span class="cl">    <span class="n">chunks</span><span class="o">.</span><span class="n">map</span> <span class="p">{</span> <span class="o">|</span><span class="n">chunk</span><span class="o">|</span> <span class="no">ChunkResult</span><span class="o">.</span><span class="n">new</span><span class="p">(</span><span class="n">chunk</span><span class="p">)</span> <span class="p">}</span>
</span></span><span class="line"><span class="cl">  <span class="k">end</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">  <span class="kp">private</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">  <span class="k">def</span> <span class="nf">execute_vector_search</span><span class="p">(</span><span class="n">embedding</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="n">sql</span> <span class="o">=</span> <span class="s">&lt;&lt;-SQL
</span></span></span><span class="line"><span class="cl">      <span class="no">SELECT</span> <span class="n">chunks</span><span class="o">.</span><span class="n">*</span><span class="p">,</span> <span class="n">vec_distance_cosine</span><span class="p">(</span><span class="n">chunks</span><span class="o">.</span><span class="n">embedding</span><span class="p">,</span> <span class="sc">?)</span> <span class="n">as</span> <span class="n">distance</span>
</span></span><span class="line"><span class="cl">      <span class="no">FROM</span> <span class="n">vec_chunks</span>
</span></span><span class="line"><span class="cl">      <span class="no">JOIN</span> <span class="n">chunks</span> <span class="no">ON</span> <span class="n">vec_chunks</span><span class="o">.</span><span class="n">rowid</span> <span class="o">=</span> <span class="n">chunks</span><span class="o">.</span><span class="n">id</span>
</span></span><span class="line"><span class="cl">      <span class="no">WHERE</span> <span class="n">vec_distance_cosine</span><span class="p">(</span><span class="n">chunks</span><span class="o">.</span><span class="n">embedding</span><span class="p">,</span> <span class="sc">?)</span> <span class="o">&lt;</span> <span class="p">?</span>
</span></span><span class="line"><span class="cl">      <span class="no">ORDER</span> <span class="no">BY</span> <span class="n">distance</span>
</span></span><span class="line"><span class="cl">      <span class="no">LIMIT</span> <span class="p">?</span>
</span></span><span class="line"><span class="cl">    <span class="no">SQL</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="n">sanitized_sql</span> <span class="o">=</span> <span class="no">ActiveRecord</span><span class="o">::</span><span class="no">Base</span><span class="o">.</span><span class="n">sanitize_sql_array</span><span class="p">(</span><span class="o">[</span>
</span></span><span class="line"><span class="cl">      <span class="n">sql</span><span class="p">,</span> <span class="n">embedding</span><span class="p">,</span> <span class="n">embedding</span><span class="p">,</span> <span class="vi">@threshold</span><span class="p">,</span> <span class="vi">@limit</span>
</span></span><span class="line"><span class="cl">    <span class="o">]</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="no">ActiveRecord</span><span class="o">::</span><span class="no">Base</span><span class="o">.</span><span class="n">connection</span><span class="o">.</span><span class="n">execute</span><span class="p">(</span><span class="n">sanitized_sql</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">  <span class="k">end</span>
</span></span><span class="line"><span class="cl"><span class="k">end</span></span></span></code></pre></div></div>
<h3 id="challenge-4-background-job-processing">Challenge 4: Background job processing</h3>
<p>Document processing is computationally expensive—text extraction, chunking, and
embedding generation can take significant time. This work needs to happen
asynchronously to avoid blocking user interactions.</p>
<p>Rails 8&rsquo;s Solid Queue with the Puma integration was an excellent choice for
this use case. No separate worker processes to manage—everything runs in the
background while keeping the app responsive. The in-process approach simplifies
deployment significantly.</p>
<div class="code-block code-line-numbers" style="counter-reset: code-block 0">
    <div class="code-header language-ruby">
        <span class="code-title"><i class="arrow fas fa-chevron-right fa-fw" aria-hidden="true"></i></span>
        <span class="ellipses"><i class="fas fa-ellipsis-h fa-fw" aria-hidden="true"></i></span>
        <span class="copy" title="Copy to clipboard"><i class="far fa-copy fa-fw" aria-hidden="true"></i></span>
    </div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ruby" data-lang="ruby"><span class="line"><span class="cl"><span class="c1"># app/jobs/process_document_job.rb</span>
</span></span><span class="line"><span class="cl"><span class="k">class</span> <span class="nc">ProcessDocumentJob</span> <span class="o">&lt;</span> <span class="no">ApplicationJob</span>
</span></span><span class="line"><span class="cl">  <span class="n">retry_on</span> <span class="no">StandardError</span><span class="p">,</span> <span class="ss">wait</span><span class="p">:</span> <span class="ss">:exponentially_longer</span><span class="p">,</span> <span class="ss">attempts</span><span class="p">:</span> <span class="mi">3</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">  <span class="k">def</span> <span class="nf">perform</span><span class="p">(</span><span class="n">document</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="n">document</span><span class="o">.</span><span class="n">update!</span><span class="p">(</span><span class="ss">status</span><span class="p">:</span> <span class="ss">:processing</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="c1"># Extract text content</span>
</span></span><span class="line"><span class="cl">    <span class="n">text_content</span> <span class="o">=</span> <span class="no">DocumentProcessing</span><span class="o">::</span><span class="no">TextExtractor</span><span class="o">.</span><span class="n">extract</span><span class="p">(</span><span class="n">document</span><span class="o">.</span><span class="n">file</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="c1"># Create chunks with overlap</span>
</span></span><span class="line"><span class="cl">    <span class="n">chunks</span> <span class="o">=</span> <span class="no">DocumentProcessing</span><span class="o">::</span><span class="no">TextChunker</span><span class="o">.</span><span class="n">chunk</span><span class="p">(</span><span class="n">text_content</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="c1"># Generate embeddings in batches</span>
</span></span><span class="line"><span class="cl">    <span class="n">embeddings</span> <span class="o">=</span> <span class="no">EmbeddingGenerator</span><span class="o">.</span><span class="n">generate_batch</span><span class="p">(</span><span class="n">chunks</span><span class="o">.</span><span class="n">map</span><span class="p">(</span><span class="o">&amp;</span><span class="ss">:content</span><span class="p">))</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="c1"># Store chunks with embeddings</span>
</span></span><span class="line"><span class="cl">    <span class="n">chunks</span><span class="o">.</span><span class="n">each_with_index</span> <span class="k">do</span> <span class="o">|</span><span class="n">chunk</span><span class="p">,</span> <span class="n">index</span><span class="o">|</span>
</span></span><span class="line"><span class="cl">      <span class="n">document</span><span class="o">.</span><span class="n">chunks</span><span class="o">.</span><span class="n">create!</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">        <span class="ss">content</span><span class="p">:</span> <span class="n">chunk</span><span class="o">[</span><span class="ss">:content</span><span class="o">]</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">        <span class="ss">position</span><span class="p">:</span> <span class="n">chunk</span><span class="o">[</span><span class="ss">:position</span><span class="o">]</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">        <span class="ss">token_count</span><span class="p">:</span> <span class="n">chunk</span><span class="o">[</span><span class="ss">:token_count</span><span class="o">]</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">        <span class="ss">embedding</span><span class="p">:</span> <span class="n">embeddings</span><span class="o">[</span><span class="n">index</span><span class="o">]</span>
</span></span><span class="line"><span class="cl">      <span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="k">end</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="n">document</span><span class="o">.</span><span class="n">update!</span><span class="p">(</span><span class="ss">status</span><span class="p">:</span> <span class="ss">:completed</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">  <span class="k">rescue</span> <span class="o">=&gt;</span> <span class="n">error</span>
</span></span><span class="line"><span class="cl">    <span class="n">document</span><span class="o">.</span><span class="n">update!</span><span class="p">(</span><span class="ss">status</span><span class="p">:</span> <span class="ss">:failed</span><span class="p">,</span> <span class="ss">error_message</span><span class="p">:</span> <span class="n">error</span><span class="o">.</span><span class="n">message</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="k">raise</span>
</span></span><span class="line"><span class="cl">  <span class="k">end</span>
</span></span><span class="line"><span class="cl"><span class="k">end</span></span></span></code></pre></div></div>
<p>This in-process approach means way less deployment headache while still giving
you reliable background job processing. Sometimes simpler really is better.</p>
<h2 id="frontend-architecture-vuejs-3--composition-api">Frontend architecture: Vue.js 3 + Composition API</h2>
<p>Building a chat interface that doesn&rsquo;t feel clunky is surprisingly hard. You
need to manage conversation state, message history, real-time updates&hellip; it
gets complicated fast.</p>
<p>Vue.js 3&rsquo;s Composition API turned out to be perfect for this. It gives you
these clean patterns for organizing complex component logic without everything
turning into spaghetti code. The chat interface keeps track of the
conversation, shows you messages as they come in, and makes citations
clickable—click one and it&rsquo;ll highlight the exact passage in the document.</p>
<p>I used Pinia for state management because it makes debugging so much easier,
and the whole component structure follows that separation of concerns principle
that keeps you sane when the app gets complex.</p>
<div class="code-block code-line-numbers" style="counter-reset: code-block 0">
    <div class="code-header language-javascript">
        <span class="code-title"><i class="arrow fas fa-chevron-right fa-fw" aria-hidden="true"></i></span>
        <span class="ellipses"><i class="fas fa-ellipsis-h fa-fw" aria-hidden="true"></i></span>
        <span class="copy" title="Copy to clipboard"><i class="far fa-copy fa-fw" aria-hidden="true"></i></span>
    </div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-javascript" data-lang="javascript"><span class="line"><span class="cl"><span class="c1">// ChatInterface.vue (simplified)
</span></span></span><span class="line"><span class="cl"><span class="kr">import</span> <span class="p">{</span> <span class="nx">ref</span><span class="p">,</span> <span class="nx">computed</span><span class="p">,</span> <span class="nx">onMounted</span> <span class="p">}</span> <span class="nx">from</span> <span class="s1">&#39;vue&#39;</span>
</span></span><span class="line"><span class="cl"><span class="kr">import</span> <span class="p">{</span> <span class="nx">useChatStore</span> <span class="p">}</span> <span class="nx">from</span> <span class="s1">&#39;@/stores/chat&#39;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="kr">export</span> <span class="k">default</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">  <span class="nx">setup</span><span class="p">()</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="kr">const</span> <span class="nx">chatStore</span> <span class="o">=</span> <span class="nx">useChatStore</span><span class="p">()</span>
</span></span><span class="line"><span class="cl">    <span class="kr">const</span> <span class="nx">message</span> <span class="o">=</span> <span class="nx">ref</span><span class="p">(</span><span class="s1">&#39;&#39;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="kr">const</span> <span class="nx">loading</span> <span class="o">=</span> <span class="nx">ref</span><span class="p">(</span><span class="kc">false</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="kr">const</span> <span class="nx">sendMessage</span> <span class="o">=</span> <span class="kr">async</span> <span class="p">()</span> <span class="p">=&gt;</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">      <span class="k">if</span> <span class="p">(</span><span class="o">!</span><span class="nx">message</span><span class="p">.</span><span class="nx">value</span><span class="p">.</span><span class="nx">trim</span><span class="p">())</span> <span class="k">return</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">      <span class="nx">loading</span><span class="p">.</span><span class="nx">value</span> <span class="o">=</span> <span class="kc">true</span>
</span></span><span class="line"><span class="cl">      <span class="k">try</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">        <span class="kr">await</span> <span class="nx">chatStore</span><span class="p">.</span><span class="nx">sendMessage</span><span class="p">(</span><span class="nx">message</span><span class="p">.</span><span class="nx">value</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">        <span class="nx">message</span><span class="p">.</span><span class="nx">value</span> <span class="o">=</span> <span class="s1">&#39;&#39;</span>
</span></span><span class="line"><span class="cl">      <span class="p">}</span> <span class="k">finally</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">        <span class="nx">loading</span><span class="p">.</span><span class="nx">value</span> <span class="o">=</span> <span class="kc">false</span>
</span></span><span class="line"><span class="cl">      <span class="p">}</span>
</span></span><span class="line"><span class="cl">    <span class="p">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="k">return</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">      <span class="nx">message</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">      <span class="nx">loading</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">      <span class="nx">currentChat</span><span class="o">:</span> <span class="nx">computed</span><span class="p">(()</span> <span class="p">=&gt;</span> <span class="nx">chatStore</span><span class="p">.</span><span class="nx">currentChat</span><span class="p">),</span>
</span></span><span class="line"><span class="cl">      <span class="nx">sendMessage</span>
</span></span><span class="line"><span class="cl">    <span class="p">}</span>
</span></span><span class="line"><span class="cl">  <span class="p">}</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span></span></span></code></pre></div></div>
<h2 id="citation-extraction-and-storage">Citation extraction and storage</h2>
<p>Here&rsquo;s something that drives me crazy about some AI apps: you get these
confident-sounding answers but have no idea where they came from. That&rsquo;s
terrible for user trust.</p>
<p>So I made sure every answer comes with citations. The <code>AnswerGenerator</code> service
basically tells the AI &ldquo;hey, when you answer this, tell me exactly which chunks
you used&rdquo; and stores all that in a nice structured format. This way users can
actually verify the answers, which is huge for building trust.</p>
<div class="code-block code-line-numbers" style="counter-reset: code-block 0">
    <div class="code-header language-ruby">
        <span class="code-title"><i class="arrow fas fa-chevron-right fa-fw" aria-hidden="true"></i></span>
        <span class="ellipses"><i class="fas fa-ellipsis-h fa-fw" aria-hidden="true"></i></span>
        <span class="copy" title="Copy to clipboard"><i class="far fa-copy fa-fw" aria-hidden="true"></i></span>
    </div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ruby" data-lang="ruby"><span class="line"><span class="cl"><span class="c1"># app/services/rag/answer_generator.rb</span>
</span></span><span class="line"><span class="cl"><span class="k">class</span> <span class="nc">Rag</span><span class="o">::</span><span class="no">AnswerGenerator</span>
</span></span><span class="line"><span class="cl">  <span class="k">def</span> <span class="nf">initialize</span><span class="p">(</span><span class="ss">context</span><span class="p">:,</span> <span class="ss">question</span><span class="p">:)</span>
</span></span><span class="line"><span class="cl">    <span class="vi">@context</span> <span class="o">=</span> <span class="n">context</span>
</span></span><span class="line"><span class="cl">    <span class="vi">@question</span> <span class="o">=</span> <span class="n">question</span>
</span></span><span class="line"><span class="cl">  <span class="k">end</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">  <span class="k">def</span> <span class="nf">call</span>
</span></span><span class="line"><span class="cl">    <span class="n">response</span> <span class="o">=</span> <span class="n">ruby_llm</span><span class="o">.</span><span class="n">chat</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">      <span class="ss">messages</span><span class="p">:</span> <span class="n">build_prompt</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">      <span class="ss">temperature</span><span class="p">:</span> <span class="mi">0</span><span class="o">.</span><span class="mi">3</span><span class="p">,</span> <span class="c1"># Lower temperature for more consistent responses</span>
</span></span><span class="line"><span class="cl">      <span class="ss">response_format</span><span class="p">:</span> <span class="p">{</span> <span class="ss">type</span><span class="p">:</span> <span class="s2">&#34;json_object&#34;</span> <span class="p">}</span>
</span></span><span class="line"><span class="cl">    <span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="n">parse_response</span><span class="p">(</span><span class="n">response</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">  <span class="k">end</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">  <span class="kp">private</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">  <span class="k">def</span> <span class="nf">build_prompt</span>
</span></span><span class="line"><span class="cl">    <span class="no">PromptBuilder</span><span class="o">.</span><span class="n">new</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">      <span class="ss">context</span><span class="p">:</span> <span class="vi">@context</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">      <span class="ss">question</span><span class="p">:</span> <span class="vi">@question</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">      <span class="ss">citation_format</span><span class="p">:</span> <span class="s2">&#34;structured_json&#34;</span>
</span></span><span class="line"><span class="cl">    <span class="p">)</span><span class="o">.</span><span class="n">build</span>
</span></span><span class="line"><span class="cl">  <span class="k">end</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">  <span class="k">def</span> <span class="nf">parse_response</span><span class="p">(</span><span class="n">response</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="n">data</span> <span class="o">=</span> <span class="no">JSON</span><span class="o">.</span><span class="n">parse</span><span class="p">(</span><span class="n">response</span><span class="o">.</span><span class="n">content</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="no">AnswerResult</span><span class="o">.</span><span class="n">new</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">      <span class="ss">content</span><span class="p">:</span> <span class="n">data</span><span class="o">[</span><span class="s1">&#39;answer&#39;</span><span class="o">]</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">      <span class="ss">citations</span><span class="p">:</span> <span class="n">build_citations</span><span class="p">(</span><span class="n">data</span><span class="o">[</span><span class="s1">&#39;citations&#39;</span><span class="o">]</span> <span class="o">||</span> <span class="o">[]</span><span class="p">),</span>
</span></span><span class="line"><span class="cl">      <span class="ss">metadata</span><span class="p">:</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">        <span class="ss">model</span><span class="p">:</span> <span class="n">response</span><span class="o">.</span><span class="n">model</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">        <span class="ss">usage</span><span class="p">:</span> <span class="n">response</span><span class="o">.</span><span class="n">usage</span>
</span></span><span class="line"><span class="cl">      <span class="p">}</span>
</span></span><span class="line"><span class="cl">    <span class="p">)</span>
</span></span><span class="line"><span class="cl">  <span class="k">end</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">  <span class="k">def</span> <span class="nf">build_citations</span><span class="p">(</span><span class="n">citation_data</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="n">citation_data</span><span class="o">.</span><span class="n">map</span> <span class="k">do</span> <span class="o">|</span><span class="n">citation</span><span class="o">|</span>
</span></span><span class="line"><span class="cl">      <span class="p">{</span>
</span></span><span class="line"><span class="cl">        <span class="ss">chunk_id</span><span class="p">:</span> <span class="n">citation</span><span class="o">[</span><span class="s1">&#39;chunk_id&#39;</span><span class="o">]</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">        <span class="ss">document_id</span><span class="p">:</span> <span class="n">citation</span><span class="o">[</span><span class="s1">&#39;document_id&#39;</span><span class="o">]</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">        <span class="ss">document_title</span><span class="p">:</span> <span class="n">citation</span><span class="o">[</span><span class="s1">&#39;document_title&#39;</span><span class="o">]</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">        <span class="ss">relevance</span><span class="p">:</span> <span class="n">citation</span><span class="o">[</span><span class="s1">&#39;relevance_score&#39;</span><span class="o">]</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">        <span class="ss">position</span><span class="p">:</span> <span class="n">citation</span><span class="o">[</span><span class="s1">&#39;position_in_document&#39;</span><span class="o">]</span>
</span></span><span class="line"><span class="cl">      <span class="p">}</span>
</span></span><span class="line"><span class="cl">    <span class="k">end</span>
</span></span><span class="line"><span class="cl">  <span class="k">end</span>
</span></span><span class="line"><span class="cl"><span class="k">end</span></span></span></code></pre></div></div>
<p>All those citations get stored as JSON in the messages table, which means you
can replay conversations later and see exactly how the AI arrived at its
answers. Pretty handy for debugging and audit trails.</p>
<h2 id="production-deployment-strategy">Production deployment strategy</h2>
<p>Getting an AI app to production is&hellip; an adventure. You&rsquo;ve got infrastructure,
security, and a million operational concerns to think about. I went with Docker
containers and Fly.io because they strike a nice balance between power and
simplicity.</p>
<h3 id="container-architecture">Container architecture</h3>
<p>The multi-stage Dockerfile was actually pretty fun to build. It optimizes for
both development and production, which means faster builds when you&rsquo;re
iterating and smaller images when you&rsquo;re deploying:</p>
<div class="code-block code-line-numbers" style="counter-reset: code-block 0">
    <div class="code-header language-dockerfile">
        <span class="code-title"><i class="arrow fas fa-chevron-right fa-fw" aria-hidden="true"></i></span>
        <span class="ellipses"><i class="fas fa-ellipsis-h fa-fw" aria-hidden="true"></i></span>
        <span class="copy" title="Copy to clipboard"><i class="far fa-copy fa-fw" aria-hidden="true"></i></span>
    </div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-dockerfile" data-lang="dockerfile"><span class="line"><span class="cl"><span class="c"># Multi-stage production Dockerfile</span><span class="err">
</span></span></span><span class="line"><span class="cl"><span class="k">FROM</span><span class="w"> </span><span class="s">ruby:3.3-slim</span><span class="w"> </span><span class="k">AS</span><span class="w"> </span><span class="s">base</span><span class="err">
</span></span></span><span class="line"><span class="cl"><span class="k">WORKDIR</span><span class="w"> </span><span class="s">/app</span><span class="err">
</span></span></span><span class="line"><span class="cl"><span class="k">COPY</span> Gemfile Gemfile.lock ./<span class="err">
</span></span></span><span class="line"><span class="cl"><span class="k">RUN</span> bundle install --deployment --without development test<span class="err">
</span></span></span><span class="line"><span class="cl"><span class="err">
</span></span></span><span class="line"><span class="cl"><span class="c"># Build stage for frontend assets</span><span class="err">
</span></span></span><span class="line"><span class="cl"><span class="k">FROM</span><span class="w"> </span><span class="s">node:18-alpine</span><span class="w"> </span><span class="k">AS</span><span class="w"> </span><span class="s">frontend-build</span><span class="err">
</span></span></span><span class="line"><span class="cl"><span class="k">WORKDIR</span><span class="w"> </span><span class="s">/app/frontend</span><span class="err">
</span></span></span><span class="line"><span class="cl"><span class="k">COPY</span> frontend/package*.json ./<span class="err">
</span></span></span><span class="line"><span class="cl"><span class="k">RUN</span> npm ci<span class="err">
</span></span></span><span class="line"><span class="cl"><span class="k">COPY</span> frontend/ ./<span class="err">
</span></span></span><span class="line"><span class="cl"><span class="k">RUN</span> npm run build<span class="err">
</span></span></span><span class="line"><span class="cl"><span class="err">
</span></span></span><span class="line"><span class="cl"><span class="c"># Production stage</span><span class="err">
</span></span></span><span class="line"><span class="cl"><span class="k">FROM</span><span class="w"> </span><span class="s">ruby:3.3-slim</span><span class="w"> </span><span class="k">AS</span><span class="w"> </span><span class="s">production</span><span class="err">
</span></span></span><span class="line"><span class="cl"><span class="k">WORKDIR</span><span class="w"> </span><span class="s">/app</span><span class="err">
</span></span></span><span class="line"><span class="cl"><span class="k">COPY</span> --from<span class="o">=</span>base /usr/local/bundle/ /usr/local/bundle/<span class="err">
</span></span></span><span class="line"><span class="cl"><span class="k">COPY</span> . .<span class="err">
</span></span></span><span class="line"><span class="cl"><span class="k">COPY</span> --from<span class="o">=</span>frontend-build /app/frontend/dist /app/public/frontend<span class="err">
</span></span></span><span class="line"><span class="cl"><span class="err">
</span></span></span><span class="line"><span class="cl"><span class="c"># Production configuration and startup</span><span class="err">
</span></span></span><span class="line"><span class="cl"><span class="k">RUN</span> bin/rails assets:precompile<span class="err">
</span></span></span><span class="line"><span class="cl"><span class="k">EXPOSE</span><span class="w"> </span><span class="s">8080</span><span class="err">
</span></span></span><span class="line"><span class="cl"><span class="k">CMD</span> <span class="p">[</span><span class="s2">&#34;./bin/docker-entrypoint&#34;</span><span class="p">]</span></span></span></code></pre></div></div>
<h3 id="operational-stuff-that-actually-matters">Operational stuff that actually matters</h3>
<p>Production isn&rsquo;t just about getting it running—it&rsquo;s about keeping it running.
Here&rsquo;s what I built in:</p>
<ul>
<li><strong>Health checks</strong>: Custom endpoints so I can actually tell if the app and database are happy</li>
<li><strong>Proper logging</strong>: Structured logging with correlation IDs because debugging production without context is hell</li>
<li><strong>Secrets management</strong>: Rails credentials for API keys (never commit those to git!)</li>
<li><strong>Persistent storage</strong>: Fly.io persistent volumes for the SQLite database and uploaded files</li>
<li><strong>Cross-platform builds</strong>: Docker buildx so I can build on my ARM64 Mac but deploy to AMD64 servers</li>
</ul>
<p>All this stuff matters way more than most people think when they&rsquo;re starting out.</p>
<h2 id="code-quality-and-testing-approach">Code quality and testing approach</h2>
<p>Look, here&rsquo;s the thing about AI systems: they&rsquo;re nondeterministic. The same
input can give you slightly different outputs, which makes testing&hellip;
interesting. But comprehensive testing is still absolutely crucial.</p>
<p>The test suite covers all the important bits:</p>
<div class="code-block code-line-numbers" style="counter-reset: code-block 0">
    <div class="code-header language-ruby">
        <span class="code-title"><i class="arrow fas fa-chevron-right fa-fw" aria-hidden="true"></i></span>
        <span class="ellipses"><i class="fas fa-ellipsis-h fa-fw" aria-hidden="true"></i></span>
        <span class="copy" title="Copy to clipboard"><i class="far fa-copy fa-fw" aria-hidden="true"></i></span>
    </div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ruby" data-lang="ruby"><span class="line"><span class="cl"><span class="c1"># RAG Pipeline Integration Test</span>
</span></span><span class="line"><span class="cl"><span class="no">RSpec</span><span class="o">.</span><span class="n">describe</span> <span class="s2">&#34;RAG Pipeline Integration&#34;</span><span class="p">,</span> <span class="ss">type</span><span class="p">:</span> <span class="ss">:request</span> <span class="k">do</span>
</span></span><span class="line"><span class="cl">  <span class="n">it</span> <span class="s2">&#34;processes document and answers question with citations&#34;</span> <span class="k">do</span>
</span></span><span class="line"><span class="cl">    <span class="c1"># Upload document</span>
</span></span><span class="line"><span class="cl">    <span class="n">document</span> <span class="o">=</span> <span class="n">create_document_with_file</span><span class="p">(</span><span class="s2">&#34;sample.pdf&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="c1"># Process document through pipeline</span>
</span></span><span class="line"><span class="cl">    <span class="no">ProcessDocumentJob</span><span class="o">.</span><span class="n">perform_now</span><span class="p">(</span><span class="n">document</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="n">expect</span><span class="p">(</span><span class="n">document</span><span class="o">.</span><span class="n">reload</span><span class="o">.</span><span class="n">status</span><span class="p">)</span><span class="o">.</span><span class="n">to</span> <span class="n">eq</span><span class="p">(</span><span class="s2">&#34;completed&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="n">expect</span><span class="p">(</span><span class="n">document</span><span class="o">.</span><span class="n">chunks</span><span class="o">.</span><span class="n">count</span><span class="p">)</span><span class="o">.</span><span class="n">to</span> <span class="n">be</span> <span class="o">&gt;</span> <span class="mi">0</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="c1"># Ask question</span>
</span></span><span class="line"><span class="cl">    <span class="n">retriever</span> <span class="o">=</span> <span class="no">Rag</span><span class="o">::</span><span class="no">ChunkRetriever</span><span class="o">.</span><span class="n">new</span><span class="p">(</span><span class="ss">query</span><span class="p">:</span> <span class="s2">&#34;What is the main topic?&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="n">chunks</span> <span class="o">=</span> <span class="n">retriever</span><span class="o">.</span><span class="n">call</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="c1"># Generate answer</span>
</span></span><span class="line"><span class="cl">    <span class="n">generator</span> <span class="o">=</span> <span class="no">Rag</span><span class="o">::</span><span class="no">AnswerGenerator</span><span class="o">.</span><span class="n">new</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">      <span class="ss">context</span><span class="p">:</span> <span class="n">chunks</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">      <span class="ss">question</span><span class="p">:</span> <span class="s2">&#34;What is the main topic?&#34;</span>
</span></span><span class="line"><span class="cl">    <span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="n">answer</span> <span class="o">=</span> <span class="n">generator</span><span class="o">.</span><span class="n">call</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="n">expect</span><span class="p">(</span><span class="n">answer</span><span class="o">.</span><span class="n">content</span><span class="p">)</span><span class="o">.</span><span class="n">not_to</span> <span class="n">be_empty</span>
</span></span><span class="line"><span class="cl">    <span class="n">expect</span><span class="p">(</span><span class="n">answer</span><span class="o">.</span><span class="n">citations</span><span class="p">)</span><span class="o">.</span><span class="n">not_to</span> <span class="n">be_empty</span>
</span></span><span class="line"><span class="cl">    <span class="n">expect</span><span class="p">(</span><span class="n">answer</span><span class="o">.</span><span class="n">citations</span><span class="o">.</span><span class="n">first</span><span class="o">[</span><span class="ss">:document_id</span><span class="o">]</span><span class="p">)</span><span class="o">.</span><span class="n">to</span> <span class="n">eq</span><span class="p">(</span><span class="n">document</span><span class="o">.</span><span class="n">id</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">  <span class="k">end</span>
</span></span><span class="line"><span class="cl"><span class="k">end</span></span></span></code></pre></div></div>
<p>Right now I&rsquo;ve got 222 passing tests, including:</p>
<ul>
<li>Unit tests for all the services and models</li>
<li>Integration tests that test the whole RAG pipeline end-to-end</li>
<li>API endpoint tests for every controller</li>
<li>Frontend component tests for the Vue.js interfaces</li>
</ul>
<p>That many tests might seem like overkill for a portfolio project, but when
you&rsquo;re dealing with AI systems, you need all the confidence you can get.</p>
<h2 id="key-technical-trade-offs">Key technical trade-offs</h2>
<p>Building Ragtime meant making some interesting calls. Here are the big ones:</p>
<h3 id="sqlite-vs-postgresql-for-vector-storage">SQLite vs PostgreSQL for vector storage</h3>
<ul>
<li><strong>What I chose</strong>: | SQLite + sqlite-vec</li>
<li><strong>Why</strong>: Rails 8&rsquo;s SQLite optimizations make production-scale deployment viable</li>
<li><strong>The trade-off</strong>: Native extension complexity vs single-container deployment simplicity</li>
</ul>
<p>Here&rsquo;s something that might surprise you: Rails 8 ships with SQLite optimizations
that can handle 50K concurrent users and up to 50K writes/sec. That&rsquo;s legitimate
production scale that completely changes the old assumption that SQLite is just
for small apps.</p>
<p>The real challenge wasn&rsquo;t SQLite itself—it was the sqlite-vec extension. That
required significant engineering effort to make work reliably—manual
bootstrapping, custom test support, health check verification. But with Rails 8&rsquo;s
improvements, choosing SQLite for production is actually a defensible decision
for many use cases.</p>
<h3 id="background-jobs-solid-queue-vs-sidekiq">Background jobs: Solid Queue vs Sidekiq</h3>
<ul>
<li><strong>What I chose</strong>: Solid Queue with in-process Puma integration</li>
<li><strong>Why</strong>: Rails 8 integration means no separate worker processes to manage</li>
<li><strong>The trade-off</strong>: Less isolation vs way simpler deployment</li>
</ul>
<p>This leverages Rails 8&rsquo;s new features while cutting down on operational
complexity. If this were a bigger system, I&rsquo;d probably go with dedicated
Sidekiq workers for better isolation and monitoring.</p>
<h3 id="frontend-vuejs-vs-hotwire">Frontend: Vue.js vs Hotwire</h3>
<ul>
<li><strong>What I chose</strong>: Vue.js SPA</li>
<li><strong>Why</strong>: Better UX for chat interfaces with complex state management</li>
<li><strong>The trade-off</strong>: More complex setup vs staying within the Rails ecosystem</li>
</ul>
<p>Vue.js gives you better tools for managing conversation state, real-time
updates, and interactive citations—all critical for a chat experience that
doesn&rsquo;t feel clunky.</p>
<h2 id="what-id-do-differently-at-scale">What I&rsquo;d do differently at scale</h2>
<p>Ragtime is perfectly suited for its purpose as a portfolio project that
demonstrates solid engineering and trade-offs. But if I were building this for
different production scenarios? Some decisions might change:</p>
<ol>
<li><strong>Vector extension</strong>: For really large scale, I&rsquo;d evaluate pgvector for its mature ecosystem, but with Rails 8&rsquo;s SQLite improvements, SQLite remains viable for many production workloads</li>
<li><strong>Background jobs</strong>: Dedicated Sidekiq workers for better isolation and monitoring at larger scale</li>
<li><strong>Asset serving</strong>: CDN integration for static assets</li>
<li><strong>Monitoring</strong>: Full observability stack with Prometheus, Grafana, and proper alerting</li>
<li><strong>Caching</strong>: Redis for frequent queries and expensive operations</li>
<li><strong>Security</strong>: Zero-trust architecture with proper API rate limiting</li>
</ol>
<p>The key is knowing when to optimize for simplicity and when to optimize for
scale—and with Rails 8, that scale threshold is higher than most people think.</p>
<h2 id="lessons-learned">Lessons learned</h2>
<p>Building Ragtime provided valuable insights into modern AI application development:</p>
<p><strong>Technical learnings</strong></p>
<ul>
<li>sqlite-vec is not plug-and-play—it requires significant bootstrapping and error handling</li>
<li>Rails 8 features significantly improve developer experience for API applications</li>
<li>Vector similarity tuning is crucial for RAG quality—threshold selection requires testing and iteration</li>
<li>Container cross-platform builds require careful dependency management</li>
<li>Native extensions in production containers need special handling and verification</li>
</ul>
<p><strong>Process learnings</strong></p>
<ul>
<li>Comprehensive test coverage is essential for AI systems with nondeterministic outputs</li>
<li>Documentation as a design tool prevents over-engineering</li>
<li>Simple deployment strategies accelerate iteration and learning</li>
<li>Error boundaries and graceful degradation are non-negotiable for production AI systems</li>
</ul>
<p><strong>Architecture insights</strong></p>
<ul>
<li>Modularity enables testing and iteration on complex pipelines</li>
<li>Background job patterns determine user experience quality</li>
<li>Separation of concerns simplifies AI integration</li>
<li>Production readiness requires operational thinking from day one</li>
</ul>
<h2 id="conclusion">Conclusion</h2>
<p>Ragtime demonstrates how to build modern AI-powered applications with solid engineering practices. The system showcases:</p>
<ul>
<li><strong>System architecture</strong>: Clean separation of concerns with modern Rails 8 patterns</li>
<li><strong>AI integration</strong>: Practical RAG implementation with production considerations</li>
<li><strong>Frontend development</strong>: Vue.js 3 with proper state management and UX focus</li>
<li><strong>DevOps practices</strong>: Container deployment with operational awareness</li>
<li><strong>Code quality</strong>: Comprehensive testing and maintainable code organization</li>
</ul>
<p>More importantly, it shows how to make thoughtful technology decisions based on
project constraints rather than simply following trends. Sometimes the right
solution isn&rsquo;t the most complex one—it&rsquo;s the one that solves the actual problem
efficiently and maintainably.</p>
<p><strong><a href="https://ragtime-demo.fly.dev" target="_blank" rel="noopener noreffer ">Request access to live demo</a></strong> - Password-protected demo
<strong><a href="https://github.com/grymoire7/ragtime" target="_blank" rel="noopener noreffer ">View source code</a></strong> - Complete implementation
<strong><a href="https://tracyatteberry.com/about" target="_blank" rel="noopener noreffer ">Portfolio &amp; contact</a></strong> - More projects and info</p>
<p>If you need the kind of technical leadership and engineering excellence that
balances technical chops with practical constraints to build solutions that
actually work in the real world, please reach out.</p>
]]></description></item><item><title>Ragtime - Document Q&amp;A System</title><link>https://tracyatteberry.com/portfolio/ragtime/</link><pubDate>Tue, 11 Nov 2025 00:00:00 +0000</pubDate><author>Tracy Atteberry</author><guid>https://tracyatteberry.com/portfolio/ragtime/</guid><description><![CDATA[<div class="featured-image">
                <img src="https://tracyatteberry.com/posts/ragtime/appview.png" referrerpolicy="no-referrer">
            </div><p>A sophisticated document Q&amp;A system built with Rails 8 that demonstrates modern AI application architecture. Upload PDFs, Word docs, and text files, then ask natural language questions and receive accurate answers with source citations.</p>
<h2 id="engineering-highlights">Engineering Highlights</h2>
<ul>
<li><strong>Modern Rails 8 Architecture</strong>: API-first design with Solid Queue integration</li>
<li><strong>Vector Search Innovation</strong>: SQLite-vec implementation without dedicated databases</li>
<li><strong>Production Ready</strong>: Container-based deployment with health checks</li>
<li><strong>Real-time Interface</strong>: Vue.js 3 chat interface with comprehensive testing</li>
<li><strong>Comprehensive Testing</strong>: 222 passing tests ensuring reliability</li>
</ul>
<p>This project showcases how to build production-ready AI systems with thoughtful technology choices and engineering excellence.</p>
]]></description></item></channel></rss>