<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom" xml:lang="en">
  <title>João Gilberto Saraiva (EN)</title>
  <link href="https://0jonjo.github.io/en/feed.xml" rel="self" type="application/atom+xml" />
  <link href="https://0jonjo.github.io/en/" rel="alternate" type="text/html" />
  <updated>2026-09-06T16:14:30+00:00</updated>
  <id>https://0jonjo.github.io/en/feed.xml</id>
  <author><name>João Gilberto Saraiva</name></author>
  
  
  <entry>
    <title>Polished Ruby Programming: A book about fundamentals that landed right in my AI work</title>
    <link href="https://0jonjo.github.io/blog/2026/polished-ruby-programming/" rel="alternate" type="text/html" />
    <id>https://0jonjo.github.io/blog/2026/polished-ruby-programming/</id>
    <published>2026-08-13T00:00:00+00:00</published>
    <updated>2026-08-13T00:00:00+00:00</updated>
    <summary>Reviewing the second edition of Jeremy Evans&apos; book: what it covers, and the two chapters that answered questions I had been chewing on all year while shipping AI features in Rails.</summary>
    <content type="html">&lt;p&gt;A few weeks ago, someone from &lt;a href=&quot;https://www.packtpub.com/&quot;&gt;Packt&lt;/a&gt; reached out to me on LinkedIn. They had just released the second edition of &lt;a href=&quot;https://code.jeremyevans.net/polished-ruby-programming.html&quot;&gt;Polished Ruby Programming&lt;/a&gt; and offered to send me a copy if I was willing to share my thoughts on it. You know I enjoy good tech books and work heavily with Ruby, so here we are. When the ebook arrived, I started skimming through it, expecting a solid, standard refresher on best practices.&lt;/p&gt;

&lt;p&gt;It turned out to be much more than that—and in a direction I completely didn’t expect.&lt;/p&gt;

&lt;h2 id=&quot;whats-inside&quot;&gt;What’s inside&lt;/h2&gt;

&lt;p&gt;The scope is massive. Across 428 pages, &lt;a href=&quot;https://code.jeremyevans.net/&quot;&gt;Jeremy Evans&lt;/a&gt; covers core classes, variable and method design, error handling, code formatting, library and plugin architecture, metaprogramming, DSLs, testing, refactoring, deprecation, design patterns, concurrency, static versus duck typing, and optimization.&lt;/p&gt;

&lt;p&gt;Evans is a Ruby committer and the maintainer of &lt;a href=&quot;https://roda.jeremyevans.net/&quot;&gt;Roda&lt;/a&gt; and &lt;a href=&quot;https://sequel.jeremyevans.net/&quot;&gt;Sequel&lt;/a&gt;, so his opinions come with receipts. What I appreciated most is that almost nothing is handed down as an absolute rule. Even the SOLID principles arrive with a warning against applying them dogmatically. Every technique—a plugin system, a DSL, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;method_missing&lt;/code&gt;, static types—is presented as a trade-off. You are expected to decide for yourself, but you get a clear description of what each choice will cost you later. That’s a rarity in programming books.&lt;/p&gt;

&lt;p&gt;A lot of the content is highly valuable on its own. But two chapters genuinely stopped me in my tracks because they answered questions I’d been wrestling with all year while building AI features into production Rails apps.&lt;/p&gt;

&lt;p&gt;Here are my two highlights.&lt;/p&gt;

&lt;h2 id=&quot;first-highlight-breaking-circuits&quot;&gt;First highlight: Breaking circuits&lt;/h2&gt;

&lt;p&gt;This one hit close to home.&lt;/p&gt;

&lt;p&gt;Chapter 5 is all about error handling: return values versus exceptions, designing APIs that are hard to misuse, fail-open versus fail-closed, retrying transient errors with backoff, and shaping exception hierarchies. Deep in the chapter, Evans builds a small circuit breaker from scratch: if a service fails three times in a minute, you stop calling it for a while, preventing every request from queuing up behind a provider that’s already down.&lt;/p&gt;

&lt;p&gt;Then he closes the section with the exact kind of pragmatic advice I wish more books gave:&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;“If you need a circuit breaker for production code, you should probably use one of the many circuit breaker gems for Ruby instead of trying to implement a circuit breaker yourself, unless you have specific requirements not handled by an existing gem.”&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That is the exact conclusion I reached last year when writing &lt;a href=&quot;https://jetrockets.com/blog/building-a-resilient-ai-client-in-ruby-with-stoplight-and-ruby_llm&quot;&gt;Building a Resilient AI Client in Ruby with Stoplight and ruby_llm&lt;/a&gt; for the &lt;a href=&quot;https://www.linkedin.com/company/jetrockets/&quot;&gt;JetRockets&lt;/a&gt; blog. The goal was to detect when a model provider is failing, trip the circuit, fail over to a backup model, and keep the conversation history intact. I went with &lt;a href=&quot;https://github.com/bolshakov/stoplight&quot;&gt;Stoplight&lt;/a&gt; rather than rolling my own, for the exact reason Evans points out.&lt;/p&gt;

&lt;p&gt;Still, reading the from-scratch implementation was incredibly useful. You end up understanding exactly what the gem handles for you, and more importantly, what it doesn’t. This chapter also gave me an insight my own article was missing: separating permanent from transient errors is a structural decision that belongs in your exception hierarchy, not in a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;rescue&lt;/code&gt; clause bolted on later. A rate limit and a malformed request are fundamentally different failures, and only one of them deserves a retry. Until now, I’d been treating both simply as “the provider is having a bad day.”&lt;/p&gt;

&lt;h2 id=&quot;second-highlight-only-mock-what-you-cannot-control&quot;&gt;Second highlight: Only mock what you cannot control&lt;/h2&gt;

&lt;p&gt;The testing chapter drops this rule:&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;“If you must mock, only mock what you cannot control, such as calls to external APIs.”&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;For standard HTTP clients, most of us just nod and move on. But try applying that to a feature built around a Large Language Model, and it becomes the definitive answer to a testing problem I’d been circling for weeks.&lt;/p&gt;

&lt;p&gt;An AI model isn’t a normal dependency. It’s slow, it costs money on every call, and—the part that really wrecks a test suite—it’s non-deterministic. Same input, different output. This usually leaves you with two bad options. Option A: Mock the entire pipeline, turning your tests into pure theater. They pass forever, asserting only that your own stubs were called, while real regressions walk straight through to production. Option B: Mock nothing, leaving you with a test suite that is flaky, painfully slow, and bills you on every CI run.&lt;/p&gt;

&lt;p&gt;Evans’ rule puts the boundary exactly where it belongs. The model provider API is the &lt;em&gt;only&lt;/em&gt; thing I cannot control, so it is the &lt;em&gt;only&lt;/em&gt; thing I stub. Everything else runs for real: prompt assembly, the batching loop over long content, the context management that keeps terminology consistent across chunks, the state machine of the background job, and saving the final result. If I break the batching logic, a test fails. That’s exactly what tests are for.&lt;/p&gt;

&lt;p&gt;I also learned two hard lessons by getting this wrong initially. First, stubbing the client isn’t enough—you also have to pin the exact payload it hands back. Otherwise, the non-determinism just moves one layer down and your assertions start drifting. Second, this testing style severely punishes heavy inline setup. Once I moved the sample AI context into fixtures, the specs became readable and just as fast as any other model test in our suite.&lt;/p&gt;

&lt;p&gt;The same chapter states the trade-off plainly, and it’s a quote I’d gladly put on my wall:&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;“Slower, reliable tests are better than fast tests that break without reason (false positives) and don’t catch actual breakage (false negatives).”&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2 id=&quot;zero-hits&quot;&gt;“Zero” hits&lt;/h2&gt;

&lt;p&gt;Out of curiosity, I eventually ran a search across the entire ebook for the terms “LLM”, “AI”, and “ChatGPT”.&lt;/p&gt;

&lt;p&gt;I found exactly one hit: a passing sentence about how users search for library documentation. As a guide for building AI features? Zero hits.&lt;/p&gt;

&lt;p&gt;And that is the part I keep coming back to. This isn’t a book about AI, and that is precisely why it helped so much. A model provider is slow, occasionally unavailable, rate-limited, non-deterministic, and entirely outside your control. We already have decades of solid engineering practices for handling dependencies that behave exactly like that.&lt;/p&gt;

&lt;p&gt;The only genuinely new thing is that AI failures are more convincing. A broken API simply returns a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;503&lt;/code&gt;. A broken model returns a fluent, confident, and nicely formatted wrong answer.&lt;/p&gt;

&lt;p&gt;The architectural boundary is exactly where it always was. It just matters a lot more now.&lt;/p&gt;

&lt;h2 id=&quot;who-should-read-it&quot;&gt;Who should read it&lt;/h2&gt;

&lt;p&gt;Intermediate and senior Ruby developers, without hesitation. If you’re still learning the syntax, learn it somewhere else first—this book assumes you already write Ruby and want to write it better.&lt;/p&gt;

&lt;p&gt;And if you maintain any long-lived applications, Chapter 12 earns the read all on its own. Evans argues that features should be treated as liabilities rather than assets, since every single one of them carries a permanent maintenance cost. Therefore, removing a feature is a net gain. It’s the exact same conviction I wrote about a while back regarding &lt;a href=&quot;/blog/2024/remocao-codigo/&quot;&gt;removing legacy code&lt;/a&gt;, just stated much more sharply than I managed at the time.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Disclosure: Packt sent me a review copy of the book. The opinions here are entirely my own.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;img width=&quot;640&quot; height=&quot;333&quot; alt=&quot;HannaBarakat -CambridgeDiversity FundPas(t)imesin the Computer Lab -640x333&quot; src=&quot;https://github.com/user-attachments/assets/489984c5-0dbc-40a5-af1e-f1838b7f41c7&quot; /&gt;&lt;/p&gt;

&lt;p&gt;Image: “Pas(t)imes in the Computer Lab”, Hanna Barakat &amp;amp; Cambridge Diversity Fund. &lt;a href=&quot;https://betterimagesofai.org/images?artist=HannaBarakat&amp;amp;title=Pas%28t%29imesintheComputerLab&quot;&gt;Better Images of AI, Creative Commons 4.0&lt;/a&gt;&lt;/p&gt;
</content>
  </entry>
  
  <entry>
    <title>Five resources and one strategy for Claude Certification</title>
    <link href="https://0jonjo.github.io/blog/2026/studing-for-claude-architect/" rel="alternate" type="text/html" />
    <id>https://0jonjo.github.io/blog/2026/studing-for-claude-architect/</id>
    <published>2026-07-06T00:00:00+00:00</published>
    <updated>2026-07-06T00:00:00+00:00</updated>
    <summary>Preparing for the Claude Architect Foundations certification: five essential study resources, a dynamic learning strategy, and the mindset needed to pass the exam.</summary>
    <content type="html">&lt;p&gt;For the past month, I’ve been preparing for the &lt;a href=&quot;https://anthropic-partners.skilljar.com/page/partner-certifications&quot;&gt;Claude Architect Foundations&lt;/a&gt; certification. The exam validates your knowledge of the Claude AI model, its architecture, and the best practices for building robust applications. Since any company aiming to complete the &lt;a href=&quot;https://claude.com/partners&quot;&gt;Claude Partner Network program&lt;/a&gt; needs a minimum number of certified architects, my team at &lt;a href=&quot;https://www.linkedin.com/company/jetrockets/&quot;&gt;JetRockets&lt;/a&gt; and I are preparing to take it soon. The exam is not free, so the goal is to pass on the first attempt.&lt;/p&gt;

&lt;p&gt;The exam consists of 60 multiple-choice questions with a 120-minute limit. The syllabus is divided into five main areas: Agentic Architecture &amp;amp; Orchestration (27%), Tool Design &amp;amp; MCP Integration (18%), Claude Code Configuration &amp;amp; Workflows (20%), Prompt Engineering &amp;amp; Structured Output (20%), and Context Management &amp;amp; Reliability (15%). It tests both theoretical knowledge and practical skills — meaning hands-on experience with Claude and its ecosystem is essential.&lt;/p&gt;

&lt;p&gt;Anthropic provides a &lt;a href=&quot;https://anthropic-partners.skilljar.com/page/partner-certifications&quot;&gt;study guide&lt;/a&gt; with recommended resources, including the official documentation and sample questions. Additionally, there are several online courses breaking down the objectives. I did some research, exchanged materials with colleagues also studying for the exam, and here are the resources I found most useful:&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;&lt;a href=&quot;https://anthropic-partners.skilljar.com/collections&quot;&gt;Official Antrophic courses&lt;/a&gt;: video, texts, and quizzes organized into specific learning paths, plus free courses on Claude and AI in general.&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://anthropic-partners.skilljar.com/page/partner-certifications&quot;&gt;Claude Architect Foundations Study Guide&lt;/a&gt;: the official reference for example questions, in and out-of-scope topics, and exam preparation details.&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://www.claude-certification-guide.com/&quot;&gt;Claude Certification Guide&lt;/a&gt;: a non-official website that I’m using to test my knowledge with mock exams and community discussions.&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://www.youtube.com/watch?v=reDRM0tqhNs&quot;&gt;Claude Certified Architect - Foundations by freeCodeCamp.org&lt;/a&gt;: an excellent crash course video to complement the official materials and help you prepare for the exam.&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://github.com/paullarionov/claude-certified-architect&quot;&gt;Claude Certified Architect Study Guide (paullarionov)&lt;/a&gt;: an open-source GitHub repository with a curated guide and additional study materials.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;To prepare for the exam, I am not following a rigid, linear plan. Instead, I am dynamically alternating between the four required official courses, reading external articles, and practicing with mock questions. This flexible strategy is very similar to the one I used to pass the Google Associate Cloud Engineer certification some time ago.&lt;/p&gt;

&lt;p&gt;That being said, there is a distinct difference between these two exams. The Google Cloud certification is extremely broad — it covers a massive landscape of services, expecting you to understand many topics without necessarily deep-diving into all of them. The Anthropic exam is the opposite. It has fewer main themes, but it requires you to go much deeper into concepts like agentic architecture, prompt engineering, and context management.&lt;/p&gt;

&lt;p&gt;Despite this contrast, both exams share the same core philosophy: they are highly contextual. You won’t see questions asking for simple definitions. Instead, they give you a real-world problem and ask for the most appropriate solution.&lt;/p&gt;

&lt;p&gt;For example, a typical question for the Anthropic exam looks something like this:&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;&lt;strong&gt;Example Question:&lt;/strong&gt;
&lt;em&gt;You are designing a customer support agent using Claude that needs to check order statuses from a secure internal database. Which approach is the best way to handle this while maintaining security and efficiency?&lt;/em&gt;&lt;/p&gt;

  &lt;p&gt;A) Provide the database credentials in the system prompt so Claude can write and execute SQL queries directly.
B) Provide a massive CSV export of the database in the context window.
C) Create a Model Context Protocol (MCP) server that exposes a specific &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;get_order_status&lt;/code&gt; tool, passing only the necessary parameters.
D) Use a generic web-search tool to scrape the internal dashboard.&lt;/p&gt;

  &lt;p&gt;&lt;em&gt;(The correct answer is C, because it uses the proper architectural pattern for tools and keeps credentials secure outside the model).&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;This means that simply reading the docs is not enough. You really need to think about the situation and understand &lt;em&gt;why&lt;/em&gt; you are choosing a specific architectural pattern.&lt;/p&gt;

&lt;p&gt;In the end, preparing for the Claude Architect Foundations is less about cramming documentation and more about adopting an agentic mindset — putting yourself in the shoes of an AI architect who is constantly balancing efficiency, security, and context limits to solve real-world constraints.&lt;/p&gt;

&lt;p&gt;&lt;img width=&quot;100%&quot; alt=&quot;Weaving wires into a computer monitor&quot; src=&quot;https://github.com/user-attachments/assets/d95a655f-d120-4c67-8b0b-ade52a251026&quot; /&gt;&lt;/p&gt;

&lt;p&gt;Image: &lt;em&gt;Weaving Wires 1&lt;/em&gt; by Hanna Barakat / AIxDESIGN / &lt;a href=&quot;https://betterimagesofai.org&quot;&gt;Better Images of AI&lt;/a&gt;, Creative-Commons License.&lt;/p&gt;
</content>
  </entry>
  
  <entry>
    <title>From Backend to MLOps: a runner’s road to business rules, deployments and monitoring</title>
    <link href="https://0jonjo.github.io/blog/2026/from-backend-to-mlops/" rel="alternate" type="text/html" />
    <id>https://0jonjo.github.io/blog/2026/from-backend-to-mlops/</id>
    <published>2026-06-06T00:00:00+00:00</published>
    <updated>2026-06-06T00:00:00+00:00</updated>
    <summary>The intersection of Backend Development and MLOps, a deep dive into applying DevOps principles, Design Thinking, and deployment strategies to machine learning systems using the Calcpace app.</summary>
    <content type="html">&lt;p&gt;By the end of last semester, I was preparing for a deep dive into deployments, monitoring, maintenance, and the operational crossroads where Backend meets DevOps. My goal was to bridge my previous cloud experience—especially with GCP—with the modern stack we rely on at &lt;a href=&quot;https://www.linkedin.com/company/jetrockets/&quot;&gt;JetRockets&lt;/a&gt;, such as &lt;a href=&quot;https://kamal-deploy.org/&quot;&gt;Kamal&lt;/a&gt;, &lt;a href=&quot;https://www.appsignal.com/&quot;&gt;AppSignal&lt;/a&gt;, &lt;a href=&quot;https://www.digitalocean.com/&quot;&gt;DigitalOcean&lt;/a&gt;, and &lt;a href=&quot;https://www.cloudflare.com/&quot;&gt;Cloudflare&lt;/a&gt;. When the syllabus dropped for my second semester in the &lt;a href=&quot;https://pes.imd.ufrn.br/pes/index&quot;&gt;Artificial Intelligence program at UFRN&lt;/a&gt;, I was thrilled to spot MLOps (Machine Learning Operations) on the list. While the name might sound niche, &lt;a href=&quot;https://en.wikipedia.org/wiki/MLOps&quot;&gt;MLOps&lt;/a&gt; is essentially the application of strict software engineering and DevOps principles to machine learning, ensuring models aren’t just local experiments, but are effectively deployed, monitored, and maintained in production.&lt;/p&gt;

