Skip to content

Commit 3c4b538

Browse files
committed
fix(core): exclude only the edge's own sources from the weight sum
Follow-up hardening of the edge-weight fix for the reprocess/partial-write path. The weight filter must exclude sources ALREADY reflected in the stored scalar weight. That scalar comes from the graph edge (already_weights), so filter on the edge's own source_ids (already_source_ids) — NOT existing_full_source_ids, which can come from relation_chunks_storage, a SEPARATE store. relation_chunks_storage and the graph edge can diverge on a partial write (chunks upserted, upsert_edge not yet): - chunks present, edge absent -> filtering on chunks recovered the edge with weight 0 (raised in the Codex review of #3373); - chunks AHEAD of the edge -> filtering on chunks skipped a genuinely new source, under-counting the weight. Filtering on already_source_ids keeps the exclusion consistent with the stored scalar in both cases: no stored edge -> nothing excluded (a recovered edge keeps its full weight); a re-fed already-stored source -> dropped (no reprocess double-count). Regression tests: partial-write recovery keeps weight 1 (not 0); a new source re-fed while relation_chunks is ahead of the edge still adds weight (2, not 1); same-source reprocess stays fixed; distinct sources still accumulate.
1 parent cc5d8c4 commit 3c4b538

2 files changed

Lines changed: 124 additions & 2 deletions

File tree

lightrag/operate.py

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2510,12 +2510,23 @@ async def _merge_edges_then_upsert(
25102510
# per reprocess (the #3367 sibling of description accumulation).
25112511
# Genuinely new sources still add their weight, preserving legitimate
25122512
# multi-document growth.
2513-
already_source_id_set = set(existing_full_source_ids)
2513+
#
2514+
# Filter on the EDGE's own source_ids (already_source_ids) — the same
2515+
# source as already_weights — NOT existing_full_source_ids, which can
2516+
# come from relation_chunks_storage, a SEPARATE store. relation_chunks
2517+
# can run ahead of the graph edge on a partial write (chunks upserted,
2518+
# upsert_edge not yet), so filtering on it would wrongly skip a genuinely
2519+
# new source (under-count), or, with no stored weight at all, recover the
2520+
# edge with weight 0. Filtering on already_source_ids keeps the exclusion
2521+
# consistent with the stored scalar: when there is no stored edge
2522+
# (already_source_ids empty) nothing is excluded, so a recovered edge
2523+
# keeps its full weight; a re-fed already-stored source is dropped.
2524+
already_edge_source_set = set(already_source_ids)
25142525
weight = sum(
25152526
[
25162527
dp["weight"]
25172528
for dp in edges_data
2518-
if dp.get("source_id") not in already_source_id_set
2529+
if dp.get("source_id") not in already_edge_source_set
25192530
]
25202531
+ already_weights
25212532
)

tests/extraction/test_summary_description_dedup.py

Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
_handle_entity_relation_summary,
2222
_merge_edges_then_upsert,
2323
_merge_nodes_then_upsert,
24+
make_relation_chunk_key,
2425
)
2526
from lightrag.constants import GRAPH_FIELD_SEP
2627

@@ -257,3 +258,113 @@ def _edge(src: str) -> dict:
257258
await _merge_edges_then_upsert("A", "B", [_edge(src)], g, None, None, cfg)
258259

259260
assert await g.get_edge("A", "B") is not None
261+
262+
263+
class _MemKV:
264+
"""No-op-ish KV that just serves a preset get_by_id."""
265+
266+
def __init__(self, data: dict):
267+
self.data = dict(data)
268+
269+
async def get_by_id(self, key):
270+
return self.data.get(key)
271+
272+
async def upsert(self, data):
273+
self.data.update(data)
274+
275+
276+
@pytest.mark.offline
277+
@pytest.mark.asyncio
278+
async def test_edge_weight_recovers_to_one_when_edge_missing_but_chunks_stored():
279+
"""Partial-write recovery: relation_chunks_storage already has the source but
280+
the graph edge does not exist yet (a crash between the two writes). The weight
281+
filter must NOT treat those sources as already-weighted — already_weights is
282+
empty, so filtering them would recover the edge with weight 0. With no stored
283+
scalar, sum all contributions (weight = 1, not 0)."""
284+
g = _MemGraph()
285+
cfg = _cfg()
286+
for name in ("A", "B"):
287+
await g.upsert_node(
288+
name,
289+
{
290+
"entity_id": name,
291+
"description": name,
292+
"source_id": "c1",
293+
"entity_type": "X",
294+
"file_path": "f",
295+
},
296+
)
297+
rcs = _MemKV({make_relation_chunk_key("A", "B"): {"chunk_ids": ["c1"]}})
298+
await _merge_edges_then_upsert(
299+
"A",
300+
"B",
301+
[
302+
{
303+
"weight": 1.0,
304+
"source_id": "c1",
305+
"description": "rel",
306+
"keywords": "k",
307+
"file_path": "f",
308+
}
309+
],
310+
g,
311+
None,
312+
None,
313+
cfg,
314+
relation_chunks_storage=rcs,
315+
)
316+
edge = await g.get_edge("A", "B")
317+
assert edge is not None
318+
assert edge["weight"] == 1.0
319+
320+
321+
@pytest.mark.offline
322+
@pytest.mark.asyncio
323+
async def test_edge_weight_adds_new_source_when_chunks_store_is_ahead():
324+
"""relation_chunks_storage can run AHEAD of the graph edge (chunks upserted,
325+
edge not yet updated). A genuinely new source re-fed then must still add
326+
weight — the filter uses the EDGE's own source_ids (consistent with the
327+
stored scalar), not the chunk store, so the new source is not wrongly skipped
328+
(which would under-count the weight)."""
329+
g = _MemGraph()
330+
cfg = _cfg()
331+
for name in ("A", "B"):
332+
await g.upsert_node(
333+
name,
334+
{
335+
"entity_id": name,
336+
"description": name,
337+
"source_id": "c1",
338+
"entity_type": "X",
339+
"file_path": "f",
340+
},
341+
)
342+
# Existing edge reflects only c1 (weight 1)...
343+
g.edges[("A", "B")] = {
344+
"weight": 1.0,
345+
"source_id": "c1",
346+
"description": "rel",
347+
"keywords": "k",
348+
"file_path": "f",
349+
}
350+
# ...but relation_chunks already lists c1 AND c2 (ahead of the edge).
351+
rcs = _MemKV({make_relation_chunk_key("A", "B"): {"chunk_ids": ["c1", "c2"]}})
352+
await _merge_edges_then_upsert(
353+
"A",
354+
"B",
355+
[
356+
{
357+
"weight": 1.0,
358+
"source_id": "c2",
359+
"description": "rel",
360+
"keywords": "k",
361+
"file_path": "f",
362+
}
363+
],
364+
g,
365+
None,
366+
None,
367+
cfg,
368+
relation_chunks_storage=rcs,
369+
)
370+
assert (await g.get_edge("A", "B"))["weight"] == 2.0

0 commit comments

Comments
 (0)