Spaces:
Sleeping
Sleeping
Download src/reranking.py from Jky71/doculearn-rag: direct link, hf CLI and curl.
- Browser
- Download file 3.74 kB
-
https://huggingface.co/spaces/Jky71/doculearn-rag/resolve/main/src/reranking.py
- Command line
-
hf download hf://spaces/Jky71/doculearn-rag/src/reranking.py
-
curl -L -o reranking.py https://huggingface.co/spaces/Jky71/doculearn-rag/resolve/main/src/reranking.py
3.74 kB
| from __future__ import annotations | |
| import math | |
| import re | |
| from collections import Counter, defaultdict | |
| from src.schemas import RetrievedChunk | |
| TOKEN_PATTERN: re.Pattern[str] = re.compile(r"\w+", re.UNICODE) | |
| STOP_WORDS = { | |
| "ai", | |
| "bao", | |
| "bị", | |
| "các", | |
| "cho", | |
| "có", | |
| "của", | |
| "để", | |
| "được", | |
| "dùng", | |
| "gì", | |
| "giúp", | |
| "hình", | |
| "khi", | |
| "là", | |
| "mô", | |
| "một", | |
| "nào", | |
| "này", | |
| "như", | |
| "những", | |
| "theo", | |
| "thì", | |
| "trong", | |
| "từ", | |
| "và", | |
| "về", | |
| "với", | |
| } | |
| def tokenize_for_reranking(text: str) -> list[str]: | |
| tokens = (match.group(0) for match in TOKEN_PATTERN.finditer(text.casefold())) | |
| return [ | |
| token | |
| for token in tokens | |
| if len(token) > 1 and token not in STOP_WORDS and not token.isdigit() | |
| ] | |
| def _normalize(values: list[float]) -> list[float]: | |
| if not values: | |
| return [] | |
| low = min(values) | |
| high = max(values) | |
| if high == low: | |
| return [0.0] * len(values) | |
| return [(value - low) / (high - low) for value in values] | |
| def bm25_scores(query: str, chunks: list[RetrievedChunk]) -> list[float]: | |
| if not chunks: | |
| return [] | |
| token_counts = [Counter(tokenize_for_reranking(chunk.text)) for chunk in chunks] | |
| document_frequency = Counter(token for counts in token_counts for token in counts) | |
| query_terms = list(dict.fromkeys(tokenize_for_reranking(query))) | |
| average_length = sum(sum(counts.values()) for counts in token_counts) / len(token_counts) | |
| k1 = 1.5 | |
| b = 0.75 | |
| scores: list[float] = [] | |
| for counts in token_counts: | |
| document_length = sum(counts.values()) | |
| score = 0.0 | |
| for term in query_terms: | |
| frequency = counts[term] | |
| if not frequency: | |
| continue | |
| inverse_document_frequency = math.log( | |
| 1 | |
| + (len(chunks) - document_frequency[term] + 0.5) | |
| / (document_frequency[term] + 0.5) | |
| ) | |
| denominator = frequency + k1 * ( | |
| 1 - b + b * document_length / max(average_length, 1) | |
| ) | |
| score += inverse_document_frequency * frequency * (k1 + 1) / denominator | |
| scores.append(score) | |
| return scores | |
| def rerank_candidates( | |
| query: str, | |
| chunks: list[RetrievedChunk], | |
| *, | |
| k: int, | |
| dense_weight: float = 0.0, | |
| max_chunks_per_page: int | None = 1, | |
| ) -> list[RetrievedChunk]: | |
| if k < 1: | |
| raise ValueError("k must be positive.") | |
| if not 0 <= dense_weight <= 1: | |
| raise ValueError("dense_weight must be between 0 and 1.") | |
| if max_chunks_per_page is not None and max_chunks_per_page < 1: | |
| raise ValueError("max_chunks_per_page must be positive when provided.") | |
| if not chunks: | |
| return [] | |
| dense_scores = _normalize([chunk.score for chunk in chunks]) | |
| lexical_scores = _normalize(bm25_scores(query, chunks)) | |
| combined_scores = [ | |
| dense_weight * dense + (1 - dense_weight) * lexical | |
| for dense, lexical in zip(dense_scores, lexical_scores, strict=True) | |
| ] | |
| ranked = sorted( | |
| zip(combined_scores, chunks, strict=True), | |
| key=lambda item: item[0], | |
| reverse=True, | |
| ) | |
| selected: list[RetrievedChunk] = [] | |
| page_counts: defaultdict[tuple[str, int], int] = defaultdict(int) | |
| for score, chunk in ranked: | |
| page_key = (chunk.metadata.filename, chunk.metadata.page) | |
| if max_chunks_per_page is not None and page_counts[page_key] >= max_chunks_per_page: | |
| continue | |
| page_counts[page_key] += 1 | |
| selected.append(chunk.model_copy(update={"score": score})) | |
| if len(selected) == k: | |
| break | |
| return selected | |