-
Notifications
You must be signed in to change notification settings - Fork 105
[FEATURE] Add reranker fallback chains to statement reranking #406
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -20,6 +20,8 @@ | |
| from llama_index.core.schema import QueryBundle, NodeWithScore, TextNode | ||
| from llama_index.core.node_parser import TokenTextSplitter | ||
|
|
||
| from graphrag_toolkit.lexical_graph.retrieval.processors.reranker_chain import normalize_reranker_chain | ||
|
|
||
| logger = logging.getLogger(__name__) | ||
|
|
||
| def default_reranking_source_metadata_fn(source:Source): | ||
|
|
@@ -52,13 +54,25 @@ def format_value(s): | |
| return ', '.join([format_value(v) for v in source.metadata.values()]) | ||
|
|
||
| class RerankStatements(ProcessorBase): | ||
| """ | ||
| """Rerank the final statements in a search result collection. | ||
|
|
||
| ``reranker`` may specify one strategy or an ordered fallback chain. A | ||
| fallback policy is consulted after a scorer raises and before the next | ||
| strategy is attempted. ``None`` or ``'none'`` disables statement reranking; | ||
| entity reranking is an independent TF-IDF stage. | ||
| """ | ||
| def __init__(self, args:ProcessorArgs, filter_config:FilterConfig, reranking_model=None): | ||
| self.reranking_model = reranking_model or GraphRAGConfig.reranking_model | ||
| super().__init__(args, filter_config) | ||
| self.reranking_source_metadata_fn = self.args.reranking_source_metadata_fn or default_reranking_source_metadata_fn | ||
| self.reranker_chain = normalize_reranker_chain(self.args.reranker) | ||
| fallback_policy = getattr(self.args, 'reranker_fallback_policy', None) | ||
| if fallback_policy is not None and not callable(fallback_policy): | ||
| raise TypeError( | ||
| f'reranker_fallback_policy must be a callable accepting ' | ||
| f'(reranker, *, error=None), got {type(fallback_policy).__name__}' | ||
| ) | ||
| self.reranker_fallback_policy = fallback_policy | ||
|
|
||
| def _score_values_with_tfidf(self, values:List[str], query:QueryBundle, entity_contexts:EntityContexts): | ||
| """ | ||
|
|
@@ -205,13 +219,80 @@ def rerank_text(text_query, text_sources, num_results, model_package_arn): | |
| for result in results | ||
| } | ||
|
|
||
| def _should_fallback(self, reranker:str, *, error=None) -> bool: | ||
| if self.reranker_fallback_policy is None: | ||
| return True | ||
|
|
||
| try: | ||
| return bool(self.reranker_fallback_policy(reranker, error=error)) | ||
| except Exception: | ||
| logger.error( | ||
| f'reranker_fallback_policy raised an exception for reranker ' | ||
| f'"{reranker}"; re-raising', | ||
| exc_info=True | ||
| ) | ||
| raise | ||
|
|
||
| def _score_values_with_chain(self, values:List[str], query:QueryBundle, entity_contexts:EntityContexts): | ||
| """Score values with the first successful statement reranker. | ||
|
|
||
| Raised exceptions may advance to the next strategy when the fallback | ||
| policy permits. A returned empty score map is a successful, terminal | ||
| response: it is logged at error level and returned without consulting | ||
| the fallback policy. Once reached, ``'none'`` performs no scoring and | ||
| returns ``None``. | ||
| """ | ||
| reranker_chain = self.reranker_chain | ||
| if not reranker_chain: | ||
| return None | ||
|
|
||
| reranker_registry = { | ||
| 'model': self._score_values, | ||
| 'tfidf': self._score_values_with_tfidf, | ||
| 'bedrock': self._score_values_with_bedrock | ||
| } | ||
|
|
||
| for i, reranker in enumerate(reranker_chain): | ||
| has_next = i < len(reranker_chain) - 1 | ||
|
|
||
| if reranker == 'none': | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. better to use None here and check false-y values |
||
| return None | ||
|
|
||
| if reranker not in reranker_registry: | ||
| # Only the unvalidated legacy single-string form can reach here. | ||
| logger.warning(f'Unknown reranker "{reranker}"; skipping reranking') | ||
| return None | ||
|
|
||
| try: | ||
| scored_values = reranker_registry[reranker](values, query, entity_contexts) | ||
| except Exception as e: | ||
| if not has_next: | ||
| raise | ||
| if not self._should_fallback(reranker, error=e): | ||
| raise | ||
| next_reranker = reranker_chain[i + 1] | ||
| logger.warning( | ||
| f'Reranking with {reranker} failed ({type(e).__name__}); ' | ||
| f'trying {next_reranker}', | ||
| exc_info=True | ||
| ) | ||
| continue | ||
|
|
||
| if not scored_values: | ||
| logger.error(f'Reranking with {reranker} returned an empty score map; all statements will be dropped') | ||
| else: | ||
| logger.debug(f'Reranking succeeded with {reranker}') | ||
|
|
||
| return scored_values | ||
|
|
||
| def _process_results(self, search_results:SearchResultCollection, query:QueryBundle) -> SearchResultCollection: | ||
| """ | ||
| Processes search results by reranking statements within each topic based on their relevance scores. | ||
| Rerank statements within each topic based on their relevance scores. | ||
|
|
||
| This method processes a set of search results, and if a reranking approach is specified, | ||
| it calculates scores for the statements within topics using the supplied query and entities. | ||
| It then reranks statements based on the computed scores and updates the search results accordingly. | ||
| The configured chain applies only to this final statement stage. Entity | ||
| reranking occurs independently with TF-IDF. A score map is terminal even | ||
| when empty; an empty map therefore drops every statement without trying | ||
| another fallback. | ||
|
|
||
| Args: | ||
| search_results (SearchResultCollection): A collection of search results that contain topics | ||
|
|
@@ -224,9 +305,14 @@ def _process_results(self, search_results:SearchResultCollection, query:QueryBun | |
| topic are reranked based on the relevance scores computed by the specified reranking method. | ||
|
|
||
| Raises: | ||
| Any exception raised during the reranking process will propagate to the caller. | ||
| Exceptions raised by the final (or only) reranker propagate to the | ||
| caller. Earlier rerankers fall back when the configured policy | ||
| permits it (by default, on any exception). Reaching a final | ||
| ``'none'`` also requires the policy to permit the preceding failure; | ||
| ``'none'`` itself performs no scoring and cannot raise. | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. also accepts 'None' |
||
| """ | ||
| if not self.args.reranker or self.args.reranker.lower() == 'none': | ||
| reranker_chain = self.reranker_chain | ||
| if not reranker_chain or reranker_chain[0] == 'none': | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. also accepts 'None' |
||
| return search_results | ||
|
|
||
| values_to_score = [] | ||
|
|
@@ -245,16 +331,13 @@ def _process_results(self, search_results:SearchResultCollection, query:QueryBun | |
|
|
||
| start = time.time() | ||
|
|
||
| scored_values = None | ||
|
|
||
| reranker = self.args.reranker.lower() | ||
| if reranker == 'model': | ||
| scored_values = self._score_values(values_to_score, query, search_results.entity_contexts) | ||
| elif reranker == 'tfidf': | ||
| scored_values = self._score_values_with_tfidf(values_to_score, query, search_results.entity_contexts) | ||
| elif reranker == 'bedrock': | ||
| scored_values = self._score_values_with_bedrock(values_to_score, query, search_results.entity_contexts) | ||
| else: | ||
| scored_values = self._score_values_with_chain( | ||
| values_to_score, | ||
| query, | ||
| search_results.entity_contexts | ||
| ) | ||
|
|
||
| if scored_values is None: | ||
| return search_results | ||
|
|
||
| end = time.time() | ||
|
|
@@ -268,21 +351,7 @@ def _process_results(self, search_results:SearchResultCollection, query:QueryBun | |
| logger.debug('Scored values:\n' + '\n--------------\n'.join([str(scored_value) for scored_value in scored_values.items()])) | ||
|
|
||
| def rerank_statements(topic:Topic, source_str:str): | ||
| """ | ||
| Represents a processor that reranks statements within topics based on their scores. | ||
|
|
||
| This class is used to process and reorder statements in a `SearchResultCollection` | ||
| object using scores associated with the statements. Statements are filtered and | ||
| sorted in descending order by their scores. | ||
|
|
||
| Attributes: | ||
| No attributes are defined directly for this class. | ||
|
|
||
| Methods: | ||
| _process_results: Processes the search results to rerank the statements based | ||
| on their scores. | ||
|
|
||
| """ | ||
| """Apply scores to one topic, dropping statements without scores.""" | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. please revert and update |
||
| topic_str = topic.topic | ||
| surviving_statements = [] | ||
| for statement in topic.statements: | ||
|
|
@@ -295,24 +364,8 @@ def rerank_statements(topic:Topic, source_str:str): | |
| return topic | ||
|
|
||
| def rerank_search_result(index:int, search_result:SearchResult): | ||
| """ | ||
| Re-ranks search results based on specific criteria and applies modifications to their | ||
| topics using a metadata source function and a specified reranking method. This | ||
| processor works by overriding the `_process_results` method and provides the | ||
| necessary mechanisms to handle individual search results in a collection. | ||
|
|
||
| Methods: | ||
| _process_results: Processes a collection of search results and applies the reranking | ||
| to individual entries based on the provided query. | ||
|
|
||
| Args: | ||
| index (int): Position index of the search result to rerank within the collection. | ||
| search_result (SearchResult): Individual search result object containing the | ||
| data to be reranked. | ||
| """ | ||
| """Apply statement reranking to one search result.""" | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. please revert and update |
||
| source_str = self.reranking_source_metadata_fn(search_result.source) | ||
| return self._apply_to_topics(search_result, rerank_statements, source_str=source_str) | ||
|
|
||
| return self._apply_to_search_results(search_results, rerank_search_result) | ||
|
|
||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,69 @@ | ||
| # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. | ||
| # SPDX-License-Identifier: Apache-2.0 | ||
|
|
||
| """Statement reranker chain configuration and fallback policies. | ||
|
|
||
| A reranker chain is an ordered list of reranking strategies. Processors such as | ||
| ``RerankStatements`` try each strategy in turn, consulting a fallback policy to | ||
| decide whether a failure should be recovered by moving to the next strategy. | ||
| Entity reranking is a separate TF-IDF stage and is not controlled by this chain. | ||
|
|
||
| A fallback policy is a callable ``policy(reranker, *, error=None) -> bool``, | ||
| consulted when a strategy raises an exception and a next strategy remains. | ||
| When no policy is configured, the chain falls back on any exception. An | ||
| exception from the final (or only) strategy always propagates. Before any next | ||
| entry is reached, including ``'none'``, the preceding failure must be permitted | ||
| by the fallback policy. Once reached, ``'none'`` performs no scoring and cannot | ||
| raise. Fallback is per-query and stateless: a strategy that failed on one query | ||
| is attempted again on the next, so transient conditions such as throttling | ||
| recover automatically once they clear server-side. | ||
| """ | ||
|
|
||
| KNOWN_RERANKERS = ( | ||
| 'model', | ||
| 'tfidf', | ||
| 'bedrock', | ||
| 'none', | ||
| ) | ||
|
|
||
|
|
||
| def normalize_reranker_chain(reranker): | ||
| """Normalize a statement reranker configuration into strategy names. | ||
|
|
||
| Any falsy value returns an empty chain before type validation. Strings are | ||
| stripped and lowercased. Lists are stripped, lowercased, and emptied of | ||
| blank entries; their names and placement of ``'none'`` are validated. The | ||
| legacy single-string form is not validated against ``KNOWN_RERANKERS``, | ||
| preserving the historical silently-skip behaviour for unknown names. | ||
|
|
||
| Raises: | ||
| TypeError: If a truthy value is neither a string nor a list. | ||
| ValueError: If a list contains a non-string entry, an unknown strategy, | ||
| or ``'none'`` anywhere except the final position. | ||
| """ | ||
| if not reranker: | ||
| return [] | ||
|
|
||
| if isinstance(reranker, str): | ||
| return [reranker.strip().lower()] if reranker.strip() else [] | ||
|
|
||
| if not isinstance(reranker, list): | ||
| raise TypeError( | ||
| f'reranker must be a string or list of strings, got {type(reranker).__name__}' | ||
| ) | ||
|
|
||
| for r in reranker: | ||
| if not isinstance(r, str): | ||
| raise ValueError( | ||
| f'reranker list entries must be strings, got {type(r).__name__}: {r!r}' | ||
| ) | ||
| chain = [r.strip().lower() for r in reranker if r.strip()] | ||
| unknown = [r for r in chain if r not in KNOWN_RERANKERS] | ||
| if unknown: | ||
| raise ValueError( | ||
| f'Unknown reranker(s) in chain: {unknown}. ' | ||
| f'Expected values from: {list(KNOWN_RERANKERS)}' | ||
| ) | ||
| if 'none' in chain[:-1]: | ||
| raise ValueError('none can only be used alone or as the final fallback in a reranker chain') | ||
| return chain |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think it would be best if we can take advantage of the
Nonetype to identify the end of the list. Then we don't need to add a robust String checked (none or None or NONE or Nil, etc...)