Repository navigation
Deduplicate repeated statements in bulk import - #1754
Merged
Merged
Conversation
An RDF graph is a set of triples, but the bulk-import pipeline stored every input line verbatim: no dedup stage existed on either the single-chunk or the chunked path (the merge ran with skip_dedup: true), so duplicated input lines became duplicate index rows and inflated every COUNT and aggregate over the affected predicates. The index now squashes duplicates while the commit blobs keep the raw ops (the event log stays faithful; the index is the set view): - Chunk sort: an in-place dedup_by pass over the already-sorted chunk buffer collapses within-chunk copies before the sorted commit is written, upstream of the stats hooks and every downstream artifact. - K-way merges: a new next_unique_assert() collapses cross-chunk copies adjacently in the SPOT and secondary-order merges (earliest t wins — a fact is present from its first assertion). Retracts pass through verbatim; cascade intermediates stay lossless. The ambiguous skip_dedup flag is renamed import_unique_asserts. - Stats: worker hooks are fed per chunk, so the SPOT merge tallies the copies it collapsed by (g, p, o_type) and discounts the exact counters at stats finalize; HLL sketches are duplicate-insensitive and stay untouched. Per-property counts and stats.flakes describe the set view. - Counters: flake_count / cumulative_flakes / total_asserts / fuel keep describing raw commit ops; ImportResult.duplicates_removed reports the collapsed copies and the CLI prints both when non-zero. Dedup identity is the existing five-field key (s, p, o_type, o_key, o_i), so language tags, datatypes, and list positions all keep distinct facts distinct; anonymous blank nodes and reifiers mint per-occurrence ids and intentionally never collapse. Vector literals are excluded (the arena appends per occurrence, so copies carry distinct handles). Adjacent-identity dedup requires remapped chunk streams to stay sorted, which exposed a latent ordering bug: the unified language dictionary was built in first-seen order across chunks while chunk-local ids were parse -order, making local-to-global lang remaps non-monotone — a remapped stream could un-sort within (s, p) and equal langString records would never meet. Both sides now assign language ids lexically (normalized), making every remap monotone; a chunk-level reassignment rewrites record lang_ids before the sort. Each lane is pinned by a test proven to fail with its fix reverted: chunk dedup (spool unit test), merge dedup (merge unit test + cross- chunk e2e including the six-line issue repro asserting COUNT(*) = 3), and lang monotonicity (remap unit test + adversarial tag-order e2e). Near-miss e2e coverage pins lang/datatype/plain variants as distinct.
zonotope
approved these changes
Sep 2, 2026
zonotope
left a comment
Contributor
There was a problem hiding this comment.
Looks good besides one style thing.
…e_complexity build_spot_index_from_commits returned a bare three-tuple whose third field (the cross-chunk duplicate tally) read as an opaque FxHashMap<(u32, u16), u64> at every call site. A named SpotBuild struct documents the map's key and drops the clippy allow.
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.
Summary
Bulk import stored every input statement verbatim — no deduplication existed on either import path (the merge ran with
skip_dedup: true), so duplicated input lines became duplicate index rows, violating RDF set semantics and silently inflatingCOUNT, aggregates, and join cardinalities (#1651). This PR makes the index the set view while the commit blobs keep the raw ops: duplicates are squashed where the data is already sorted, at no measurable cost to clean imports.Closes #1651
Design
Two dedup lanes, both O(1) memory, both operating on the existing five-field identity
(s_id, p_id, o_type, o_key, o_i)— so language tags (folded intoo_type), datatypes, and list positions all keep distinct facts distinct:dedup_bypass immediately after the per-chunk graph-prefixed SPOT sort collapses within-chunk copies before the sorted-commit artifact is written — upstream of the stats hooks and every downstream artifact. Guarded (samet, both asserts) so anything unexpected is kept rather than collapsed.KWayMerge::next_unique_assert()collapses cross-chunk copies adjacently in the SPOT and secondary-order merges. Earliesttwins (a fact is present from its first assertion; later copies are no-op re-asserts). Retracts pass through verbatim; cascade intermediates stay lossless; the rebuild arm (next_deduped_with_history) is untouched. The now-misleadingskip_dedupflag is renamedimport_unique_asserts.Stats stay tallied in the parallel remap workers exactly where they are today (no pipeline restructuring): the chunk-lane dedup lands upstream of the hooks so within-chunk copies never get counted, and the SPOT merge tallies cross-chunk squashes into a lazily-allocated
(g, p, o_type)map — never touched on clean data — applied as signed-delta discounts at stats finalize via the hook's existing delta machinery. HLL sketches are duplicate-insensitive and need no correction. Per-property counts andstats.flakesnow describe the set view.Counters:
flake_count/cumulative_flakes/total_asserts/ fuel keep describing raw commit ops (the commit chain deliberately retains every op). A newImportResult.duplicates_removedreports the collapsed copies, and the CLI prints both when non-zero:Latent language-remap ordering bug (fixed here; adjacency dedup depends on it)
Adjacent-identity dedup requires remapped chunk streams to stay sorted, which exposed a pre-existing hazard: the unified language dictionary was built in first-seen order across chunks while chunk-local ids were parse-order, so a local→global lang remap could be non-monotone — un-sorting a remapped stream within
(s, p)(lang_id is the langStringo_typepayload, part of the sort key) so equal langString records never meet in the merge. Both sides now assign language ids lexically (normalized): the chunk sort reassigns local ids and rewrites records,build_lang_remap_from_vocabsbuilds the unified dict lexically, making every remap monotone. Cost: O(distinct tags).Semantics preserved by construction
f:reifies*) bundles are separate records keyed by the reifier subject — base-triple dedup cannot drop annotation data.Benchmarks
DBLP N-Triples, first 50,000,000 lines of the 2026-06-01 dump (6.0 GB), fresh ledger per run, interleaved runs, Apple M4 Max 16c/48GB. Three binaries: A = released v4.1.6 (homebrew), C =
origin/mainunpatched, B = this branch (C + this patch, isolating its cost):Speed and peak memory are identical within ±1% run noise across all three (B's mean is nominally lowest). Correctness reconciles to the digit: the prefix holds 49,175,004 triple statements (plus 824,996 blank lines) of which 49,156,066 are distinct; A/C store all 49,175,004, B collapses exactly 18,938 and stores exactly the distinct count, and the A−B row delta equals
duplicates_removed.Two findings from the ground-truthing worth recording:
duplicates_removed ≈ 67,202and store the true count.Testing
Every dedup lane is pinned by a test proven to fail with its fix reverted (mutation-tested):
sorted_commit_collapses_duplicate_records).o_inear-miss, retract passthrough) + cross-chunk e2e — including the issue's 6-line repro assertingCOUNT(*) = 3, and a chunked variant asserting per-property stats reflect the set view.@enstatement under one(s, p).Additional coverage: near-miss e2e (same literal under
@en/@de/ plain /xsd:byte/ integer all preserved,duplicates_removed == 0); one pre-existing overlap fixture updated whose seam records — identical-comparing by design — now correctly collapse (spot_hierarchical_matches_flat, CID flat/hierarchical parity assertion intact).Green: indexer lib + integration (389+),
grp_import(105),grp_misc(263),grp_transact(190); clippy clean on touched crates;cargo fmt --all --checkclean.