<?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: David Anderson</title>
    <description>The latest articles on DEV Community by David Anderson (@davidf2s).</description>
    <link>https://dev.to/davidf2s</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%2F4097974%2Ff5cfed6b-f344-4eaa-8987-58f0481f4f1b.png</url>
      <title>DEV Community: David Anderson</title>
      <link>https://dev.to/davidf2s</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/davidf2s"/>
    <language>en</language>
    <item>
      <title>I checked 12,680 links in an OSINT tool directory. 39 of them now serve casino spam.</title>
      <dc:creator>David Anderson</dc:creator>
      <pubDate>Mon, 14 Sep 2026 14:09:34 +0000</pubDate>
      <link>https://dev.to/davidf2s/i-checked-12680-links-in-an-osint-tool-directory-39-of-them-now-serve-casino-spam-bde</link>
      <guid>https://dev.to/davidf2s/i-checked-12680-links-in-an-osint-tool-directory-39-of-them-now-serve-casino-spam-bde</guid>
      <description>&lt;p&gt;Curated link lists rot. Everyone knows this in the abstract. What I wanted to know was the shape of the rot: how much of it a normal link checker catches, and how much of it looks perfectly healthy from the outside.&lt;/p&gt;

&lt;p&gt;So I took one well-known OSINT tool directory — 12,680 outbound links, 9,889 unique URLs across 7,069 hosts — and checked every single one. Not a sample. Every row.&lt;/p&gt;

&lt;p&gt;The headline number is not the dead links. It is the ones that return HTTP 200.&lt;/p&gt;

&lt;h2&gt;
  
  
  39 links now serve gambling or SEO spam
&lt;/h2&gt;

&lt;p&gt;A status-code checker gives all of these a clean bill of health. Seventeen of the thirty-nine serve the spam content &lt;strong&gt;on the originally listed URL, with no redirect at all&lt;/strong&gt; — the domain simply changed hands and the new owner put a casino on it. Your checker asks "is this 200?", gets "yes", and moves on.&lt;/p&gt;

&lt;p&gt;The ones that read worst out of context:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A row titled &lt;strong&gt;"Russian Disinformation Dashboard"&lt;/strong&gt; now lands on a 1xBet affiliate.&lt;/li&gt;
&lt;li&gt;A row titled &lt;strong&gt;"Bellingcat's Online Investigation Toolkit"&lt;/strong&gt; — the link went to a document-hosting service that shut down, and the path was repurposed. It now serves an Indonesian slot site.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Two voter-records entries&lt;/strong&gt; under Public Records / Legal. One serves a live casino page, the other redirects to a betting-app download.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Three separate links to counter-extremism research.&lt;/strong&gt; The entire domain now serves "Best Online Casinos Australia 2026" on every path.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Two more are not spam, but no longer are what the row says. One tool listed as a face-search service now redirects to an unrelated mobile game. Another returns 200 with the title "Redirecting..." over a body that is pure adblock-detection JavaScript — the search engine is gone, the monetised shell remains. That one appears four times across two sections, so one fix cleans four rows.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The detection method that works:&lt;/strong&gt; fetch the page and compare the &lt;code&gt;&amp;lt;title&amp;gt;&lt;/code&gt; and final effective domain against what the row claims. Status codes tell you nothing here.&lt;/p&gt;

&lt;h2&gt;
  
  
  A string-handling bug was hiding 99 working tools
&lt;/h2&gt;

&lt;p&gt;This was the cheapest finding in the whole audit and needed no research at all.&lt;/p&gt;

&lt;p&gt;135 rows had a description concatenated into the URL field — two spreadsheet columns merged at some point during collection. They look like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;http://archive.is / archive.fo
http://blacklistalert.org blacklist check tool
http://checkshorturl.com expand url
http://thatsthem.com / reverse-ip-lookup
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Every one of the 135 is broken as rendered. I probed the base URL before the first whitespace on all of them: &lt;strong&gt;99 return HTTP 200&lt;/strong&gt;. Ninety-nine live, working tools were unreachable purely because of a data-entry artifact.&lt;/p&gt;

&lt;p&gt;Truncating each URL at the first whitespace fixes all 99 in one pass. If you maintain a list in a CSV, this is worth grepping for right now.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I deliberately refused to call dead
&lt;/h2&gt;

