Auditing a 660-Article Archive for Near-Duplicate Content: Our Three-Pass Dedup Process
How we audited a 660-article archive for near-duplicates using hashing, MinHash shingles, and embedding similarity — plus the triage rules for merging, differentiating, or keeping overlapping posts.
An archive does not develop a duplicate content problem in one bad week. It gets there one reasonable decision at a time: a topic queue surfaces the same launch from Hacker News on Monday and from Bluesky on Thursday. A comparison post gets written as “A vs B” in March and “B vs A” in September. A yearly refresh ships as a new slug instead of an update to the old one. None of those choices is wrong in isolation. Stacked 660 times, they produce an archive where you genuinely cannot tell whether a given draft already exists.
We ran a full near-duplicate audit across our own archive. What follows is the process, the thresholds, and the part nobody writes about — deciding what to actually do with a cluster once you have found it.
Why near-duplicates cost you something
Google’s own documentation is clear that duplicate content is not a penalty in the ordinary case. What happens instead is consolidation: the crawler picks one URL as canonical and the others stop earning their own impressions. That is not a fine, but it is still a loss. You paid to write four articles and you are being served as one.
The sharper cost shows up in retrieval. Answer engines chunk your pages and rank chunks by similarity to a query. If six of your articles contain a near-identical paragraph explaining what an agentic CLI is, those six chunks compete with each other for the same slot. Redundancy inside your own corpus is self-inflicted dilution, and unlike a ranking drop it produces no obvious signal in Search Console.
The third cost is editorial. When you cannot answer “have we covered this?” in under a minute, your topic pipeline starts generating work you have already done.
The three-pass audit
We ran three passes, cheapest first, each catching a different failure mode. Running them in this order matters — the expensive pass only has to look at what the cheap passes could not resolve.
Pass 1 — normalized hashing. Strip frontmatter, strip MDX component tags, collapse whitespace, lowercase, then SHA-256 the remaining body. Exact matches after normalization mean a file was copied. This found nothing in our archive, which is the expected result. Run it anyway: it takes seconds and it rules out the embarrassing category before you start interpreting fuzzy scores.
Pass 2 — lexical overlap via shingles. Break each normalized body into overlapping 5-word shingles, hash them into a MinHash signature, and compare signatures by estimated Jaccard similarity. This catches copy-paste: a methodology section reused verbatim, an intro paragraph lifted from a sibling post, a pricing table pasted into three reviews.
One detail decides whether this pass is useful or noise: strip shared boilerplate first. Disclosure blocks, AI-assistance notes, newsletter copy, and standard closing sections are identical by design. On a 900-word post, boilerplate can be a third of the token count, which pushes every short article into apparent similarity with every other short article. We excluded any block that appears in more than five percent of the archive before computing signatures.
Pass 3 — semantic similarity via embeddings. Embed each article body, then compute pairwise cosine similarity. This is the pass that finds the real problem: two articles making the same argument, in different words, with different examples. Lexical overlap on those pairs can sit near zero.
At 660 articles the full pairwise matrix is roughly 217,000 comparisons, which is trivial in NumPy. The embedding call itself is the only meaningful cost, and at a couple of thousand tokens per article you are embedding well under two million tokens total — a rounding error against any provider’s embedding pricing.
Here is how the three passes compare on what they catch and what they cost:
| Pass | Catches | Misses | Relative cost |
|---|---|---|---|
| Normalized hash | Verbatim copies, republished files | Anything reworded | Seconds |
| MinHash shingles | Reused sections, pasted tables, template drift | Same argument, new wording | Under a minute |
| Embedding cosine | Semantic restatement, split-topic overlap | Nothing structural — but produces false positives | Minutes plus embedding tokens |
Triage is the actual work
Finding clusters took an afternoon. Deciding what to do with them took considerably longer, because a high similarity score is a question, not a verdict. We sorted every cluster into one of four outcomes.
Merge and redirect. Two articles answer the same question for the same reader. Pick the winner by inbound links and existing impressions, not by which one you like better or which is newer. Fold any unique material from the loser into the winner, then 301 the loser’s URL. Deleting without a redirect throws away whatever authority the page had accumulated.
Differentiate. The overlap is real but the articles have legitimately different jobs — one is a hands-on review, the other a buying comparison that happens to restate the same setup context. Here you rewrite the overlapping section in one of them rather than merging, and add explicit cross-links so each page points at the other for the part it does not cover.
Keep both. The score is high because the corpus is narrow. Two independent reviews of two competing tools in the same category will always look similar to an embedding model. If a reader searching for one would be annoyed to land on the other, they are not duplicates. This bucket was larger than we expected, and it is the reason a purely automated dedup pass is a bad idea.
Delete. Reserved for pages with no traffic, no links, and nothing worth folding into the survivor. Still redirect to the closest relevant page.
One rule saved a lot of arguing: the article with the most inbound links wins by default, and overriding that default requires a stated reason. Without it, every cluster turns into a discussion about writing quality.
OpenCode
An open-source terminal coding agent. Useful for the mechanical half of a dedup audit — running the same merge, rewrite, or redirect edit across dozens of MDX files from the terminal, with a diff you review before it lands.
Free and open source; bring your own model API key
Affiliate link · We earn a commission at no cost to you.
Stopping the archive from re-accumulating
A one-time audit buys you a clean archive and nothing else. The drift resumes the next time the topic queue runs.
The fix is to move the check upstream. We embed each draft before publish and compare it against the archive’s existing vectors. If the nearest neighbor scores above our calibrated threshold, the pipeline stops and prints the matching slug and its score. Most of the time the right response is to update the existing article instead of publishing a new one, which is usually the better SEO outcome anyway.
Two things make this practical. First, cache the archive’s embeddings and key them on a content hash, so a re-run only embeds what actually changed. Second, log the nearest neighbor and its score on every publish, even when it passes. That log is what lets you re-calibrate the threshold later using real data rather than re-guessing.
We re-run the full pairwise audit quarterly. The pre-publish check catches direct restatements; the full audit catches slower drift, like a category where six articles converge on the same framing over a year without any single pair tripping the gate.
FAQ
What similarity score means two articles are duplicates?
Should I delete near-duplicate pages or merge them?
Do I need embeddings, or is text comparison enough?
The audit is worth running once even if you never automate it. Our most useful finding was not any single duplicate pair — it was discovering which categories the topic pipeline kept circling, which changed what we queued next.
Tools used in this review
Some links above are affiliate links. We may earn a commission if you sign up. See our disclosure for details.
Related reading
2026-08-13
Your Automation's Numbers Are Probably Wrong: An Upsert Bug and 89% Bot Clicks
Two production metric failures from running scheduled agents: an upsert that overwrote instead of incremented, and a click counter where only 11 percent of hits were human. How we found both and what we check now.
2026-08-13
Scheduled Agents Die Silently: The Cron Failures That Never Throw an Error
Cron-driven agents rarely fail loudly. They exit 0 and do nothing. Four silent failure modes from running scheduled agents in production, and the three assertions that catch them.
2026-05-26
AI Agent Pipelines for Developer Productivity: What Actually Saves Hours
We tested a four-stage AI agent pipeline for code review, test generation, and deployment over two weeks. Here's where the gains are real and where the failure modes hide.
2026-08-12
Canonical URLs vs llms.txt: What We Expose to Search Engines and What We Expose to AI Crawlers
How pickuma splits its publishing surface: one canonical URL per article for search engines, three noindex machine-readable files for AI crawlers, and the rules that keep the two from fighting.
2026-08-12
Google's Publisher Policies Changed How We Run Ads, Consent, and Privacy
A working log of how Google's publisher rules — certified CMPs, Consent Mode v2, per-impression AdSense, and the scaled-content spam policy — changed the ad, consent, and disclosure code on pickuma.com.
Get the best tools, weekly
One email every Friday. No spam, unsubscribe anytime.