feat(graph): deterministic key-based entity dedup via merged_into edges (SDK-208)#4136
Draft
rshkarin wants to merge 3 commits into
Draft
Conversation
- Fold EntityType name into Entity.id_for at construction time - Add canonicalization phase to in-batch deduplication - Add cross-run fetch and MergePolicy for conflict resolution - Add MergeRecord, FieldResolution, ContradictionEdge logs - Fix edge key delimiter to prevent collisions
…lization as a pure key [SDK-208]
The original PR did not work end-to-end (all runtime-verified):
- The canonicalization mutated `node.name` *after* ids were fixed, but
`deduplicate_nodes_and_edges` keys on `str(node.id)`; surface variants
("USA"/"United States") were never merged while their display names were
destructively mangled to slugs.
- `Entity.id_for(node_id, type_node.name)` in `_create_entity_node` broke node
lookup: `_process_graph_edges`/`_resolve_node` still resolve endpoints by
name-only `Entity.id_for(name)`, so every extracted entity->entity edge was
silently dropped. It also changed all entity ids (orphaning re-cognified
graphs) and other single-arg `id_for` call sites.
- The 3-tuple return broke every un-updated caller (four delete tests unpack
two values; ~12 add_data_points tests mock a 2-arg 2-tuple).
- `MergeRecord`/`FieldResolution` provenance was computed then discarded;
`merge_with_existing` and `ContradictionEdge` were dead code.
This commit reverts the two core-algorithm files to their dev versions, removes
the dead/broken modules, and keeps only the sound piece — the name
normaliser — as a pure grouping-key function (cached alias-map load, no name
mutation). The author's misplaced/red tests are removed; real coverage follows.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…208] Approach A of gh #3627: resolve name-variant duplicate entities with cheap, deterministic string rules — no LLM, no embeddings. `resolve_deterministic_duplicates(nodes)` groups same-type nodes by a canonical key (NFKC + diacritics + lowercase + alias map + punctuation/whitespace collapse + leading-article strip); within each group the longest original name is kept canonical (ties -> smaller id) and every other node emits a `merged_into` edge (duplicate -> canonical) carrying `resolution="deterministic_key"` and the `canonical_key`. Edge properties are already persisted by cognee, so the merge is recorded in the graph — provenance-preserving (what merged into what + why), non-destructive (both nodes and all their edges kept), and reversible (drop the edge to undo) — matching the sibling Approaches B/D, differing only in the detection signal. Wired into `add_data_points` right after `deduplicate_nodes_and_edges` (extends the existing path, not a parallel pipeline), opt-in via `deterministic_dedup=True` (default off; a disabled run does zero extra work). Deterministic unit tests import the real resolver: case/punctuation and alias variants -> merged_into edges to one canonical, type-gating, single-node / same-id / no-name negatives, input non-mutation, corrected canonicalization tests; plus add_data_points wiring tests proving the default path is unchanged and the merged_into edge reaches the graph write when enabled. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
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.
Description
Review + rework of community hackathon PR #4028 (by @Darshan3690) for hackathon issue #3627 — Approach A: deterministic, key-based dedup (no LLM). Linear: SDK-208.
Approach A is 1 of 5 parallel approach tickets (A–E) exploring contradiction & dedup resolution; the goal is to compare them on shared fixtures and adopt the best (or a hybrid). This PR keeps the original author's commit as the base (attribution intact) and layers the rework as separate commits on top.
What the original PR did (all runtime-verified)
The normalisation logic itself is reasonable, but the feature did not work end-to-end:
deduplicate_nodes_and_edgeskeys onstr(node.id); the canonicalization rewrotenode.nameafter ids were assigned and never recomputed the id. Two surface variants kept distinct ids and were not merged — while both display names were destructively mangled to a slug (e.g. both became"united_states"yet stayed two nodes). (verified: 2 nodes in → 2 nodes out, 0 merges.)_create_entity_nodecomputedEntity.id_for(name, type_node.name), but_process_graph_edges/_resolve_nodestill resolve endpoints by name-onlyEntity.id_for(name), so no edge matched a node key and every extracted relationship was dropped in_populate_node_relations. (verified: inter-entity edge count 1 → 0.) It also changed all entity ids (orphaning re-cognified graphs) and other single-argid_forcall sites, with no migration.MergeRecords were computed then discarded;merge_with_existingandContradictionEdgehad no call sites.too many values to unpack (expected 2)in the four delete tests, and the newalias_map=/merge_policy=kwargs break the ~12add_data_pointstests that mock a 2-arg 2-tuple.The rework
canonicalize_entity_name(NFKC + diacritics + lowercase + alias lookup + punctuation/whitespace collapse + leading-article strip) anddefault_aliases.jsonare kept — but used purely as a grouping key, never to mutate a node's display name (added a cached alias-map load).resolve_deterministic_duplicates(nodes)matching the sibling utils (resolve_structural_duplicates,resolve_fuzzy_duplicate_entities): group same-type nodes by canonical key; within each group the longest original name is kept canonical (ties → smaller id) and every other node emits amerged_intoedge (duplicate → canonical) carryingresolution="deterministic_key"and thecanonical_key.add_data_pointsright afterdeduplicate_nodes_and_edges, opt-in viaadd_data_points(..., deterministic_dedup=True)(default off; a disabled run does zero extra work).deduplicate_nodes_and_edgesandexpand_with_nodes_and_edgesare reverted to their dev versions byte-for-byte — no core-algorithm drift, no caller breakage.MergePolicy,MergeRecord/FieldResolution/ContradictionEdge, andmerge_with_existingentirely — dead or broken, and redundant with themerged_intoedge.Net change vs
dev: additive only (canonicalization util + alias map + one new module + a 17-line opt-in hook + tests); the two core-algorithm files are unchanged.Requirements (issue #3627)
merged_intoedge propertiesadd_data_pointswiringOut of scope (deferred)
Retrieval-side consumption of
merged_into(collapsing duplicates at query time), cross-batch / already-persisted duplicate detection, threadingdeterministic_dedupthrough the publiccognify()API, and destructive property-conflict merging (kept reversible via edges instead of a last-write / non-null merge policy) — deferred until siblings A–E are compared on a shared fixture and the best (or a hybrid) is adopted (per #3627's plan). Coordinate the audit trail with #3604.Testing
cognee/tests/unit/modules/graph/test_deterministic_dedup.py,test_canonicalization.py, and the newadd_data_pointswiring tests pass; the fulltest_add_data_points.pysuite stays green (44 passed).merged_intoedges that persist and read back with their properties intact, both pointing at the canonical node.ruff format/ruff checkclean.Type of Change
DCO Affirmation
I affirm that all code in every commit of this pull request conforms to the terms of the Topoteretes Developer Certificate of Origin.
🤖 Generated with Claude Code