&lt;p&gt;This is the part that matters more than any individual finding, and it is where link audits usually destroy their own credibility.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1,096 links returned HTTP 403.&lt;/strong&gt; I called none of them dead. A 403 during a bulk sweep is a bot defence, not a death. The list included &lt;code&gt;inteltechniques.com&lt;/code&gt; — Michael Bazzell's site, one of the most important live resources in the OSINT world — which answers a clean 200 on an individual request. A naive checker would have flagged it as broken.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;36 returned 429.&lt;/strong&gt; That is rate limiting caused by my own request volume. It is a property of my sweep, not of the target.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;86 &lt;code&gt;.onion&lt;/code&gt; addresses, never probed.&lt;/strong&gt; Tor hidden services do not resolve from the clearnet at all, so a failed probe proves exactly nothing. Separately worth noting: the 14 clearnet Tor-gateway hosts in the list — &lt;code&gt;onion.pet&lt;/code&gt;, &lt;code&gt;onion.to&lt;/code&gt;, &lt;code&gt;onion.ws&lt;/code&gt; and friends — are genuinely NXDOMAIN now, so those rows are broken even for readers who do not run Tor.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Three &lt;code&gt;.mil&lt;/code&gt; hosts and three geo-filtered government portals&lt;/strong&gt; (China, Russia, Ukraine). These filter by geography and client fingerprint. Unreachable from my vantage point is not the same as gone.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;443 additional hosts that resolve but refuse connections on every scheme.&lt;/strong&gt; Probably mostly dead. I am not asserting it. "Resolves yet refuses" can equally be a firewall, a geo-block, or an origin that is down this afternoon.&lt;/p&gt;

&lt;p&gt;I would rather hand a maintainer 513 findings they can trust than 956 they cannot. One wrong claim in a report like this and nothing else in it gets read.&lt;/p&gt;

&lt;h2&gt;
  
  
  The two-pass rule
&lt;/h2&gt;

&lt;p&gt;A single-pass &lt;code&gt;curl&lt;/code&gt; sweep produces a pile of false positives. The second pass is not optional.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Pass 1&lt;/strong&gt; — cheap, parallel, over everything:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-sS&lt;/span&gt; &lt;span class="nt"&gt;-o&lt;/span&gt; /dev/null &lt;span class="nt"&gt;-w&lt;/span&gt; &lt;span class="s1"&gt;'%{http_code} %{url_effective}\n'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-L&lt;/span&gt; &lt;span class="nt"&gt;--max-time&lt;/span&gt; 20 &lt;span class="nt"&gt;-A&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$BROWSER_UA&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$URL&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Pass 2&lt;/strong&gt; — only for what looked dead, and properly:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;try &lt;strong&gt;both schemes&lt;/strong&gt;, &lt;code&gt;http://&lt;/code&gt; and &lt;code&gt;https://&lt;/code&gt;, and both with and without &lt;code&gt;www&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;send a &lt;strong&gt;full browser header set&lt;/strong&gt;, not just a User-Agent (&lt;code&gt;Accept&lt;/code&gt;, &lt;code&gt;Accept-Language&lt;/code&gt;, &lt;code&gt;Sec-Fetch-*&lt;/code&gt;, &lt;code&gt;--compressed&lt;/code&gt;) — many hosts reject a bare curl and serve a real browser fine&lt;/li&gt;
&lt;li&gt;resolve the domain: &lt;code&gt;dig +short &amp;lt;domain&amp;gt;&lt;/code&gt; — no A/AAAA record across multiple public resolvers is the only really solid death&lt;/li&gt;
&lt;li&gt;follow redirects and look at the &lt;strong&gt;final&lt;/strong&gt; URL; a redirect to a parking page is a different finding from a 404 and deserves its own bucket&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;On this dataset the second pass removed 149 false deaths that the first pass had flagged. On a smaller list I audited earlier — 428 outbound links — it removed 5 false positives out of 29. Skip it and roughly a sixth of your report is wrong.&lt;/p&gt;

&lt;h2&gt;
  
  
  Have a skeptic re-check your own findings
&lt;/h2&gt;

&lt;p&gt;After the audit was written, I ran every single death claim through a second independent pass whose only job was to &lt;strong&gt;disprove&lt;/strong&gt; them: re-resolve against three public resolvers plus two DoH providers, five curl probes per URL, and a confirmation from a different physical network.&lt;/p&gt;