&lt;p&gt;&lt;img width=&quot;640&quot; height=&quot;460&quot; alt=&quot;image&quot; src=&quot;https://github.com/user-attachments/assets/ae6da9b6-b918-4b44-a468-b3cd89dcf641&quot; /&gt;&lt;/p&gt;

&lt;p&gt;Going in, I fully expected we’d immediately dive into server clusters, CI/CD pipelines, and configuration files. I’m glad I was wrong. The course actually kicked off with &lt;a href=&quot;https://online.hbs.edu/blog/post/what-is-design-thinking&quot;&gt;Design Thinking&lt;/a&gt;. It forced us to step back from the terminal and discuss the importance of understanding the business problem, the user, and the real-world context before throwing infrastructure at a problem.&lt;/p&gt;

&lt;p&gt;To bring these concepts out of the classroom and into reality, I needed a concrete project. I chose running analysis—a domain I know deeply after nine years in the sport. While I already had the core math mapped out in &lt;a href=&quot;https://rubygems.org/gems/calcpace&quot;&gt;an open-source gem&lt;/a&gt;, I wanted to build a complete system to test these operational practices in the wild. The ultimate result of this effort is &lt;a href=&quot;https://calcpace.app/&quot;&gt;Calcpace&lt;/a&gt;. Recently shipped to production, it is a fully-fledged web application offering running conversions, VO2 max estimates, race calendars, and pace predictions in 15 different languages. Under the hood, it serves as the perfect sandbox for merging backend and ML operations, powered by Ruby on Rails, PostgreSQL, Redis, Sidekiq, Kamal, AppSignal, DigitalOcean, and Cloudflare.&lt;/p&gt;

&lt;p&gt;Here are some highlights of how the MLOps principles we studied mapped directly onto this project:&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;
    &lt;p&gt;&lt;strong&gt;Design Thinking and the Cost of an Error&lt;/strong&gt;: In ML, optimizing a mathematical metric means nothing if it doesn’t solve a user’s problem. For Calcpace, this meant studying existing market solutions and identifying exactly where runners struggle with pace and VO2 max calculations. It echoed the principles of &lt;a href=&quot;https://www.domainlanguage.com/ddd/blue-book/&quot;&gt;Domain-Driven Design, by Eric Evans&lt;/a&gt;—you have to align your software architecture with the business reality. In a running app, a ‘bad prediction’ isn’t just a UI bug; it could mean an athlete pacing a marathon entirely wrong and hitting the wall.&lt;/p&gt;
  &lt;/li&gt;
  &lt;li&gt;
    &lt;p&gt;&lt;strong&gt;TDD as a Pipeline Foundation&lt;/strong&gt;: We revisited Test-Driven Development not just as a coding habit, but as a critical safeguard. Automated testing is the backbone of both standard software engineering and MLOps. In ML, you don’t just test if the code compiles; you test if the data schema is valid and if the model’s baseline performance holds. Solidifying the test suite for Calcpace was essential to ensure the core math remained completely reliable as new features were added.&lt;/p&gt;
  &lt;/li&gt;
  &lt;li&gt;
    &lt;p&gt;&lt;strong&gt;The “Last Mile” of Model Serving with Rails&lt;/strong&gt;: Ruby on Rails might not be the default choice for training models, but it is an exceptional tool for the “last mile”—delivering ML insights to the end user. My focus here isn’t on the mathematical training itself, but on building a robust, containerized environment using Kamal. By treating the complex logic (currently encapsulated in the Ruby Gem) as a decoupled service within a Rails/Docker stack, I’m ensuring that when we swap a heuristic for a complex predictive model, the infrastructure won’t blink. Furthermore, managing the latency of serving these predictions within the standard Rails request-response cycle is the real engineering challenge that separates a simple script from a production-ready system.&lt;/p&gt;
  &lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;img width=&quot;772&quot; height=&quot;462&quot; alt=&quot;Screenshot from 2026-06-06 17-35-34&quot; src=&quot;https://github.com/user-attachments/assets/c7ee6436-7613-49a5-a995-5c9599452fcf&quot; /&gt;&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;
    &lt;p&gt;&lt;strong&gt;CI/CD and Workflow Automation&lt;/strong&gt;: Continuous Integration and Deployment are non-negotiable. While the course explored ML-specific tracking tools like Weights &amp;amp; Biases alongside GitHub Actions, I focused on mastering the latter for workflow automation. Every time a new version is released, GitHub Actions runs the test suite and seamlessly triggers a Kamal deployment. It’s a closed loop that ensures the live application is always perfectly synced with the repository.&lt;/p&gt;
  &lt;/li&gt;
  &lt;li&gt;
    &lt;p&gt;&lt;strong&gt;Beyond Uptime (Monitoring Data Sanity)&lt;/strong&gt;: The course introduced the LGTM stack (Looker, Grafana, Tempo, Mimir) for deep observability, which is standard for tracking infrastructure and data drift in ML. For the Calcpace ecosystem, AppSignal paired with Cloudflare serves as our equivalent. It provides fantastic out-of-the-box APM, tracking request latency and error rates. But an MLOps mindset demands more: we must monitor data sanity. If a runner’s VO2 Max prediction suddenly spikes to 99 due to a malformed input, our APM and strict validations help us catch that “silent failure” before it ruins the user’s trust.&lt;/p&gt;
  &lt;/li&gt;
  &lt;li&gt;
    &lt;p&gt;&lt;strong&gt;Lean Infrastructure and Domain Management&lt;/strong&gt;: We zoomed out to look at infrastructure as a whole—managing VMs, securing CI/CD secrets, and configuring scalable storage. In practice, this also became an exercise in cost efficiency. By leveraging Kamal’s container orchestration, I managed to pack the entire production stack—the Rails app, PostgreSQL database, and Sidekiq for async jobs—into a single $6/month DigitalOcean droplet without sacrificing the ability to scale later. Furthermore, establishing the production environment meant handling the custom &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;.app&lt;/code&gt; domain registration and managing DNS and strict SSL configurations through Cloudflare, alongside setting up Cloudflare R2 for cost-effective, S3-compatible object storage.&lt;/p&gt;
  &lt;/li&gt;
  &lt;li&gt;
    &lt;p&gt;&lt;strong&gt;DevOps in an AI-Scraped World&lt;/strong&gt;: Finally, exposing an API or web app today means dealing with a massive influx of automated traffic. I configured custom WAF (Web Application Firewall) rules to block malicious actors while ensuring the site remains fully accessible to legitimate crawlers. This balance is vital for SEO and for ensuring the content is properly indexed by modern AI search bots.&lt;/p&gt;
  &lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Ultimately, the final deliverable for this discipline isn’t just a static repository; it’s a living, breathing product. By building &lt;a href=&quot;https://calcpace.app/&quot;&gt;Calcpace&lt;/a&gt; — and seeing its core open-source gem cross the 10,000 downloads milestone — it reinforced my conviction that a real challenge of a production-ready system lies in reliability, deployment pipelines, and operational monitoring.&lt;/p&gt;

&lt;h3 id=&quot;further-reading&quot;&gt;Further Reading&lt;/h3&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;&lt;a href=&quot;https://github.com/adaj/mlops-2026-1&quot;&gt;UFRN MLOps Course Repository&lt;/a&gt;&lt;/strong&gt;: The official repository from the MLOps discipline at UFRN. It contains the reference architecture used in our classes, including Python model serving and observability with Kibana.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;&lt;a href=&quot;https://jetrockets.com/blog/how-to-use-basecamp-s-kamal-with-aws-and-github&quot;&gt;How to use Basecamp’s Kamal with AWS and GitHub&lt;/a&gt;&lt;/strong&gt;: A practical guide by JetRockets’ CTO, Igor Alexandrov, on orchestrating container deployments with Kamal and GitHub Actions.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;&lt;a href=&quot;https://martinfowler.com/articles/cd4ml.html&quot;&gt;Continuous Delivery for Machine Learning (CD4ML)&lt;/a&gt;&lt;/strong&gt;: core article on applying standard CI/CD and DevOps practices to ML systems.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;&lt;a href=&quot;https://www.domainlanguage.com/ddd/&quot;&gt;Domain-Driven Design by Eric Evans&lt;/a&gt;&lt;/strong&gt;: The foundational book on aligning software models with business reality.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;img width=&quot;1024&quot; height=&quot;1024&quot; alt=&quot;image&quot; src=&quot;https://github.com/user-attachments/assets/cce45f91-8dce-41e1-92e1-2480d1f9dbd2&quot; /&gt;&lt;/p&gt;

&lt;p&gt;Images: “Running down the shadows” of Vinoth Chandar. &lt;a href=&quot;https://openverse.org/image/b85eb957-92f7-43b9-8560-3922b2beb361&quot;&gt;Open Verse, Creative Commons 2.0&lt;/a&gt; and MLops (&lt;a href=&quot;https://en.wikipedia.org/wiki/MLOps&quot;&gt;Wikipedia&lt;/a&gt;)&lt;/p&gt;
</content>
  </entry>
  
  <entry>
    <title>Migrating Rails views to Jet UI: a guide with ViewComponent and Tailwind v4</title>
    <link href="https://0jonjo.github.io/blog/2026/jet-ui-migration/" rel="alternate" type="text/html" />
    <id>https://0jonjo.github.io/blog/2026/jet-ui-migration/</id>
    <published>2026-05-06T00:00:00+00:00</published>
    <updated>2026-05-06T00:00:00+00:00</updated>
    <summary>Ar real-world experience migrating Calcpace to Jet UI, a ViewComponent-based library built on Tailwind CSS v4. How we standardized our UI, leveraged generators, and solved production gotchas.</summary>
    <content type="html">&lt;p&gt;Last week, I wrote an article about migrating Calcpace views to use jet_ui to &lt;a href=&quot;https://jetrockets.com/blog/migrating-rails-views-to-jet-ui-a-real-world-guide-with-viewcomponent-and-tailwind-v4&quot;&gt;JetRockets blog&lt;/a&gt;. Here is the full article:&lt;/p&gt;

&lt;p&gt;Maintaining a consistent UI in a growing Rails application is a classic challenge. We often start with the best intentions, clean HTML and utility classes, but as the app scales, we inevitably fall into “UI boilerplate fatigue.” Whether it’s copy-pasting the same “Avatar with initials” logic across dozens of views or reinventing the wheel for every animated toast, this duplication slowly erodes our development velocity.&lt;/p&gt;

&lt;p&gt;To solve this, I recently migrated &lt;a href=&quot;https://calcpace.app&quot;&gt;Calcpace&lt;/a&gt;, a running and cycling tracker built with Rails 8, to &lt;a href=&quot;https://github.com/jetrockets/jet_ui&quot;&gt;jet_ui&lt;/a&gt;, JetRockets’ component library. In this article, I’ll show you how we used Calcpace as a real-world playground to standardize our interface, leverage Tailwind CSS v4, and solve the production “gotchas” that often come with gem-based assets.&lt;/p&gt;

&lt;h2 id=&quot;the-problem-copy-paste-debt&quot;&gt;The Problem: Copy-Paste Debt&lt;/h2&gt;

&lt;p&gt;Before the migration, our UI was functional but repetitive. Handling profile pictures required manual conditional logic for avatars and initials in every view:&lt;/p&gt;

&lt;div class=&quot;language-erb highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c&quot;&gt;&amp;lt;%# Before: UI logic leaking into views %&amp;gt;&lt;/span&gt;
&lt;span class=&quot;cp&quot;&gt;&amp;lt;%&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;current_profile&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;avatar&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;attached?&lt;/span&gt; &lt;span class=&quot;cp&quot;&gt;%&amp;gt;&lt;/span&gt;
  &lt;span class=&quot;cp&quot;&gt;&amp;lt;%=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;image_tag&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;current_profile&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;avatar&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;variant&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;ss&quot;&gt;resize_to_fill: &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;24&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;24&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]),&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;class: &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;w-6 h-6 rounded-full&quot;&lt;/span&gt; &lt;span class=&quot;cp&quot;&gt;%&amp;gt;&lt;/span&gt;
&lt;span class=&quot;cp&quot;&gt;&amp;lt;%&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;else&lt;/span&gt; &lt;span class=&quot;cp&quot;&gt;%&amp;gt;&lt;/span&gt;
  &lt;span class=&quot;nt&quot;&gt;&amp;lt;div&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;class=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;w-6 h-6 rounded-full bg-gray-200 flex items-center justify-center text-xs&quot;&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;&amp;gt;&lt;/span&gt;
    &lt;span class=&quot;cp&quot;&gt;&amp;lt;%=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;current_profile&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;initials&lt;/span&gt; &lt;span class=&quot;cp&quot;&gt;%&amp;gt;&lt;/span&gt;
  &lt;span class=&quot;nt&quot;&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
&lt;span class=&quot;cp&quot;&gt;&amp;lt;%&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;end&lt;/span&gt; &lt;span class=&quot;cp&quot;&gt;%&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;&lt;img width=&quot;615&quot; height=&quot;344&quot; alt=&quot;image&quot; src=&quot;https://github.com/user-attachments/assets/044907e3-1f49-434f-b929-0edc1e33f210&quot; /&gt;&lt;/p&gt;

&lt;p&gt;This “inline Tailwind” approach lacks a single source of truth. Changing a border radius meant a tedious search-and-replace across the entire codebase.&lt;/p&gt;

&lt;h2 id=&quot;the-strategy-incremental-migration&quot;&gt;The Strategy: Incremental Migration&lt;/h2&gt;

&lt;p&gt;One of the biggest concerns when adopting a component library is the “big bang” rewrite. Do you have to change every view at once? Absolutely not.&lt;/p&gt;

&lt;p&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;jet_ui&lt;/code&gt; is designed for incremental adoption. In Calcpace, we didn’t touch our legacy views initially. We started by replacing the most “noisy” elements—flashes and avatars—and then moved to complex data tables. You can have a page powered entirely by &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;jet_ui&lt;/code&gt; components sitting right next to a legacy ERB view using plain Tailwind utility classes. They coexist perfectly because &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;jet_ui&lt;/code&gt; respects your existing Tailwind configuration while providing the structure of ViewComponent. This removes the psychological barrier of migration: you can improve your app one component at a time.&lt;/p&gt;

&lt;h2 id=&quot;requirements-and-plugging-in-jet-ui&quot;&gt;Requirements and Plugging in Jet UI&lt;/h2&gt;

&lt;p&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;jet_ui&lt;/code&gt; is built on &lt;a href=&quot;https://viewcomponent.org&quot;&gt;ViewComponent&lt;/a&gt; and &lt;a href=&quot;https://tailwindcss.com&quot;&gt;Tailwind CSS v4&lt;/a&gt;. It follows a “Rails-native” philosophy, leveraging the latest tools in the ecosystem:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Ruby &amp;gt;= 3.0 and Rails &amp;gt;= 7.0 (Calcpace runs on Rails 8.1).&lt;/li&gt;
  &lt;li&gt;Tailwind CSS v4 (via &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;tailwindcss-rails &amp;gt;= 4.x&lt;/code&gt;).&lt;/li&gt;
  &lt;li&gt;Stimulus and Turbo (standard in modern Rails).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In your &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Gemfile&lt;/code&gt;:&lt;/p&gt;

&lt;div class=&quot;language-ruby highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;n&quot;&gt;gem&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;view_component&quot;&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;gem&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;jet_ui&quot;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h2 id=&quot;the-power-of-generators&quot;&gt;The Power of Generators&lt;/h2&gt;

&lt;p&gt;One of &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;jet_ui&lt;/code&gt;’s standout features is its suite of generators. They don’t just copy files; they wire up your entire application.&lt;/p&gt;

&lt;h3 id=&quot;jet_uiinstall&quot;&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;jet_ui:install&lt;/code&gt;&lt;/h3&gt;
&lt;p&gt;This sets up the library in your application (CSS + JS). It is safe to re-run after gem upgrades, as already-configured steps are automatically skipped:&lt;/p&gt;
&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;rails generate jet_ui:install
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h3 id=&quot;jet_uieject&quot;&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;jet_ui:eject&lt;/code&gt;&lt;/h3&gt;
&lt;p&gt;If you need to customize a component beyond standard options, you can “eject” it. This copies the Ruby class, ERB template, and Stimulus controller directly into your &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;app/components/jet_ui/&lt;/code&gt; folder. The ejected files take precedence automatically.&lt;/p&gt;

&lt;p&gt;You can eject multiple components at once and use flags to keep your codebase lean:&lt;/p&gt;
&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c&quot;&gt;# Eject button, card, and flash&lt;/span&gt;
rails generate jet_ui:eject btn card flash

&lt;span class=&quot;c&quot;&gt;# Skip specific files if you only want to customize the template&lt;/span&gt;
rails generate jet_ui:eject btn &lt;span class=&quot;nt&quot;&gt;--skip-test&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;--skip-preview&lt;/span&gt;
rails generate jet_ui:eject flash &lt;span class=&quot;nt&quot;&gt;--skip-javascript&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h2 id=&quot;production-readiness-the-vendoring-strategy&quot;&gt;Production Readiness: The Vendoring Strategy&lt;/h2&gt;

&lt;p&gt;While the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;install&lt;/code&gt; generator works perfectly for local development by pointing to the gem’s path, production environments like Docker or CI require a more portable approach. To ensure a deterministic build and clean logs, we adopt a Vendoring strategy. Instead of relying on absolute filesystem paths that change between environments, we copy the CSS directly into the repository but place it outside the standard Rails asset search path to avoid duplicate serving.&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;Vendor the assets programmatically:
    &lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nb&quot;&gt;mkdir&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;-p&lt;/span&gt; vendor/stylesheets
