<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Yassine Sadgui </title>
    <description>The latest articles on DEV Community by Yassine Sadgui  (@yassines99).</description>
    <link>https://dev.to/yassines99</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4064872%2Fcb2b2b45-e62f-470b-bc6a-8219870b814d.png</url>
      <title>DEV Community: Yassine Sadgui </title>
      <link>https://dev.to/yassines99</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/yassines99"/>
    <language>en</language>
    <item>
      <title>Why I Stopped Trusting Similarity Scores and Built a System That Says "No"</title>
      <dc:creator>Yassine Sadgui </dc:creator>
      <pubDate>Wed, 12 Aug 2026 06:45:43 +0000</pubDate>
      <link>https://dev.to/yassines99/why-i-stopped-trusting-similarity-scores-and-built-a-system-that-says-no-3iof</link>
      <guid>https://dev.to/yassines99/why-i-stopped-trusting-similarity-scores-and-built-a-system-that-says-no-3iof</guid>
      <description>&lt;p&gt;I'll be honest about where this started. I could get a vision model to look at a photo and spit back a caption. I could get two pieces of text to embed into vectors and compute a similarity score between them. What I couldn't do, not really, was explain what happens when those two things almost agree but shouldn't.&lt;/p&gt;

&lt;p&gt;That gap bothered me. So for my capstone at FlyRank, I picked a project that forced me to sit inside that gap instead of walking around it: build a system that matches blog posts to the right animal photo, and refuses to guess when it isn't sure. A red fox post should get a red fox photo. A gray wolf photo should never sneak onto it, even if the two animals look close enough to fool a similarity score.&lt;/p&gt;

&lt;p&gt;This is what that actually looked like, including the part where a coyote almost got past me.&lt;/p&gt;

&lt;p&gt;What I was working with&lt;/p&gt;

&lt;p&gt;About 50 images across five species that all kind of resemble each other if you're not paying close attention: red fox, gray wolf, coyote, Siberian husky, German shepherd. I picked those on purpose. Fox versus penguin versus shark would have been trivial, any system clears that bar. I wanted animals genuinely hard to tell apart, hard enough that even the model doing the classifying would sometimes hedge.&lt;/p&gt;

&lt;p&gt;The pipeline, roughly: a vision model looks at each image and produces structured tags, subject, category, visible attributes, a caption, a confidence score. Image captions and post text both get embedded into the same vector space. Images get ranked by similarity to the post. And before anything gets suggested to a human, it has to survive a guard that's allowed to veto the top result entirely.&lt;/p&gt;

&lt;p&gt;That last piece is the one I actually care about.&lt;/p&gt;

&lt;p&gt;Why ranking alone wasn't enough&lt;/p&gt;

&lt;p&gt;I wrote a test post. "Amazing shot of a wolf pack howling at night." Ran it through the ranker. Wolves came back on top, which is exactly what should happen. Then I kept scrolling through the rest of the results and found this sitting in the middle of them:&lt;/p&gt;

&lt;p&gt;coyote_002.jpg | similarity: 0.6360 | REJECTED&lt;br&gt;
reason: subject mismatch: post implies 'gray wolf', image is 'coyote'&lt;/p&gt;

&lt;p&gt;A coyote photo scored 0.636 against my wolf post. The actual wolves in that same batch scored between 0.634 and 0.642. That coyote wasn't some outlier sitting off in the noise somewhere, it was sitting right inside the cluster of correct answers. A similarity only system would have handed it back with total confidence, and there would have been nothing in the output that looked wrong. High score, clean looking match, wrong animal.&lt;/p&gt;

&lt;p&gt;I didn't sit down and design that test case to prove a point. I was just poking at my own system out of curiosity, and it handed me the exact proof I needed.&lt;/p&gt;

&lt;p&gt;The guard itself checks two things. Is the similarity score above a threshold, and does the image's actual subject match what the post seems to be about. Fail either one and the pairing gets rejected with a plain English reason attached, not a silent no.&lt;/p&gt;

&lt;p&gt;The part nobody warns you about&lt;/p&gt;

&lt;p&gt;The AI logic took less time than getting the AI providers to cooperate.&lt;/p&gt;

&lt;p&gt;I started with OpenAI and hit a billing wall almost immediately, there isn't really a free tier for vision anymore. Switched to Gemini and got a limit: 0 quota error on one model and a 20 requests a day cap on another, which meant my batch job classifying 49 images ended up spread across three separate days because I kept running out of quota partway through a run. Tried Groq next since I'd used it before on a different project, only to find my account had zero vision capable models on it at all.&lt;/p&gt;