&lt;p&gt;It killed two claims — two domains I had bucketed as dead were alive. It also caught something subtler: I had written that a particular dead domain "has no nameserver delegation at all", and &lt;code&gt;whois&lt;/code&gt; shows three delegated nameservers. The delegation exists; the in-bailiwick nameservers simply do not answer, which is why every resolver returns SERVFAIL. The link is dead either way, but the stated evidence was falsifiable in one command, and that is exactly the kind of thing a maintainer checks first.&lt;/p&gt;

&lt;p&gt;If you publish findings about someone else's work, build the refutation step into the process. Your own second pass is cheaper than their loss of trust.&lt;/p&gt;

&lt;h2&gt;
  
  
  Other categories worth knowing about
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;117 rows where HTTPS fails but HTTP works.&lt;/strong&gt; These hosts serve nothing on port 443 and respond normally on port 80. Chrome and Firefox both do HTTPS-first now, so a reader clicking through sees a connection failure and concludes the tool is dead. It is not — the row just needs its scheme changed. Jeffrey Friedl's EXIF viewer is in this bucket.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;37 rows with TLS certificates that fail verification&lt;/strong&gt; — but split them into two tiers, because they do not behave the same way. Expired (verify code 10) and self-signed (18) throw a full browser interstitial. Incomplete-chain (20/21) usually loads anyway, because Chrome fetches the missing intermediate via AIA. Reporting both tiers as "broken TLS" overstates the problem.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;498 rows returning 404 while the parent site is perfectly healthy.&lt;/strong&gt; Deep-link rot. The organisation is fine, the page moved. These are cheap to repair by repointing at the site root, and lumping them in with genuine deaths is what makes naive audits look catastrophic.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;One row shipping &lt;code&gt;http://127.0.0.1:5001&lt;/code&gt; as a public link.&lt;/strong&gt; Worth grepping your own data for private-range addresses before any release.&lt;/p&gt;

&lt;h2&gt;
  
  
  Dead tools that have living successors
&lt;/h2&gt;

&lt;p&gt;The most useful column in an audit like this is not "dead" — it is "dead, and here is what replaced it". A few from this run, each verified 200 at the time of writing:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Dead&lt;/th&gt;
&lt;th&gt;Living successor&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;botometer.iuni.iu.edu&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;botometer.osome.iu.edu&lt;/code&gt; — same project, same team, retired hostname&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;gigablast.com&lt;/code&gt; (engine dissolved, 2023)&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;mojeek.com&lt;/code&gt; and &lt;code&gt;marginalia-search.com&lt;/code&gt; — the surviving independent crawlers&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;spyse.com&lt;/code&gt; (shut down 2022)&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;netlas.io&lt;/code&gt; — where the userbase actually migrated&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;findmyfbid.com&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;lookup-id.com&lt;/code&gt; — same function&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;browsershots.org&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;browsershots.at&lt;/code&gt; — the official continuation&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;darksearch.io&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;ahmia.fi&lt;/code&gt; — maintained clearnet-accessible Tor index&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ASKfm (shut down Dec 2024)&lt;/td&gt;
&lt;td&gt;revived in 2026 at &lt;code&gt;askfm.app&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;That last one is a reminder to check before deleting a row. A tool being gone in 2024 does not mean it is gone now.&lt;/p&gt;

&lt;h2&gt;
  
  
  If you maintain a list
&lt;/h2&gt;

