<?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: Dietly</title>
    <description>The latest articles on DEV Community by Dietly (@dietly).</description>
    <link>https://dev.to/dietly</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%2F4034160%2Fe4db8fe8-19bd-4646-8d00-61180f36dbad.png</url>
      <title>DEV Community: Dietly</title>
      <link>https://dev.to/dietly</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/dietly"/>
    <language>en</language>
    <item>
      <title>Taming Open Food Facts: building a fast, searchable nutrition database</title>
      <dc:creator>Dietly</dc:creator>
      <pubDate>Fri, 17 Jul 2026 16:18:52 +0000</pubDate>
      <link>https://dev.to/dietly/taming-open-food-facts-building-a-fast-searchable-nutrition-database-403l</link>
      <guid>https://dev.to/dietly/taming-open-food-facts-building-a-fast-searchable-nutrition-database-403l</guid>
      <description>&lt;h2&gt;
  
  
  Taming Open Food Facts: building a fast, searchable nutrition database
&lt;/h2&gt;

&lt;p&gt;Every nutrition-app developer hits the same wall. Open Food Facts is a gift — millions of real, barcoded products — but it's crowdsourced: duplicates, unit chaos, half-filled rows and outright impossible values (I've seen a cracker with 6,000 kcal per 100g).&lt;/p&gt;

&lt;p&gt;I spent months on the cleanup so you don't have to. The result is DietlyAPI — 4.2M foods behind one fast endpoint (~40ms typical search). This post is about what the cleanup actually involves, because most of it was not obvious to me at the start.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Normalization is 80% of the work
&lt;/h3&gt;

&lt;p&gt;Every source has its own idea of a serving. OFF products report per-100g, per-serving, per-package or a mix; fields can be in kJ or kcal, mg or g, sometimes with the unit embedded in a string. Everything gets converted to a canonical per-serving row with grams as the base unit, keeping the original serving description for display.&lt;/p&gt;

&lt;p&gt;The nastiest class of bug: values that are individually plausible but jointly impossible. Protein + carbs + fat adding to more than the serving weight. Calories wildly inconsistent with the macro breakdown (4/4/9 rule). Each row gets plausibility checks, and failures either get repaired from other fields (kJ→kcal, per-100g→per-serving) or dropped.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Dedupe, then rank by confidence
&lt;/h3&gt;

&lt;p&gt;The same yogurt exists in OFF five times with slightly different barcodes and completion levels. Rather than pick one winner at import time, every row gets a confidence score — data completeness, source reliability, image presence, plausibility-check results — and search ranks by it. The messy duplicates are still there; they just never win.&lt;/p&gt;

&lt;p&gt;This turned out to be the single biggest UX improvement: search for "banana" and you get an actual banana with an image, not "BANANA FLAVORED SYRUP CONCENTRATE" from a wholesale distributor.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Serving it cheap
&lt;/h3&gt;

&lt;p&gt;The whole thing runs on one 4GB Hetzner box: FastAPI + Postgres (trigram + full-text indexes), Cloudflare in front, 850k pre-rendered static pages for browsing, weekly incremental rebuilds via systemd timers. A public status page publishes real probe latency so "fast" is verifiable, not a claim.&lt;/p&gt;

&lt;h3&gt;
  
  
  Use it
&lt;/h3&gt;

&lt;p&gt;The free tier needs no card — instant key, 30 req/min:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;&amp;gt; curl "https://api.getdietly.com/search?q=greek+yogurt&amp;amp;limit=3"&lt;br&gt;
&lt;/code&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;1. Product page: &lt;a href="https://www.getdietly.com/api" rel="noopener noreferrer"&gt;https://www.getdietly.com/api&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;2. OpenAPI spec: &lt;a href="https://www.getdietly.com/openapi.json" rel="noopener noreferrer"&gt;https://www.getdietly.com/openapi.json&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;3. Data license: underlying data is ODbL via Open Food Facts — if you show it publicly, credit them (you should anyway; they're the reason any of this is possible).
Questions about the pipeline welcome in the comments.&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>api</category>
      <category>postgres</category>
      <category>python</category>
      <category>opensource</category>
    </item>
  </channel>
</rss>
