MODULE 7/LESSON 1
๐Ÿงน AI Data Engineering & Deduplication

MinHash & Locality-Sensitive Hashing (LSH)

Deduplicate millions of documents in O(N) using MinHash signature vectors & Locality-Sensitive Hashing

โฑ 16 minโšก Interactive Tool๐Ÿ“Š Diagram
Deduplicating training data for LLMs (or cleaning vector database ingestion) is mandatory to prevent memorization and hallucination. However, exact pairwise Jaccard similarity calculation takes O(Nยฒ) time โ€” requiring 50 trillion comparisons for just 10 million documents. MinHash LSH (Locality-Sensitive Hashing) reduces this to near-O(N) by generating fixed-size 'signatures' and hashing similar signatures into identical collision buckets.

Key Concepts

Shingling (n-grams)

First, documents are converted into overlapping word sets (shingles). For example, 3-grams: {'the quick brown', 'quick brown fox'}. This captures structure, unlike single words.

MinHash Signatures

Instead of comparing massive sets, MinHash runs k hash functions over the shingles and keeps only the minimum hash value for each. The probability that two documents share a min-hash equals their exact Jaccard Similarity.

LSH Banding (b x r)

Comparing k-length signatures is still O(Nยฒ). LSH splits the signature into b bands of r rows. If any band matches perfectly between two documents, they are hashed to the same bucket and flagged as candidates.

The S-Curve Math

Probability of collision follows the S-curve: P = 1 - (1 - s^r)^b. This creates a sharp threshold. Below the similarity threshold (s), probability is 0%. Above it, probability jumps to 100%.

โšก Interactive Architecture Simulator

MinHash LSH & Banding Simulator
Hash Functions (k): 100
Bands (b): 20
Jaccard Similarity (s): 75%
5Rows / Band (r)r = k รท b
23.73%Band Match (s^r)per-band prob.
99.6%LSH Candidate P1-(1-s^r)^b
Duplicate โœ“Dedup StatusLSH Bucket

LSH Banding Matrix โ€” 12 of 20 bands

100 MinHash values รท 20 bands = 5 rows each. At 75% similarity โ†’ 99.6% bucket collision chance.

Band #1
5ร— hash sig
โœ— No match
Band #2
5ร— hash sig
โœ— No match
Band #3
5ร— hash sig
โœ— No match
Band #4
5ร— hash sig
โœ— No match
Band #5
5ร— hash sig
โœ— No match
Band #6
5ร— hash sig
โœ— No match
Band #7
5ร— hash sig
โœ— No match
Band #8
5ร— hash sig
โœ— No match
Band #9
5ร— hash sig
โœ— No match
Band #10
5ร— hash sig
โœ— No match
Band #11
5ร— hash sig
โœ— No match
Band #12
5ร— hash sig
โœ— No match

MinHash LSH Pipeline Architecture

MinHash LSH: O(Nยฒ) to O(N) Deduplication Document A "the fast brown fox" Document B "a fast brown fox" 1. Shingling (3-grams) 14 92 33 15 87 41 2. MinHash Signatures k = 6 Hash Functions 14 92 33 77 87 21 3. LSH Banding b = 3 bands r = 2 rows Band 1 Band 1 Band 2 Band 2 (Mismatch) Identical Band Hash! Bucket #1492 [Doc A, Doc B] 4. Candidate Pairs Found (O(N))

Tuning the Math: Threshold Probability (k = 100 hashes)

Notice how the S-Curve mathematically separates duplicates from unique documents sharply around the target Jaccard similarity threshold.
Similarity (s)P (b=20, r=5)P (b=50, r=2)
10% similar0.00% (No match)40.10% (High false positives)
30% similar0.04% (Ignored)99.41% (Matches correctly)
70% similar91.80% (Matches correctly)100.00%
90% similar100.00% (Perfect match)100.00%

Production Code 1: Fast Python Deduplication (datasketch)

minhash_local_dedup.pypython
1import re
2from datasketch import MinHash, MinHashLSH
3
4# โ”€โ”€ 1. Text Preprocessing โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
5def tokenize_shingles(text: str, n: int = 3) -> set:
6    """Extract character n-grams (shingles) from raw text."""
7    text = re.sub(r'\s+', ' ', text.lower().strip())
8    words = text.split(' ')
9    return set(" ".join(words[i:i+n]) for i in range(max(1, len(words)-n+1)))
10
11# โ”€โ”€ 2. Create MinHash Signature โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
12def create_minhash(text: str, num_perm: int = 128) -> MinHash:
13    m = MinHash(num_perm=num_perm)
14    for shingle in tokenize_shingles(text):
15        m.update(shingle.encode('utf-8'))
16    return m
17
18# โ”€โ”€ 3. Initialize LSH Index โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
19# Setting threshold=0.8 automatically computes optimal (b, r) for num_perm=128
20# e.g., b=25, r=5
21lsh = MinHashLSH(threshold=0.80, num_perm=128)
22
23documents = {
24    "doc_1": "NVIDIA Hopper architecture delivers massive performance gains for transformer models.",
25    "doc_2": "The NVIDIA Hopper architecture provides massive performance improvements for transformer models.",
26    "doc_3": "PostgreSQL indexing strategies using B-Trees and Hash indexes."
27}
28
29# โ”€โ”€ 4. Indexing (O(N)) โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
30for doc_id, content in documents.items():
31    lsh.insert(doc_id, create_minhash(content))
32
33# โ”€โ”€ 5. Querying (O(1) lookup per query) โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
34query_doc = "NVIDIA Hopper architecture yields massive performance gains on transformer models."
35query_hash = create_minhash(query_doc)
36
37candidates = lsh.query(query_hash)
38print(f"Candidates for query: {candidates}")
39# Output expected: ['doc_1', 'doc_2'] (doc_3 is completely ignored without being compared)
40

Production Code 2: PySpark Distributed LSH (10M+ Docs)

pyspark_lsh_pipeline.pypython
1from pyspark.sql import SparkSession
2from pyspark.ml.feature import Tokenizer, NGram, MinHashLSH
3
4spark = SparkSession.builder.appName("LSH-Dedup").getOrCreate()
5
6# Load massive dataset
7df = spark.read.json("s3://llm-corpus/wikipedia_dump.jsonl")
8
9# 1. Tokenize into words
10tokenizer = Tokenizer(inputCol="text", outputCol="words")
11wordsData = tokenizer.transform(df)
12
13# 2. Generate 3-gram shingles
14ngram = NGram(n=3, inputCol="words", outputCol="ngrams")
15ngramData = ngram.transform(wordsData)
16
17# 3. Create LSH Model with specific Bucket Length
18# (In PySpark, bucketLength controls the collision probability, similar to threshold)
19mh = MinHashLSH(inputCol="ngrams", outputCol="hashes", numHashTables=5, seed=42)
20model = mh.fit(ngramData)
21
22# 4. Find all duplicate pairs across the entire 10M document cluster!
23# Computes near-O(N) distributed join based on LSH buckets
24duplicate_pairs = model.approxSimilarityJoin(
25    ngramData, 
26    ngramData, 
27    threshold=0.80, 
28    distCol="JaccardDistance"
29)
30
31# Filter self-matches
32duplicates = duplicate_pairs.filter("datasetA.id < datasetB.id")
33duplicates.show(10)
34
๐Ÿ’ก
Senior Architect Insight: Do not blindly increase `num_perm` (e.g. k=512 or 1024) to get higher accuracy. More hashes exponentially increase RAM usage and slow down LSH insertion time. k=128 or k=256 is the industry sweet spot for deduplicating billions of tokens for LLM pre-training.