&lt;p&gt;The audit is mechanical and you can run it yourself with the two-pass recipe above. If you would rather not, I will run it over your list and send you the CSV — findings only, no strings, and privately rather than published. I did exactly that with the directory in this post, which is why it is not named here: the point is the pattern, not the pillory. Most of those 39 rows were completely fine on the day they were added.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;I build &lt;a href="https://face2social.com" rel="noopener noreferrer"&gt;Face2social&lt;/a&gt;, a face search engine over public social media profiles — which is how I ended up reading a lot of OSINT tool directories in the first place. It indexes public social profile pictures only and does not crawl the open web, so for whole-web face search PimEyes and FaceCheck.ID cover far more ground. US only.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>osint</category>
      <category>security</category>
      <category>webdev</category>
      <category>opensource</category>
    </item>
    <item>
      <title>How we search a face across 1 billion photos in under a second (with FAISS)</title>
      <dc:creator>David Anderson</dc:creator>
      <pubDate>Thu, 27 Aug 2026 19:11:03 +0000</pubDate>
      <link>https://dev.to/davidf2s/how-we-search-a-face-across-1-billion-photos-in-under-a-second-with-faiss-2lm</link>
      <guid>https://dev.to/davidf2s/how-we-search-a-face-across-1-billion-photos-in-under-a-second-with-faiss-2lm</guid>
      <description>&lt;p&gt;&lt;em&gt;Disclosure: I'm the founder of &lt;a href="https://face2social.com" rel="noopener noreferrer"&gt;Face2social&lt;/a&gt;, a face-recognition search engine. This post is about the vector-search engineering behind it — no product pitch, just the stuff that kept me up at night (including the part where I tell you what we're bad at). It's also the write-up I wish I'd found when I started.&lt;/em&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  TL;DR
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;A face becomes a &lt;strong&gt;vector&lt;/strong&gt;. Similarity search becomes &lt;strong&gt;nearest-neighbor search&lt;/strong&gt; in high-dimensional space.&lt;/li&gt;
&lt;li&gt;Brute force dies somewhere around a few million vectors. We're at ~&lt;strong&gt;1 billion&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/facebookresearch/faiss" rel="noopener noreferrer"&gt;FAISS&lt;/a&gt; is what makes "compare against a billion" feel like "compare against a thousand."&lt;/li&gt;
&lt;li&gt;The real trick isn't one magic index — it's the trade-off triangle: &lt;strong&gt;speed ↔ memory ↔ recall&lt;/strong&gt;. You don't get all three.&lt;/li&gt;
&lt;li&gt;And the biggest trade-off isn't even in the index. It's &lt;strong&gt;what you choose to put in it&lt;/strong&gt; — more on that, honestly, near the end.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you've ever wondered how "search by image" actually works at scale, this one's for you. 👇&lt;/p&gt;




&lt;h2&gt;
  
  
  The problem, honestly stated
&lt;/h2&gt;

&lt;p&gt;Someone uploads a photo. We need to answer one question fast: &lt;em&gt;does this face appear anywhere in a very large set of public profile pictures?&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;The naive version is easy to describe and impossible to ship:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Turn the uploaded face into a vector. Turn every known face into a vector. Compare the new vector against all of them. Return the closest matches.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That last step is the whole ballgame. Compare against a thousand faces? Trivial. A million? Now you're sweating. A &lt;strong&gt;billion&lt;/strong&gt;? A single brute-force scan would have a user staring at a spinner long enough to close the tab, make coffee, and reconsider their life choices.&lt;/p&gt;

&lt;p&gt;So the interesting engineering isn't "how do we compare two faces." It's "how do we &lt;em&gt;not&lt;/em&gt; compare against all billion and still find the right one."&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 1: a face is just a point in space
&lt;/h2&gt;

&lt;p&gt;Before any search happens, every face — the uploaded one and every one we already know about — goes through the same pipeline:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;detect the face → align it → turn it into a fixed-length vector (an "embedding").&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The important intuition: two photos of the &lt;em&gt;same&lt;/em&gt; person land &lt;strong&gt;close together&lt;/strong&gt; in this space, even across different angles, lighting, crops, or filters. Two &lt;em&gt;different&lt;/em&gt; people land far apart. "How similar are these faces?" collapses into "how close are these two points?" — a plain distance measurement.&lt;/p&gt;

&lt;p&gt;That's the quiet superpower of embeddings: they turn a messy perceptual problem ("do these look like the same person?") into clean geometry ("what's the distance between these vectors?"). Everything downstream is just... finding nearby points. A lot of nearby points. Very fast.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;(I'm deliberately staying vendor-neutral on the embedding model itself — pick your favorite; the search problem below is the same regardless.)&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 2: why brute force falls off a cliff
&lt;/h2&gt;

&lt;p&gt;Exact nearest-neighbor search means measuring the distance from your query to &lt;strong&gt;every&lt;/strong&gt; stored vector, then sorting. It's &lt;code&gt;O(n)&lt;/code&gt; per query, and each comparison is real work in a high-dimensional space.&lt;/p&gt;

&lt;p&gt;Here's the mental model for why it doesn't scale:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;A thousand vectors:&lt;/strong&gt; instant. Do it in a loop. Nobody cares.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A million:&lt;/strong&gt; still fine on a good box, especially batched.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A hundred million:&lt;/strong&gt; now every query is chewing through serious memory bandwidth, and your p99 latency is a horror story.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A billion:&lt;/strong&gt; the vectors alone don't comfortably fit in RAM on one machine, and a full scan per query is simply off the table for anything interactive.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The wall isn't CPU cleverness. It's physics — memory and bandwidth. You cannot touch a billion things per request and stay under a second. So you stop being exact.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 3: approximate is the whole point
&lt;/h2&gt;