&lt;span class=&quot;nb&quot;&gt;cp&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;-r&lt;/span&gt; &lt;span class=&quot;si&quot;&gt;$(&lt;/span&gt;bundle show jet_ui&lt;span class=&quot;si&quot;&gt;)&lt;/span&gt;/app/assets/stylesheets/&lt;span class=&quot;k&quot;&gt;*&lt;/span&gt; vendor/stylesheets/
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;    &lt;/div&gt;
  &lt;/li&gt;
  &lt;li&gt;Update your Tailwind source file:
    &lt;div class=&quot;language-css highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c&quot;&gt;/* app/assets/tailwind/application.css */&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;@import&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;&quot;tailwindcss&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;@import&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;&quot;../../../vendor/stylesheets/jet_ui.css&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;    &lt;/div&gt;
  &lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Why this approach?&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;Portability: The build works in Docker, CI, and any developer’s machine without modifications.&lt;/li&gt;
  &lt;li&gt;Clean Logs: By placing files in &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;vendor/stylesheets&lt;/code&gt; (which Propshaft ignores by default), the asset pipeline won’t try to serve individual component files (like &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;popover.css&lt;/code&gt;). This prevents the “404 Not Found” noise in production logs for files already bundled into your main CSS.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;img width=&quot;734&quot; height=&quot;312&quot; alt=&quot;image&quot; src=&quot;https://github.com/user-attachments/assets/839cb7b0-a01c-4e3b-9765-2cdf2f5176dc&quot; /&gt;&lt;/p&gt;

&lt;h2 id=&quot;customizing-the-theme-with-tailwind-v4&quot;&gt;Customizing the Theme with Tailwind v4&lt;/h2&gt;

&lt;p&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;jet_ui&lt;/code&gt; uses modern CSS variables. Instead of overriding thousands of utility classes, you update the theme variables in your CSS source:&lt;/p&gt;

&lt;div class=&quot;language-css highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;@theme&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;py&quot;&gt;--accent-hue&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;m&quot;&gt;163&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;      &lt;span class=&quot;c&quot;&gt;/* Calcpace Emerald */&lt;/span&gt;
  &lt;span class=&quot;py&quot;&gt;--accent-chroma&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;m&quot;&gt;0.2&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;py&quot;&gt;--accent-lightness&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;m&quot;&gt;0.52&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Every component, from buttons to focus rings, will now use your custom palette.&lt;/p&gt;

&lt;h2 id=&quot;replacing-the-noise-real-world-examples&quot;&gt;Replacing the Noise: Real-World Examples&lt;/h2&gt;

&lt;h3 id=&quot;interactive-form-groups&quot;&gt;Interactive Form Groups&lt;/h3&gt;
&lt;p&gt;We used &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;jet_ui.group&lt;/code&gt; to standardize selectors. For our activity unit toggle (KM/MI), the component handles the styling and layout, leaving us with a clean DSL:&lt;/p&gt;

&lt;div class=&quot;language-erb highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;cp&quot;&gt;&amp;lt;%=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;jet_ui&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;group&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;do&lt;/span&gt; &lt;span class=&quot;cp&quot;&gt;%&amp;gt;&lt;/span&gt;
  &lt;span class=&quot;cp&quot;&gt;&amp;lt;%=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;form&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;radio_button&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;:unit&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;km&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;checked: &lt;/span&gt;&lt;span class=&quot;kp&quot;&gt;true&lt;/span&gt; &lt;span class=&quot;cp&quot;&gt;%&amp;gt;&lt;/span&gt;
  &lt;span class=&quot;cp&quot;&gt;&amp;lt;%=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;form&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;radio_button&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;:unit&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;mi&quot;&lt;/span&gt; &lt;span class=&quot;cp&quot;&gt;%&amp;gt;&lt;/span&gt;
&lt;span class=&quot;cp&quot;&gt;&amp;lt;%&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;end&lt;/span&gt; &lt;span class=&quot;cp&quot;&gt;%&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h3 id=&quot;advanced-composition-tables-and-tabs&quot;&gt;Advanced Composition: Tables and Tabs&lt;/h3&gt;
&lt;p&gt;The World Records page was our “stress test” for displaying dense data. By composing &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;tabs&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;card&lt;/code&gt;, and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;table&lt;/code&gt;, we reduced a complex view to a readable DSL:&lt;/p&gt;

&lt;div class=&quot;language-erb highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;cp&quot;&gt;&amp;lt;%=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;jet_ui&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;tabs&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;do&lt;/span&gt; &lt;span class=&quot;cp&quot;&gt;%&amp;gt;&lt;/span&gt;
  &lt;span class=&quot;cp&quot;&gt;&amp;lt;%=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;jet_ui&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;tabs_item&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;KM&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;href: &lt;/span&gt;&lt;span class=&quot;n&quot;&gt;records_path&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;ss&quot;&gt;unit: &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;km&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;),&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;active: &lt;/span&gt;&lt;span class=&quot;vi&quot;&gt;@unit&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;==&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;km&quot;&lt;/span&gt; &lt;span class=&quot;cp&quot;&gt;%&amp;gt;&lt;/span&gt;
  &lt;span class=&quot;cp&quot;&gt;&amp;lt;%=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;jet_ui&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;tabs_item&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;MI&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;href: &lt;/span&gt;&lt;span class=&quot;n&quot;&gt;records_path&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;ss&quot;&gt;unit: &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;mi&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;),&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;active: &lt;/span&gt;&lt;span class=&quot;vi&quot;&gt;@unit&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;==&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;mi&quot;&lt;/span&gt; &lt;span class=&quot;cp&quot;&gt;%&amp;gt;&lt;/span&gt;
&lt;span class=&quot;cp&quot;&gt;&amp;lt;%&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;end&lt;/span&gt; &lt;span class=&quot;cp&quot;&gt;%&amp;gt;&lt;/span&gt;

&lt;span class=&quot;cp&quot;&gt;&amp;lt;%=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;jet_ui&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;card&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;class: &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;overflow-hidden&quot;&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;do&lt;/span&gt; &lt;span class=&quot;cp&quot;&gt;%&amp;gt;&lt;/span&gt;
  &lt;span class=&quot;cp&quot;&gt;&amp;lt;%=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;jet_ui&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;table&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;hovered: &lt;/span&gt;&lt;span class=&quot;kp&quot;&gt;true&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;do&lt;/span&gt; &lt;span class=&quot;cp&quot;&gt;%&amp;gt;&lt;/span&gt;
    &lt;span class=&quot;cp&quot;&gt;&amp;lt;%=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;jet_ui&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;table_thead&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;do&lt;/span&gt; &lt;span class=&quot;cp&quot;&gt;%&amp;gt;&lt;/span&gt;
      &lt;span class=&quot;cp&quot;&gt;&amp;lt;%=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;jet_ui&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;table_tr&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;do&lt;/span&gt; &lt;span class=&quot;cp&quot;&gt;%&amp;gt;&lt;/span&gt;
        &lt;span class=&quot;cp&quot;&gt;&amp;lt;%=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;jet_ui&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;table_th&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;Event&quot;&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;cp&quot;&gt;%&amp;gt;&lt;/span&gt;
        &lt;span class=&quot;c&quot;&gt;&amp;lt;%# ... %&amp;gt;&lt;/span&gt;
      &lt;span class=&quot;cp&quot;&gt;&amp;lt;%&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;end&lt;/span&gt; &lt;span class=&quot;cp&quot;&gt;%&amp;gt;&lt;/span&gt;
    &lt;span class=&quot;cp&quot;&gt;&amp;lt;%&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;end&lt;/span&gt; &lt;span class=&quot;cp&quot;&gt;%&amp;gt;&lt;/span&gt;
  &lt;span class=&quot;cp&quot;&gt;&amp;lt;%&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;end&lt;/span&gt; &lt;span class=&quot;cp&quot;&gt;%&amp;gt;&lt;/span&gt;
&lt;span class=&quot;cp&quot;&gt;&amp;lt;%&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;end&lt;/span&gt; &lt;span class=&quot;cp&quot;&gt;%&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;This composition proves the architectural leverage: we get a professional data grid with integrated navigation, all following the same design system with zero manual CSS.&lt;/p&gt;

&lt;p&gt;&lt;img width=&quot;1001&quot; height=&quot;514&quot; alt=&quot;image&quot; src=&quot;https://github.com/user-attachments/assets/77774ac0-f804-472e-89ae-670751139237&quot; /&gt;&lt;/p&gt;

&lt;h2 id=&quot;why-jet_ui-the-alternatives&quot;&gt;Why jet_ui? (The Alternatives)&lt;/h2&gt;

&lt;p&gt;You might ask: “Why not just use Flowbite, shadcn-rails, or RailsUI?”&lt;/p&gt;

&lt;p&gt;While those are great tools, they occupy different niches. Flowbite is fantastic for Tailwind-first projects but isn’t built as a first-class &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ViewComponent&lt;/code&gt; library, often requiring you to wrap their HTML yourself. shadcn-rails follows the “copy-paste” philosophy which is great for total control, but lacks a clean, gem-based upgrade path for those who want their design system managed as a dependency. RailsUI is a premium, template-oriented solution that is excellent for rapid prototyping but might feel too opinionated for existing apps.&lt;/p&gt;

&lt;p&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;jet_ui&lt;/code&gt; sits in the “Goldilocks” zone: it’s ViewComponent-native, Tailwind v4-native, and gem-distributed with an “eject-on-demand” safety valve. You get the maintenance benefits of a gem with the flexibility of local code when you need it.&lt;/p&gt;

&lt;h2 id=&quot;conclusion-architectural-leverage&quot;&gt;Conclusion: Architectural Leverage&lt;/h2&gt;

&lt;p&gt;Migration isn’t just about “fancy” code. It’s about reducing cognitive load. Developers can focus on building features using high-level components rather than wrestling with low-level utility classes in every single view. If you’re building a modern Rails app, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;jet_ui&lt;/code&gt; is the bridge between the flexibility of Tailwind and the structure of a professional design system.&lt;/p&gt;

&lt;h2 id=&quot;links&quot;&gt;Links&lt;/h2&gt;
&lt;ul&gt;
  &lt;li&gt;📦 &lt;a href=&quot;https://rubygems.org/gems/jet_ui&quot;&gt;Jet_UI on RubyGems&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;📁 &lt;a href=&quot;https://github.com/jetrockets/jet_ui&quot;&gt;Jet_UI Repo&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;🏃 &lt;a href=&quot;https://calcpace.app&quot;&gt;Calcpace&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
</content>
  </entry>
  
  <entry>
    <title>Calcpace: open beta, maps, bot protection and a growing gem</title>
    <link href="https://0jonjo.github.io/blog/2026/calcpace-open-beta/" rel="alternate" type="text/html" />
    <id>https://0jonjo.github.io/blog/2026/calcpace-open-beta/</id>
    <published>2026-04-06T00:00:00+00:00</published>
    <updated>2026-04-06T00:00:00+00:00</updated>
    <summary>From closed beta to open registration: new gem modules, GPS tracking with maps, bot protection, and the infrastructure decisions behind each feature.</summary>
    <content type="html">&lt;p&gt;Calcpace.app is now open to anyone — no invite code needed. This post covers what changed since the closed beta: new gem modules, GPS tracking with maps, bot protection, the infrastructure decisions behind each feature, and a few lessons learned the hard way.&lt;/p&gt;

&lt;p&gt;The &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;calcpace&lt;/code&gt; gem is a pure Ruby library — no I/O, no Rails, no external dependencies. Every formula lives in its own module, included into the main &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Calcpace&lt;/code&gt; class. The site consumes it as a regular gem dependency; when a new module ships, the site updates the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Gemfile&lt;/code&gt; and builds on top of it. Since v1.8, three modules were added.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;CameronPredictor&lt;/strong&gt; — an exponential race prediction formula that runs alongside the existing Riegel predictor (&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;T2 = T1 × (D2/D1)^1.06&lt;/code&gt;). Cameron tends to be more conservative than Riegel for shorter base distances, which matters when you’re predicting a marathon from a 5K time. Having both lets you compare and pick the more realistic estimate for your training context.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;TrackCalculator&lt;/strong&gt; — GPS math: Haversine distance between coordinate pairs, cumulative elevation gain, and per-km splits from an array of raw trackpoints. Pure calculation — no map rendering, no file parsing. The module receives coordinates and returns numbers; the site handles everything else.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Vo2maxEstimator&lt;/strong&gt; — Daniels &amp;amp; Gilbert formula. Given a race time and distance, returns an estimated VO2max and a fitness classification (from “Poor” to “Elite”). The gem API convention applies here: inputs use symbols (&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;:km&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;:mi&lt;/code&gt;), outputs are always strings — never symbols.&lt;/p&gt;

&lt;p&gt;The GPS flow on the site: when a user uploads a GPX file, Active Storage saves it to Cloudflare R2 and enqueues &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;GpxParseJob&lt;/code&gt; to Sidekiq. The job runs &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;GpxParser&lt;/code&gt;, which extracts trackpoints (lat, lon, elevation, time), then calls &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;TrackCalculator&lt;/code&gt; from the gem to compute distance, elevation gain, and splits. Trackpoints are persisted and the activity is updated with the computed stats. The map renders with Leaflet.js and OpenStreetMap tiles — no API key, no vendor lock-in, no usage limits.&lt;/p&gt;

&lt;p&gt;Moving from invite-only to open registration meant adding real bot protection. The approach: Cloudflare Turnstile on the registration form, Rack::Attack for rate limiting at the Rails level. Turnstile was straightforward to integrate, with two gotchas that cost more time than expected. The widget script URL uses &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;/v0/&lt;/code&gt;, not &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;/v1/&lt;/code&gt; — the docs aren’t always consistent about this. And the form param is &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;cf-turnstile-response&lt;/code&gt; (hyphens), not &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;cf_turnstile_response&lt;/code&gt; (underscores) — Rails’ &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;params&lt;/code&gt; hash keeps hyphens, so the usual convention doesn’t apply. There’s also a pending issue: when the registration form reloads after a validation error, Turbo replaces the DOM but doesn’t re-initialize the widget, so the next submission fails silently. The fix is re-initializing after a Turbo render — still pending.&lt;/p&gt;

&lt;p&gt;For file storage, Active Storage with Cloudflare R2 as the backend handles avatars, activity photos, and GPX files. S3-compatible, free egress within Cloudflare’s network, no CDN configuration needed.&lt;/p&gt;

&lt;p&gt;Rails 8 ships &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;generates_token_for&lt;/code&gt; as a first-class model API for signed, expiring tokens. The email verification flow uses it: on registration a token is generated with 48h expiry, the verification email includes a signed URL, and unverified accounts are blocked from logging in with a resend option shown. The verification email is HTML now — styled with a button and a copyable fallback link. Before this it was plain text, which looked unfinished for an open product. Transactional emails go through Resend API; the password reset flow was already using it and verification plugged into the same setup.&lt;/p&gt;

&lt;p&gt;Every user gets a public profile at &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;calcpace.app/:username&lt;/code&gt; — avatar, bio, city, country, and activity feed. Each activity has an individual &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;public&lt;/code&gt; boolean: toggle it on the activity form and it appears or disappears from the public feed immediately. Activities can also have a photo attached. Your profile page only shows what you choose to share.&lt;/p&gt;

&lt;p&gt;The clearest architectural decision in this project: the gem does pure calculation, the site does I/O. No file parsing in the gem, no HTTP, no database. This makes the gem testable in isolation (Minitest, no Rails required) and keeps the site thin — controllers call gem methods and persist results, nothing more. The delivery sequence is always: implement module in gem → write tests → publish new version → update &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Gemfile&lt;/code&gt; in site → build the UI on top. GitHub Actions publishes to RubyGems automatically on version bump in &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;lib/calcpace/version.rb&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;What’s next: internationalization first — the app is English-only for now, but the i18n infrastructure is already in place. Adding PT-BR, Spanish, German, and French means auditing hardcoded strings, migrating &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;GpxParser&lt;/code&gt; error messages to i18n keys, and translating the emails. Then content pages: pace conversion tables, race equivalent tables, Boston qualifying times, world records. Mile splits in the activity view are also coming — the gem already supports it via &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;track_splits(points, 1.609)&lt;/code&gt;, the UI just needs a toggle. Strava and Garmin direct connection is planned — GPX import already works for the manual export flow, the next step is OAuth2 for automatic sync.&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;a href=&quot;https://calcpace.app&quot;&gt;calcpace.app&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://rubygems.org/gems/calcpace&quot;&gt;calcpace gem on RubyGems&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://github.com/0jonjo/calcpace_web&quot;&gt;calcpace_web on GitHub&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;img width=&quot;1024&quot; height=&quot;683&quot; alt=&quot;image&quot; src=&quot;https://github.com/user-attachments/assets/da5c62bd-2290-4118-99a4-ddf27140c870&quot; /&gt;&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;Image: &lt;a href=&quot;https://openverse.org/image/c512f324-473b-4a6c-b769-0a993b9aa445&quot;&gt;“BP Running Track @ Glasgow Airport”&lt;/a&gt; by JCDecaux Creative Solutions. &lt;a href=&quot;https://creativecommons.org/licenses/by-nc-nd/2.0/&quot;&gt;Open Verse, Creative Commons BY-NC-ND 2.0&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
</content>
  </entry>
  
  <entry>
    <title>Calcpace Web: the calculator now in the browser</title>
    <link href="https://0jonjo.github.io/blog/2026/calpace-web/" rel="alternate" type="text/html" />
    <id>https://0jonjo.github.io/blog/2026/calpace-web/</id>
    <published>2026-03-26T00:00:00+00:00</published>
    <updated>2026-03-26T00:00:00+00:00</updated>
    <summary>The calcpace gem just hit 7,000 downloads, so I ran a 5k and built calcpace.app to bring those calculations to everyone, no Ruby required.</summary>
    <content type="html">&lt;p&gt;The &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;calcpace&lt;/code&gt; gem just hit 7,000 downloads, so I celebrated the best way I know how: I ran a 5k and finally built &lt;a href=&quot;https://calcpace.app&quot;&gt;calcpace.app&lt;/a&gt; to bring those calculations to everyone, no Ruby console required.&lt;/p&gt;

