Part of Language AI Handbook
Explains how MinHash compresses documents into compact signatures that estimate Jaccard similarity, enabling near-duplicate detection.
Choose your expertise level to adjust how many terms are explained. Beginners see more tooltips, experts see fewer to maintain reading flow. Hover over underlined terms for instant definitions.
Article links
Make inline references clickable
MinHash
When you train a language model on web-scale text, you quickly discover that the internet contains vast amounts of duplicate and near-duplicate content. The same news article republished on dozens of sites. Forum posts copied and pasted across communities. Product descriptions cloned from manufacturers to retailers. Blog posts scraped, lightly paraphrased, and reposted under different bylines. Wikipedia content mirrored across hundreds of fansites. Legal boilerplate repeated verbatim across thousands of documents.
If you include all of this duplicated content in your training data, the consequences are concrete and measurable. The model sees certain texts disproportionately often, which distorts the learned token distribution toward the most replicated documents on the web. Models trained on unprocessed Common Crawl data memorize popular web content more readily, producing outputs that closely mirror specific training examples rather than generalizing from patterns. Benchmark contamination becomes a serious concern when test set passages appear verbatim or near-verbatim in training data, inflating reported performance metrics beyond what the model would achieve on previously unseen text.
The naive solution to this problem is exact deduplication: remove files with identical content. This works perfectly for verbatim copies but fails the moment someone changes a single word, adjusts the formatting, or reorders a sentence. A paraphrase that alters 10% of words is still functionally duplicate content from a training-data perspective. Near-duplicate detection requires measuring textual similarity, and measuring similarity between billions of document pairs using brute-force comparison is computationally infeasible.
MinHash is an algorithm that makes near-duplicate detection tractable at scale. It compresses each document into a compact numerical signature, called a MinHash signature, such that the probability of two signatures agreeing on any position equals the Jaccard similarity of the original documents. With these compact signatures, you can estimate similarity without ever comparing the full documents. With an extension called Locality Sensitive Hashing (LSH), you can find all near-duplicate pairs in a large corpus without comparing every pair, reducing what would be a quadratic-time problem to something approaching linear time in practice.
This chapter covers Jaccard similarity as the mathematical foundation, the probabilistic mechanism behind MinHash, how to implement it efficiently from first principles, and how LSH scales the approach to billions of documents. As we discussed in the deduplication chapter, MinHash is one of the dominant techniques used in cleaning training corpora for large language models, appearing in the pipelines that produced C4, RefinedWeb, RedPajama, and many other influential datasets.
Jaccard Similarity
Before building MinHash, you need to understand what similarity measure it estimates: Jaccard similarity. This measure has a long history in ecology and information retrieval, and its properties make it particularly well suited for detecting near-duplicate documents.
Definition and Intuition
The Jaccard similarity between two sets and measures overlap as a fraction of union:
where:
- : the number of elements appearing in both and
- : the number of distinct elements appearing in either or
Jaccard similarity ranges from 0 to 1. A score of 0 means the sets share no elements at all. A score of 1 means the sets are identical, containing exactly the same elements. For text documents, you represent each document as a set of overlapping substrings (called shingles or n-grams), then compute Jaccard similarity over those sets.
The intuition behind using Jaccard for documents is straightforward. If two documents are near-duplicates, they share most of their substrings, so the intersection of their shingle sets is nearly as large as the union. If two documents are completely unrelated, they share almost none of their characteristic phrases, so the intersection is tiny relative to the union. The ratio captures exactly how much of the content is shared.
Why Jaccard Rather Than Cosine Similarity?
Cosine similarity is another popular choice for comparing text representations, but it has different properties that make it less ideal for near-duplicate detection. Cosine similarity measures the angle between two vectors in a shared feature space, which makes it insensitive to magnitude differences. Two documents can have high cosine similarity even if one is much longer than the other, as long as their term frequencies are proportionally similar. For duplicate detection, you typically want a measure that penalizes documents that share only a small fraction of their content, and Jaccard does this naturally through the union in the denominator.
Jaccard is also naturally defined over sets, which maps cleanly onto the shingling representation of documents. There is no need to build a vocabulary or construct term-frequency vectors; you just extract substrings and compare sets. This simplicity is computationally important when processing billions of documents.
Worked Example of Jaccard Similarity
Consider these two sentences:
- Document A: "the cat sat on the mat"
- Document B: "the cat sat on a mat"
If you extract 3-word shingles (overlapping windows of 3 consecutive words):
Document A generates: {the cat sat, cat sat on, sat on the, on the mat}
Document B generates: {the cat sat, cat sat on, sat on a, on a mat}
The intersection contains 2 shingles: {the cat sat, cat sat on}. The union contains 6 distinct shingles. Therefore:
Even though these documents differ by only one word ("the" vs "a"), their Jaccard similarity over 3-word shingles is only 0.33. This is because each changed word affects multiple overlapping shingles. Shorter shingles capture changes more coarsely; longer shingles are more sensitive to local edits. Choosing the right shingle size is one of the key design decisions in a MinHash pipeline.
Jaccard Similarity Is a Metric
Jaccard distance, defined as , is a valid metric: it is symmetric, non-negative, zero only for identical sets, and satisfies the triangle inequality. This metric property has useful consequences. It means you can use Jaccard distance in metric data structures, and it provides theoretical guarantees for clustering algorithms based on Jaccard distance. MinHash, which approximates Jaccard similarity, inherits these properties approximately.
From Sets to Shingles
To apply Jaccard similarity to documents, you first convert each document into a set of overlapping substrings called shingles or k-grams. A shingle of size is a contiguous sequence of characters or words extracted by sliding a window of length across the document one step at a time.
Character-Level Shingles
Character-level shingles (for example, 5-character n-grams) offer fine-grained coverage of document content. The sentence "hello world" produces 5-gram character shingles: {hello, ello , llo w, lo wo, o wor, worl, world}. Notice that whitespace is included in the shingles, so word boundaries are implicitly captured.
Character-level shingles are robust to synonym substitution because replacing one word with a synonym changes many character-level shingles but preserves all the others. They are also more robust to minor formatting differences, like added punctuation or changed capitalization (after lowercasing). The main drawback is that they produce many more shingles per document than word-level shingles, increasing memory requirements during signature computation.
Word-Level Shingles
Word-level shingles treat words as the atomic units. A 3-word shingle captures consecutive trigrams of words, which are sensitive to phrase-level structure. Word-level shingles are better at capturing semantic phrases like "machine learning model" as a unit, making them more sensitive to reorderings and paraphrasings that preserve individual words but change their arrangement.
The downside of word-level shingles is sensitivity to synonym substitution: replacing "big" with "large" changes a word-level shingle entirely while barely changing the character-level representation. For near-duplicate detection of web documents where paraphrasing is the primary near-duplicate mechanism, character-level shingles typically perform better.
Shingle Size Selection
The choice of has a direct effect on sensitivity. Very short shingles (k=2 or k=3 characters) appear so frequently across unrelated documents that even dissimilar texts get high Jaccard similarity by chance. Very long shingles (k=20+ characters) become unique to each document, causing near-duplicates that differ slightly to appear unrelated.
In practice, most deduplication pipelines for LLM training use character-level shingles with between 5 and 9. The exact value is usually determined empirically by checking whether known near-duplicate pairs get high Jaccard scores and known unrelated pairs get low scores on a small labeled sample. The Pile, RefinedWeb, and similar datasets used character shingles as a standard choice.
Hashing Shingles to Integers
You can represent the shingle set compactly using a hash function. Rather than storing the raw strings (which can be hundreds of bytes for long shingles or many bytes even for short ones), you hash each shingle to a 32-bit or 64-bit integer. This trades a small collision probability for a large memory reduction. With 64-bit hashes, the probability of any two distinct shingles colliding is approximately , which is negligible in practice.
Hashing shingles to integers also accelerates the MinHash computation, because comparing integers is faster than comparing strings, and the subsequent hash function computations operate directly on integer inputs.
The MinHash Algorithm
Computing exact Jaccard similarity for every document pair in a large corpus is infeasible. If you have documents, there are pairs. For a corpus with 100 billion documents, you would need to compare roughly pairs. Even if each comparison took just one nanosecond, completing all comparisons would take over 150,000 years. No hardware can do this.
MinHash offers an elegant probabilistic approximation. Rather than computing the full shingle sets and comparing them, you compute a compact signature for each document such that signature similarity approximates Jaccard similarity. The idea originates in a 1997 paper by Broder, and it is built on a beautiful probabilistic identity about random permutations.
The Core Insight: Random Permutations
Imagine all possible shingles arranged in some arbitrary order, a permutation of the shingle universe. For a given document, look at the shingles it contains, then find the first shingle in the permutation that appears in the document. This is the minimum element under that permutation, hence the name "MinHash."
The key insight is a clean probabilistic fact. If you draw a uniformly random permutation of the shingle universe and compute the minimum element for documents and separately, the probability that both minima are the same equals the Jaccard similarity:
Why This Probability Identity Holds
The proof is elegant and worth understanding. Consider the union , which is the set of all shingles appearing in either document. Under a random permutation , exactly one shingle in lands in the minimum position, that is, appears first in among all shingles in the union. Call this element .
There are two cases:
- falls in (it appears in both documents). Then and , so the minima agree.
- falls in or (it appears in only one document). Then one minimum is and the other is some different element, so the minima disagree.
By symmetry of a uniformly random permutation, each element in is equally likely to be . The fraction of elements in that also belong to is precisely , which is the Jaccard similarity. Therefore:
This is a single-bit estimator: it tells you whether the minima match, but a single comparison gives a binary answer (match or no match) with no granularity. To build a useful estimator, you need many independent trials.
Building the Signature with Multiple Hash Functions
To estimate Jaccard similarity reliably, you generate independent random permutations and compute minima per document. These values form the MinHash signature: a vector of integers. The fraction of positions where two signatures agree gives an estimate of Jaccard similarity:
where is the indicator function, equal to 1 when the condition holds and 0 otherwise.
The variance of this estimator decreases with following the formula for a Bernoulli average:
With hash functions, the standard deviation at (the worst case) is . With , it falls to roughly . The tradeoff is memory: 32-bit hash values per document requires bytes, so 128-hash signatures cost 512 bytes and 256-hash signatures cost 1024 bytes per document. For a billion documents, this is 512 GB and 1 TB respectively, manageable in a distributed system.
Hash Function Implementation
Generating actual random permutations over a large shingle universe is impractical in two ways. First, a permutation over a universe of elements requires integer entries, or 16 GB just to store one permutation. Second, applying the permutation to find the minimum requires scanning all elements.
Instead, you simulate permutations using random hash functions. For each of the "permutations," define a hash function that maps shingle integers to a range of integers. The minimum hash value of the document's shingles under approximates the minimum element under a random permutation , provided the hash function distributes values approximately uniformly.
A classic approach from the universal hashing literature uses linear functions modulo a prime:
where:
- : the integer-hashed shingle value
- : a random integer drawn uniformly from
- : a random integer drawn uniformly from
- : a large prime number larger than (for example, the Mersenne prime )
- : the size of the hash space (for example, )
For each of the hash functions, you pick different pairs. The minimum over all shingles in the document under gives one component of the MinHash signature.
Modern implementations often use faster non-cryptographic hash functions like MurmurHash3 or xxHash with different seeds for each of the positions. The critical requirement is that the hash functions distribute values uniformly and independently, which both MurmurHash and xxHash achieve with high quality. In very large pipelines, a technique called "one permutation MinHash" (or densified one permutation MinHash) uses a single hash of the document into a partitioned range, computing one minimum per partition rather than separate hashes, which is significantly faster.
The Minimum Hash Value Is the Key
It may seem surprising that simply taking the minimum hash value encodes useful similarity information. The intuition is that under a hash function with uniform output distribution, the minimum hash value of a large set of shingles is essentially a random sample from the shingle distribution. If two documents share many shingles, there is a high chance that the shingle with the smallest hash value appears in both. If they share few shingles, the minimums are likely to come from their non-overlapping portions and will differ.
More precisely, the minimum hash value is a random representative of the shingle set. Two documents with high Jaccard similarity are likely to pick the same representative because their shingle sets are nearly the same. Two documents with low Jaccard similarity are likely to pick different representatives.
Worked Example
Let's walk through a concrete MinHash computation with a small example that you can follow step by step. This will solidify both the mechanics of the algorithm and the intuition behind why it works.
Suppose we have two documents:
- Document A: "data science is great"
- Document B: "data science is wonderful"
We extract 2-word shingles:
- A's shingles:
{data science, science is, is great} - B's shingles:
{data science, science is, is wonderful}
The intersection is {data science, science is}, with 2 elements. The union has 4 distinct shingles: {data science, science is, is great, is wonderful}. So the exact Jaccard similarity is:
Now suppose we assign integer IDs to the 4 distinct shingles: data science = 0, science is = 1, is great = 2, is wonderful = 3. We use two hash functions:
Computing hash values for each shingle ID:
- , , ,
- , , ,
Document A contains shingle IDs , so its MinHash signature is:
Document B contains shingle IDs , so its MinHash signature is:
The estimated Jaccard similarity is the fraction of positions where the signatures agree:
Both positions disagree, giving an estimate of 0. The true Jaccard is 0.5. This is a bad estimate, but that's expected: with only 2 hash functions, variance is enormous. The standard deviation at with is , so getting an estimate anywhere from 0 to 1 is quite plausible.
With hash functions, the standard deviation would be , and the estimate would almost certainly fall close to 0.5. This variance reduction is why practical MinHash implementations use 128 to 256 hash functions.
MinHash LSH: Finding Near-Duplicates at Scale
Computing MinHash signatures reduces the comparison problem: instead of comparing full documents (potentially thousands of characters), you compare compact -dimensional integer vectors. But you still face the pair comparison problem. For a billion documents with 128-dimensional signatures, comparing all pairs would require roughly integer comparisons. Even at 10 billion comparisons per second, this would take over 6 million seconds, or about 74 days, on a single machine.
Locality Sensitive Hashing solves this by converting the problem into a bucketing problem. Instead of comparing all pairs, you organize documents into buckets such that similar documents tend to fall into the same bucket. You then only compare documents within the same bucket, dramatically reducing the number of comparisons without losing many true near-duplicate pairs.
The Core LSH Idea
Locality Sensitive Hashing is a general framework for approximate nearest neighbor search. The key requirement is a hash function family that is "locally sensitive," meaning pairs with high similarity hash to the same bucket with high probability, while pairs with low similarity hash to different buckets with high probability.
For MinHash, the natural LSH family is to use each component of the signature as a hash function. If two documents have the same value at position of their signatures, they hash to the same bucket under hash function . By our earlier analysis, two documents share a signature value at position with probability equal to their Jaccard similarity.
A single hash function gives very coarse bucketing. With large enough buckets, everything ends up in the same bucket; with small enough buckets, nothing shares a bucket. The banding technique refines this into a more useful filter.
The Band Technique
The standard MinHash LSH approach divides the -length signature into bands of rows each, so . For each band, you hash the values in that band (treated as a unit, for example, a tuple of integers) into a bucket. Two documents become candidate pairs if and only if they share a bucket in at least one band.
The probability that two documents with true Jaccard similarity share all values in a given band is . The probability that they do not share all values in that band is . The probability that they fail to match in all bands is . Therefore, the probability that they share at least one band (and thus become candidates) is:
where:
- : the true Jaccard similarity between the pair
- : the number of rows per band
- : the number of bands
This function is an S-curve in : it transitions sharply from near 0 (low-similarity pairs are almost never candidates) to near 1 (high-similarity pairs are almost always candidates). The location of the transition is controlled by and .
The Threshold and S-Curve Shape
The midpoint of the S-curve transition (where the probability equals 0.5) provides a useful threshold approximation. Setting and solving approximately:
For example, with bands and rows (a 100-dimensional signature), the threshold is approximately . Pairs with Jaccard similarity well above 0.55 are detected as candidates with very high probability; pairs well below 0.55 almost never become candidates.
The S-curve sharpness increases as increases. With large , the condition that all rows match is stringent, producing a sharp transition. With small , the transition is more gradual because matching 2 out of 2 rows is less discriminating than matching 5 out of 5.
This tunability is one of MinHash LSH's great practical advantages. By choosing and , you can set the detection threshold precisely:
- For aggressive deduplication at similarity 0.7, use and such that the S-curve midpoint falls near 0.7.
- For conservative deduplication targeting only near-perfect duplicates at similarity 0.9, use parameters that push the threshold higher.
- For a fixed budget of hash functions, you trade off sharpness versus threshold by varying how you split into .
LSH as a Two-Stage Pipeline
LSH does not give you the exact similarity of candidate pairs. It identifies a manageable set of candidate pairs that you then verify by computing exact (or more accurate estimated) Jaccard similarity from the full signatures or from the actual shingle sets. This two-stage architecture, coarse LSH filtering followed by fine verification, makes near-duplicate detection tractable at scale.
The first stage runs in linear time per document (computing the signature and performing band hash lookups) plus time proportional to the number of candidate pairs (checking each bucket). The second stage runs in time proportional to the number of candidate pairs, which is much smaller than for realistic similarity distributions.
In production deduplication pipelines for LLM training, the two-stage approach looks like this. All documents are processed in parallel to compute MinHash signatures. Band hashing is performed in a distributed shuffle that groups documents by band bucket. Each bucket is then checked: if it contains more than one document, those documents are emitted as candidate pairs. The candidate pairs are then verified and deduplicated, keeping one representative from each near-duplicate cluster.
False Positives and False Negatives
The banding technique introduces two types of errors. False positives occur when two documents that are not near-duplicates happen to share a band bucket, leading you to compare them unnecessarily. False negatives occur when two true near-duplicates fail to share any band bucket, causing them to be missed.
The S-curve shape directly characterizes these errors. At similarity , the probability of a false negative is : the pair falls below the detection curve. For well above the threshold, this probability is very small. At similarity , the probability of a false positive (unnecessary comparison) is the same formula applied at the wrong point: for pairs that should not be candidates. For well below the threshold, this is also very small.
Near the threshold, both errors occur with non-trivial probability. This is an inherent statistical tradeoff: no threshold-based filter can simultaneously have zero false positives and zero false negatives unless the similarity distribution has a hard gap around the threshold.
For LLM training data deduplication, some false negatives are acceptable. The goal is not perfect deduplication but substantial reduction in near-duplicate content. Missing a few near-duplicate pairs is less costly than the enormous computational burden of verifying all pairs.
Implementation
This section walks through a complete MinHash LSH implementation from scratch, starting with shingle extraction and working up to candidate pair detection and verification.
Setting Up
We begin by importing required libraries. No external packages beyond NumPy are required for the core implementation.
Shingle Extraction
The first step converts each document into a set of character-level shingles. We normalize whitespace and lowercase the text before shingling to ensure consistent matching across minor formatting differences.
def get_shingles(text: str, k: int = 5) -> set[int]:
"""Extract character-level k-shingles from text, returned as hashed integers."""
text = re.sub(r"\s+", " ", text.lower().strip())
shingles = set()
for i in range(len(text) - k + 1):
shingle = text[i : i + k]
# Hash shingle to integer for efficiency
h = int(hashlib.md5(shingle.encode()).hexdigest(), 16) % (2**32)
shingles.add(h)
return shinglesThe function slides a window of width over the normalized text, hashing each shingle to a 32-bit integer. Using MD5 here is acceptable for hashing purposes (not security), and in production you would use a faster non-cryptographic hash like xxHash.
Document A shingle count: 28 Document B shingle count: 28 Document C shingle count: 52 Exact Jaccard(A, B): 0.6970 Exact Jaccard(A, C): 0.0000 Exact Jaccard(B, C): 0.0000
Documents A and B are near-duplicates (one word differs), so they share most of their 5-character shingles and receive a high Jaccard similarity score. Document C is completely unrelated to both, giving near-zero overlap with either. This confirms that the 5-gram character shingling correctly separates similar from dissimilar documents.
MinHash Signature Generation
Next, we implement the MinHash signature computation. We use independent hash functions, each parameterized by a distinct random pair drawn from a Mersenne prime field.
class MinHash:
"""MinHash signature generator using random linear hash functions."""
_PRIME = (1 << 31) - 1 # Mersenne prime 2^31 - 1
_MAX_HASH = 2**32
def __init__(self, num_hashes: int = 128, seed: int = 42):
self.num_hashes = num_hashes
rng = random.Random(seed)
# Generate random coefficients for each hash function
self.a = [rng.randint(1, self._PRIME - 1) for _ in range(num_hashes)]
self.b = [rng.randint(0, self._PRIME - 1) for _ in range(num_hashes)]
def signature(self, shingles: set[int]) -> np.ndarray:
"""Compute MinHash signature: minimum hash value under each hash function."""
sig = np.full(self.num_hashes, self._MAX_HASH, dtype=np.uint64)
for shingle in shingles:
for i in range(self.num_hashes):
h = (self.a[i] * shingle + self.b[i]) % self._PRIME
if h < sig[i]:
sig[i] = h
return sig
def similarity(self, sig1: np.ndarray, sig2: np.ndarray) -> float:
"""Estimate Jaccard similarity from two signatures."""
return float(np.mean(sig1 == sig2))The signature method initializes each position to the maximum possible hash value (serving as infinity), then iterates over all shingles in the document. For each shingle, it computes the hash value under each of the linear hash functions and updates the minimum if the new value is smaller. After processing all shingles, the vector of minimums is the MinHash signature.
MinHash similarity estimates (128 hashes): A vs B (expected ~high): 0.7734 | exact: 0.6970 A vs C (expected ~low): 0.0000 | exact: 0.0000 B vs C (expected ~low): 0.0000 | exact: 0.0000
The MinHash estimates closely track the exact Jaccard values. Documents A and B, which differ by a single word, receive a high estimated similarity close to the exact value. Document C, which is unrelated, receives near-zero estimates for both A vs C and B vs C. With 128 hash functions, the estimates are quite accurate.
LSH Banding
Now we implement the banding technique that makes near-duplicate detection scalable. We divide the signature into bands of rows each, and bucket documents by their band values.
class LSH:
"""Locality Sensitive Hashing for MinHash signatures using the band technique."""
def __init__(self, num_bands: int, rows_per_band: int):
self.num_bands = num_bands
self.rows_per_band = rows_per_band
self.num_hashes = num_bands * rows_per_band
# Each band maintains its own hash table mapping band-hash -> list of doc ids
self.buckets: list[dict[int, list]] = [
defaultdict(list) for _ in range(num_bands)
]
def add(self, doc_id: int, signature: np.ndarray) -> None:
"""Add a document signature to the LSH index."""
for band_idx in range(self.num_bands):
start = band_idx * self.rows_per_band
end = start + self.rows_per_band
band_values = tuple(signature[start:end])
# Hash the band tuple to a bucket key
bucket_key = hash(band_values)
self.buckets[band_idx][bucket_key].append(doc_id)
def candidate_pairs(self) -> set[tuple]:
"""Return all (doc_i, doc_j) pairs that share at least one bucket."""
candidates = set()
for band in self.buckets:
for bucket_docs in band.values():
if len(bucket_docs) > 1:
for pair in combinations(bucket_docs, 2):
candidates.add(tuple(sorted(pair)))
return candidatesThe add method computes band hashes for each document and inserts the document ID into the corresponding bucket in each band. The candidate_pairs method iterates over all buckets and emits all pairs of documents that share a bucket in any band.
Candidate pairs detected: {(0, 1)}
Approximate LSH threshold: 0.707
Pair verification:
Documents (0, 1): estimated similarity = 0.7734LSH correctly identifies documents A and B as candidates and correctly ignores the pairing with document C. The configuration of 16 bands times 8 rows gives a threshold near 0.72. Documents A and B, with similarity well above this threshold, are reliably detected as candidates. Document C, with near-zero similarity to the others, is correctly excluded.
Scaling to a Larger Corpus
Let's simulate the approach on a larger corpus to observe how MinHash LSH scales. We generate a collection of documents where some are near-duplicates, then measure the candidate reduction.
def generate_corpus(
num_docs: int = 1000, dup_fraction: float = 0.2, seed: int = 0
) -> list[str]:
"""Generate a corpus with a known fraction of near-duplicate pairs."""
rng = random.Random(seed)
base_texts = [
"machine learning models learn patterns from data through iterative optimization",
"natural language processing enables computers to understand human text",
"neural networks are computational models inspired by biological neurons",
"transformer architectures use self attention to process sequences efficiently",
"pretraining on large corpora gives models broad language understanding",
]
words_pool = [
"the",
"a",
"large",
"small",
"deep",
"modern",
"efficient",
"powerful",
"scalable",
"robust",
"accurate",
"fast",
"simple",
]
corpus = []
for i in range(num_docs):
base = rng.choice(base_texts)
words = base.split()
if i < int(num_docs * dup_fraction):
# Near-duplicate: swap one word
idx = rng.randint(0, len(words) - 1)
words[idx] = rng.choice(words_pool)
else:
# Unique: replace several words
for _ in range(rng.randint(3, 6)):
idx = rng.randint(0, len(words) - 1)
words[idx] = rng.choice(words_pool)
corpus.append(" ".join(words))
return corpus
corpus = generate_corpus(num_docs=1000)# Compute MinHash signatures for all documents
mh = MinHash(num_hashes=128, seed=42)
lsh_large = LSH(num_bands=16, rows_per_band=8)
t0 = time.time()
all_sigs = []
for doc_id, text in enumerate(corpus):
shingles = get_shingles(text, k=5)
sig = mh.signature(shingles)
all_sigs.append(sig)
lsh_large.add(doc_id, sig)
signature_time = time.time() - t0
# Find candidate pairs
t1 = time.time()
candidates_large = lsh_large.candidate_pairs()
lsh_time = time.time() - t1Corpus size: 1000 documents Total possible pairs: 499,500 Candidate pairs found: 2,260 Candidate reduction factor: 221.0x Timing: Signature computation: 1.556s LSH candidate search: 0.001s
LSH reduces the number of pairs to compare by orders of magnitude. Rather than checking all nearly 500,000 possible pairs, you only compare the small fraction that share at least one LSH bucket. This reduction factor grows rapidly with corpus size, making LSH the key ingredient that allows MinHash to scale to billions of documents.
Visualizations
Let's visualize two key properties of MinHash: how the similarity estimate accuracy improves with more hash functions, and the S-curve behavior of LSH banding.