&lt;p&gt;None of that was a code problem. It just ate far more hours than the actual guard logic did. What came out of it was a batch script with retries and a check that skips anything already classified, built specifically so I could stop the process, walk away, come back the next day, and pick up exactly where the quota had cut me off, without wasting API calls reclassifying images I already had results for.&lt;/p&gt;

&lt;p&gt;Small thing. It was also the difference between a script I could actually walk away from and one I had to babysit.&lt;/p&gt;

&lt;p&gt;What actually shipped&lt;/p&gt;

&lt;p&gt;Vision classification with a validated schema, where low confidence results get flagged for human review instead of quietly trusted. Batch processing with retries and per call cost tracking, the whole project ended up costing something like $0.0002 total. Semantic matching that handles paraphrasing, a post written with "Vulpes vulpes" instead of "red fox" still correctly pulls up real fox photos, zero words shared between them. The mismatch guard itself, proven on the coyote case above. A small review API to approve or reject suggested pairings. And 100% top-1 precision on a labeled evaluation set.&lt;/p&gt;

&lt;p&gt;I had some momentum left after all of that, so I built a React frontend on top of it. Type a post, click a button, watch the match appear. Watch a rejected candidate show up grayed out with the reason sitting right underneath it, instead of just disappearing silently.&lt;/p&gt;

&lt;p&gt;What I'd tell someone starting this same project&lt;/p&gt;

&lt;p&gt;Go looking for the case that breaks your system, don't wait for it to find you. I didn't design the coyote scoring 0.636 scenario, I found it by throwing an ambiguous post at my own pipeline out of idle curiosity. If I'd only ever tested the easy cases, a clean fox post, a clean wolf post, I would have shipped something that looked finished and wasn't.&lt;/p&gt;

&lt;p&gt;And leave room in your timeline for the parts that have nothing to do with the skill you're actually trying to practice. Three different providers, three different ways of falling over, taught me more about reading error messages slowly and not trusting my own first assumption than the machine learning side of this project ever did. I once decided a completely valid API key had to be broken, based on nothing but a prefix I didn't recognize. I was wrong, and testing it directly instead of arguing with myself would have saved me twenty minutes.&lt;/p&gt;

&lt;p&gt;None of this was hard in the sense of requiring brilliance. It was hard in the sense of requiring patience, and a willingness to actually sit with why something broke instead of guessing at fixes until the error went away.&lt;/p&gt;

&lt;p&gt;The repo's up on GitHub if you want to see the schema, the guard logic, and how the whole pipeline fits together: [&lt;a href="https://github.com/YassineS99/semantic-image-matcher.git" rel="noopener noreferrer"&gt;https://github.com/YassineS99/semantic-image-matcher.git&lt;/a&gt;]&lt;/p&gt;

&lt;p&gt;Next up, I want to grow the corpus and see whether the guard holds up on species that don't look nearly as similar as foxes and wolves do. If you've built something with a similar "refuse instead of guess" layer in it, I'd genuinely like to hear where yours drew the line.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>machinelearning</category>
      <category>python</category>
      <category>showdev</category>
    </item>
    <item>
      <title>Why I Stopped Trusting "It Works on My Machine" and Actually Learned to Ship</title>
      <dc:creator>Yassine Sadgui </dc:creator>
      <pubDate>Thu, 06 Aug 2026 23:33:07 +0000</pubDate>
      <link>https://dev.to/yassines99/why-i-stopped-trusting-it-works-on-my-machine-and-actually-learned-to-ship-41ml</link>
      <guid>https://dev.to/yassines99/why-i-stopped-trusting-it-works-on-my-machine-and-actually-learned-to-ship-41ml</guid>
      <description>&lt;p&gt;I'll be honest about where this started. I could run &lt;code&gt;docker-compose up&lt;/code&gt; and get an app running locally. I could follow a tutorial and end up with something that "worked." But if you'd asked me to actually explain what happens when a real user hits my app over HTTPS, how traffic gets from the internet to my container, or why my database kept losing data every time I restarted a container, I would have gone quiet.&lt;/p&gt;

&lt;p&gt;That gap bothered me. So I picked a project, the Conduit RealWorld app, and decided I wasn't going to stop until I could deploy it the way an actual production system gets deployed. Not a toy setup. Not "good enough for a demo." A real containerized stack with a reverse proxy, real SSL, and infrastructure that wouldn't fall apart the second I looked away from it.&lt;/p&gt;