&lt;p&gt;The core logic for this project has actually been brewing in my head for about five years. Combining my daily routine as a runner with the logic I apply professionally when building delivery and routing systems, it just made perfect sense to model pace, distance, and time mathematically. I eventually sat down and started writing the core Ruby code four years ago, which evolved into the open-source gem.&lt;/p&gt;

&lt;p&gt;When the gem crossed the 7k mark, I realized it was time to make it accessible to non-programmers.&lt;/p&gt;

&lt;p&gt;The app provides a suite of free running tools: pace, speed, finish time, split breakdowns, and a race predictor using both the Riegel and Cameron formulas. The converter covers 30 combinations across distance, speed, and pace—km, mi, meters, yards, feet, knots—all easily switchable between metric and imperial systems.&lt;/p&gt;

&lt;p&gt;I didn’t want it to be just another “enter your numbers” calculator. There are dedicated guide pages explaining the math behind each formula. For instance, you can read about why the Cameron predictor tends to be more conservative than Riegel for shorter base distances, and what that actually means for your training blocks.&lt;/p&gt;

&lt;p&gt;From planning to writing the code and setting up the infrastructure, the web version took me about four days. I used this as an opportunity to build with a modern, pragmatic stack:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;Ruby on Rails 8:&lt;/strong&gt; The foundation of the app. It provides a solid structure and allows me to practice “dogfooding” by consuming my own gem in a production environment. The gem does all the heavy lifting for the calculations, keeping the Rails controllers incredibly clean.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;The Basecamp Way:&lt;/strong&gt; I treated this project as a playground to strictly follow 37signals’ design principles. That means pushing the logic down to rich models, keeping controllers thin, and leveraging &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Current&lt;/code&gt; attributes to handle global state cleanly.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Tailwind CSS:&lt;/strong&gt; To keep my focus on the backend and infrastructure, I used Tailwind to rapidly build a clean, responsive interface without writing custom CSS files.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Testing &amp;amp; CI/CD:&lt;/strong&gt; Since this serves as a live environment for the gem, reliability is key. The app is fully tested with Minitest, and GitHub Actions runs the CI/CD pipeline on every push to &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;main&lt;/code&gt;.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Kamal &amp;amp; VPS:&lt;/strong&gt; I wanted to step away from traditional, expensive PaaS solutions. I deployed the application using Kamal, which packages the Rails app into Docker containers and handles zero-downtime deployments directly to a DigitalOcean Virtual Private Server (VPS).&lt;/li&gt;
&lt;/ul&gt;

&lt;h3 id=&quot;take-it-for-a-spin&quot;&gt;Take it for a spin&lt;/h3&gt;

&lt;p&gt;While the personal activity tracker (run/ride logging) is technically in closed testing, I built a sandbox and share with some friends so they can explore the UI and see the gem working in a real database environment.&lt;/p&gt;

&lt;p&gt;To keep things tidy, a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;GuestResetJob&lt;/code&gt; runs via &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;sidekiq-cron&lt;/code&gt; to wipe the guest activities and recreate the sample profile every Monday at 3 AM.&lt;/p&gt;

&lt;h3 id=&quot;whats-next&quot;&gt;What’s Next&lt;/h3&gt;

&lt;p&gt;The engine is running smoothly, but there’s more to come. I am currently working on adding VO2max, VDOT, and training zone calculations—these will be introduced as new modules in the gem and will get their own guide pages on the site.&lt;/p&gt;

&lt;p&gt;Links:&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;&lt;a href=&quot;https://calcpace.app&quot;&gt;calcpace.app&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://rubygems.org/gems/calcpace&quot;&gt;calcpace gem&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://github.com/0jonjo/calcpace_web&quot;&gt;GitHub repo&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;img width=&quot;1535&quot; height=&quot;932&quot; alt=&quot;image&quot; src=&quot;https://github.com/user-attachments/assets/45e56e58-7039-4121-af7d-c6ab846c2045&quot; /&gt;&lt;/p&gt;
</content>
  </entry>
  
  <entry>
    <title>Building Semantic Search with AI and Vector Embedding in Rails</title>
    <link href="https://0jonjo.github.io/blog/2026/embbendings-ai/" rel="alternate" type="text/html" />
    <id>https://0jonjo.github.io/blog/2026/embbendings-ai/</id>
    <published>2026-02-26T00:00:00+00:00</published>
    <updated>2026-02-26T00:00:00+00:00</updated>
    <summary>Creating semantic search with vector embeddings in Ruby using the ruby_llm gem and PostgreSQL&apos;s pgvector extension.</summary>
    <content type="html">&lt;p&gt;This month, I wrote an article on building semantic search with vector embeddings in Ruby using the ruby_llm gem and PostgreSQL’s pgvector extension to &lt;a href=&quot;https://jetrockets.com/blog/building-semantic-search-with-ai-and-vector-embedding-in-rails&quot;&gt;JetRockets blog&lt;/a&gt;. Here is the full article:&lt;/p&gt;

&lt;p&gt;Traditional keyword search is fundamentally limited—it can only match exact words or their basic variations. Search for “customer pain points” and you’ll miss documents titled “User Frustrations” or “Client Challenges,” even though they’re semantically identical. This limitation becomes critical when managing large document repositories where users need to find information based on meaning, not memorized keywords. The solution is semantic search using vector embeddings, which represent text as mathematical vectors that capture conceptual similarity. In previous articles, we explored how ruby_llm simplifies working with AI providers for &lt;a href=&quot;https://jetrockets.com/blog/building-intelligent-ai-agents-with-function-calling-in-ruby&quot;&gt;function calling&lt;/a&gt; and &lt;a href=&quot;https://jetrockets.com/blog/building-a-resilient-ai-client-in-ruby-with-stoplight-and-ruby_llm&quot;&gt;resilient architectures&lt;/a&gt;. Now we’ll leverage ruby_llm’s embedding capabilities to build semantic search with PostgreSQL’s pgvector extension—creating a system that finds “churn analysis” when users search for “why customers leave.”&lt;/p&gt;

&lt;h2 id=&quot;what-are-vector-embeddings&quot;&gt;What Are Vector Embeddings?&lt;/h2&gt;

&lt;p&gt;Think of vector embeddings as a way to translate text into the language of mathematics—or as some describe it, “searching by vibes” rather than exact keywords. Each piece of text becomes an array of numbers (a “vector”) where similar meanings produce similar numbers. The embedding model has learned, through training on billions of texts, that certain concepts cluster together in this mathematical space.&lt;/p&gt;

&lt;p&gt;We’ll use OpenAI’s &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;text-embedding-3-small&lt;/code&gt; (1,536 dimensions) for its solid performance-to-cost ratio, but ruby_llm also supports other models: Gemini’s &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;text-embedding-004&lt;/code&gt; (768 dimensions), Voyage AI’s models, or even local alternatives like &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;all-MiniLM-L6-v2&lt;/code&gt; via Ollama for privacy-sensitive applications. The choice depends on your budget, latency requirements, and whether you prefer cloud or self-hosted solutions.&lt;/p&gt;

&lt;p&gt;For example:&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;“customer churn analysis” → &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;[0.234, -0.891, 0.456, ...]&lt;/code&gt;&lt;/li&gt;
  &lt;li&gt;“user retention study” → &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;[0.221, -0.883, 0.449, ...]&lt;/code&gt; (very close!)&lt;/li&gt;
  &lt;li&gt;“quarterly revenue report” → &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;[-0.678, 0.234, -0.123, ...]&lt;/code&gt; (very different)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;When a user searches for “why are customers leaving”, the system converts this query into a vector and finds documents with similar vectors—automatically surfacing reports about “churn factors” and “cancellation reasons” without requiring exact keyword matches.&lt;/p&gt;

&lt;h2 id=&quot;setting-up-ruby_llm-for-embeddings&quot;&gt;Setting Up ruby_llm for Embeddings&lt;/h2&gt;

&lt;p&gt;Just as we used ruby_llm for function calling in our previous article, we can use it for embeddings too. The gem provides a unified interface with consistent error handling and automatic retries:&lt;/p&gt;

&lt;div class=&quot;language-ruby highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c1&quot;&gt;# config/initializers/ruby_llm.rb&lt;/span&gt;
&lt;span class=&quot;nb&quot;&gt;require&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;ruby_llm&quot;&lt;/span&gt;

&lt;span class=&quot;no&quot;&gt;RubyLLM&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;configure&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;do&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;|&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;config&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;|&lt;/span&gt;
  &lt;span class=&quot;n&quot;&gt;config&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;openai_api_key&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;Rails&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;application&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;credentials&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;dig&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;ss&quot;&gt;:openai&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;:api_key&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;

&lt;span class=&quot;c1&quot;&gt;# Create a reusable client instance&lt;/span&gt;
&lt;span class=&quot;no&quot;&gt;LLM_CLIENT&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;RubyLLM&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Now create a wrapper that handles the embedding API calls:&lt;/p&gt;

&lt;div class=&quot;language-ruby highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c1&quot;&gt;# app/services/embedding_service.rb&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;EmbeddingService&lt;/span&gt;
  &lt;span class=&quot;no&quot;&gt;MODEL&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;text-embedding-3-small&quot;&lt;/span&gt;
  &lt;span class=&quot;no&quot;&gt;DIMENSIONS&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;1536&lt;/span&gt;

  &lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;embed&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;texts&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;texts&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;Array&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;texts&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;[]&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;texts&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;empty?&lt;/span&gt;

    &lt;span class=&quot;c1&quot;&gt;# ruby_llm handles the API call automatically&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;result&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;LLM_CLIENT&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;embed&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;texts&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;model: &lt;/span&gt;&lt;span class=&quot;no&quot;&gt;MODEL&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;dimensions: &lt;/span&gt;&lt;span class=&quot;no&quot;&gt;DIMENSIONS&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;result&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;vectors&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;That’s it! Using ruby_llm gives us the same benefits we saw in the function calling article: provider abstraction, automatic error handling, and consistent behavior across different AI services.&lt;/p&gt;

&lt;h2 id=&quot;setting-up-pgvector-for-storage&quot;&gt;Setting Up pgvector for Storage&lt;/h2&gt;

&lt;p&gt;Why pgvector? If you’re already running PostgreSQL (and many Rails apps are), pgvector lets you store and search vectors without adding new infrastructure. No separate vector database to maintain, no data synchronization issues, and transactions work normally—your embeddings live right next to your application data. The &lt;a href=&quot;https://github.com/pgvector/pgvector&quot;&gt;pgvector extension&lt;/a&gt; adds specialized vector types and HNSW indexing for sub-100ms similarity searches across millions of vectors.&lt;/p&gt;

&lt;p&gt;Before storing embeddings, enable the extension:&lt;/p&gt;

&lt;div class=&quot;language-ruby highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c1&quot;&gt;# db/migrate/create_documents.rb&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;CreateDocuments&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;lt;&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;ActiveRecord&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;no&quot;&gt;Migration&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;mf&quot;&gt;8.0&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;change&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;enable_extension&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;vector&quot;&lt;/span&gt;

    &lt;span class=&quot;n&quot;&gt;create_table&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;:documents&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;do&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;|&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;t&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;|&lt;/span&gt;
      &lt;span class=&quot;n&quot;&gt;t&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;string&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;:title&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;null: &lt;/span&gt;&lt;span class=&quot;kp&quot;&gt;false&lt;/span&gt;
      &lt;span class=&quot;n&quot;&gt;t&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;text&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;:content&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;null: &lt;/span&gt;&lt;span class=&quot;kp&quot;&gt;false&lt;/span&gt;
      &lt;span class=&quot;n&quot;&gt;t&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;references&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;:user&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;null: &lt;/span&gt;&lt;span class=&quot;kp&quot;&gt;false&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;foreign_key: &lt;/span&gt;&lt;span class=&quot;kp&quot;&gt;true&lt;/span&gt;
      &lt;span class=&quot;n&quot;&gt;t&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;timestamps&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;

    &lt;span class=&quot;n&quot;&gt;create_table&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;:document_chunks&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;do&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;|&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;t&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;|&lt;/span&gt;
      &lt;span class=&quot;n&quot;&gt;t&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;references&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;:document&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;null: &lt;/span&gt;&lt;span class=&quot;kp&quot;&gt;false&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;foreign_key: &lt;/span&gt;&lt;span class=&quot;kp&quot;&gt;true&lt;/span&gt;
      &lt;span class=&quot;n&quot;&gt;t&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;integer&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;:chunk_index&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;null: &lt;/span&gt;&lt;span class=&quot;kp&quot;&gt;false&lt;/span&gt;
      &lt;span class=&quot;n&quot;&gt;t&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;text&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;:content&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;null: &lt;/span&gt;&lt;span class=&quot;kp&quot;&gt;false&lt;/span&gt;
      &lt;span class=&quot;n&quot;&gt;t&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;vector&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;:embedding&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;limit: &lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;1536&lt;/span&gt;
      &lt;span class=&quot;n&quot;&gt;t&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;timestamps&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;

    &lt;span class=&quot;c1&quot;&gt;# HNSW index for fast nearest-neighbor search&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;add_index&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;:document_chunks&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;:embedding&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;using: :hnsw&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;opclass: :vector_l2_ops&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;The HNSW (Hierarchical Navigable Small World) index creates a graph structure that allows fast approximate nearest-neighbor search—essential for sub-100ms queries across thousands of vectors.&lt;/p&gt;

&lt;h2 id=&quot;basic-pattern-chunking-documents&quot;&gt;Basic Pattern: Chunking Documents&lt;/h2&gt;

&lt;p&gt;Here’s the challenge: documents can be very long, but embedding models work best on focused text segments (500-2000 characters). Embed an entire 50-page document and you’ll get a diluted embedding that doesn’t capture nuances. The solution is chunking with overlap. The overlap (typically 10-20%) prevents losing context at boundaries:&lt;/p&gt;

&lt;div class=&quot;language-ruby highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c1&quot;&gt;# app/models/document.rb&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;Document&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;lt;&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;ApplicationRecord&lt;/span&gt;
  &lt;span class=&quot;no&quot;&gt;CHUNK_SIZE&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;1_000&lt;/span&gt;
  &lt;span class=&quot;no&quot;&gt;OVERLAP_SIZE&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;200&lt;/span&gt;

  &lt;span class=&quot;n&quot;&gt;has_many&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;:document_chunks&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;dependent: :destroy&lt;/span&gt;

  &lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;generate_chunks!&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;step&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;CHUNK_SIZE&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;OVERLAP_SIZE&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;chunks&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;[]&lt;/span&gt;

    &lt;span class=&quot;c1&quot;&gt;# Split content into overlapping chunks&lt;/span&gt;
    &lt;span class=&quot;mi&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;step&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;content&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;length&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;step&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;do&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;|&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;offset&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;|&lt;/span&gt;
      &lt;span class=&quot;n&quot;&gt;chunk&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;content&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;offset&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;CHUNK_SIZE&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]&lt;/span&gt;
      &lt;span class=&quot;n&quot;&gt;chunks&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;lt;&amp;lt;&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;chunk&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;chunk&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;present?&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;

    &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;chunks&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;empty?&lt;/span&gt;

    &lt;span class=&quot;c1&quot;&gt;# Generate embeddings for all chunks in one call&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;embeddings&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;EmbeddingService&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;embed&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;chunks&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;

    &lt;span class=&quot;no&quot;&gt;ActiveRecord&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;no&quot;&gt;Base&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;transaction&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;do&lt;/span&gt;
      &lt;span class=&quot;n&quot;&gt;document_chunks&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;destroy_all&lt;/span&gt;

      &lt;span class=&quot;n&quot;&gt;chunks&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;zip&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;embeddings&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;).&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;each_with_index&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;do&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;|&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;chunk&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;embedding&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;),&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;index&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;|&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;document_chunks&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;create!&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
          &lt;span class=&quot;ss&quot;&gt;chunk_index: &lt;/span&gt;&lt;span class=&quot;n&quot;&gt;index&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
          &lt;span class=&quot;ss&quot;&gt;content: &lt;/span&gt;&lt;span class=&quot;n&quot;&gt;chunk&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
          &lt;span class=&quot;ss&quot;&gt;embedding: &lt;/span&gt;&lt;span class=&quot;n&quot;&gt;embedding&lt;/span&gt;
        &lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
      &lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Notice how we generate embeddings for &lt;em&gt;all&lt;/em&gt; chunks in a single call. Instead of making 50 separate API requests for a 50-chunk document, we make just one. The ruby_llm gem handles the batching automatically, just like it does with function calling.&lt;/p&gt;

&lt;p&gt;The character-based approach above works, but cutting mid-sentence degrades semantic meaning. A better strategy splits on sentence boundaries using &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;text.scan(/[^.!?]+[.!?](?:\s+|$)/)&lt;/code&gt;, accumulating sentences until reaching the size limit. This preserves complete thoughts while maintaining consistent chunk sizes—particularly valuable for technical documentation where sentence context matters.&lt;/p&gt;

&lt;h2 id=&quot;advanced-pattern-hybrid-scoring-semantic--temporal&quot;&gt;Advanced Pattern: Hybrid Scoring (Semantic + Temporal)&lt;/h2&gt;

&lt;p&gt;Pure semantic search has a problem: outdated documents with perfect semantic matches outrank recent documents with good matches. When users search for “current market size”, they might get a 2023 report instead of the fresh 2025 one. The solution is hybrid scoring—combine semantic similarity (70%) with recency (30%):&lt;/p&gt;

