Как вы дедуплицируете документы перед индексацией в RAG?

Краткий тезис

Дедупликация документов — это этап очистки данных перед индексацией, который удаляет или помечает дубликаты (точные и семантические). В RAG дубликаты могут искажать retrieval (много одинаковых чанков снижают разнообразие контекста) и увеличивать размер индекса. Используются четыре подхода: точное хэширование (SHA-256) для exact duplicates, MinHash + LSH для near-duplicates, векторная кластеризация на основе эмбеддингов и LLM-проверка для семантических рерайтов. Лучшая практика — не удалять дубликаты полностью, а помечать их в метаданных, чтобы RAG-система могла учитывать множественные источники.


1. Термин: Дедупликация документов

Дедупликация (deduplication) — процесс поиска и устранения повторяющихся или почти повторяющихся документов в корпусе. В контексте RAG дедупликация выполняется перед этапом chunking и индексации, чтобы:

  • Уменьшить размер векторной базы данных (экономия памяти и ускорение поиска).
  • Избежать «зашумления» контекста: если LLM получает несколько одинаковых чанков, она может повторять одну и ту же информацию, упуская другие релевантные фрагменты.
  • Повысить качество retrieval: дубликаты могут искусственно завышать метрики (например, recall@k), но не добавляют новой информации.

Важно различать точные дубликаты (exact duplicates) — документы, идентичные по содержанию, и почти дубликаты (near-duplicates) — документы, которые отличаются незначительно (например, перефразирование, разные форматы, опечатки).


2. Точные дубликаты (Exact duplicates)

Самый простой и быстрый метод — вычислить криптографический хэш содержимого документа (например, SHA-256) и сравнить хэши. Если хэши совпадают — документы идентичны.

Почему SHA-256

  • Детерминированность: одинаковый текст → одинаковый хэш.
  • Высокая скорость: хэширование даже больших документов занимает микросекунды.
  • Коллизионная стойкость: вероятность случайного совпадения хэшей разных документов пренебрежимо мала.

Пример реализации на Python

import hashlib

def compute_hash(text: str) -> str:
    return hashlib.sha256(text.encode('utf-8')).hexdigest()

def deduplicate_exact(documents: list[dict]) -> list[dict]:
    seen_hashes = set()
    unique_docs = []
    for doc in documents:
        doc_hash = compute_hash(doc['content'])
        if doc_hash not in seen_hashes:
            seen_hashes.add(doc_hash)
            unique_docs.append(doc)
    return unique_docs

Ограничения

  • Не обнаруживает документы, отличающиеся пробелами, знаками препинания или регистром (можно предварительно нормализовать текст).
  • Не работает для перефразированных версий.

3. Почти дубликаты (Near-duplicates) с MinHash + LSH

Для обнаружения документов, которые не идентичны, но очень похожи (например, одна новость с небольшими изменениями), используется MinHash (Minwise Hashing) в сочетании с LSH (Locality Sensitive Hashing).

3.1 Jaccard similarity

Мера сходства между двумя множествами (например, множествами шинглов — n-грамм слов или символов):

[ J(A, B) = \frac{|A \cap B|}{|A \cup B|} ]

Для текстов шинглы — это последовательности из k слов (обычно k=3..5). Два документа считаются near-duplicates, если Jaccard similarity превышает порог (например, 0.8).

3.2 MinHash

MinHash — это техника, которая позволяет оценить Jaccard similarity без попарного сравнения всех шинглов. Идея:

  • Представить каждый документ как множество шинглов.
  • Применить h хэш-функций к каждому шинглу и для каждой функции записать минимальное хэш-значение. Получится сигнатура документа из h чисел.
  • Вероятность того, что сигнатуры двух документов совпадут по позиции, равна Jaccard similarity.

3.3 LSH (Locality Sensitive Hashing)

LSH группирует похожие сигнатуры в «бакеты» (buckets). Для этого сигнатура разбивается на b полос (bands) по r строк в каждой. Если хотя бы в одной полосе сигнатуры двух документов полностью совпадают, они помещаются в один бакет и считаются кандидатами на near-duplicate.

Параметры

  • Порог Jaccard similarity (обычно 0.7–0.9).
  • Количество полос b и строк r: чем больше b, тем выше чувствительность, но больше ложных срабатываний.

Пример с библиотекой datasketch

from datasketch import MinHash, MinHashLSH

def create_minhash(text: str, num_perm=128):
    m = MinHash(num_perm=num_perm)
    for shingle in [text[i:i+3] for i in range(len(text)-2)]:  # 3-граммы символов
        m.update(shingle.encode('utf-8'))
    return m

lsh = MinHashLSH(threshold=0.8, num_perm=128)
for i, doc in enumerate(documents):
    m = create_minhash(doc['content'])
    lsh.insert(f"doc_{i}", m)

# Поиск дубликатов для нового документа
candidates = lsh.query(create_minhash(new_doc))

Преимущества

  • Масштабируемость: работает для миллионов документов.
  • Не требует попарного сравнения.

Недостатки

  • Чувствителен к выбору шинглов и порога.
  • Может пропускать семантически похожие, но лексически разные тексты.

4. Векторная дедупликация (эмбеддинги + кластеризация)

