Repository navigation
fix(filters): hash the trailing characters in NgramHashDeduplicateFilter - #570
Open
cyberspace-cs wants to merge 1 commit into
Open
cyberspace-cs wants to merge 1 commit into
cyberspace-cs wants to merge 1 commit into
Conversation
_split_chunks derives the chunk size as len(text) // n_gram and then slices
range(n_gram) chunks of that size. Two problems follow from that arithmetic:
- When len(text) % n_gram != 0 the trailing characters are never hashed, so
texts that differ only in their tail compare equal. "abcdefghij" and
"abcdefghix" both reduce to the chunks abc/def/ghi with n_gram=3.
- When len(text) < n_gram the chunk size is 0 and every chunk is the empty
string, so all short rows collapse onto hash("") and all but the first are
silently dropped. ["a", "b", "c"] deduplicates down to ["a"].
Split on rounded boundaries instead, which covers every character while keeping
the chunk count at n_gram, and hash texts no longer than n_gram as a single
chunk so that they still carry a distinguishing hash.
The uniqueness check also compared each new row against every kept row, which is
quadratic in the number of rows. Keep an inverted index from chunk hash to the
rows that contain it and walk only those postings, stopping as soon as one kept
row reaches diff_size shared chunks. The result is unchanged: a row is kept iff
no earlier kept row shares at least diff_size chunks with it. On 20k distinct
rows the run drops from 20.1s to 0.08s.
diff_size < 1 previously made every row after the first a duplicate; reject it
in the constructor rather than dropping the data silently.
This branch has not been deployed
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.
What this fixes
NgramHashDeduplicateFiltersplits each text inton_gramchunks with a single integer chunk size:That slice covers
n_gram * (len(text) // n_gram)characters, which is only the whole text when the length is an exact multiple. Two silent data-loss cases follow:1. The tail is never hashed. With
n_gram=3,"abcdefghij"and"abcdefghix"both reduce toabc / def / ghi. The last character is invisible to the comparison, so the pair is judged a duplicate and the second row is dropped. The larger the remainder (len % n_gram), the more of the text is ignored.2. Short rows collapse onto one hash. When
len(text) < n_gram,gram_lengthis0, every chunk is"", and the hash set is{hash("")}for every short row.["a", "b", "c"]deduplicates down to["a"].Neither case is reported: the operator logs
Total unique items: 1and moves on.MinHashDeduplicateFilteralready carries a fix for the same short-text collapse (test_minhash_short_texts.py); this brings the n-gram variant in line.The fix
_split_chunksplaces the boundaries atround(i * len(text) / n_gram)instead of using a uniform integer chunk size. The chunk count stays atn_gram, but the chunks now tile the text exactly, so every character contributes to at least one hash. Texts no longer thann_grambecome a single chunk, so they still hash to something that distinguishes them.Second change: the uniqueness check was quadratic
Every row intersected its chunk set with every kept row. The predicate is equivalent to "no earlier kept row shares at least
diff_sizechunks", which an inverted index from chunk hash to kept rows answers without touching rows that share nothing:n_gram=1)Both runs keep all rows, so the outputs are identical. The 20k case is ~250× faster and the cost is no longer quadratic in the row count.
diff_size < 1used to make every row after the first a duplicate (len(...) < 0is never true). It is now rejected in the constructor instead of dropping data silently.Tests
New file
test/cpu_only/test_ngramhash_chunk_coverage.py, 15 cases:test_texts_differing_only_in_the_tail_are_not_duplicates— 10/3, 7/4 and 11/5 characters per chunk, withdiff_size == n_gramso the tail is the only distinguishing signal.test_texts_shorter_than_n_gram_stay_distinct—["a", "b", "c"]forn_gram2/3/5/8.test_chunk_overlap_below_diff_size_is_kept— thediff_sizethreshold still separates 2 shared chunks from 3.test_exact_duplicates_are_still_removed,test_empty_texts_are_deduplicated_into_a_single_row,test_n_gram_one_hashes_the_whole_text,test_multiple_input_keys_cover_the_tail_of_the_joined_text,test_large_distinct_batch_keeps_every_row.test_invalid_chunk_configuration_is_rejected—n_gram=0anddiff_size=0.On
main, 10 of the 15 fail; with the patch all 15 pass, and the existingtest_dedup_single_input_keys.py+test_minhash_short_texts.pystill pass (28 passed together).Happy to drop the inverted index if you would rather review the correctness fix on its own — the chunking change does not depend on it.