&lt;div class=&quot;language-ruby highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c1&quot;&gt;# app/models/document_chunk.rb&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;DocumentChunk&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;lt;&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;ApplicationRecord&lt;/span&gt;
  &lt;span class=&quot;n&quot;&gt;belongs_to&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;:document&lt;/span&gt;
  &lt;span class=&quot;n&quot;&gt;has_neighbors&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;:embedding&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;dimensions: &lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;1536&lt;/span&gt;

  &lt;span class=&quot;nb&quot;&gt;attr_accessor&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;:search_score&lt;/span&gt;

  &lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;search_by_semantics&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;query&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;user&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:,&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;limit: &lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;20&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;threshold: &lt;/span&gt;&lt;span class=&quot;mf&quot;&gt;0.8&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;c1&quot;&gt;# Convert query to embedding using ruby_llm&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;query_embedding&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;EmbeddingService&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;embed&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;query&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;).&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;first&lt;/span&gt;

    &lt;span class=&quot;c1&quot;&gt;# Get more candidates than needed for scoring&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;neighbors&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;joins&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;ss&quot;&gt;:document&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
                  &lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;where&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;ss&quot;&gt;documents: &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;user: &lt;/span&gt;&lt;span class=&quot;n&quot;&gt;user&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;})&lt;/span&gt;
                  &lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;nearest_neighbors&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;ss&quot;&gt;:embedding&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;query_embedding&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;distance: &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;cosine&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
                  &lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;limit&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;limit&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;*&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;5&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;

    &lt;span class=&quot;c1&quot;&gt;# Calculate hybrid scores&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;scored_neighbors&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;neighbors&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;map&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;do&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;|&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;chunk&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;|&lt;/span&gt;
      &lt;span class=&quot;n&quot;&gt;semantic_similarity&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;1&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;chunk&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;neighbor_distance&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;/&lt;/span&gt; &lt;span class=&quot;mf&quot;&gt;2.0&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
      &lt;span class=&quot;n&quot;&gt;recency_score&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;calculate_recency_score&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;chunk&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;created_at&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;

      &lt;span class=&quot;c1&quot;&gt;# 70% semantic, 30% recency&lt;/span&gt;
      &lt;span class=&quot;n&quot;&gt;chunk&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;search_score&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;semantic_similarity&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;*&lt;/span&gt; &lt;span class=&quot;mf&quot;&gt;0.7&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;+&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;recency_score&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;*&lt;/span&gt; &lt;span class=&quot;mf&quot;&gt;0.3&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
      &lt;span class=&quot;n&quot;&gt;chunk&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;

    &lt;span class=&quot;c1&quot;&gt;# Filter and return top results&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;scored_neighbors&lt;/span&gt;
      &lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;select&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;|&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;c&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;c&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;neighbor_distance&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;lt;&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;threshold&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
      &lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;sort_by&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;|&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;c&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;-&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;c&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;search_score&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
      &lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;first&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;limit&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;

  &lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;calculate_recency_score&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;created_at&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;age_in_days&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;no&quot;&gt;Time&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;current&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;created_at&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;/&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;day&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;mf&quot;&gt;1.0&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;age_in_days&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;lt;=&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;30&lt;/span&gt;  &lt;span class=&quot;c1&quot;&gt;# Recent: full score&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;mf&quot;&gt;0.7&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;age_in_days&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;gt;&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;365&lt;/span&gt;  &lt;span class=&quot;c1&quot;&gt;# Old: 30% penalty&lt;/span&gt;
    &lt;span class=&quot;mf&quot;&gt;1.0&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;((&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;age_in_days&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;30&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;/&lt;/span&gt; &lt;span class=&quot;mf&quot;&gt;335.0&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;*&lt;/span&gt; &lt;span class=&quot;mf&quot;&gt;0.3&lt;/span&gt;  &lt;span class=&quot;c1&quot;&gt;# Linear decay&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;The key insight: we fetch &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;limit * 5&lt;/code&gt; candidates first, then score and filter. Why? A chunk might be the 50th-best semantic match but jump into the top 5 after adding recency. By casting a wider net initially, we don’t miss valuable recent documents.&lt;/p&gt;

&lt;h2 id=&quot;putting-it-all-together&quot;&gt;Putting It All Together&lt;/h2&gt;

&lt;p&gt;Now let’s see the complete flow from upload to search:&lt;/p&gt;

&lt;div class=&quot;language-ruby highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;DocumentsController&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;lt;&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;ApplicationController&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;create&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;document&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;current_user&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;documents&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;create!&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
      &lt;span class=&quot;ss&quot;&gt;title: &lt;/span&gt;&lt;span class=&quot;n&quot;&gt;params&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;ss&quot;&gt;:title&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;],&lt;/span&gt;
      &lt;span class=&quot;ss&quot;&gt;content: &lt;/span&gt;&lt;span class=&quot;n&quot;&gt;params&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;ss&quot;&gt;:content&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]&lt;/span&gt;  &lt;span class=&quot;c1&quot;&gt;# or extract from uploaded file&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;

    &lt;span class=&quot;c1&quot;&gt;# Generate chunks and embeddings asynchronously&lt;/span&gt;
    &lt;span class=&quot;no&quot;&gt;GenerateEmbeddingsJob&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;perform_later&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;document&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;id&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;

    &lt;span class=&quot;n&quot;&gt;render&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;json: &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;id: &lt;/span&gt;&lt;span class=&quot;n&quot;&gt;document&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;id&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;status: &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;processing&quot;&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;

  &lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;search&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;results&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;DocumentChunk&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;search_by_semantics&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
      &lt;span class=&quot;n&quot;&gt;params&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;ss&quot;&gt;:query&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;],&lt;/span&gt;
      &lt;span class=&quot;ss&quot;&gt;user: &lt;/span&gt;&lt;span class=&quot;n&quot;&gt;current_user&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
      &lt;span class=&quot;ss&quot;&gt;limit: &lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;10&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
      &lt;span class=&quot;ss&quot;&gt;threshold: &lt;/span&gt;&lt;span class=&quot;mf&quot;&gt;0.75&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;

    &lt;span class=&quot;n&quot;&gt;render&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;json: &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
      &lt;span class=&quot;ss&quot;&gt;query: &lt;/span&gt;&lt;span class=&quot;n&quot;&gt;params&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;ss&quot;&gt;:query&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;],&lt;/span&gt;
      &lt;span class=&quot;ss&quot;&gt;results: &lt;/span&gt;&lt;span class=&quot;n&quot;&gt;results&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;map&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;do&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;|&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;chunk&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;|&lt;/span&gt;
        &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
          &lt;span class=&quot;ss&quot;&gt;document_title: &lt;/span&gt;&lt;span class=&quot;n&quot;&gt;chunk&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;document&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;title&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
          &lt;span class=&quot;ss&quot;&gt;excerpt: &lt;/span&gt;&lt;span class=&quot;n&quot;&gt;chunk&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;content&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;..&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;300&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;],&lt;/span&gt;
          &lt;span class=&quot;ss&quot;&gt;relevance_score: &lt;/span&gt;&lt;span class=&quot;n&quot;&gt;chunk&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;search_score&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;round&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;2&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
        &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
      &lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;The architecture is completely asynchronous where it matters. Document uploads don’t block waiting for embeddings—users get immediate feedback, and embeddings are generated in the background. Meanwhile, searches are fast (typically under 100ms) because pgvector’s HNSW index does the heavy lifting.&lt;/p&gt;

&lt;h2 id=&quot;advantages-of-this-approach&quot;&gt;Advantages of This Approach&lt;/h2&gt;

&lt;p&gt;The benefits go far beyond “better search.” The system demonstrates &lt;strong&gt;semantic understanding&lt;/strong&gt;—users can search for “why customers switch providers” and find reports titled “Competitive Migration Patterns,” even though those exact words don’t appear in the query.&lt;/p&gt;

&lt;p&gt;From an engineering perspective, the architecture is production-ready: &lt;strong&gt;context preservation&lt;/strong&gt; through overlapping chunks means no information is lost at boundaries, &lt;strong&gt;temporal awareness&lt;/strong&gt; via hybrid scoring ensures recent insights aren’t buried, and &lt;strong&gt;ruby_llm&lt;/strong&gt; provides the same advantages we explored in previous articles—automatic retry logic, consistent error handling, and clean abstractions for AI interactions.&lt;/p&gt;

&lt;h2 id=&quot;when-to-use-semantic-search&quot;&gt;When to Use Semantic Search&lt;/h2&gt;

&lt;p&gt;Semantic search shines when users need to find documents by meaning rather than exact keywords—scenarios like customer support knowledge bases, research archives, or legal document discovery where synonyms and conceptual similarity matter. It’s especially valuable powering AI features: chatbots citing documents, assistants surfacing relevant research, or agents (using ruby_llm’s function calling) that search intelligently.&lt;/p&gt;

&lt;p&gt;However, skip it for small document sets (&amp;lt;100 documents) with well-structured content, cases requiring exact phrase matching (legal contracts), or real-time updated content where the 1-2 second embedding latency is problematic. Traditional keyword search with proper indexing often suffices for simpler use cases.&lt;/p&gt;

&lt;h2 id=&quot;best-practices&quot;&gt;Best Practices&lt;/h2&gt;

&lt;p&gt;Through implementing semantic search systems, several best practices have emerged:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Always Use Overlapping Chunks:&lt;/strong&gt; The 200-character overlap prevents context loss at boundaries. Resist the temptation to eliminate overlap to save on storage—it’s a false economy that degrades search quality.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Batch Your Embeddings:&lt;/strong&gt; Generate embeddings for all chunks in a single call to &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;EmbeddingService.embed(chunks)&lt;/code&gt;. Process 20-chunk documents with one request instead of 20, reducing latency by 95% and API costs proportionally.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Tune Thresholds Per Use Case:&lt;/strong&gt; The 0.8 default threshold works for most cases, but use 0.85 for mission-critical searches (where precision matters) and 0.75 for exploratory searches (where recall matters). Monitor your analytics and adjust accordingly.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Async Everything That Can Be Async:&lt;/strong&gt; Never block user requests waiting for embeddings. Generate them in background jobs and show a “processing” indicator. Users can continue working while documents are indexed.&lt;/p&gt;

&lt;h2 id=&quot;conclusion&quot;&gt;Conclusion&lt;/h2&gt;

&lt;p&gt;Transitioning from keyword-based to semantic search transforms how users interact with your data, and as we’ve seen, it doesn’t require adopting entirely new infrastructure. By keeping our architecture resilient with PostgreSQL and pgvector, we avoided the overhead of maintaining a separate vector database. We then used ruby_llm to abstract the complexity of interacting with AI providers, ensuring our API calls are batched and fault-tolerant.&lt;/p&gt;

&lt;p&gt;Finally, we solved the real-world UX challenges of AI search by implementing overlapping text chunks to preserve context, and hybrid scoring to balance semantic accuracy with temporal relevance. The magic of modern AI tools in the Rails ecosystem is that they allow us to build highly sophisticated features—like “searching by vibes”—while relying on the same pragmatic, robust engineering principles we use every day.&lt;/p&gt;

&lt;h2 id=&quot;additional-resources&quot;&gt;Additional Resources&lt;/h2&gt;

&lt;ul&gt;
  &lt;li&gt;📄 &lt;a href=&quot;https://simonwillison.net/2023/Oct/23/embeddings/&quot;&gt;Embeddings: What they are and why they matter&lt;/a&gt; by Simon Willison&lt;/li&gt;
  &lt;li&gt;📄 &lt;a href=&quot;https://vickiboykis.com/what_are_embeddings/&quot;&gt;What are embeddings?&lt;/a&gt; by Vicki Boykis&lt;/li&gt;
  &lt;li&gt;📁 &lt;a href=&quot;https://github.com/pgvector/pgvector&quot;&gt;pgvector GitHub Repository&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;📁 &lt;a href=&quot;https://github.com/ankane/neighbor&quot;&gt;neighbor gem for Rails&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;📁 &lt;a href=&quot;https://github.com/crmne/ruby_llm&quot;&gt;ruby_llm gem on GitHub&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Image: “Word Search” by Morgan Frederick. &lt;a href=&quot;https://openverse.org/image/79fc1531-5557-43e8-8314-0ef15b6c36b9&quot;&gt;Open Verse, Creative Commons 2.0&lt;/a&gt;&lt;/p&gt;
</content>
  </entry>
  
  <entry>
    <title>A City Called Christmas</title>
    <link href="https://0jonjo.github.io/blog/2025/city-christmas/" rel="alternate" type="text/html" />
    <id>https://0jonjo.github.io/blog/2025/city-christmas/</id>
    <published>2025-12-24T00:00:00+00:00</published>
    <updated>2025-12-24T00:00:00+00:00</updated>
    <summary>An evocative stroll through Natal’s past and present, where light and tradition turn ordinary days into celebration</summary>
    <content type="html">&lt;p&gt;Tomorrow will be Christmas in many parts of the world, but there is a city where it has been Christmas every day since 1599. This is Natal, my hometown, a name that means exactly “Christmas” in Portuguese.&lt;/p&gt;

&lt;p&gt;It boasts beautiful beaches, dunes, wind, marvelous food, and a strategic position as one of the closest points in the Americas to Africa and Europe. The area was originally inhabited by the Potiguar indigenous people. Then, during the Age of Discovery, the Portuguese established a fort and a city. After some decades, it passed into Dutch hands before returning to the Portuguese until Brazil’s independence. During World War II, this was considered one of the four most strategic places in the world, hosting the largest airbase for the Allies. However, today is not a day to talk about wars.&lt;/p&gt;

&lt;p&gt;Over time, this city has embraced diverse Christmas traditions. The end of the year is the height of Summer in South America, so forget winter clothes and snow; imagine beaches and parties with happy people. But some traditions remain. Natal has a Christmas tree made of light that is 20 meters taller than the Statue of Liberty in New York. It illuminates the night with dynamic, ever-changing colors in a city that has been Christmas every day for the last four centuries.&lt;/p&gt;

&lt;!-- Feel free to change the width and height to your desired video size. --&gt;

&lt;div class=&quot;embed-container&quot;&gt;
  &lt;iframe src=&quot;https://www.youtube.com/embed/pUjHjIWPj9Y&quot; width=&quot;700&quot; height=&quot;480&quot; frameborder=&quot;0&quot; allowfullscreen=&quot;&quot;&gt;
  &lt;/iframe&gt;
&lt;/div&gt;
</content>
  </entry>
  
  <entry>
    <title>Cat vs. Dog: A Machine Learning Experiment</title>
    <link href="https://0jonjo.github.io/blog/2025/cat-dog-machine-learning/" rel="alternate" type="text/html" />
    <id>https://0jonjo.github.io/blog/2025/cat-dog-machine-learning/</id>
    <published>2025-12-07T00:00:00+00:00</published>
    <updated>2025-12-07T00:00:00+00:00</updated>
    <summary>A hands‑on pipeline: feature engineering, dimensionality reduction, model tuning and statistical comparison — step‑by‑step notebooks linked</summary>
    <content type="html">&lt;p&gt;“Cat vs. Dog” is the “Hello World” of Computer Vision. But solving it is one thing; understanding the mathematical and statistical foundations behind the solution is another.&lt;/p&gt;

&lt;p&gt;For my final project in IMD3002 - Supervised Machine Learning at &lt;a href=&quot;https://pes.imd.ufrn.br/pes/index&quot;&gt;Artificial Intelligence program at UFRN&lt;/a&gt;, instructed by Prof. João Carlos Xavier Junior, I didn’t just trained models. We build a complete, rigorous scientific framework to test the fundamentals of Machine Learning.&lt;/p&gt;

&lt;p&gt;Here is how I broke down the workflow into a series of sequential experiments.&lt;/p&gt;

&lt;h3 id=&quot;the-objective&quot;&gt;The Objective&lt;/h3&gt;

&lt;p&gt;The goal was to classify images of specific breeds (Miniature Pinscher/English Setter vs. Birman/Ragdoll) by exercising every fundamental stage of a classic ML pipeline: from raw pixels to statistical validation.&lt;/p&gt;

&lt;h3 id=&quot;step-1-feature-extraction-beyond-pixels&quot;&gt;Step 1: Feature Extraction (Beyond Pixels)&lt;/h3&gt;

&lt;p&gt;Raw images are noisy and high-dimensional. To make them digestible for classical algorithms, I implemented two distinct visual descriptors:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;HOG (Histogram of Oriented Gradients): To capture the shape and edge structures.&lt;/li&gt;
  &lt;li&gt;LBP (Local Binary Patterns): To capture the texture of the fur.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3 id=&quot;step-2-dimensionality-reduction&quot;&gt;Step 2: Dimensionality Reduction&lt;/h3&gt;

&lt;p&gt;With high-dimensional feature vectors, the “Curse of Dimensionality” becomes a real threat. I applied PCA (Principal Component Analysis) to project the data into a lower-dimensional space, balancing computational efficiency with information retention.&lt;/p&gt;

&lt;h3 id=&quot;step-3-model-training--tuning&quot;&gt;Step 3: Model Training &amp;amp; Tuning&lt;/h3&gt;

&lt;p&gt;I implemented and compared five distinct classes of algorithms to see how they handled the visual data:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;k-NN: Instance-based learning.&lt;/li&gt;
  &lt;li&gt;Naive Bayes: Probabilistic modeling.&lt;/li&gt;
  &lt;li&gt;Decision Trees: Rule-based learning.&lt;/li&gt;
  &lt;li&gt;MLP (Multi-Layer Perceptron): Neural Networks.&lt;/li&gt;
  &lt;li&gt;Ensembles: Random Forest, AdaBoost, Voting, and Stacking.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Each model underwent rigorous hyperparameter tuning (GridSearch) to ensure we were comparing the best versions of each.&lt;/p&gt;

&lt;h4 id=&quot;step-4-statistical-evaluation&quot;&gt;Step 4: Statistical Evaluation&lt;/h4&gt;

&lt;p&gt;This is where many projects stop, but scientific rigor requires more than just comparing average accuracy. I applied the Friedman Test to determine if the differences in performance were statistically significant and the Nemenyi Post-hoc Test to pinpoint exactly which models outperformed the others.&lt;/p&gt;

&lt;h3 id=&quot;the-verdict&quot;&gt;The Verdict&lt;/h3&gt;