The heatmap reveals the two clusters of near-duplicates clearly. Documents 1 through 5 are paraphrases of the first base text; documents 6 through 10 are paraphrases of the second. Within each cluster, MinHash estimates high similarity (green). Across clusters, similarity is near zero (red). This block structure is exactly what MinHash LSH exploits: the LSH bands quickly identify the within-cluster pairs as candidates while ignoring the cross-cluster pairs.
Accuracy Analysis: How Many Hash Functions Do You Need?
The estimation variance of MinHash follows directly from the properties of Bernoulli trials. Each of the positions in the signature independently agrees with probability . The estimated similarity is a sample mean of independent Bernoulli trials with success probability . By standard statistics:
and the standard deviation is:
where:
- : the true Jaccard similarity
- : the number of hash functions
- : the MinHash estimate
The variance is maximized when , giving . At the extremes ( or ), the variance is zero because the answer is known exactly with no uncertainty. For deduplication, the most error-prone regime is when you are near the threshold, which is typically around 0.7 to 0.9 for LLM training pipelines. At , the variance is , somewhat lower than the worst case.
For and , the standard deviation is . A 95% confidence interval spans roughly . For deduplication decisions, you typically only care whether similarity exceeds a threshold (say, 0.8), not the exact value. An error of 0.09 around the threshold means pairs close to the threshold are uncertain, but pairs well above or well below are reliably classified.
# Analyze the accuracy of MinHash estimation as a function of k
def minhash_std(j: float, k: int) -> float:
"""Theoretical standard deviation of MinHash similarity estimate."""
return np.sqrt(j * (1 - j) / k)
k_values = [32, 64, 128, 256, 512]
jaccard_test = 0.5 # Worst-case variance at J=0.5MinHash estimation standard deviation at J=0.5:
k Std Dev 95% CI width
------------------------------------
32 0.0884 0.3465
64 0.0625 0.2450
128 0.0442 0.1732
256 0.0312 0.1225
512 0.0221 0.0866At hash functions, the 95% confidence interval width is roughly 0.17. At , it narrows to about 0.12. In practice, 128 hash functions is the most common choice: it provides sufficient accuracy for deduplication decisions while keeping memory requirements manageable (512 bytes per document).
Confidence Intervals and Threshold Decisions
The practical question is not "how accurate is the estimate?" but "how likely are we to make the wrong deduplication decision?" If your threshold is and two documents have true similarity , what is the probability that the MinHash estimate falls below 0.8, causing a false negative?
With and , the standard deviation is . The threshold is standard deviations below the true value. The probability of a false negative is approximately , or about 5.5%. Increasing to 256 reduces this to about 1.5%.
This analysis explains why practitioners choose 128 to 256 hash functions rather than fewer: the marginal cost in memory and computation is small, but the reduction in deduplication errors is meaningful.
MinHash in LLM Training Pipelines
MinHash LSH is the dominant deduplication method for web-scale LLM training corpora. Its practical dominance comes from a combination of properties that make it uniquely suited to this application: linear time signature computation, compact storage, tunable precision-recall tradeoffs, and easy parallelization.
Historical Applications
The landmark C4 dataset, used to train the T5 family of models from Google, applied MinHash deduplication to Common Crawl data. The authors found that deduplicated data significantly improved downstream task performance compared to training on the raw corpus. This result has been replicated consistently: removing near-duplicate content improves model quality by reducing memorization of repeated text and improving the diversity of patterns learned.
The Pile, a large open training corpus assembled by EleutherAI, used MinHash LSH to remove near-duplicate documents within each of its 22 constituent datasets. The RedPajama dataset, an open reproduction of the LLaMA training data, applied MinHash deduplication at multiple stages of processing. RefinedWeb, which underpins the Falcon models, ran aggressive MinHash deduplication as a central component of its data processing pipeline, retaining approximately 65% of the documents after deduplication and other filtering steps.
How Web-Scale Pipelines Work
At web scale (trillions of tokens across billions of documents), the MinHash LSH pipeline is distributed across a large compute cluster. The processing proceeds in stages.
In the first stage, documents are partitioned across worker nodes. Each worker computes MinHash signatures for its shard of the corpus independently, since signature computation is embarrassingly parallel. Workers output (document ID, signature) pairs.
In the second stage, the signatures are processed to perform LSH bucketing. This requires a distributed shuffle: for each band, all documents whose signature values in that band are identical must be grouped together. In Spark or MapReduce frameworks, this is a natural reduce-by-key operation where the key is the (band index, band hash value) pair and the value is the document ID. After the shuffle, each reducer receives all document IDs that share a given band hash, which become candidate pairs.
In the third stage, candidate pairs are verified. For each candidate pair, the exact MinHash similarity is computed from the full signatures. Pairs exceeding the threshold are confirmed as near-duplicates.
In the fourth stage, clusters are formed. Near-duplicate detection identifies pairs, not clusters. If document A is a near-duplicate of B, and B is a near-duplicate of C, but A and C do not exceed the threshold directly, all three should be deduplicated together. Connected component analysis on the candidate graph identifies maximal clusters. One document is kept from each cluster, typically the one appearing earliest in the data or the one from the highest-quality source.
Choosing Parameters for LLM Deduplication
The typical configuration for LLM training data deduplication uses:
- Shingle type: character-level 5-grams, normalized and lowercased
- Number of hash functions: or
- Similarity threshold: 0.70 to 0.90, depending on aggressiveness
- Band configuration: and chosen so the S-curve midpoint matches the desired threshold
For a threshold of 0.8 with , a common configuration is bands of rows, giving a threshold near . A more aggressive threshold of 0.65 might use bands of rows.
The Impact on Model Quality
Research consistently shows that deduplication improves model quality in measurable ways. Models trained on deduplicated data exhibit lower memorization of training examples, meaning they are less likely to reproduce verbatim text from training data when prompted. This is both a quality improvement (the model generalizes better) and a safety improvement (the model is less likely to inadvertently reproduce copyrighted or sensitive text).
Deduplication also improves benchmark integrity. When test benchmarks are contaminated by near-duplicate examples from the training set, reported scores are inflated relative to true generalization ability. Removing near-duplicates reduces this contamination. The GPT-3 paper reported that near-duplicate test examples in training data led to modestly inflated benchmark scores, and subsequent models have been more careful about this.
Beyond near-duplicate removal, MinHash is a lightweight pre-filter for more expensive deduplication checks. You first run MinHash to find approximate duplicate clusters, then verify the most suspicious pairs with exact string matching or more expensive similarity methods. This two-stage approach dramatically reduces the cost of exact deduplication without sacrificing recall.
Limitations and Practical Considerations
MinHash is a remarkably effective tool, but understanding its limitations prevents misapplication and guides parameter selection.
Sensitivity to Document Length
Very short documents (tweets, headings, short snippets of fewer than 100 characters) have few shingles, which inflates Jaccard estimates due to small set size effects. A document with only 10 unique shingles is likely to share a few with almost any other short document by chance, leading to spuriously high Jaccard estimates. A common remedy is to require a minimum document length before applying MinHash, discarding very short documents from the deduplication pipeline or applying a separate deduplication method for short texts. Most LLM training pipelines filter out documents below a word count threshold (often 50 to 100 words) before applying MinHash.
Shingle Choice Affects Sensitivity
Character-level shingles of size 5 are sensitive to most paraphrasing but may incorrectly flag documents that simply share common boilerplate text, like legal disclaimers, terms of service sections, or navigation menus in otherwise distinct web pages. Word-level shingles are more robust to character-level noise but fail to detect documents that rewrite content with synonyms. Neither choice is universally correct; the right shingling strategy depends on the type of near-duplicates you want to catch. In practice, most LLM pipelines use character-level 5-grams and accept that some boilerplate similarity is tolerable.
A more sophisticated approach uses weighted MinHash, which assigns higher weight to rare shingles and lower weight to common ones, reducing the influence of boilerplate phrases on the similarity estimate. This is analogous to the IDF weighting used in TF-IDF representations.
LSH False Negatives
The banding technique trades recall for efficiency. Near-duplicate pairs with similarity near or just below the threshold may be missed entirely (false negatives). The probability of a false negative at similarity is . For pairs significantly above the threshold, this probability is negligible. For pairs at or below the threshold, it can be substantial.
For deduplication of training data, some false negatives are acceptable. The goal is to reduce redundancy substantially, not eliminate it perfectly. If 95% of near-duplicate pairs are detected and removed, the training data is dramatically cleaner than the original, even if a few pairs slip through. The computational savings from missing that 5% are significant.
Cluster Transitivity and Graph Construction
MinHash identifies pairs, not clusters. Two documents A and B may each be near-duplicates of a central document C without being near-duplicates of each other (if A and C share many shingles, C and B share many shingles, but A and B have little direct overlap). Naive pair-based deduplication would retain A and B while removing only duplicates of C, leaving two near-duplicate documents in the corpus.
The correct approach is connected component analysis. Build a graph where nodes are documents and edges connect near-duplicate pairs. Each connected component is a cluster of mutually near-duplicate documents (by transitivity). Keep one representative per component, discarding the rest. This is standard practice in production pipelines but adds a post-processing step.
Implementation Complexity at Scale
On billions of documents, the LSH bucketing step requires careful engineering. The hash tables can exceed available memory on a single machine. A corpus of one billion documents with bands creates up to (document, band) entries for the hash tables. Storing these in memory requires specialized distributed hash table implementations or disk-backed storage.
Production systems partition the signature matrix across nodes and perform the band-hash bucketing in a distributed shuffle. Each node handles a subset of bands, and the shuffle redistributes (document, band hash) pairs so that all documents with the same band hash arrive at the same node. This is an efficient use of distributed compute resources but requires careful partitioning to avoid hot spots where one bucket contains a disproportionate share of documents.
Memory bandwidth is another bottleneck. Reading integers per document from a corpus of billions of documents involves terabytes of memory reads. Careful layout of the signature matrix in memory (row-major order for sequential access) and batched processing of documents significantly improve throughput.
MinHash Does Not Detect Semantic Similarity
MinHash detects near-duplicate surface text, not semantic similarity. Two documents can express identical ideas in completely different words and receive a Jaccard similarity of near zero. Conversely, two documents that are formally near-duplicates (high Jaccard similarity) might contain importantly different information if the differing words carry key content.
For LLM training data curation, this limitation is usually acceptable. The primary concern is surface redundancy, which distorts the training distribution and causes memorization. Semantic deduplication (removing documents that express the same ideas differently) is a separate and harder problem, typically approached using embedding-based similarity methods rather than MinHash.
Despite these limitations, MinHash LSH remains the practical gold standard for near-duplicate detection at scale. Its linear-time signature computation, compact storage, tunable parameters, and probabilistic guarantees make it uniquely suited to the data volumes required for LLM pretraining. No alternative approach comes close to its combination of efficiency and reliability at billion-document scale.
Summary
MinHash compresses each document into a compact signature that preserves Jaccard similarity: the probability that two signatures agree at any position equals the true Jaccard similarity of the underlying shingle sets. This probabilistic identity allows you to estimate document similarity without comparing the full texts, reducing per-pair comparison cost from the document size to the signature size.
Key takeaways from this chapter:
- Jaccard similarity measures set overlap as the ratio of intersection to union, ranging from 0 to 1. It is a natural similarity measure for shingled documents and is robust to length differences between documents.
- Shingling converts documents into sets of overlapping substrings. Character-level 5-grams are the standard choice for LLM training data deduplication because they are robust to minor paraphrasing and synonym substitution.
- The MinHash identity states that for a random permutation , the probability that the minimum elements of two sets agree is exactly the Jaccard similarity: .
- MinHash signatures of length are computed by applying independent hash functions and taking the minimum hash value per function. The fraction of matching positions estimates Jaccard similarity with standard deviation .
- LSH banding divides the signature into bands of rows, bucketing documents by their band values. Documents sharing any bucket become candidate pairs with probability , creating a tunable S-curve threshold at approximately .
- Practical tradeoffs include shingle size (character vs. word level), number of hash functions (), and LSH threshold (, ). These parameters balance precision, recall, and computational cost for the specific deduplication task.
- At LLM training scale, MinHash LSH is applied to billions of documents in distributed pipelines, reducing near-duplicate content that would otherwise distort the learned distribution, inflate benchmark scores, and increase memorization of training text.
The next chapter explores quality filtering techniques that complement deduplication by removing documents that are syntactically valid but semantically poor, completing the picture of how raw web crawl data is transformed into a high-quality training corpus.
Quiz
Ready to test your understanding? Take this quick quiz to reinforce what you've learned about MinHash and Jaccard similarity.
MinHash and LSH Quiz
Reference
Citation details
Cite or share this article.
Continue with the full handbook
This chapter is part of Language AI Handbook. Use the handbook page to browse the complete table of contents and continue reading in sequence.
Explore Language AI HandbookStay up to date
Get articles, book updates, and news delivered to your inbox.
No spam, unsubscribe anytime.
Join the community
Sign in to remove popups, track your reading progress, and join the discussion.

Comments
No comments yet. Be the first to share your thoughts!