&lt;p&gt;This is the mindset shift that makes billion-scale search possible:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;You don't need the &lt;em&gt;mathematically perfect&lt;/em&gt; nearest neighbor. You need a &lt;em&gt;very good&lt;/em&gt; neighbor, &lt;em&gt;almost always&lt;/em&gt;, &lt;em&gt;fast&lt;/em&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That's &lt;strong&gt;Approximate Nearest Neighbor (ANN)&lt;/strong&gt; search, and it's where &lt;a href="https://github.com/facebookresearch/faiss" rel="noopener noreferrer"&gt;FAISS&lt;/a&gt; (Facebook AI Similarity Search) earns its keep. FAISS is a library built for exactly one job: nearest-neighbor search over massive vector sets, with a menu of indexes that each pick a different spot on the trade-off triangle.&lt;/p&gt;

&lt;p&gt;Two ideas do most of the heavy lifting. Neither is exotic once you see the intuition:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Don't search everything — search a neighborhood.&lt;/strong&gt;&lt;br&gt;
Instead of scanning all billion vectors, you pre-cluster them into buckets during an offline training step. At query time you figure out which few buckets your query probably lives in, and you only scan those. You've quietly turned "compare against a billion" into "compare against a few million" — the same answer, a tiny fraction of the work. The knob for &lt;em&gt;how many&lt;/em&gt; buckets you probe is your dial between speed and recall.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Don't store fat vectors — store compressed sketches.&lt;/strong&gt;&lt;br&gt;
Full-precision embeddings are heavy, and a billion of them is a lot of RAM. So you &lt;strong&gt;compress&lt;/strong&gt; each vector into a compact code that's cheap to store and cheap to compare — trading a little precision for a massive memory win. This is what makes "a billion vectors" go from &lt;em&gt;rent-a-data-center&lt;/em&gt; to &lt;em&gt;actually-runs&lt;/em&gt;. You lose a hair of accuracy; you gain the ability to exist.&lt;/p&gt;

&lt;p&gt;Stack those two — search a neighborhood, over compressed sketches — and a billion-vector search starts to feel routine. I'm intentionally not naming the exact FAISS index recipe we run in production (that part's ours to keep), but everything above is the standard playbook, and FAISS gives you all the building blocks out of the box.&lt;/p&gt;
&lt;h2&gt;
  
  
  Step 4: the trade-off triangle nobody escapes
&lt;/h2&gt;

&lt;p&gt;Here's the thing I most wish someone had tattooed on my monitor early on. Every knob you turn is a negotiation between three things, and &lt;strong&gt;you only get to optimize two&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;⚡ &lt;strong&gt;Speed&lt;/strong&gt; — how fast a single query comes back.&lt;/li&gt;
&lt;li&gt;🧠 &lt;strong&gt;Memory&lt;/strong&gt; — how much RAM the index eats (this &lt;em&gt;is&lt;/em&gt; your infra bill).&lt;/li&gt;
&lt;li&gt;🎯 &lt;strong&gt;Recall&lt;/strong&gt; — how often the true match is actually in your results.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Want blazing speed &lt;em&gt;and&lt;/em&gt; tiny memory? Recall suffers — you'll miss real matches. Want near-perfect recall &lt;em&gt;and&lt;/em&gt; speed? Get your wallet out for RAM. Want cheap &lt;em&gt;and&lt;/em&gt; accurate? It'll be slow.&lt;/p&gt;

&lt;p&gt;There is no free lunch. The entire job is choosing &lt;em&gt;which&lt;/em&gt; corner to sacrifice for &lt;em&gt;your&lt;/em&gt; use case — and for a face-search product, quietly missing a real match is the worst failure mode, so we bias toward recall and pay for it elsewhere. Your product's priorities will land you somewhere different, and that's the point: the "right" index is the one that fits your failure modes, not a leaderboard.&lt;/p&gt;
&lt;h2&gt;
  
  
  Step 5: one machine ends, sharding begins
&lt;/h2&gt;

&lt;p&gt;Even compressed, a billion vectors plus the overhead to serve them will eventually blow past a single machine. So you &lt;strong&gt;shard&lt;/strong&gt;: split the index into pieces across several machines, run the query against all shards in parallel, then merge the top results.&lt;/p&gt;