&lt;p&gt;The statistical analysis revealed that for this specific dataset, an MLP combined with LBP features offered the best trade-off between predictive power and computational cost, significantly outperforming shape-based approaches (HOG).&lt;/p&gt;

&lt;p&gt;You can explore the full step-by-step workflow, organized in sequential Jupyter Notebooks, on my GitHub: &lt;a href=&quot;https://github.com/0jonjo/cat_dog_ml&quot;&gt;https://github.com/0jonjo/cat_dog_ml&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;img width=&quot;1080&quot; height=&quot;522&quot; alt=&quot;image&quot; src=&quot;https://github.com/user-attachments/assets/ecdf1cce-c708-4bf7-966b-a1865aee1480&quot; /&gt;&lt;/p&gt;

&lt;p&gt;Image: “Ai is… Banner”, Rick Payne and Team. &lt;a href=&quot;https://betterimagesofai.org/images?artist=RickPayneandteam&amp;amp;title=Aiis...Banner&quot;&gt;Better Images of AI, Creative Commons 4.0&lt;/a&gt;&lt;/p&gt;
</content>
  </entry>
  
  <entry>
    <title>Building Robust AI Agents with Ruby_LLM</title>
    <link href="https://0jonjo.github.io/blog/2025/building-ai-agents-ruby/" rel="alternate" type="text/html" />
    <id>https://0jonjo.github.io/blog/2025/building-ai-agents-ruby/</id>
    <published>2025-11-15T00:00:00+00:00</published>
    <updated>2025-11-15T00:00:00+00:00</updated>
    <summary>A roundup of my two new technical posts on building robust AI in Ruby. Learn about intelligent Function Calling and building resilient, fault-tolerant clients</summary>
    <content type="html">&lt;p&gt;I’ve been busy writing this month and wanted to share two new technical posts I published over on the JetRockets blog. Both are focused on building more capable and reliable AI applications in Ruby, and they complement each other well.&lt;/p&gt;

&lt;h3 id=&quot;first-up-building-intelligent-ai-agents-with-function-calling-in-ruby&quot;&gt;First up: Building Intelligent AI Agents with Function Calling in Ruby&lt;/h3&gt;

&lt;p&gt;This post dives into one of the most exciting features of modern LLMs: Function Calling (also known as Tool Calling). This is what elevates a model from a simple text generator into an intelligent agent that can interact with real-world data and services.&lt;/p&gt;

&lt;p&gt;In the article, I explore how you can use the ruby_llm gem to give your AI “tools” it can use to:&lt;/p&gt;

&lt;p&gt;Access real-time, external data (like calling a live weather API).&lt;/p&gt;

&lt;p&gt;Enforce your own internal business logic (like checking a user’s membership status before granting access).&lt;/p&gt;

&lt;p&gt;Extract structured JSON data directly from a user’s request (for example, turning “a 3-day trip to Tokyo” into a structured itinerary).&lt;/p&gt;

&lt;p&gt;If you’re looking to build an AI that can do things rather than just talk, this post is for you.&lt;/p&gt;

&lt;p&gt;Check it out here: &lt;a href=&quot;https://jetrockets.com/blog/building-intelligent-ai-agents-with-function-calling-in-ruby&quot;&gt;https://jetrockets.com/blog/building-intelligent-ai-agents-with-function-calling-in-ruby&lt;/a&gt;&lt;/p&gt;

&lt;h3 id=&quot;next-building-a-resilient-ai-client-in-ruby-with-stoplight-and-ruby_llm&quot;&gt;Next: Building a Resilient AI Client in Ruby with Stoplight and ruby_llm&lt;/h3&gt;

&lt;p&gt;Calling external AI services is powerful, but what happens when those services are slow, return errors, or go down completely? This post is all about building a resilient client that can handle these inevitable failures gracefully.&lt;/p&gt;

&lt;p&gt;I introduce the Circuit Breaker pattern and show how to implement it using the stoplight gem. The core idea is to build a system that can:&lt;/p&gt;

&lt;p&gt;Detect when a specific AI provider (like GPT-4o) is failing.&lt;/p&gt;

&lt;p&gt;“Trip a circuit” to temporarily stop sending requests to that failing service.&lt;/p&gt;

&lt;p&gt;Automatically and seamlessly failover to a backup model (like Gemini or a different GPT model), preserving the conversation history.&lt;/p&gt;

&lt;p&gt;This one is all about ensuring your application remains stable and fault-tolerant, even when its external dependencies are having a bad day.&lt;/p&gt;

&lt;p&gt;You can read the full post here: &lt;a href=&quot;https://jetrockets.com/blog/building-a-resilient-ai-client-in-ruby-with-stoplight-and-ruby_llm&quot;&gt;https://jetrockets.com/blog/building-a-resilient-ai-client-in-ruby-with-stoplight-and-ruby_llm&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Together, these two articles provide a solid foundation for building AI applications in Ruby that are not only intelligent but also robust and reliable. I hope you find them useful!&lt;/p&gt;

&lt;p&gt;&lt;img width=&quot;756&quot; height=&quot;525&quot; alt=&quot;image&quot; src=&quot;https://github.com/user-attachments/assets/c4129e7b-8815-451e-bea3-1fc5bfad6c24&quot; /&gt;&lt;/p&gt;

&lt;p&gt;Image: “Weaving Wires 2 by Hanna Barakat &amp;amp; Archival Images of AI + AIxDESIGN”. &lt;a href=&quot;https://betterimagesofai.org/&quot;&gt;Better Images of AI, Creative Commons 4.0&lt;/a&gt;&lt;/p&gt;
</content>
  </entry>
  
  <entry>
    <title>Immersing in AI: Notes from a Developer Studying Machine Learning, Ethics and Data</title>
    <link href="https://0jonjo.github.io/blog/2025/immersing-AI/" rel="alternate" type="text/html" />
    <id>https://0jonjo.github.io/blog/2025/immersing-AI/</id>
    <published>2025-09-22T00:00:00+00:00</published>
    <updated>2025-09-22T00:00:00+00:00</updated>
    <summary>Parallels between an AI course and work as a developer on AI projects</summary>
    <content type="html">&lt;p&gt;This semester, I chose Supervised Machine Learning and Ethics and Data as my first courses in the &lt;a href=&quot;https://pes.imd.ufrn.br/pes/index&quot;&gt;Artificial Intelligence program at UFRN&lt;/a&gt;. The first deals with algorithms that learn from examples, covering everything from data collection, systematization, and adjustments to the construction of decision trees and artificial neural networks. The goal is that, by the end of the semester, we’ll be able to build predictive models from real datasets. For example, a machine could receive a patient’s exam data and predict whether or not they have a predisposition to a certain disease. The second course, on the other hand, covers the historical, philosophical, and legal dimensions of using data and algorithms, including topics like bias, privacy, transparency, and responsibility.&lt;/p&gt;

&lt;p&gt;In these first few months of the program, I’ve drawn several connections to the backend work I’ve been doing over the last few years, especially with the AI projects I’m working on at &lt;a href=&quot;https://www.linkedin.com/company/jetrockets/&quot;&gt;JetRockets&lt;/a&gt;. Some of these connections relate to mathematical questions; others, to methods for adjusting data sent to and received from AI models; and some deal directly with ethics and privacy.&lt;/p&gt;

&lt;p&gt;Here are the main connections I’ve observed:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Communicating with external systems and models&lt;/strong&gt;: A common aspect of large backend projects is integration with various third-party systems via queues, APIs, and webhooks. Beyond the purely technical details, this communication requires developing abstractions that can produce and consume information designed for other business rules—for example, payment gateways, ERP systems, and CRMs. The combination, smoothing, and transformation of data from different sources is a constant challenge.&lt;/p&gt;

&lt;p&gt;In Machine Learning, during the training that gives rise to models, we study and practice how to interpret and prepare data from various sources for use in training and testing. It’s necessary to understand the format, structure, and meaning of the data, as well as to identify and handle potential problems like missing values, inconsistencies, and noise.&lt;/p&gt;

&lt;p&gt;Something similar occurs when communicating with AI models via API (like Claude, OpenAI, and Gemini): you need to interpret and prepare the input and output data to ensure it’s in the correct format and meets the model’s requirements. In both cases, a thorough understanding of the data and system needs is essential for effective communication.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Understanding vectorization&lt;/strong&gt;: In AI projects, vectorization is the process of transforming data into numerical vectors so it can be processed by machine learning algorithms. This is especially important when working with unstructured data like text and images. Vectorization allows algorithms to extract relevant features and facilitates analysis and decision-making.&lt;/p&gt;

&lt;p&gt;In backend projects, vectorization can also optimize data storage and querying, particularly when working with large volumes of information. Understanding its concepts and techniques has been fundamental in the projects I’m involved in; studying and practicing them has been an important part of my AI learning journey.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Normalizing data&lt;/strong&gt;: Various data merging, comparison, and transformation processes involve normalization—adjusting values to a common scale without distorting differences in value ranges. This ensures data is handled consistently, especially in scale-sensitive algorithms.&lt;/p&gt;

&lt;p&gt;In Machine Learning, normalization is a crucial data preprocessing step that improves the performance and convergence of algorithms. In AI projects, steps like preparing data for RAG (Retrieval-Augmented Generation) also involve normalization to meet model requirements. For this reason, it has been useful to revisit concepts like mean, median, mode, variance, and standard deviation, applying them in both my coursework and the AI projects I’m developing.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Understanding and Controlling Bias in Data and Algorithms&lt;/strong&gt;: bias refers to the political, social, or gender inclinations present in data sources. They can have historical and cultural roots or be a product of conscious or unconscious human decisions. They influence everything from the improper selection of data to the unequal representation of different groups.&lt;/p&gt;

&lt;p&gt;In Machine Learning, controlling bias is a constant concern, as it can lead to unfair or inaccurate results, especially when working with sensitive data. Bias control also appears in the preparation of prompts for AI models, where it’s necessary to ensure the model’s response is appropriate for the query, often by mitigating undesirable biases. I’ve been studying techniques and strategies to identify, mitigate, and control biases, ensuring fairer, more transparent, and responsible models.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Managing Private Data&lt;/strong&gt;: when dealing with personal and sensitive information, ensuring data privacy and security is essential. This involves adopting practices and technologies to protect against unauthorized access, leaks, and misuse.&lt;/p&gt;

&lt;p&gt;In the relationship with AI models, managing private data is a constant concern. It is also critical to ensure compliance with privacy laws and regulations, such as the LGPD in Brazil. I have been studying legislation and best practices, adopting techniques and strategies to protect data privacy with secure and responsible approaches.&lt;/p&gt;

&lt;p&gt;&lt;img width=&quot;551&quot; height=&quot;1023&quot; alt=&quot;image&quot; src=&quot;https://github.com/user-attachments/assets/3ea8d360-e1a1-4bf3-8f14-1adc3bc68134&quot; /&gt;&lt;/p&gt;

&lt;p&gt;Image: “Mergulho” de Anderson Freire. &lt;a href=&quot;hhttps://openverse.org/image/b952887f-8c48-4bb7-a5b6-e9ea9f97a00d&quot;&gt;Open Verse, Creative Commons 2.0&lt;/a&gt;&lt;/p&gt;
</content>
  </entry>
  
  <entry>
    <title>The Potters of the Unknown</title>
    <link href="https://0jonjo.github.io/blog/2025/potters/" rel="alternate" type="text/html" />
    <id>https://0jonjo.github.io/blog/2025/potters/</id>
    <published>2025-08-26T00:00:00+00:00</published>
    <updated>2025-08-26T00:00:00+00:00</updated>
    <summary>The joy of making mistakes and the path of the apprentice.</summary>
    <content type="html">&lt;p&gt;Sensations and memories are sometimes unexpected and welcome guests, like the taste of a dish that brings back the shiver on your skin from contact with the sea water, or a song that escapes from a random window in the middle of the night and takes you back to a beloved place. These days, because just started the courses of the &lt;a href=&quot;https://www.linkedin.com/school/metropoledigital/posts/?feedView=all&quot;&gt;Associate’s degree in Artificial Intelligence (UFRN)&lt;/a&gt; with Supervised Machine Learning. I’ve returned to the Python programming language. Well, the first time I wrote something in this language was a years ago, back when I was just starting to learn how to program. So, these past few days, I’ve been working on small programs like someone trying to draw with a pen long forgotten at the bottom of a drawer. I’ve gone back to the path of the apprentice.&lt;/p&gt;

&lt;p&gt;Between tests and searches for the exercises, I remembered how everything seemed hazy back then. Today, even though I don’t master the language, I have a more solid foundation and can grasp the basics of what to do in each situation. Before, as is typical of someone just starting out, the challenge was often not just making something work, but understanding why it worked or didn’t. Between one line of code and the next, I’d risk a new option and get a red error on the screen. Then I’d ask more experienced friends or search online for a solution and adapt the varied answers to what I intended to do. Key concepts for understanding the context and how everything worked were still forming, so every step was uncertain.&lt;/p&gt;

&lt;p&gt;Learning is the challenge of shaping the unknown, like a potter making a jar from clay. Depending on your age, you might be thinking of that &lt;a href=&quot;https://www.youtube.com/watch?v=zG5xlakL7kI&quot;&gt;classic scene from the movie Ghost (1990)&lt;/a&gt; with the couple making a crooked clay pot together, but I’m serious. Learning isn’t simply accumulating theoretical and practical knowledge in a shopping bag; it’s refining the technique through which we organize and use what we know. And it’s certain that there’s no stopping point in this art of learning; there’s always something new to discover and test. Not to stray from the topic, it’s worth remembering an anecdote about Pablo Picasso. When he was older, &lt;a href=&quot;https://baillygallery.com/exhibitions/104-picasso-and-the-art-of-ceramics-the-madoura-chapter/&quot;&gt;he visited potters in the small French town of Vallauris&lt;/a&gt;. The painter was so captivated by the clay, the potter’s wheel, and the colors on the ceramics that he lived in the town for seven years and produced thousands of pieces over twenty years. In two decades, his hands as a painter and draftsman gained the strength and skill of refined potters who pull forms from clay that previously only existed in the world of ideas. For that to happen, of course, there was an uncountable number of vases, plates, sculptures, and other pieces that his hands distorted, clumps of clay that were ruined as he learned or invented new forms.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;https://github.com/0jonjo/0jonjo.github.io/assets/64807181/e2e238eb-f3f4-4828-85a3-48d87f226482&quot; alt=&quot;image&quot; /&gt;&lt;/p&gt;

&lt;p&gt;Among the first courses I took, &lt;a href=&quot;https://www.youtube.com/watch?v=YO58tXerKDc&amp;amp;list=PLUukMN0DTKCtbzhbYe2jdF4cr8MOWClXc&quot;&gt;Python for Zombies&lt;/a&gt; by the competent and good-humored professor &lt;a href=&quot;https://www.linkedin.com/in/fmasanori/&quot;&gt;Fernando Masanori&lt;/a&gt; caught my attention. The focus is on introducing new people to the world of programming, which was exactly what I was looking for at the time. Throughout the videos, in various situations of a screen error after a command, he’d let out one of his mantras: “I am happy; I will learn more.” Beyond the discontent we all feel when we make a mistake, Masanori points out that getting it wrong gives us the opportunity to expand our knowledge. His perspective is that of someone who knows what learning is all about, of someone who seeks to understand the mistake to get around it, refining their technique and adjusting their course. The joy with which he says this might sound naive and unrealistic at first. At the time, I myself looked at the screen and weighed my situation: thirty years old, eleven years studying History, just a few months away from becoming a father, over 300 students’ exams to correct, and this guy tells me to be happy for making a mistake. Honestly, I must have thought a bad word and maybe moved the mouse pointer to close the window. But something there captured me; the page stayed open.&lt;/p&gt;

&lt;p&gt;Months passed, I finished that introductory course, then another, and I kept changing technologies as I was introduced to them. I continued to share what I was learning with people close to me, also on social media and in blog posts. One of my friends, &lt;a href=&quot;https://www.linkedin.com/in/brennovich/&quot;&gt;Brenno&lt;/a&gt;, asked me at some point what it was like to explore something totally outside of what I knew. I don’t know exactly what words I said at the time, but the feeling was a mix of satisfaction and curiosity that we get with new discoveries. With several years of teaching and various degrees stored in a folder, I was once again experiencing the joy of not knowing, of being able to make mistakes freely and gradually discovering the world. Of being hit by the comprehension of a concept while taking a shower, of waking up early to work on a solution to problem X at my fingertips, of discovering something and commenting to myself: “Wow, that was there the whole time.” Going back to the Python exercises reminded me of the happiness of the apprentice that I experienced a few years before, that the sparkle in our eyes from discoveries is not just a right for children and teenagers, but for all those willing to learn.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;https://github.com/0jonjo/0jonjo.github.io/assets/64807181/9364065b-f8c8-489c-a2e3-55bff1acd8c1&quot; alt=&quot;image&quot; /&gt;&lt;/p&gt;
&lt;blockquote&gt;
  &lt;p&gt;Image: “Free person making handmade pottery”. &lt;a href=&quot;https://www.rawpixel.com/image/5927428/photo-image-public-domain-art-free&quot;&gt;Open Verse, Creative Commons 1.0&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
</content>
  </entry>
  
  <entry>
    <title>Updating Calcpace with Gemini CLI</title>
    <link href="https://0jonjo.github.io/blog/2025/gemini-cli-calcpace/" rel="alternate" type="text/html" />
    <id>https://0jonjo.github.io/blog/2025/gemini-cli-calcpace/</id>
    <published>2025-07-05T00:00:00+00:00</published>
    <updated>2025-07-05T00:00:00+00:00</updated>
    <summary>Exploring Google&apos;s Generative AI Command Line Interface</summary>
    <content type="html">&lt;p&gt;A few weeks ago, the first public releases of &lt;a href=&quot;https://github.com/google-gemini/gemini-cli&quot;&gt;Gemini CLI&lt;/a&gt; were launched. I was curious to try out Google’s solution for generative AI in the command line, similar to Claude Code or OpenAI CLI. I wanted to see it in action with a real project, so I decided to work on the next update of my gem &lt;a href=&quot;https://rubygems.org/gems/calcpace&quot;&gt;Calcpace&lt;/a&gt; with it. It’s a project about calculations and conversions of distance, time and velocity that has more than 3,400 downloads on RubyGems.&lt;/p&gt;

