|
| 1 | +# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. |
| 2 | +# SPDX-License-Identifier: Apache-2.0 |
| 3 | + |
| 4 | +""" |
| 5 | +RerankTopics processor. |
| 6 | +
|
| 7 | +Follows the same convention as RerankStatements: the reranking strategy is selected by a |
| 8 | +string arg (``ProcessorArgs.topic_reranker`` -> 'tfidf' | 'bedrock' | 'none') and |
| 9 | +dispatched here, rather than via ad-hoc flags. Where RerankStatements reranks statements |
| 10 | +*within* a topic, this processor reranks whole topics by scoring each topic's |
| 11 | +``name + all its statements`` against the query, keeps the top ``max_topics``, and drops the |
| 12 | +rest before the token-budget truncation runs. |
| 13 | +
|
| 14 | +It also propagates each surviving topic's relevance score down to any statement that does not |
| 15 | +already carry a score. This lets the statement reranker be turned off (``reranker='none'``) |
| 16 | +so that statement selection is driven purely by topic relevance -- the ablation that answers |
| 17 | +"if a topic ranks high by the reranker, is statement-level tfidf still needed?". |
| 18 | +""" |
| 19 | +import logging |
| 20 | +import time |
| 21 | +from typing import List, Dict |
| 22 | + |
| 23 | +import boto3 |
| 24 | + |
| 25 | +from graphrag_toolkit.lexical_graph import GraphRAGConfig |
| 26 | +from graphrag_toolkit.lexical_graph.retrieval.processors.processor_base import ProcessorBase |
| 27 | +from graphrag_toolkit.lexical_graph.retrieval.processors.processor_args import ProcessorArgs |
| 28 | +from graphrag_toolkit.lexical_graph.retrieval.model import Topic |
| 29 | +from graphrag_toolkit.lexical_graph.utils.reranker_utils import score_values_with_tfidf |
| 30 | +from graphrag_toolkit.lexical_graph.metadata import FilterConfig |
| 31 | + |
| 32 | +from llama_index.core.schema import QueryBundle |
| 33 | + |
| 34 | +logger = logging.getLogger(__name__) |
| 35 | + |
| 36 | + |
| 37 | +class RerankTopics(ProcessorBase): |
| 38 | + """Reranks and prunes whole topics by query relevance, using the strategy named in |
| 39 | + ``ProcessorArgs.topic_reranker``. No-op when topic_reranker is unset/'none'.""" |
| 40 | + |
| 41 | + def __init__(self, args: ProcessorArgs, filter_config: FilterConfig): |
| 42 | + super().__init__(args, filter_config) |
| 43 | + |
| 44 | + def _topic_text(self, topic: Topic) -> str: |
| 45 | + stmts = ' '.join([s.statement_str or '' for s in topic.statements]) |
| 46 | + return (f'{topic.topic}\n{stmts}')[:4000] # cap per-doc length for the reranker |
| 47 | + |
| 48 | + def _score_with_tfidf(self, texts: List[str], query: QueryBundle) -> List[float]: |
| 49 | + scored = score_values_with_tfidf(texts, [query.query_str], len(texts)) |
| 50 | + return [scored.get(t, 0.0) for t in texts] |
| 51 | + |
| 52 | + def _score_with_bedrock(self, texts: List[str], query: QueryBundle) -> List[float]: |
| 53 | + region = boto3.Session().region_name or getattr(GraphRAGConfig, 'aws_region', None) or 'us-east-1' |
| 54 | + client = boto3.client('bedrock-agent-runtime', region_name=region) |
| 55 | + model_arn = f'arn:aws:bedrock:{region}::foundation-model/{GraphRAGConfig.bedrock_reranking_model}' |
| 56 | + sources = [{'type': 'INLINE', 'inlineDocumentSource': {'type': 'TEXT', 'textDocument': {'text': t}}} |
| 57 | + for t in texts] |
| 58 | + resp = client.rerank( |
| 59 | + queries=[{'type': 'TEXT', 'textQuery': {'text': query.query_str}}], |
| 60 | + sources=sources, |
| 61 | + rerankingConfiguration={ |
| 62 | + 'type': 'BEDROCK_RERANKING_MODEL', |
| 63 | + 'bedrockRerankingConfiguration': { |
| 64 | + 'numberOfResults': len(texts), |
| 65 | + 'modelConfiguration': {'modelArn': model_arn}, |
| 66 | + }, |
| 67 | + }, |
| 68 | + ) |
| 69 | + scores = [0.0] * len(texts) |
| 70 | + for r in resp['results']: |
| 71 | + scores[r['index']] = r['relevanceScore'] |
| 72 | + return scores |
| 73 | + |
| 74 | + def _process_results(self, search_results, query: QueryBundle): |
| 75 | + mode = (self.args.topic_reranker or 'none').lower() |
| 76 | + if mode == 'none': |
| 77 | + return search_results |
| 78 | + |
| 79 | + # Flatten (result_index, topic) pairs and build one document per topic. |
| 80 | + pairs = [] |
| 81 | + texts = [] |
| 82 | + for ri, sr in enumerate(search_results.results): |
| 83 | + for topic in sr.topics: |
| 84 | + pairs.append((ri, topic)) |
| 85 | + texts.append(self._topic_text(topic)) |
| 86 | + if not texts: |
| 87 | + return search_results |
| 88 | + |
| 89 | + start = time.time() |
| 90 | + if mode == 'tfidf': |
| 91 | + scores = self._score_with_tfidf(texts, query) |
| 92 | + elif mode == 'bedrock': |
| 93 | + scores = self._score_with_bedrock(texts, query) |
| 94 | + else: |
| 95 | + logger.warning(f'Unknown topic_reranker "{mode}", skipping topic rerank') |
| 96 | + return search_results |
| 97 | + logger.debug(f'Topic rerank ({mode}) of {len(texts)} topics: {(time.time()-start)*1000:.0f}ms') |
| 98 | + |
| 99 | + # Rank topics globally; keep the top max_topics. |
| 100 | + order = sorted(range(len(pairs)), key=lambda i: scores[i], reverse=True) |
| 101 | + keep = set(order[: self.args.max_topics]) |
| 102 | + |
| 103 | + kept_topics_by_result: Dict[int, List[Topic]] = {} |
| 104 | + for i, (ri, topic) in enumerate(pairs): |
| 105 | + if i not in keep: |
| 106 | + continue |
| 107 | + # Propagate topic relevance to statements that have no score yet, so selection can |
| 108 | + # run on topic relevance alone when the statement reranker is disabled. |
| 109 | + for stmt in topic.statements: |
| 110 | + if stmt.score is None or stmt.score == 0.0: |
| 111 | + stmt.score = scores[i] |
| 112 | + kept_topics_by_result.setdefault(ri, []).append(topic) |
| 113 | + |
| 114 | + # Rebuild results, dropping topics (and now-empty sources) that did not survive. |
| 115 | + new_results = [] |
| 116 | + for ri, sr in enumerate(search_results.results): |
| 117 | + kept = kept_topics_by_result.get(ri) |
| 118 | + if not kept: |
| 119 | + continue |
| 120 | + sr.topics = kept |
| 121 | + new_results.append(sr) |
| 122 | + search_results = search_results.with_new_results(results=new_results) |
| 123 | + return search_results |
0 commit comments