&lt;p&gt;Conceptually it's the classic scatter–gather:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;query ──► [ shard 1 ]─┐
      ├──► [ shard 2 ]─┤
      ├──► [ shard 3 ]─┼──► merge top-K ──► results
      └──► [ shard N ]─┘
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Your effective latency becomes the &lt;em&gt;slowest&lt;/em&gt; shard plus a cheap merge — not the sum. Add shards to grow capacity, keep each shard small enough to stay fast, and you've got a search tier that scales sideways instead of falling over.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 6: the trade-off that actually defines the product
&lt;/h2&gt;

&lt;p&gt;Everything above is about the index. But the decision that shapes your product most isn't &lt;em&gt;how&lt;/em&gt; you search — it's &lt;strong&gt;what you put in the corpus&lt;/strong&gt;. And this is where I should be straight with you, because it's our biggest limitation.&lt;/p&gt;

&lt;p&gt;We index &lt;strong&gt;social media profile pictures&lt;/strong&gt;. That's it. Instagram, Facebook, TikTok, X.&lt;/p&gt;

&lt;p&gt;The upside is real: a narrow, dense, well-understood corpus means the index is tuned for one kind of photo, matches come back fast, and when someone asks "does this face have a social profile?" we're very good at answering it.&lt;/p&gt;

&lt;p&gt;The downside is just as real, and I'd rather say it out loud than have a user discover it: &lt;strong&gt;we're blind to everything else.&lt;/strong&gt; News articles, personal blogs, forums, company team pages, image boards, the open web in general — we don't see any of it. A face that's all over the web but has no social presence will come back empty from us, and that's not a bug we're about to fix; it's a direct consequence of the corpus we chose. Tools that crawl the open web will beat us on exactly those queries. They'll also index a much noisier space and inherit different problems, but that's their trade-off, not a defense of ours.&lt;/p&gt;

&lt;p&gt;So: if your question is &lt;em&gt;"is this person on social media, and where?"&lt;/em&gt; — that's our lane and we're strong in it. If your question is &lt;em&gt;"where does this face appear anywhere online?"&lt;/em&gt; — we are the wrong tool, and you should use something that crawls the whole web.&lt;/p&gt;

&lt;p&gt;I'm spelling this out because the corpus decision gets glossed over in most write-ups about vector search, and it's the one your users actually feel. A perfectly tuned index over the wrong data is still the wrong answer, delivered quickly. &lt;strong&gt;Recall against your index means nothing if the thing you're looking for was never in it.&lt;/strong&gt; Define your corpus honestly, then tell people where its edges are.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I'd tell past me
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The embedding model gets the glory; the index does the work.&lt;/strong&gt; A great model that you can't search at scale is a demo, not a product.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;"Approximate" is not a compromise you apologize for — it's the enabling idea.&lt;/strong&gt; Chasing exact nearest-neighbor at a billion scale is how you burn a quarter and ship nothing.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Pick your sacrifice on purpose.&lt;/strong&gt; Speed, memory, recall — decide &lt;em&gt;before&lt;/em&gt; you tune which one you're willing to bleed, based on what "wrong" costs your users.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Your corpus is a product decision, not a data decision.&lt;/strong&gt; Narrow and deep beats broad and shallow — as long as you're upfront about the edges.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://github.com/facebookresearch/faiss" rel="noopener noreferrer"&gt;FAISS&lt;/a&gt; is genuinely the boring-in-a-good-way choice.&lt;/strong&gt; Battle-tested, flexible, and it lets you move the trade-off dials instead of reinventing them.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;We run this in production at &lt;a href="https://face2social.com" rel="noopener noreferrer"&gt;Face2social&lt;/a&gt; (US-only, for the privacy-law reasons you'd expect) to match an uploaded photo against public social profiles — mostly for catfish and romance-scam checks, and for people who want to know whether their own photos are being used on fake accounts. But honestly, swap "faces" for "product images," "audio fingerprints," or "document embeddings" and the entire playbook above is identical. Vector search at scale is one of those skills that quietly transfers everywhere.&lt;/p&gt;

&lt;p&gt;If you're building anything that searches by similarity, learn FAISS, internalize the trade-off triangle, and be honest about your corpus. Future-you will thank present-you. 🙏&lt;/p&gt;

&lt;p&gt;&lt;em&gt;What are you using for large-scale vector search — FAISS, or something else? And how did you scope your corpus? Curious what trade-offs you landed on. Drop it in the comments.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>vectordatabase</category>
      <category>ai</category>
      <category>machinelearning</category>
      <category>architecture</category>
    </item>
  </channel>
</rss>
