4141from collections import Counter , defaultdict
4242from typing import Any , AsyncIterator
4343
44+ from aperag .concurrent_control import MultiLock , get_or_create_lock
45+
4446from .base import (
4547 BaseGraphStorage ,
4648 BaseKVStorage ,
@@ -242,38 +244,86 @@ async def _merge_nodes_then_upsert(
242244 force_llm_summary_on_merge : int ,
243245 lightrag_logger : LightRAGLogger | None = None ,
244246):
245- """Get existing nodes from knowledge graph use name,if exists, merge data, else create, then upsert."""
247+ """
248+ Merge multiple entity nodes with the same name and upsert the result to knowledge graph.
249+
250+ This function handles entity deduplication by:
251+ 1. Retrieving existing entity data from knowledge graph
252+ 2. Merging existing data with new entity data
253+ 3. Determining the final entity properties through aggregation
254+ 4. Optionally using LLM to summarize lengthy descriptions
255+ 5. Upserting the merged entity back to knowledge graph
256+
257+ Args:
258+ entity_name: The name of the entity to merge
259+ nodes_data: List of new entity data dictionaries to merge
260+ knowledge_graph_inst: Knowledge graph storage instance
261+ llm_model_func: LLM function for description summarization
262+ tokenizer: Tokenizer for text processing
263+ llm_model_max_token_size: Maximum token size for LLM input
264+ summary_to_max_tokens: Maximum tokens for summary output
265+ language: Language for LLM summarization
266+ force_llm_summary_on_merge: Threshold for triggering LLM summarization
267+ lightrag_logger: Optional logger instance
268+
269+ Returns:
270+ dict: The merged node data that was upserted
271+ """
272+
273+ # 1. Initialize containers for collecting existing entity data
246274 already_entity_types = []
247275 already_source_ids = []
248276 already_description = []
249277 already_file_paths = []
250278
279+ # 2. Retrieve existing entity from knowledge graph if it exists
251280 already_node = await knowledge_graph_inst .get_node (entity_name )
252281 if already_node :
282+ # 2.1. Collect existing entity type
253283 already_entity_types .append (already_node ["entity_type" ])
284+
285+ # 2.2. Split and collect existing source IDs (multiple IDs separated by GRAPH_FIELD_SEP)
254286 already_source_ids .extend (split_string_by_multi_markers (already_node ["source_id" ], [GRAPH_FIELD_SEP ]))
287+
288+ # 2.3. Split and collect existing file paths (multiple paths separated by GRAPH_FIELD_SEP)
255289 already_file_paths .extend (split_string_by_multi_markers (already_node ["file_path" ], [GRAPH_FIELD_SEP ]))
290+
291+ # 2.4. Collect existing description
256292 already_description .append (already_node ["description" ])
257293
294+ # 3. Merge and determine final entity properties
295+
296+ # 3.1. Determine entity type by frequency count (most common type wins)
258297 entity_type = sorted (
259298 Counter ([dp ["entity_type" ] for dp in nodes_data ] + already_entity_types ).items (),
260299 key = lambda x : x [1 ],
261300 reverse = True ,
262301 )[0 ][0 ]
302+
303+ # 3.2. Merge descriptions with field separator, sorted and deduplicated
263304 description = GRAPH_FIELD_SEP .join (sorted (set ([dp ["description" ] for dp in nodes_data ] + already_description )))
305+
306+ # 3.3. Merge source IDs, deduplicated
264307 source_id = GRAPH_FIELD_SEP .join (set ([dp ["source_id" ] for dp in nodes_data ] + already_source_ids ))
308+
309+ # 3.4. Merge file paths, deduplicated
265310 file_path = GRAPH_FIELD_SEP .join (set ([dp ["file_path" ] for dp in nodes_data ] + already_file_paths ))
266311
267- num_fragment = description .count (GRAPH_FIELD_SEP ) + 1
268- num_new_fragment = len (set ([dp ["description" ] for dp in nodes_data ]))
312+ # 4. Calculate description fragment counts for summarization decision
313+ num_fragment = description .count (GRAPH_FIELD_SEP ) + 1 # Total description fragments
314+ num_new_fragment = len (set ([dp ["description" ] for dp in nodes_data ])) # New unique descriptions
269315
316+ # 5. Handle description summarization if there are multiple fragments
270317 if num_fragment > 1 :
318+ # 5.1. Check if LLM summarization is needed based on fragment count threshold
271319 if num_fragment >= force_llm_summary_on_merge :
320+ # 5.1.1. Log LLM summarization decision
272321 if lightrag_logger :
273322 lightrag_logger .log_entity_merge (entity_name , num_fragment , num_new_fragment , is_llm_summary = True )
274323 else :
275324 logger .info (f"LLM merge N: { entity_name } | { num_new_fragment } +{ num_fragment - num_new_fragment } " )
276325
326+ # 5.1.2. Use LLM to summarize lengthy descriptions
277327 description = await _handle_entity_relation_summary (
278328 entity_name ,
279329 description ,
@@ -285,11 +335,13 @@ async def _merge_nodes_then_upsert(
285335 lightrag_logger ,
286336 )
287337 else :
338+ # 5.2. Simple merge without LLM summarization (fragment count below threshold)
288339 if lightrag_logger :
289340 lightrag_logger .log_entity_merge (entity_name , num_fragment , num_new_fragment , is_llm_summary = False )
290341 else :
291342 logger .info (f"Merge N: { entity_name } | { num_new_fragment } +{ num_fragment - num_new_fragment } " )
292343
344+ # 6. Create final node data structure
293345 node_data = dict (
294346 entity_id = entity_name ,
295347 entity_type = entity_type ,
@@ -298,10 +350,14 @@ async def _merge_nodes_then_upsert(
298350 file_path = file_path ,
299351 created_at = int (time .time ()),
300352 )
353+
354+ # 7. Upsert the merged entity to knowledge graph
301355 await knowledge_graph_inst .upsert_node (
302356 entity_name ,
303357 node_data = node_data ,
304358 )
359+
360+ # 8. Add entity_name to returned data and return the final merged entity
305361 node_data ["entity_name" ] = entity_name
306362 return node_data
307363
@@ -445,6 +501,65 @@ async def _merge_edges_then_upsert(
445501
446502@timing_wrapper ("Merge & Update" )
447503async def merge_nodes_and_edges (
504+ chunk_results : list ,
505+ component : list [str ],
506+ workspace : str ,
507+ knowledge_graph_inst : BaseGraphStorage ,
508+ entity_vdb : BaseVectorStorage ,
509+ relationships_vdb : BaseVectorStorage ,
510+ llm_model_func ,
511+ tokenizer ,
512+ llm_model_max_token_size ,
513+ summary_to_max_tokens ,
514+ addon_params ,
515+ force_llm_summary_on_merge ,
516+ lightrag_logger : LightRAGLogger | None = None ,
517+ ) -> dict [str , int ]:
518+ """Merge nodes and edges from extraction results
519+
520+ Args:
521+ chunk_results: List of (nodes_dict, edges_dict) tuples
522+ component: List of entity names in this component (for locking)
523+ workspace: Workspace identifier for lock creation
524+ knowledge_graph_inst: Knowledge graph storage
525+ entity_vdb: Entity vector database
526+ relationships_vdb: Relationships vector database
527+ llm_model_func: LLM function
528+ tokenizer: Tokenizer
529+ llm_model_max_token_size: Max token size for LLM
530+ summary_to_max_tokens: Max tokens for summary
531+ addon_params: Additional parameters
532+ force_llm_summary_on_merge: Force LLM summary threshold
533+ lightrag_logger: Optional logger
534+
535+ Returns:
536+ Dict with entity_count and relation_count
537+ """
538+
539+ # Create locks for all entities in this component
540+ entity_locks = []
541+ for entity_name in sorted (component ): # Sort to prevent deadlock
542+ lock = get_or_create_lock (f"entity:{ entity_name } :{ workspace } " )
543+ entity_locks .append (lock )
544+
545+ # Use MultiLock to acquire all locks for this component
546+ async with MultiLock (entity_locks ):
547+ return await _merge_nodes_and_edges_impl (
548+ chunk_results ,
549+ knowledge_graph_inst ,
550+ entity_vdb ,
551+ relationships_vdb ,
552+ llm_model_func ,
553+ tokenizer ,
554+ llm_model_max_token_size ,
555+ summary_to_max_tokens ,
556+ addon_params ,
557+ force_llm_summary_on_merge ,
558+ lightrag_logger ,
559+ )
560+
561+
562+ async def _merge_nodes_and_edges_impl (
448563 chunk_results : list ,
449564 knowledge_graph_inst : BaseGraphStorage ,
450565 entity_vdb : BaseVectorStorage ,
@@ -456,8 +571,8 @@ async def merge_nodes_and_edges(
456571 addon_params ,
457572 force_llm_summary_on_merge ,
458573 lightrag_logger : LightRAGLogger | None = None ,
459- ) -> None :
460- """Merge nodes and edges from extraction results """
574+ ) -> dict [ str , int ] :
575+ """Internal implementation of merge_nodes_and_edges """
461576
462577 # Collect all nodes and edges from all chunks
463578 all_nodes = defaultdict (list )
@@ -539,6 +654,10 @@ async def merge_nodes_and_edges(
539654 }
540655 await relationships_vdb .upsert (data_for_vdb )
541656
657+ entity_count = len (entities_data )
658+ relation_count = len (relationships_data )
659+ return {"entity_count" : entity_count , "relation_count" : relation_count }
660+
542661
543662@timing_wrapper ("Entity Extraction" )
544663async def extract_entities (
0 commit comments