FjellSystem Design Lab
Lesson 11.3• 10 min read

Duplicate elimination and SimHash

Duplicate URL elimination, Bloom filters, and 64-bit SimHash fingerprints compared by Hamming distance.

What you will learn

  • Tell duplicate URLs from duplicate content.
  • Normalize URLs, and size a Bloom filter for a billion of them.
  • Detect near-duplicate pages with SimHash, and find fingerprints within kk bits quickly.

Two kinds of duplicate

  • Duplicate URLs: the same link found on many pages. Download it once.
  • Duplicate content: different URLs serving the same or nearly the same page, such as mirrors, syndicated articles, or pages that differ only in an advert. Manku, Jain and Das Sarma open their paper with "Near-duplicate web documents are abundant", and note that such differences "are irrelevant for web search".

The primer covers both. Remove duplicate URLs, with sort | unique for small lists or MapReduce at 1 billion links. Detect duplicate content by comparing page signatures for similarity, for example with the Jaccard index or cosine similarity.


Duplicate URLs

Mercator's duplicate URL eliminator checks each extracted URL against every URL "in the URL frontier or [that] has already been downloaded". Only new ones go to the frontier. It stores fixed-size fingerprints of URLs, not the URLs themselves, mostly on disk with recent entries buffered in memory.

Normalize before comparing, so trivially different spellings count as one URL: lower-case the scheme and host, drop default ports and #fragments, and resolve relative links. Stripping known tracking parameters is a policy choice, because some query parameters do change the page.

A Bloom filter (chapter 6) can answer "seen before?" in memory. Its size for nn items and false-positive rate pp is:

m=−nln⁡p(ln⁡2)2m = -\frac{n \ln p}{(\ln 2)^2}

For the primer's n=109n = 10^9 links and p=0.001p = 0.001: m≈1.44×1010m \approx 1.44 \times 10^{10} bits, about 1.8 GB, with k=mnln⁡2≈10k = \frac{m}{n}\ln 2 \approx 10 hash functions. A Bloom filter never says "new" for a URL it has seen. But it wrongly says "seen" for about 0.1% of new URLs, and those pages are skipped. Whether that is acceptable is a product decision.


Near-duplicate content: SimHash

A cryptographic hash such as MD5 or SHA-1 cannot detect near duplicates. In Manku et al.'s words, two documents that differ in a single byte hash "into two completely different hash-values".

SimHash (Charikar) is built so that similar documents get similar fingerprints. As Manku et al. apply it to web pages:

  1. Turn the page into weighted features (tokens after case folding, stop-word removal, stemming and so on).
  2. Keep a vector VV of ff counters, all zero.
  3. Hash each feature to ff bits. Where a bit is 1, add the feature's weight to that counter; where it is 0, subtract it.
  4. The fingerprint's bit ii is 1 if V[i]>0V[i] > 0, otherwise 0.

Near-duplicates' fingerprints differ in only a few bit positions. Manku et al. "experimentally validate that for a repository of 8B webpages, 64-bit simhash fingerprints and k=3k = 3 are reasonable." Here kk is the largest Hamming distance still treated as a near-duplicate.


Finding fingerprints within kk bits

Comparing a new fingerprint with billions of stored ones one by one is too slow. Manku et al. use the pigeonhole principle. Split the 64 bits into blocks: if two fingerprints differ in at most 3 bits, then with 4 blocks of 16 bits, at least one block is identical. Keep one table per block, sorted by that block, and look the new fingerprint up in each.

Each lookup still returns a group of candidates to check. A table keyed on pp bits over 2d2^d fingerprints returns about 2d−p2^{d-p} per probe. With the paper's 2342^{34} (8 billion) fingerprints, 4 tables keyed on 16 bits return about 256K candidates per probe. 16 tables keyed on 28 bits return about 64. More tables mean less work per query but more memory, and the paper works through this trade-off. For the primer's 2302^{30} (1 billion) pages, 4 tables keyed on 16 bits still return about 16,000 candidates per table.

64-bit SimHash

Words are the features, weighted by count. Similar pages get fingerprints that differ in few bits; Manku et al. found k = 3 bits reasonable for 8 billion pages.

A = 0x5aa7f254909b6331
0101101010100111111100100101010010010000100110110110001100110001
B = 0x5aa7f25590996331
0101101010100111111100100101010110010000100110010110001100110001
Hamming distance
2 of 64 bits
B is a near-duplicate: skip it
16-bit blocks that match exactly: A✓B·C·D✓With at most 3 differing bits, at least one of the 4 blocks must match, so 4 tables, each keyed on one block, find every candidate.
Figure 3: Deduplication pipeline comparing URL normalization, in-memory Bloom filters, and 64-bit SimHash locality-sensitive fingerprinting.
  • Raw HTML Stream: 4 × App Compute Server
  • DOM Text Tokenizer & Weighter: 4 × App Compute Server
  • 64-bit SimHash Calculator: 4 × App Compute Server
  • Redis 64-bit Fingerprint Table: 3 × Redis In-Memory Cache
  • S3 Unique Documents: 2 × Object Storage (S3)
  • Raw HTML Stream sends requests to DOM Text Tokenizer & Weighter
  • DOM Text Tokenizer & Weighter sends requests to 64-bit SimHash Calculator
  • 64-bit SimHash Calculator sends requests to Redis 64-bit Fingerprint Table
  • 64-bit SimHash Calculator sends requests to S3 Unique Documents

Key takeaways

  • Normalize URLs before comparing them, then check each against every URL already queued or downloaded.
  • A Bloom filter for 10910^9 URLs at a 0.1% false-positive rate takes about 1.8 GB; it never misses a seen URL but occasionally skips a new one.
  • A cryptographic hash changes completely for a one-byte difference; SimHash gives similar pages fingerprints a few bits apart (64 bits, k=3k = 3 for 8 billion pages).
  • Split the fingerprint into blocks: two within 3 bits must agree exactly on at least one block, so a few tables keyed on blocks find the candidates.

Sources

Adapted from The System Design Primer by Donne Martin, licensed under CC BY 4.0. Changes were made.