Этот метод использует эмбеддинги документов (векторные представления, полученные из языковой модели, например, text-embedding-3-small). Документы, близкие в векторном пространстве (по косинусной близости), считаются дубликатами.

Алгоритм

  1. Получить эмбеддинг для каждого документа.
  2. Применить алгоритм кластеризации (например, DBSCAN или Agglomerative Clustering) с порогом расстояния.
  3. В каждом кластере оставить один документ — ближайший к центроиду кластера (среднему вектору).

Пример:

from sklearn.cluster import DBSCAN
from sklearn.metrics.pairwise import cosine_distances
import numpy as np

embeddings = get_embeddings(documents)  # shape (N, D)
dist_matrix = cosine_distances(embeddings)
clustering = DBSCAN(eps=0.2, min_samples=2, metric='precomputed')
labels = clustering.fit_predict(dist_matrix)

unique_docs = []
for label in set(labels):
    if label == -1:  # шум (уникальные документы)
        continue
    cluster_indices = np.where(labels == label)[0]
    # Найти центроид
    centroid = embeddings[cluster_indices].mean(axis=0)
    closest_idx = cluster_indices[np.argmin(cosine_distances([centroid], embeddings[cluster_indices]))]
    unique_docs.append(documents[closest_idx])

Преимущества

  • Улавливает семантическую близость (например, «кошка» и «кот»).
  • Не требует ручного выбора шинглов.

Недостатки

  • Вычислительно дорого (эмбеддинги + кластеризация).
  • Порог расстояния подбирается эмпирически.
  • Может ошибочно объединять документы на разные темы, если эмбеддинги плохие.

5. Семантическая дедупликация с помощью LLM

Самый точный, но и самый дорогой метод — использовать LLM (например, GPT-4) для проверки, является ли документ рерайтом другого.

Промпт (пример):

Определи, является ли следующий документ рерайтом (перефразированием) первого документа. Ответь только "да" или "нет".

Документ 1: {text1}
Документ 2: {text2}

Применение

  • После грубой фильтрации (MinHash или векторной) для финальной верификации.
  • Для небольших корпусов или критически важных данных (юридические, медицинские).

Недостатки

  • Высокая стоимость (токены).
  • Задержка (latency).
  • Зависимость от качества LLM (галлюцинации).

6. Сравнение методов

МетодСкоростьТочностьОбнаруживаетКогда использовать
SHA-256⚡⚡⚡⚡⚡⚡⚡⚡⚡⚡Точные копииВсегда как первый этап
MinHash + LSH⚡⚡⚡⚡⚡⚡⚡Лексически похожие (near-duplicates)Большие корпуса (миллионы документов)
Векторная кластеризация⚡⚡⚡⚡⚡⚡Семантически похожиеСредние корпуса, когда важна семантика
LLM-проверка⚡⚡⚡⚡⚡Рерайты, сложные перефразированияФинальная верификация, малые корпуса

7. Компромиссы и best practices

Почему не стоит удалять все дубликаты?
В RAG дубликаты могут быть полезны:

  • Один и тот же факт из разных источников повышает faithfulness (LLM видит подтверждение).
  • Если удалить все дубликаты, можно потерять контекст (например, разные формулировки одного закона).

Рекомендация

  • Помечать дубликаты в метаданных (поле is_duplicate, duplicate_group_id).
  • При retrieval можно либо исключать помеченные документы, либо оставлять, но передавать LLM с пометкой «этот факт подтверждён несколькими источниками».
  • Для агентных RAG (Agentic RAG) агент может динамически решать, какие дубликаты включить в контекст, анализируя их полезность.

Влияние на метрики retrieval

  • Удаление дубликатов может снизить recall@k (если дубликаты были релевантны), но повысить precision и разнообразие контекста.
  • Рекомендуется тестировать оба варианта (с удалением и без) на валидационном наборе.

8. Пет-проект для закрепления

Задача Реализовать пайплайн дедупликации для корпуса новостных статей (1000 документов) с использованием двух методов: MinHash + LSH и векторной кластеризации. Сравнить результаты.

Инструменты

  • Python, datasketch, sentence-transformers (для эмбеддингов), scikit-learn.
  • Датасет: ag_news (или любой набор текстов).

Шаги:

  1. Загрузить датасет, нормализовать текст (нижний регистр, удаление пунктуации).
  2. Вычислить MinHash-сигнатуры (шинглы по 3 символа) и построить LSH-индекс с порогом 0.8.
  3. Для каждого документа найти кандидатов в дубликаты.
  4. Получить эмбеддинги (например, all-MiniLM-L6-v2) и выполнить DBSCAN (eps=0.3).
  5. Сравнить множества дубликатов, найденных двумя методами.
  6. Визуализировать распределение размеров кластеров.
  7. Выбрать стратегию: удалить или пометить дубликаты, и оценить влияние на retrieval (например, hit rate@5) на тестовых запросах.

Ожидаемый результат

  • Код пайплайна с комментариями.
  • Таблица сравнения: количество найденных дубликатов, время выполнения, пересечение методов.
  • Вывод: какой метод лучше подходит для данного корпуса и почему.

9. Связь с другими вопросами


10. Навигация


Навигация