Conversation
Two different problems live under one word, and this splits them. --dedup-digest skips records whose payload digest was already seen, before extraction, so a duplicate costs a hash lookup instead of an HTML parse. Export reuses the digest the WARC already carries; refetch does the check in the single writer loop and exempts failed fetches, since two dead hosts share an empty body and collapsing them would hide the failure. The scope is one shard rather than the whole run: shards are converted in parallel, so a run wide set would make which copy survives depend on scheduling, and over --shards all it would grow without a bound anyone set. Every row now carries a simhash column, a 64 bit fingerprint over overlapping three word shingles. It is stored, not acted on. Dropping near duplicates during a run means deciding once and irreversibly which copy is the good one, and that belongs to whoever knows what the dataset is for. ccrawl dedup reads the column back and reports clusters without rewriting anything. Also fixes stats.Rows in refetch, which was counted before the dedup and lang filters rather than after them, so the number the dataset card published did not match the file. That was already wrong for --lang before this change. Measured on CC-MAIN-2026-30 shard 0. Without the flag, 20861 rows. With it, 20819 rows and digest_dropped 83. An independent scan of the same WARC found 21278 HTML response records holding 21195 distinct payloads, so 83 duplicates in 28 groups, matching exactly with zero false drops. Only 42 of the 83 show up as fewer rows because the other 41 extract to nothing. Two full exports of the same shard share 20819 doc_ids with 0 simhash mismatches. One finding worth recording. Charikar weights each feature by its count, and on web text that is a trap: a mojibake page extracts to a few replacement sequences repeated thousands of times, and the fingerprint is then decided almost entirely by those. Two such pages shared 6.5 percent of their shingles and came out one bit apart, chaining 68 unrelated documents into one cluster. Voting once per distinct shingle put that pair 30 bits apart and cut the shard from 67 near duplicates to 26. A 512 byte floor on the near pass took it to 24, since a 64 bit fingerprint over fewer than 64 features is decided by noise. Refs #50
7 tasks
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #50.
Two different problems live under one word, and this splits them.
Exact:
--dedup-digestSkips a record whose payload digest was already seen. The check runs before extraction, so a duplicate costs a hash lookup instead of an HTML parse, and export reuses the digest the WARC already carries rather than recomputing one.
The scope is one shard, not the whole run. Shards are converted in parallel, so a run wide set would make the choice of which copy survives depend on scheduling, and over
--shards allthe set would grow without a bound anyone set.Refetch does the check in the single writer loop, using the digest ami already computes, and exempts failed fetches. Two dead hosts share an empty body and collapsing them would hide the failure.
Near: the
simhashcolumn andccrawl dedupEvery row now carries a 64 bit fingerprint over overlapping three word shingles of the Markdown. It is stored, not acted on. Dropping near duplicates during a run means deciding once and irreversibly which copy is the good one, and that decision belongs to whoever knows what the dataset is for.
ccrawl dedup <parquet|dir>...reads it back and reports, never rewriting the input, so it is safe to point at a published dataset. It reads three columns and leaves the rest of the parquet alone, which is why a 20k row shard takes 0.68s.parquet-go reads by name, so appending the column is backwards compatible and a file written before it existed reads as zero.
ccrawl dedupfingerprints those rows on the fly, so older datasets still work.Also in here
stats.Rowsin refetch was incremented before the dedup and lang filters rather than after them, so the number the dataset card publishes did not match the file. The test caught it as "stats say 6 rows, parquet holds 4". This was already wrong for--langbefore this change.dedupSummarywas rewritten for the same reason: itsseen = dropped + keptarithmetic assumed the pre-filter count.Done when
--dedup-digeston one crawl shard drops the expected fraction, measure it first, with zero false drops on a manual sampleMeasured on
CC-MAIN-2026-30shard 0, unfiltered first and then with the flag.digest_dropped--dedup-digest--dedup-digestRather than a manual sample I scanned the raw WARC independently: 21278 HTML response records holding 21195 distinct payloads, so 83 duplicate payloads in 28 groups. That is the pipeline's number exactly, over the whole shard rather than a sample, with zero false drops. Only 42 of the 83 show up as fewer rows because the other 41 extract to nothing and would never have been rows.
One pair looked like a false drop at first, two URLs on
btg-bestellservice.dewhose Markdown differed. It is a true duplicate:Convert(body, pageURL)resolves relative links against the page URL, so byte identical HTML at two URLs produces different Markdown. Payload identity and text identity are not the same question.For box two, two independent full exports of the same shard share 20819 doc_ids with 0 simhash mismatches, and only 2 rows in 20861 have no fingerprint. A unit test separately recomputes
Simhash(markdown)from the stored text and checks it equals the stored column.The mojibake finding
Worth recording because it changed the implementation. Charikar's simhash weights each feature by how often it occurs, and on web text that is a trap. A mojibake page, one where UTF-8 was served as Latin-1, extracts to a handful of replacement sequences repeated thousands of times, and a count weighted fingerprint is then decided almost entirely by those few features.
Two such pages measured here shared 6.5 percent of their shingles and still came out one bit apart, which chained 68 unrelated documents into a single cluster. The corpus was fine, the fingerprint was wrong.
Voting once per distinct shingle, however often it occurs, put that pair 30 bits apart and cut the shard from 67 near duplicates in 49 clusters to 26 in 25. A 512 byte floor on the near pass took it to 24, since a 64 bit fingerprint decided by fewer than 64 features is decided by noise. Short pages still get a fingerprint and still cluster as exact duplicates, which is the only claim worth making about a stub.
What survives looks like real template duplication: phpBB FAQ pages across two forums, WordPress tag archives on the same site, focus.de cancellation letter templates.
What it reports now
The largest cluster is 42 byte identical CONTENTdm digital collection pages.
Tests
ccrawl/dedup_test.gocovers determinism over 50 repeats, near versus unrelated separation, the empty case, the digest drop throughpackStream, column presence and stability across two runs, the column matching text recomputed from the row, both cluster kinds with two uniques that must not be swept in,--distance 0, a file with no simhash column, empty input, and the 512 byte floor.ccrawl/refetch_dedup_test.goruns the whole refetch path against an httptest server serving six pages of which three are identical, and assertsstats.Rowsequals the parquet's content row count.Fixtures generate prose from word banks rather than repeating one sentence, because
strings.Repeathas too few distinct shingles to fingerprint once each shingle votes once. Sizes were measured, not guessed: at n=200 words the near pair sits at distance 0 and the unrelated pair at 36.Gates
gofmt -l .clean,go vet ./...clean,go test ./... -count=1green,golangci-lint run0 issues,./scripts/docs-drift.shmatches.