&lt;p&gt;Installing Gemini CLI in Ubuntu is straightforward - just follow the instructions in the &lt;a href=&quot;https://github.com/google-gemini/gemini-cli#quickstart&quot;&gt;README&lt;/a&gt;. After that, you can use the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;gemini&lt;/code&gt; command to interact with the Gemini AI model. When using it for the first time, you need to authenticate with your Google account and set some simple configurations like theme. Currently, the CLI is free, but the model utilized is the powerful Gemini 2.5 Pro, which is the same one used in other Google paid products.&lt;/p&gt;

&lt;p&gt;I did all work inside my Calcpace project directory, and my initial good impression was that it automatically and quickly read all the project files and directories. As suggested in the documentation, I asked the AI to summarize the project, and it returned the main features, version, and license of the gem in a short and accurate summary. After that, I asked for a more detailed analysis of the codebase, and it provided a good overview of the main classes and methods. Then I requested it to identify potential improvements in the code, and it suggested some refactoring opportunities, such as quick wins to improve the project. Gemini CLI suggested improving the error handling that was already on my to-do list, and it also pointed out handling edge cases that I hadn’t considered yet, like user input in “XX:XX” format, which is not currently supported by the gem (only “XX:XX:XX” format). This was a great insight, as I hadn’t thought about this case before.&lt;/p&gt;

&lt;p&gt;Instead of just following the instructions, I prompted it to write a plan - in a Markdown file - on how to implement these improvements, and it provided a clear and concise plan with steps to follow. Then I asked it to follow the instructions of each part of the plan. Whenever Gemini CLI has to make a change in a file or run a command in the terminal, it asks for confirmation before proceeding, which is a good safety feature. I found this very useful, as it prevents accidental changes to the codebase and allows you to review the changes both before and after they are made.&lt;/p&gt;

&lt;p&gt;Another interesting aspect is that when Gemini CLI experiences some delay in answering or executing a command, it automatically switches the model to Gemini 2.5 Flash, which is faster to respond and execute commands, though with a slightly lower quality. In general, the quality of changes and suggestions made by Gemini CLI is very good. I also used it to make minor changes in my pipeline, update the &lt;a href=&quot;https://github.com/0jonjo/calcpace&quot;&gt;README&lt;/a&gt;, and modify the file with specifications of the gem to upload to RubyGems. The AI was able to understand the context of the project and provide relevant suggestions and changes.&lt;/p&gt;

&lt;p&gt;This is the Pull Request I created with the changes made using the Gemini CLI: &lt;a href=&quot;https://github.com/0jonjo/calcpace/pull/58/files&quot;&gt;[57] Improvements in calculation conversion and how handle errors&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Here is a summary of the main features of Gemini CLI that I found useful:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;Project Analysis&lt;/strong&gt;: It quickly reads and summarizes the project files, providing an overview of the main features, version, and license of the gem.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Code Review&lt;/strong&gt;: It identifies potential improvements in the code, suggesting refactoring opportunities and enhancements to error handling.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Plan Generation&lt;/strong&gt;: It can create a clear and concise plan for implementing improvements, breaking down the steps needed to achieve the desired changes.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Confirmation Prompts&lt;/strong&gt;: Before making any changes, it asks for confirmation, allowing you to review the changes before they are applied.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Model Switching&lt;/strong&gt;: It automatically switches to a faster model (Gemini 2.5 Flash) when there are delays in responses or command execution, ensuring a smoother experience.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;One aspect I particularly do not like in Gemini CLI is that you cannot see it using your existing terminal, or suggest it to use your terminal to run commands like tests or start the server. You cannot directly interact with the terminal like you can with Copilot, which allows you to write code directly in the editor.&lt;/p&gt;

&lt;p&gt;In general: &lt;del&gt;sorry Sarah Connor, Gemini CLI is my best friend now.&lt;/del&gt; I found Gemini CLI faster and more efficient than Github Copilot with Claude Sonnet 3.7 or 4.0. After this initial experience, I started to use both tools together: using Gemini CLI for project analysis and planning, and then leveraging Copilot for specific code changes, tests, and enhancements. This is my current strategy to learn more about both tools and how they can complement each other.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;https://github.com/user-attachments/assets/f062ff72-314b-4f33-9b95-b3b4cfbb2bdf&quot; alt=&quot;image&quot; /&gt;&lt;/p&gt;

&lt;p&gt;Image: Meme of Sarah Connor (Linda Hamilton), from &lt;a href=&quot;https://en.wikipedia.org/wiki/The_Terminator&quot;&gt;The Exterminator&lt;/a&gt;.&lt;/p&gt;
</content>
  </entry>
  
  <entry>
    <title>Five attitudes for the learner</title>
    <link href="https://0jonjo.github.io/blog/2025/learner/" rel="alternate" type="text/html" />
    <id>https://0jonjo.github.io/blog/2025/learner/</id>
    <published>2025-06-25T00:00:00+00:00</published>
    <updated>2025-06-25T00:00:00+00:00</updated>
    <summary>What&apos;s needed to start a learning journey</summary>
    <content type="html">&lt;p&gt;I just completed my first month as a software developer at &lt;a href=&quot;https://jetrockets.com/&quot;&gt;JetRockets&lt;/a&gt;, a really cool place. It’s been thirty days of connecting with people from different countries, understanding how things work, studying new topics, business rules, and tackling my first tasks and features. Since day one, I’ve been reflecting on my experiences over the past few years regarding starting in a new place and how to learn effectively&lt;/p&gt;

&lt;p&gt;It’s not uncommon, while idly staring into space with a cup of coffee in hand, to find ourselves reflecting on an idea, attitude, or piece of information that would have made all the difference if only we’d known it sooner. “It would have been great to understand that back then…” “If only I’d been like this ten years ago…” The endless “what ifs” that haunt us now and then, or always. Examining this through the microscope of reason reveals that who we are and what we know today is precisely the result of the challenges we’ve overcome. There’s no way we could possess this knowledge and these perspectives without the experiences that forged them. It’s even comforting to think this way, but there’s no bulletproof vest for that sudden urge to go back in time with the perfect attitude or comeback on the tip of our tongue. The past is a living thing; you can’t simply archive it in a forgotten folder on your memory drive. No AI or organic intelligence exists without continuously processing its own repository of information.&lt;/p&gt;

&lt;p&gt;I found myself pondering this when, in the same week, two young individuals asked me about the skills and knowledge needed to embark on a journey of learning and work. The first, a former elementary school student of mine, is starting a History degree. The second is a programming student eager to take their first steps into the job market. I wondered what useful advice I could offer to a teenage João Gilberto learning to play musical instruments, and to the same individual as he was about to enter university and his first job. Also, to the postgraduate professor writing his first lines of code. I combined these thoughts with my experience of learning and teaching various subjects and synthesized a few key points:&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;
    &lt;p&gt;&lt;strong&gt;Cultivate genuine curiosity&lt;/strong&gt;: learning becomes a true torment if we’re not genuinely interested in the topic or can’t even grasp the point of studying it. Of course, not every subject will be a deep passion that profoundly excites us. Still, the bricks of disinterest and the cement of laziness often form an insurmountable wall.&lt;/p&gt;
  &lt;/li&gt;
  &lt;li&gt;
    &lt;p&gt;&lt;strong&gt;Be coachable (or mentor-friendly)&lt;/strong&gt;: being open to learning from diverse people and in various contexts is crucial. Far beyond the formal classroom setting, life is rich with situations where we can absorb knowledge. Being open to this, with ears and ego prepared, is a small price to pay for the significant gains you can achieve.&lt;/p&gt;
  &lt;/li&gt;
  &lt;li&gt;
    &lt;p&gt;&lt;strong&gt;Know how to ask questions&lt;/strong&gt;: questions drive humanity forward. It’s a cliché, but a true one, as evidenced by the success of internet search engines – the largest one processes around 3.5 billion queries daily. Knowing how to ask means contextualizing your query, defining the scope of what you want to know, and genuinely considering the responses before launching a new question. This applies whether you’re questioning a classmate, a forum, or even a generative AI.&lt;/p&gt;
  &lt;/li&gt;
  &lt;li&gt;
    &lt;p&gt;&lt;strong&gt;learn how to learn&lt;/strong&gt;: there isn’t a one-size-fits-all formula through which everyone effectively learns a particular subject. What does exist are various learning strategies, some more or less effective depending on the context. In another post, I explored this very aspect, from choosing the right materials to understanding your optimal learning style. It’s worth reflecting on past experiences: when did you successfully learn something, and when did it go wrong? And most importantly, why?&lt;/p&gt;
  &lt;/li&gt;
  &lt;li&gt;
    &lt;p&gt;&lt;strong&gt;Practice&lt;/strong&gt;: the other day, while discussing the importance of practice in learning, I recalled a saying I believe is of Dutch origin: “There has never been, nor will there ever be, a good book on how to ride a bicycle.” The point isn’t to diminish books — anyone who knows me understands my love for them — but rather to underscore the importance of constant practice. Theory is dead letter if it doesn’t ground action. Good musicians helped me realize that to truly learn, the best approach is to calmly &lt;a href=&quot;https://www.youtube.com/watch?v=uNuXYT5xPbo&quot;&gt;start with simple exercises and gradually increase speed and complexity&lt;/a&gt;. There’s a virtuous cycle of encountering knowledge (whether by reading, watching, listening, etc.), attempting to do it (through exercises, personal projects, conversations, etc.), and repeating this process while adjusting your strategies at each step.&lt;/p&gt;
  &lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Here is the &lt;a href=&quot;/blog/2023/aprendiz/&quot;&gt;Portuguese version&lt;/a&gt; of this post.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;https://github.com/0jonjo/0jonjo.github.io/assets/64807181/f45b9ebc-0936-4796-9e23-a3ea2af5c97f&quot; alt=&quot;image&quot; /&gt;&lt;/p&gt;
&lt;blockquote&gt;
  &lt;p&gt;Image: “High five”, Martin Fisch. From: &lt;a href=&quot;https://openverse.org/image/0d46348d-efb5-43ae-9f0d-5206a8741299&quot;&gt;Open Verse, Creative Commons 2.0&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
</content>
  </entry>
  
  <entry>
    <title>In Clouds - getting certified as Associate Cloud Engineer</title>
    <link href="https://0jonjo.github.io/blog/2025/ace-google-cloud/" rel="alternate" type="text/html" />
    <id>https://0jonjo.github.io/blog/2025/ace-google-cloud/</id>
    <published>2025-05-27T00:00:00+00:00</published>
    <updated>2025-05-27T00:00:00+00:00</updated>
    <summary>Learning in classes, exercises, and practical labs of Google Cloud</summary>
    <content type="html">&lt;p&gt;After three months of classes, exercises, and practical labs, I’m excited to announce that I passed the exam and am now certified as an &lt;a href=&quot;https://cloud.google.com/learn/certification/cloud-engineer&quot;&gt;Associate Cloud Engineer&lt;/a&gt; by Google Cloud!&lt;/p&gt;

&lt;p&gt;A few months ago, I enrolled in the Get Certified 2025 program. It’s an excellent program that offers free credits for Qwiklabs, live synchronous classes, study groups, and practical exercises, all designed to prepare you for the certification exam. The instructors are incredibly knowledgeable, and the content is well-structured. Special shout-out to &lt;a href=&quot;https://www.linkedin.com/company/arki1&quot;&gt;Arki1&lt;/a&gt;, their partner responsible for support, who specializes in cloud computing training.&lt;/p&gt;

&lt;p&gt;I particularly enjoyed the blend of theory and hands-on practice. All exercises used real tools in &lt;a href=&quot;https://cloud.google.com/&quot;&gt;Google Cloud accounts&lt;/a&gt; with costs fully covered by Google. This was a fantastic opportunity to learn and practice cloud computing concepts in a truly hands-on way, especially for me with Kubernetes, deployments, load balancing, and networking.&lt;/p&gt;

&lt;p&gt;If you’re interested in cloud technology and considering getting certified, I highly recommend checking out the &lt;a href=&quot;https://cloud.google.com/innovators/getcertified&quot;&gt;Get Certified program&lt;/a&gt;. It’s a superb way to learn and prepare for the certification exam (FOR FREE!), with the added benefit of experienced instructors and a supportive community of learners.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;https://github.com/user-attachments/assets/e3418219-382b-4806-9b8f-0b9da3a72f36&quot; alt=&quot;Untitled&quot; /&gt;&lt;/p&gt;

</content>
  </entry>
  
  <entry>
    <title>Tropical on Rails 2025 - What an incredible event!</title>
    <link href="https://0jonjo.github.io/blog/2025/tropical-rails-2025/" rel="alternate" type="text/html" />
    <id>https://0jonjo.github.io/blog/2025/tropical-rails-2025/</id>
    <published>2025-04-15T00:00:00+00:00</published>
    <updated>2025-04-15T00:00:00+00:00</updated>
    <summary>The Rails community of the World is in São Paulo, Brazil!</summary>
    <content type="html">&lt;p&gt;I had the pleasure of attending the &lt;a href=&quot;https://tropicalonrails.com/&quot;&gt;Tropical on Rails 2025&lt;/a&gt; conference in São Paulo, Brazil, on April 03 and 04, 2025. It was a fantastic experience!
The event was organized by &lt;a href=&quot;https://www.linkedin.com/in/cirdeshenrique/&quot;&gt;Cirdes Henrique&lt;/a&gt; and his team, who did an amazing job bringing together a diverse group of speakers and attendees from around the world.&lt;/p&gt;

&lt;p&gt;Two full days of keynotes, talks, and in-depth discussions on the key technical and business challenges facing the Ruby on Rails ecosystem and the wider tech industry. Topics included LLM integration, cloud costs and deployment, asynchronous job testing, and more.&lt;/p&gt;

&lt;p&gt;Great to connect and learn from experts and major players from Brazil, the USA, Canada, Mexico, Georgia, Spain, and beyond.&lt;/p&gt;

&lt;p&gt;Impeccable organization in every detail. Thanks to Cirdes and everyone who made these two days a fantastic experience for networking, learning, and even some enjoyment!&lt;/p&gt;

&lt;p&gt;In the next weeks the videos of the talks will be available on the &lt;a href=&quot;https://www.youtube.com/@tropicalonrails&quot;&gt;YouTube channel&lt;/a&gt; of the event. I highly recommend checking them out!&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;https://github.com/user-attachments/assets/9980c90b-9b37-4fc4-a09e-d22b5a8e17e9&quot; alt=&quot;image&quot; /&gt;&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;https://github.com/user-attachments/assets/fdfe8647-278d-412c-ab02-a5c7e9f9a57c&quot; alt=&quot;image&quot; /&gt;&lt;/p&gt;

</content>
  </entry>
  
  <entry>
    <title>A Quick Guide to PostgreSQL with Docker</title>
    <link href="https://0jonjo.github.io/blog/2025/docker-postgres/" rel="alternate" type="text/html" />
    <id>https://0jonjo.github.io/blog/2025/docker-postgres/</id>
    <published>2025-02-23T00:00:00+00:00</published>
    <updated>2025-02-23T00:00:00+00:00</updated>
    <summary>A easy way to use a PostgreSQL database in a container to run tests and development</summary>
    <content type="html">&lt;p&gt;A month ago, my Ubuntu &lt;a href=&quot;https://ubuntu.com/download/desktop&quot;&gt;24.04.2 LTS&lt;/a&gt; system had a problem during a system update and wouldn’t boot.  While troubleshooting in recovery mode, I discovered the issue was with &lt;a href=&quot;https://www.postgresql.org/&quot;&gt;PostgreSQL&lt;/a&gt;. I removed PostgreSQL, performed some updates and upgrades, and the system was working again. However, I encountered some problems when trying to reinstall PostgreSQL. It was a Monday morning, and I had a lot of work to do. While considering my options, I remembered &lt;a href=&quot;https://www.docker.com/&quot;&gt;Docker&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Using a PostgreSQL database in a container is an easy way to run tests and development environments. I primarily use it for running automated tests, experimenting with changes in the console, running APIs locally to test code modifications, and so on.  Here are the steps to use a local PostgreSQL database in a container:&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;Install Docker if you haven’t already. Follow the instructions on the &lt;a href=&quot;https://docs.docker.com/get-docker/&quot;&gt;Docker website&lt;/a&gt;.&lt;/li&gt;
  &lt;li&gt;Choose the PostgreSQL image you want to use. You can find the images on &lt;a href=&quot;https://hub.docker.com/_/postgres&quot;&gt;Docker Hub&lt;/a&gt;. Pay attention to the version you select, ensuring it’s compatible with your project. Also, consider if you need any specific extensions like &lt;a href=&quot;https://postgis.net/&quot;&gt;PostGIS&lt;/a&gt;, &lt;a href=&quot;https://www.postgresql.org/docs/current/pgtrgm.html&quot;&gt;pg_trgm&lt;/a&gt;, &lt;a href=&quot;https://www.postgresql.org/docs/current/pgcrypto.html&quot;&gt;pgcrypto&lt;/a&gt; etc.&lt;/li&gt;
  &lt;li&gt;Prepare the command to run the container with the image. Be sure to set the necessary environment variables, such as the password, the port you want to use, and so on. You can find the environment variables on &lt;a href=&quot;https://hub.docker.com/_/postgres&quot;&gt;Docker Hub&lt;/a&gt; as well. Frameworks like Django or Ruby on Rails typically have a configuration file for database settings. Double-check that you’re using the correct configuration in both the Docker command and the configuration file.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Here is an example of a command to run a PostgreSQL container with the PostGIS extension:&lt;/p&gt;