&lt;p&gt;This is what that actually looked like, including the parts that broke.&lt;/p&gt;

&lt;h3&gt;
  
  
  What I was working with
&lt;/h3&gt;

&lt;p&gt;Conduit is a full-stack RealWorld implementation, basically a Medium clone, with a React 19 frontend (built with Vite and SWC), an Express.js 5 backend using Sequelize as the ORM and JWT for auth, and a PostgreSQL 15 database. Perfect project for this, honestly, because it's realistic enough to hit real problems but small enough that I wasn't drowning in complexity before I even got to the infrastructure part.&lt;/p&gt;

&lt;p&gt;The goal was simple to say and harder to do: get this running in Docker, put Nginx in front of it as a reverse proxy, get real SSL working with Let's Encrypt, and make sure the whole thing wouldn't lose data or fall over on a restart.&lt;/p&gt;

&lt;h3&gt;
  
  
  Docker Compose, and why "just Docker" wasn't enough
&lt;/h3&gt;

&lt;p&gt;My first instinct was to containerize each piece separately and wire them together by hand. That lasted about twenty minutes before I gave up and reached for Docker Compose instead.&lt;/p&gt;

&lt;p&gt;Here's the thing nobody tells you clearly enough when you're starting out: the value of Compose isn't just "less typing." It's that your entire infrastructure becomes something you can read. Frontend, backend, database, reverse proxy, all defined in one file, with their relationships to each other spelled out instead of held together by whatever commands you happened to run in whatever order you happened to run them.&lt;/p&gt;

&lt;p&gt;Once I had frontend, backend, and PostgreSQL each in their own service, connected on an internal Docker network, something clicked. I wasn't just running containers anymore. I was describing a system.&lt;/p&gt;

&lt;h3&gt;
  
  
  Nginx: the piece I almost skipped
&lt;/h3&gt;

&lt;p&gt;I'll admit I almost skipped the reverse proxy entirely. My backend already had a port. Why not just expose it directly?&lt;/p&gt;

&lt;p&gt;I'm glad I didn't take that shortcut, because it would have meant learning this lesson the hard way in a much worse context later. Nginx sitting in front of everything meant I had one single entry point handling incoming traffic, and everything behind it, frontend, backend, was invisible to the outside world unless Nginx decided to route it there.&lt;/p&gt;

&lt;p&gt;That single decision solved a handful of problems at once. It gave me a clean place to terminate SSL. It let me route &lt;code&gt;/api&lt;/code&gt; requests to the backend and everything else to the frontend without the frontend needing to know or care that a backend even existed. And it meant if I ever wanted to add caching, rate limiting, or swap out a service entirely, I had one config file to touch instead of rewriting how every piece of the app talked to the internet.&lt;/p&gt;

&lt;p&gt;I also split the setup into two separate Docker networks instead of throwing everything onto one. A proxy-tier network handles Nginx, the SSL companion, and the frontend. A separate internal network handles frontend-to-backend and backend-to-database traffic. The practical effect is that the database is never directly reachable from the proxy tier at all, it simply isn't on that network. If someone compromises the reverse proxy, they still don't have a path straight to the database. Small decision, but it's the kind of small decision that actually matters.&lt;/p&gt;

&lt;h3&gt;
  
  
  The SSL part that actually humbled me
&lt;/h3&gt;

&lt;p&gt;This is where things got interesting, and by interesting I mean I broke it more than once.&lt;/p&gt;

&lt;p&gt;Let's Encrypt sounds simple on paper. Run a command, get a certificate, done. I ended up going with the nginx-proxy and acme-companion containers instead of running Certbot by hand, and that decision taught me more than I expected.&lt;/p&gt;

&lt;p&gt;Here's how it actually works: nginx-proxy watches the Docker socket and automatically reconfigures itself whenever containers start, stop, or change, no manual config edits required. The acme-companion container sits next to it, watches for containers with the right environment variables set, and handles requesting and auto-renewing Let's Encrypt certificates on its own. The two talk to each other, and both need access to the Docker socket to do their jobs, which is its own thing to sit with, since that socket is effectively a direct line to controlling Docker itself.&lt;/p&gt;

&lt;p&gt;The part that actually tripped me up wasn't the SSL logic, it was getting the environment variables right so acme-companion knew which domain belonged to which container, and making sure the shared volumes for certs, vhost configs, and the DH param file were mounted correctly across both containers. Get that wrong and you get silent failures, no cert, no clear error telling you why.&lt;/p&gt;

