fix(L1): обрезать отпечатки равномерно по тексту, а не произвольно
winnow() возвращает set, поэтому list(fp)[:LIMIT] брал случайное подмножество: у длинного документа целые куски оставались без отпечатков, и списывание именно из них не находилось. Обнаружено при разборе того, почему фрагмент статьи PMC не искался. Добавлены winnow_ordered() — отпечатки в порядке появления в тексте, и sample_evenly() — выборка каждого n-го элемента вместо первых N. Применено в add_document и store_full_text. 7 тестов на главное свойство: выборка растянута по всей длине документа, шаг ровный, порядок сохранён. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -85,6 +85,68 @@ def winnow(text: str, k: int = 5, window: int = 4) -> set[int]:
|
||||
return fingerprint
|
||||
|
||||
|
||||
def winnow_ordered(text: str, k: int = 5, window: int = 4) -> list[int]:
|
||||
"""То же, что winnow, но с сохранением порядка появления отпечатков в тексте.
|
||||
|
||||
Нужно там, где отпечатки приходится обрезать по лимиту: множество не хранит
|
||||
порядка, и `list(winnow(text))[:limit]` берёт произвольное подмножество —
|
||||
у длинного документа целые куски остаются без покрытия, и списывание из них
|
||||
не находится. Со списком в порядке текста обрезку можно делать равномерной
|
||||
(см. sample_evenly).
|
||||
|
||||
Args:
|
||||
text: Исходный текст
|
||||
k: Размер k-граммы
|
||||
window: Размер скользящего окна
|
||||
|
||||
Returns:
|
||||
Отпечатки в порядке появления в тексте, без повторов
|
||||
"""
|
||||
tokens = text.lower().split()
|
||||
if len(tokens) < k:
|
||||
return []
|
||||
|
||||
hashes = [hash_ngram(ng) for ng in get_ngrams(tokens, k)]
|
||||
if not hashes:
|
||||
return []
|
||||
|
||||
ordered: list[int] = []
|
||||
seen: set[int] = set()
|
||||
prev_min_idx = -1
|
||||
|
||||
for i in range(len(hashes) - window + 1):
|
||||
window_hashes = hashes[i : i + window]
|
||||
min_val = min(window_hashes)
|
||||
min_idx = i + window_hashes.index(min_val)
|
||||
|
||||
if min_idx != prev_min_idx:
|
||||
if min_val not in seen:
|
||||
seen.add(min_val)
|
||||
ordered.append(min_val)
|
||||
prev_min_idx = min_idx
|
||||
|
||||
return ordered
|
||||
|
||||
|
||||
def sample_evenly(items: list[int], limit: int) -> list[int]:
|
||||
"""Оставить не больше limit элементов, равномерно по всей длине списка.
|
||||
|
||||
Берём каждый n-й элемент, а не первые limit штук: обрезка «с начала»
|
||||
оставила бы без отпечатков весь конец документа.
|
||||
|
||||
Args:
|
||||
items: Отпечатки в порядке текста (winnow_ordered)
|
||||
limit: Максимум отпечатков; 0 или меньше — не ограничивать
|
||||
|
||||
Returns:
|
||||
Подсписок длиной не больше limit, сохраняющий порядок
|
||||
"""
|
||||
if limit <= 0 or len(items) <= limit:
|
||||
return items
|
||||
step = len(items) / limit
|
||||
return [items[int(i * step)] for i in range(limit)]
|
||||
|
||||
|
||||
def jaccard_similarity(fp_a: set[int], fp_b: set[int]) -> float:
|
||||
"""
|
||||
Коэффициент Жаккара для двух fingerprint'ов.
|
||||
|
||||
Reference in New Issue
Block a user