GraphResearcher / app /graph /graph_retrieval_fusion.py
yugbirla's picture
Sync GraphRAG fusion quality cleanup and evaluation files
b7d0804
Raw
History Blame Contribute Delete
5.34 kB
from typing import List, Dict, Any, Optional
from app.graph.graph_guided_retriever import graph_guided_retrieve
def get_value(obj, key: str, default=None):
if isinstance(obj, dict):
return obj.get(key, default)
return getattr(obj, key, default)
def set_value(obj, key: str, value):
if isinstance(obj, dict):
obj[key] = value
return obj
try:
setattr(obj, key, value)
except Exception:
pass
return obj
def normalize_chunk_id(value) -> str:
if value is None:
return ""
return str(value)
def result_chunk_id(result, fallback_index: int) -> str:
chunk_id = (
get_value(result, "chunk_id")
or get_value(result, "id")
or get_value(result, "chunk", None)
)
if chunk_id:
return normalize_chunk_id(chunk_id)
content = (
get_value(result, "content")
or get_value(result, "text")
or ""
)
return f"fallback_{fallback_index}_{hash(content)}"
def convert_graph_result_to_retrieval_result(
graph_result: Dict[str, Any]
) -> Dict[str, Any]:
"""
Converts a graph-guided chunk into a retrieval-like result.
We keep it as a dict because the rest of the pipeline already supports
dict-style results in multiple places.
"""
graph_score = graph_result.get("graph_score", 0.0)
return {
"chunk_id": graph_result.get("chunk_id"),
"content": graph_result.get("text_preview", ""),
"text": graph_result.get("text_preview", ""),
"page_number": graph_result.get("page_number"),
"source_file_name": graph_result.get("source_file_name"),
"score": graph_score,
"retrieval_source": "graph",
"graph_score": graph_score,
"matched_entities": graph_result.get("matched_entities", []),
"matched_relations": graph_result.get("matched_relations", [])
}
def fuse_retrieval_results_with_graph(
document_id: Optional[str],
query: str,
retrieval_results: List[Any],
graph_entity_limit: int = 8,
graph_top_k: int = 5,
final_top_k: int = 8
) -> Dict[str, Any]:
"""
Fuses normal retrieval results with graph-guided chunks.
Strategy:
- Keep normal retrieval results.
- Add graph-guided chunks if they are not already present.
- If same chunk appears in both, mark it as graph-supported and boost score.
"""
normal_results = retrieval_results or []
graph_result = graph_guided_retrieve(
document_id=document_id,
query=query,
graph_entity_limit=graph_entity_limit,
top_k=graph_top_k
)
if graph_result.get("status") != "success":
return {
"fused_results": normal_results[:final_top_k],
"fusion_used": False,
"reason": graph_result.get("message", "Graph retrieval unavailable."),
"graph_retrieval": graph_result,
"normal_count": len(normal_results),
"graph_added_count": 0,
"final_count": len(normal_results[:final_top_k])
}
result_map: Dict[str, Any] = {}
# Add normal retrieval first
for index, item in enumerate(normal_results):
chunk_id = result_chunk_id(item, index)
set_value(item, "retrieval_source", get_value(item, "retrieval_source", "vector_or_hybrid"))
set_value(item, "graph_supported", False)
result_map[chunk_id] = item
graph_added_count = 0
graph_supported_count = 0
for graph_chunk in graph_result.get("results", []):
chunk_id = normalize_chunk_id(graph_chunk.get("chunk_id"))
if not chunk_id:
continue
if chunk_id in result_map:
existing = result_map[chunk_id]
set_value(existing, "graph_supported", True)
set_value(existing, "retrieval_source", "retrieval_and_graph")
set_value(existing, "graph_score", graph_chunk.get("graph_score"))
set_value(existing, "matched_entities", graph_chunk.get("matched_entities", []))
set_value(existing, "matched_relations", graph_chunk.get("matched_relations", []))
old_score = get_value(existing, "score", 0) or 0
try:
boosted_score = float(old_score) + float(graph_chunk.get("graph_score", 0)) * 0.05
set_value(existing, "score", boosted_score)
except Exception:
pass
graph_supported_count += 1
else:
result_map[chunk_id] = convert_graph_result_to_retrieval_result(graph_chunk)
graph_added_count += 1
fused_results = list(result_map.values())
def sort_score(item):
score = get_value(item, "score", 0) or 0
try:
return float(score)
except Exception:
return 0.0
fused_results = sorted(
fused_results,
key=sort_score,
reverse=True
)[:final_top_k]
return {
"fused_results": fused_results,
"fusion_used": True,
"reason": "Normal retrieval results fused with graph-guided chunks.",
"graph_retrieval": graph_result,
"normal_count": len(normal_results),
"graph_added_count": graph_added_count,
"graph_supported_count": graph_supported_count,
"final_count": len(fused_results)
}