&lt;p&gt;There's a particular kind of satisfaction in watching a padlock icon show up in your browser on something you deployed yourself, on your own server, with certificates you configured by hand instead of a platform doing it silently for you. It's a small thing. It still felt like a real milestone.&lt;/p&gt;

&lt;h3&gt;
  
  
  The data loss problem nobody warns you about
&lt;/h3&gt;

&lt;p&gt;Here's a mistake that taught me more than anything else in this project. Early on, I restarted a container during testing and watched all my database data disappear. Just gone.&lt;/p&gt;

&lt;p&gt;Turns out, if you don't explicitly set up persistent storage, your database's data lives inside the container's filesystem, and containers are meant to be disposable. Restart it, rebuild it, replace it, and by default you lose everything that was living inside it.&lt;/p&gt;

&lt;p&gt;The fix is named volumes: telling Docker to store the actual PostgreSQL data outside the container's ephemeral filesystem, in a location that survives no matter what happens to the container itself. Once I wired that up properly, I could tear down and rebuild containers as many times as I wanted, and the data underneath stayed exactly where it was supposed to be.&lt;/p&gt;

&lt;p&gt;It's such a simple concept once you understand it. It's also the kind of thing that will absolutely destroy you in a real production environment if you find out about it the hard way, in front of actual users, instead of alone at your desk during a side project.&lt;/p&gt;

&lt;h3&gt;
  
  
  Environment variables and the restart policy I almost forgot
&lt;/h3&gt;

&lt;p&gt;Two smaller things that mattered more than I expected going in.&lt;/p&gt;

&lt;p&gt;First, environment variables. Database credentials, API URLs, secrets, none of that belongs hardcoded into your app or your Dockerfiles. Getting into the habit of pulling all of that out into environment variables, managed through Compose, meant I could change configuration without touching code, and it meant I wasn't staring down the barrel of committing a password to GitHub by accident.&lt;/p&gt;

&lt;p&gt;Second, restart policies. By default, if a container crashes, it just stays down. That's fine on your laptop. It's not fine on a server nobody's actively watching. Setting proper restart policies meant that if something crashed, whether from a bug, a resource spike, or the server itself rebooting, the system would bring itself back up without me needing to SSH in and manually restart things at two in the morning.&lt;/p&gt;

&lt;h3&gt;
  
  
  Automating database migrations
&lt;/h3&gt;

&lt;p&gt;The last piece was making sure schema changes to the database happened automatically as part of deployment, instead of being a manual step I'd inevitably forget. I built a start.sh entrypoint script for the backend container that runs in a fixed order every time it boots: run migrations, seed the database, then start the actual app. That order matters. Try to start the app before migrations finish and you'll get errors about tables or columns that don't exist yet. Wiring it into the entrypoint like this meant the database structure and the application code stayed in sync automatically, every time, instead of relying on me remembering to run a command I'd definitely forget eventually.&lt;/p&gt;

&lt;h3&gt;
  
  
  What I'd tell someone starting this same project
&lt;/h3&gt;

&lt;p&gt;If you're where I was, comfortable running containers locally but shaky on what happens after that, here's what actually mattered:&lt;/p&gt;

&lt;p&gt;Don't skip the reverse proxy, even if it feels like unnecessary complexity for a small project. Understand SSL as a sequence of steps, not a single command. Assume your container data will disappear unless you explicitly tell Docker to keep it somewhere safe. And get comfortable with the idea that things will break in ways tutorials never mention, because tutorials are written by people who already know where the landmines are.&lt;/p&gt;

&lt;p&gt;None of this was hard in the sense of requiring brilliance. It was hard in the sense of requiring patience, and a willingness to actually understand why something broke instead of just copy-pasting a fix until the error went away.&lt;/p&gt;

&lt;p&gt;The repo's up on GitHub if you want to see the actual Compose file, Nginx config, and how everything fits together: &lt;a href="https://github.com/YassineS99/conduit-realworld-example-app" rel="noopener noreferrer"&gt;conduit-realworld-example-app&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Next up, I want to wire this into an actual CI/CD pipeline so deployment stops being something I do by hand entirely. If you've done that jump yourself, I'd genuinely like to hear what surprised you about it.&lt;/p&gt;

</description>
      <category>devops</category>
      <category>nginx</category>
      <category>docker</category>
      <category>postgressql</category>
    </item>
  </channel>
</rss>