&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;docker run &lt;span class=&quot;nt&quot;&gt;-d&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;--hostname&lt;/span&gt; localhost &lt;span class=&quot;nt&quot;&gt;-e&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;POSTGRES_PASSWORD&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;postgres &lt;span class=&quot;nt&quot;&gt;-p&lt;/span&gt; 5432:5432 postgis/postgis:16-3.5
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;In the example above, the container will run in the background (&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;-d&lt;/code&gt;). The password is &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;postgres&lt;/code&gt;, and the port is &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;5432&lt;/code&gt; (both in the container and on the host). The hostname is &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;localhost&lt;/code&gt;, and the image is &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;postgis/postgis:16-3.5&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;You can use the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;docker ps&lt;/code&gt; command (or &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;docker container list&lt;/code&gt;) to see the running containers.  You’ll see something like this:&lt;/p&gt;

&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;CONTAINER ID   IMAGE                    COMMAND                  CREATED         STATUS         PORTS                                       NAMES
d8f6f57868cb   postgis/postgis:16-3.5   &lt;span class=&quot;s2&quot;&gt;&quot;docker-entrypoint.s…&quot;&lt;/span&gt;   2 seconds ago   Up 2 seconds   0.0.0.0:5432-&amp;gt;5432/tcp, :::5432-&amp;gt;5432/tcp   objective_turtle
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Now it’s time to use the container. Try some commands to create a database, a table, insert some data, etc. You can use commands from your framework to access the database. In Django, you can use &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;python manage.py dbshell&lt;/code&gt; command to access the database. In Ruby on Rails, you can use &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;rails db&lt;/code&gt; command to access the database. You can also use the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;psql&lt;/code&gt; command. To access the database from within the container, you can use &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;docker exec -it &amp;lt;container_name&amp;gt; bash&lt;/code&gt; to enter in the container and then use &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;psql&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;When you’re finished, you can stop the container with &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;docker stop &amp;lt;container_name&amp;gt;&lt;/code&gt; command. You can remove the container with the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;docker rm &amp;lt;container_name&amp;gt;&lt;/code&gt; command.  You can remove the container with &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;docker rm &amp;lt;container_name&amp;gt;&lt;/code&gt; and the image with &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;docker rmi &amp;lt;image_name&amp;gt;&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Here the list of useful commands:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;docker ps&lt;/code&gt; or &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;docker container list&lt;/code&gt;: list the running containers&lt;/li&gt;
  &lt;li&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;docker stop &amp;lt;container_name&amp;gt;&lt;/code&gt;: stop the container&lt;/li&gt;
  &lt;li&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;docker rm &amp;lt;container_name&amp;gt;&lt;/code&gt;: remove the container&lt;/li&gt;
  &lt;li&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;docker rmi &amp;lt;image_name&amp;gt;&lt;/code&gt;: remove the image&lt;/li&gt;
  &lt;li&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;docker exec -it &amp;lt;container_name&amp;gt; bash&lt;/code&gt;: enter in the container&lt;/li&gt;
  &lt;li&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;psql&lt;/code&gt;: access the database&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;And that’s it! It’s easy and fast and you can follow the same steps to use other databases like MySQL, MariaDB, etc. Have fun!&lt;/p&gt;

&lt;table cellpadding=&quot;0&quot; cellspacing=&quot;0&quot; border=&quot;0&quot; width=&quot;100%&quot;&gt;
&lt;tr&gt;&lt;td align=&quot;center&quot;&gt;
  &lt;img src=&quot;https://live.staticflickr.com/65535/51936373085_d70f7e8117_b.jpg&quot; width=&quot;450&quot; alt=&quot;Stacks of colorful shipping containers next to a rail crossing sign&quot; /&gt;
&lt;/td&gt;&lt;/tr&gt;
&lt;/table&gt;

&lt;blockquote&gt;
  &lt;p&gt;Image: &lt;a href=&quot;https://openverse.org/image/f7b56405-8eb1-4122-b9b5-e3eab0a4564d&quot;&gt;Container City ~ Explore #15 14-03-2022&lt;/a&gt;. Source: OpenVerse - license Creative Commons 2.0.&lt;/p&gt;
&lt;/blockquote&gt;
</content>
  </entry>
  
  <entry>
    <title>The Art of Onboarding</title>
    <link href="https://0jonjo.github.io/blog/2025/art-onboarding/" rel="alternate" type="text/html" />
    <id>https://0jonjo.github.io/blog/2025/art-onboarding/</id>
    <published>2025-01-17T00:00:00+00:00</published>
    <updated>2025-01-17T00:00:00+00:00</updated>
    <summary>Five tips to accelerate and enhance your onboarding experience</summary>
    <content type="html">&lt;p&gt;Walking into a new room and feeling everyone’s eyes on you can be nerve-wracking. That initial awkwardness of being the new person eventually fades as you become integrated into the team. When joining a new company or project, there’s always an adjustment period where you’re absorbing information about people, processes, and products. This period, commonly known as onboarding, is crucial for new team members to get up to speed and start contributing effectively.&lt;/p&gt;

&lt;p&gt;Every company and team has its own onboarding process, varying in duration and intensity. It’s often a period of both anxiety for the new hire and mutual discovery. A friend who has experienced onboarding from both sides of the table says it takes at least three months for someone to reach full capacity in a new role.&lt;/p&gt;

&lt;p&gt;I’ve been actively involved in onboarding new hires and have learned from my former technical lead, Nilson Júnior, that providing comprehensive support is essential. This includes granting necessary access, explaining company ceremonies, addressing technical questions, and fostering a welcoming environment. Having entered the tech industry in my 30s with experience in other fields, I’ve developed strategies to accelerate my own onboarding and that of others.&lt;/p&gt;

&lt;p&gt;The goal of onboarding is to help new hires quickly find their footing and become productive members of the team. Here are five key strategies:&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;
    &lt;p&gt;The Detective’s Questions:
When entering a new environment with unfamiliar people, it’s natural to have many questions. Historians, doctors, detectives, and researchers from various fields share a common approach based on the scientific method: a simple set of questions that can be counted on one hand – What? When? Where? How? Why? By jotting down answers and new questions as you encounter them, you can better understand the information and identify knowledge gaps.&lt;/p&gt;
  &lt;/li&gt;
  &lt;li&gt;
    &lt;p&gt;The Rules of the Game:
While the detective’s questions provide a solid foundation, understanding the business is also crucial. Every software application serves a specific purpose. Ask questions like: Who is the end-user? What value does the product deliver? What are the current initiatives and challenges? Why were these initiatives chosen? How does this app differentiate itself from competitors? What are the industry’s jargon and terminology?&lt;/p&gt;
  &lt;/li&gt;
  &lt;li&gt;
    &lt;p&gt;Expectations and People:
Joining a new team comes with a set of expectations. Beyond the initial anxiety and first impressions, there are also projections from leaders, colleagues, and clients. Relationships are the heart of any workplace. Understanding each person’s role, their daily tasks, and their working style is crucial. Similarly, it’s important to clarify what is expected of you. Are you replacing someone? Filling a skills gap? Leading a team? Having a clear understanding of expectations helps you align your skills with the role’s requirements.&lt;/p&gt;
  &lt;/li&gt;
  &lt;li&gt;
    &lt;p&gt;Charting Your Course:
Once you have a basic understanding of the business, technology, people, and expectations, it’s time to set goals. Perhaps you’ve identified a knowledge gap in a particular technology or business area. Or maybe you see an opportunity to contribute using your existing skills. The key is to identify areas for improvement and create a plan to address them. This could involve taking a course, joining a study group, or seeking mentorship.&lt;/p&gt;
  &lt;/li&gt;
  &lt;li&gt;
    &lt;p&gt;Individual Action, Collective Process:
While onboarding is primarily the responsibility of the new hire, it’s also a collective process involving management, technical leads, and the entire team. A welcoming and supportive environment can significantly speed up the onboarding process. If you’ve been through onboarding before, reflect on your experience. What worked well? What could have been improved? Sharing your insights with others can be beneficial.&lt;/p&gt;
  &lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;In conclusion, onboarding is a journey of discovery that can be both exciting and challenging. It’s an opportunity to learn new skills and contribute to a new team. By actively seeking information, building relationships, and setting clear goals, you can make the most of this experience.&lt;/p&gt;

&lt;table cellpadding=&quot;0&quot; cellspacing=&quot;0&quot; border=&quot;0&quot; width=&quot;100%&quot;&gt;
&lt;tr&gt;&lt;td align=&quot;center&quot;&gt;
  &lt;img src=&quot;https://live.staticflickr.com/1784/42898374451_c6cb68dd17_b.jpg&quot; width=&quot;450&quot; alt=&quot;Painted door with a white rabbit set into a tree trunk, evoking a new beginning&quot; /&gt;
&lt;/td&gt;&lt;/tr&gt;
&lt;/table&gt;

&lt;blockquote&gt;
  &lt;p&gt;Image: &lt;a href=&quot;https://openverse.org/image/a76c2702-2311-45b1-8052-515abe9d0855&quot;&gt;Apollo 11 moments before landing on the Moon&lt;/a&gt;. Source: OpenVerse - license Creative Commons 2.0.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;This is a translation of a post in portuguese. The original text can be found here: &lt;a href=&quot;https://0jonjo.github.io//blog/2024/arte-onboarding/&quot;&gt;A arte do onboarding&lt;/a&gt;&lt;/p&gt;
</content>
  </entry>
  
  <entry>
    <title>1000 Tage lernen</title>
    <link href="https://0jonjo.github.io/blog/2024/1000-days/" rel="alternate" type="text/html" />
    <id>https://0jonjo.github.io/blog/2024/1000-days/</id>
    <published>2024-11-28T00:00:00+00:00</published>
    <updated>2024-11-28T00:00:00+00:00</updated>
    <summary>Learning everyday</summary>
    <content type="html">&lt;p&gt;Learning something on your own is both a challenge and a self-discovery experience. I recently passed the mark of one year studying German daily on Duolingo - hence the 1000 days of learning in the title - a good milestone to reflect a little on learning and self-teaching.&lt;/p&gt;

&lt;p&gt;One point is how to deal with the infinite paths that the Internet offers. On the one hand, the freedom to follow the direction you want and to be able to change course at any time. But at the same time, how to know if you’re going in the right direction, or measure how much you’ve actually learned. I read about this a couple of years in &lt;a href=&quot;https://www.casadocodigo.com.br/products/livro-navegando-universo&quot;&gt;William Oliveira’s book&lt;/a&gt; and I agree with the author. If you know nothing about the subject you want to study, it’s worth researching paths, maps, videos, course syllabi and the like that are introductory to that topic. There will probably be online courses, books, websites, etc. that are repeated in these recommendations, a good sign of where to start. However, in this sea of information there must also be posts, lives, reviews and the like that are actually advertising to buy certain material, it’s up to you to differentiate what is a genuine suggestion from what is marketing.&lt;/p&gt;

&lt;p&gt;It’s important to keep in mind that each person has their own affinities for a certain type of content, I personally prefer texts to videos to learn something. In addition, there are materials of various levels, it is useful to look for those that are compatible with your current knowledge. If I were to learn Chinese today, I would have to find content for those who know nothing, not an advanced course for someone who is going to live in China next semester. In this screening of materials it is pertinent to test: leaf through, watch, listen, use the app or whatever it is. See if you liked that approach, the content, etc., if you felt confident and interested to move forward in that direction. There is no one-size-fits-all recipe, each person will have their app, book or website that they found most suitable and comfortable to learn.&lt;/p&gt;

&lt;p&gt;Once the route is chosen, it’s time to walk, because just having the best study material in the world saved on your phone or in your closet won’t make you learn anything. How many plans have we all made one day that were never put into practice? Learning something requires repetition: reading, practicing, summarizing, acting, rereading and so on in a daily circle. This is the step that is both simple and difficult, going beyond the initial excitement and building a habit.&lt;/p&gt;

&lt;p&gt;This is a translation of a post in portuguese. The original text can be found here: &lt;a href=&quot;https://0jonjo.github.io/blog/2023/400-dias/&quot;&gt;https://0jonjo.github.io/blog/2023/400-dias/&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;https://user-images.githubusercontent.com/64807181/230774513-897eed58-80cb-4ba3-a883-6b3b04a7bf20.png&quot; alt=&quot;image&quot; /&gt;&lt;/p&gt;
</content>
  </entry>
  
  <entry>
    <title>Calcpace 1.4.0 released</title>
    <link href="https://0jonjo.github.io/blog/2024/calpace-140/" rel="alternate" type="text/html" />
    <id>https://0jonjo.github.io/blog/2024/calpace-140/</id>
    <published>2024-09-16T00:00:00+00:00</published>
    <updated>2024-09-16T00:00:00+00:00</updated>
    <summary>New features and improvements in the latest version of the gem</summary>
    <content type="html">&lt;p&gt;I’ve released version 1.4.0 of Calcpace!&lt;/p&gt;

&lt;p&gt;This version introduces new possibilities for distance and speed calculations, including average speed, pace, total time, and predicted distance. It also features a conversion tool with options for international, nautical, and imperial standards.&lt;/p&gt;

&lt;p&gt;You can calculate using three kinds of methods:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;Without checks&lt;/strong&gt;: Fastest, using float or integers.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;With checks&lt;/strong&gt;: Fast, using float/integers and clocktime (HH:MM:SS).&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;With clocktime return&lt;/strong&gt;: Slowest.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Calcpace supports different check methods for each calculation and offers 26 types of conversions between distances and speeds. See the table below for the new conversion options:&lt;/p&gt;

&lt;table&gt;
  &lt;thead&gt;
    &lt;tr&gt;
      &lt;th&gt;Conversion Unit&lt;/th&gt;
      &lt;th&gt;Description&lt;/th&gt;
    &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
    &lt;tr&gt;
      &lt;td&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;:km_to_mi&lt;/code&gt;&lt;/td&gt;
      &lt;td&gt;Kilometers to Miles&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;:mi_to_km&lt;/code&gt;&lt;/td&gt;
      &lt;td&gt;Miles to Kilometers&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;:nautical_mi_to_km&lt;/code&gt;&lt;/td&gt;
      &lt;td&gt;Nautical Miles to Kilometers&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;:km_to_nautical_mi&lt;/code&gt;&lt;/td&gt;
      &lt;td&gt;Kilometers to Nautical Miles&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;:meters_to_km&lt;/code&gt;&lt;/td&gt;
      &lt;td&gt;Meters to Kilometers&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;:km_to_meters&lt;/code&gt;&lt;/td&gt;
      &lt;td&gt;Kilometers to Meters&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;:meters_to_mi&lt;/code&gt;&lt;/td&gt;
      &lt;td&gt;Meters to Miles&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;:mi_to_meters&lt;/code&gt;&lt;/td&gt;
      &lt;td&gt;Miles to Meters&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;:meters_to_feet&lt;/code&gt;&lt;/td&gt;
      &lt;td&gt;Meters to Feet&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;:feet_to_meters&lt;/code&gt;&lt;/td&gt;
      &lt;td&gt;Feet to Meters&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;:meters_to_yards&lt;/code&gt;&lt;/td&gt;
      &lt;td&gt;Meters to Yards&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;:yards_to_meters&lt;/code&gt;&lt;/td&gt;
      &lt;td&gt;Yards to Meters&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;:meters_to_inches&lt;/code&gt;&lt;/td&gt;
      &lt;td&gt;Meters to Inches&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;:inches_to_meters&lt;/code&gt;&lt;/td&gt;
      &lt;td&gt;Inches to Meters&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;:m_s_to_km_h&lt;/code&gt;&lt;/td&gt;
      &lt;td&gt;Meters per Second to Kilometers per Hour&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;:km_h_to_m_s&lt;/code&gt;&lt;/td&gt;
      &lt;td&gt;Kilometers per Hour to Meters per Second&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;:m_s_to_mi_h&lt;/code&gt;&lt;/td&gt;
      &lt;td&gt;Meters per Second to Miles per Hour&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;:mi_h_to_m_s&lt;/code&gt;&lt;/td&gt;
      &lt;td&gt;Miles per Hour to Meters per Second&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;:m_s_to_nautical_mi_h&lt;/code&gt;&lt;/td&gt;
      &lt;td&gt;Meters per Second to Nautical Miles per Hour&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;:nautical_mi_h_to_m_s&lt;/code&gt;&lt;/td&gt;
      &lt;td&gt;Nautical Miles per Hour to Meters per Second&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;:m_s_to_feet_s&lt;/code&gt;&lt;/td&gt;
      &lt;td&gt;Meters per Second to Feet per Second&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;:feet_s_to_m_s&lt;/code&gt;&lt;/td&gt;
      &lt;td&gt;Feet per Second to Meters per Second&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;:m_s_to_knots&lt;/code&gt;&lt;/td&gt;
      &lt;td&gt;Meters per Second to Knots&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;:knots_to_m_s&lt;/code&gt;&lt;/td&gt;
      &lt;td&gt;Knots to Meters per Second&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;:km_h_to_mi_h&lt;/code&gt;&lt;/td&gt;
      &lt;td&gt;Kilometers per Hour to Miles per Hour&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;:mi_h_to_km_h&lt;/code&gt;&lt;/td&gt;
      &lt;td&gt;Miles per Hour to Kilometers per Hour&lt;/td&gt;
    &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;

&lt;p&gt;The documentation has been revamped with examples of how to use the new features.&lt;/p&gt;

&lt;p&gt;RubyGems: &lt;a href=&quot;https://rubygems.org/gems/calcpace&quot;&gt;https://rubygems.org/gems/calcpace&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;For more information on how to use Calcpace, visit the GitHub repository: &lt;a href=&quot;https://github.com/0jonjo/calcpace&quot;&gt;https://github.com/0jonjo/calcpace&lt;/a&gt;&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;Image: &lt;a href=&quot;https://openverse.org/image/a7b2cdea-5287-4f04-8da8-13b7351dfdf3&quot;&gt;“Map preparation, quick and sloppy, Johan G”&lt;/a&gt;. &lt;a href=&quot;https://openverse.org/&quot;&gt;Open Verse, Creative Commons 2.0&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
</content>
  </entry>
  
</feed>

