Два статистических метода — TF-IDF и semantic snapshot — находят смысловые связи между текстами без векторных embeddings и без вызовов LLM. Дёшево, прозрачно, воспроизводимо. Для блогов, баз знаний и личных архивов этого хватает на 80% задач.

Что это

TF-IDF — способ определить, какие слова характеризуют документ лучше всего. Метод считает, насколько часто слово встречается внутри конкретного текста и насколько редко — во всём остальном корпусе. Слово, которое много раз повторяется в одной статье, но почти не встречается в других, получает высокий вес. Это и есть «характерный» термин.

Представьте индекс в книге: предметный указатель, где рядом с термином стоит номер страницы. TF-IDF строит похожий индекс автоматически — для каждого документа. Только вместо страниц он взвешивает слова по их «различительной силе».

Semantic snapshot — результат применения TF-IDF ко всему корпусу в конкретный момент времени. Это не алгоритм, а артефакт: матрица весов для каждого документа, словарь значимых терминов и метрики сходства между всеми парами текстов. Снимок можно сохранить, сравнить с предыдущим и увидеть, как менялись темы.

Ключевое правило: начинайте с TF-IDF. И только когда точно знаете, что вам не хватает глубокого понимания смысла (а не лучшей разметки), переходите к embeddings.

Зачем нужно

Современный подход «для смыслового поиска подключите LLM и векторную базу» работает, но на малых и средних корпусах это часто избыточно. TF-IDF даёт 80% пользы за 1% сложности: никаких GPU, никаких векторных баз, никаких токенов на инференс.

  • Подбор «По теме» в конце статьи или страницы.
  • Поиск похожих материалов по своему архиву без векторной базы.
  • Обнаружение дублилей и перекрывающихся текстов.
  • Подсказки кластерной структуры: «эти теги выглядят как один кластер».
  • Карта реальных тем корпуса вместо того, что вы планировали написать.

Как устроено

Идея TF-IDF объясняется без формул. Для каждого документа вы хотите понять, какие слова описывают его лучше всего. Оказывается, это слова, которые одновременно:

  • часто встречаются внутри документа;
  • редко встречаются во всём остальном корпусе.

Именно два условия вместе дают «характерность». Слово «agent» в корпусе про ИИ будет всегда частым — это водяной знак темы. А вот «MCP», которое редко встречается в большинстве статей, но много в одной конкретной, — сильный маркер именно этой статьи.

TF-IDF(слово, документ) =
TF (частота внутри документа)
*
IDF (обратная частота по всему корпусу)

На выходе каждый документ превращается в вектор «слово -> вес». Слово «вектор» здесь не должно пугать. Это просто список: вот слова, вот веса. Никакой магии. Два документа «похожи», если их векторы близки — обычно по cosine similarity (косинусному сходству, числу от 0 до 1, где 1 — полное совпадение).

TF-IDF против embeddings

Embeddings — другой подход к сравнению текстов. В отличие от TF-IDF, он работает не со словами как символами, а со смыслом фраз. Метод получает числовые представления текстов через нейросеть и сравнивает их. Разница существенная:

АспектTF-IDFEmbeddings (LLM)
Что сравниваетСлова как символыСмысл фраз
Нужен ли GPUНетОбычна да
Стоимость на 1000 доковДоли рубляРубли или десятки рублей
ОбъяснимостьПолная: видны конкретные словаНизкая: «этот вектор близок к тому»
Синонимы и перефразыНе ловитИз коробки — главная сила
Опечатки и разные формыВидит как разные словаПонимает
Чувствительность к редким терминамОчень высокаяСредняя, иногда «размывает»
Обновление корпусаПересчитать за секундыНужен реиндекс векторной базы
Работа офлайнДаОбычно нет (API)

TF-IDF «лучше всего» видит характерные термины: названия инструментов, фамилии, аббревиатуры, домены. Это отлично ложится на технический контент. Embeddings лучше видят «про что вообще статья», даже если выбран разный словарь. На практике эти два подхода дополняют друг друга, а не конкурируют. Но начинать стоит с TF-IDF.

Semantic snapshot: «кадр» корпуса

TF-IDF — это рецепт. Semantic snapshot — артефакт, который вы получаете после применения рецепта ко всему корпусу в конкретный момент времени.

Этот снимок содержит:

  • Матрицу TF-IDF: для каждого документа — веса его характерных слов.
  • Словарь: список всех «значимых» терминов корпуса.
  • Метрики сходства: для каждой пары документов — число от 0 до 1.
  • Метаданные: дата сборки, версия, параметры препроцессинга.

Почему именно «снимок». Состав корпуса меняется, и веса слов меняются вместе с ним. Слово «агент» было редким в 2024-м, сейчас «общее». Поэтому snapshot — это кадр на дату. Их можно хранить, сравнивать и видеть динамику тем.

Подготовка текста: грязная сторона метода

Главный секрет хорошего TF-IDF — не в самом алгоритме, а в препроцессинге. Плохие входы — плохой результат.

Чеклист препроцессинга

  • Привести всё к нижнему регистру.
  • Убрать HTML/Markdown-разметку.
  • Убрать стоп-слова («и», «на», «что» и т.д.). Для русского и английского — разные списки.
  • Привести слова к начальной форме (lemmatize). Для русского это критично: «агент», «агенты», «агентов» — для TF-IDF это разные слова без лемматизации.
  • Убрать URLs, e-mails, имена файлов (или токенизировать особыми тегами).
  • Решить, что делать с n-граммами («prompt injection» должен быть одним термином, а не двумя).
  • Отфильтровать слишком редкие термины (min_df) и слишком частые (max_df).

Внимание: не пропускайте лемматизацию для русского. Это самая частая ошибка. Без неё TF-IDF для русского контента работает в разы хуже.

Пример на Python для русского контента

Рабочий пример препроцессинга с лемматизацией и построением semantic snapshot:

import pymorphy3
import re
from sklearn.feature_extraction.text import TfidfVectorizer
from sklearn.metrics.pairwise import cosine_similarity

morph = pymorphy3.MorphAnalyzer()

def preprocess(text: str) -> str:
    text = text.lower()
    text = re.sub(r"http\S+", " ", text)       # убираем URLs
    text = re.sub(r"[^a-zа-яё ]+", " ", text)   # только буквы
    tokens = text.split()
    lemmas = [morph.parse(t)[0].normal_form for t in tokens]
    return " ".join(lemmas)

docs = [preprocess(d) for d in raw_docs]  # raw_docs — ваш корпус
vectorizer = TfidfVectorizer(
    ngram_range=(1, 2),  # и слова, и пары слов
    min_df=2,            # редкие выкидываем
    max_df=0.5,          # слишком частые выкидываем
)
tfidf = vectorizer.fit_transform(docs)
sim = cosine_similarity(tfidf)  # матрица «док-док»

Этого хватает, чтобы получить semantic snapshot всего корпуса в памяти. Для хранения — сбрасываете vectorizer и tfidf-матрицу в файл (например, через joblib.dump).

Когда использовать

СитуацияПодходит / не подходитПочему
Блок «По теме» в конце статьиПодходитTop-N ближайших по cosine similarity, фильтрация «себя» и дублей — работает прямо в build-процессе
Семантический поиск по архивуПодходитЗапрос прогоняется через тот же vectorizer.transform() и ищется по характерным терминам
Поиск дублилейПодходитПары с cosine >= 0.6 — кандидат на дублиль, >= 0.8 — почти точно дубликат
Кластеризация архиваПодходитk-means или иерархическая кластеризация на TF-IDF-векторах даёт реальные рубрики, а не выдуманные
Карта тем и «дыр» в базе знанийПодходитTop-N характерных слов каждого дока: если слово часто в top-N, но не отдельная страница — кандидат в темы
Поиск по смыслу, а не по словамНе подходитTF-IDF не понимает синонимы и перефразы — нужен embeddings-слой

Как это применять на практике

Блок «По теме» в конце статьи

Для текущего документа — находите top-N ближайших по cosine similarity, отфильтровываете «себя» и дубли. Работает прямо встроенным в build-процесс сайта.

Семантический поиск без векторной базы

Запрос пользователя прогоняется через тот же vectorizer.transform() — и ищутся ближайшие документы. Это уже не точный поиск фразы, а поиск по характерным терминам запроса.

Определение «скрытых дублей»

Пары документов с cosine >= 0.6 — хороший кандидат на «это две статьи про одно и то же». При >= 0.8 — почти точно дублиль.

Кластеризация

На основе TF-IDF-векторов можно запустить k-means или иерархическую кластеризацию. Получите реальные рубрики архива, а не те, которые вы придумали. Расхождения с вашей ручной разметкой — ценный сигнал.

Карта тем и дыр

Из TF-IDF легко достать top-N характерных слов каждого документа. Если слово часто встречается в top-N, но не является отдельной страницей или материалом — это кандидат в темы. Идея «дыры в базе знаний»: вы находите темы, о которых реально пишете, но ещё не оформили как отдельный материал.

Как это встраивается в контур

На сайте могут сосуществовать разные подходы к анализу связей между текстами. Ручные теги — слой «как я хочу, чтобы это читалось». TF-IDF и semantic snapshot — слой «какие термины реально доминируют». Извлечение концептов через LLM — слой «какие отношения видит модель». Embeddings — «кто похож по смыслу даже на разном словаре». TF-IDF — самый дешёвый слой: он не «понимает» текст, но отлично видит, что на самом деле часто встречается в корпусе.

СлойЧто делаетСтоимость
Ручные тегиКак я хочу, чтобы это читалосьДёшево, ручной труд
TF-IDF / snapshotКакие термины реально доминируютДоли рубля, секунды
LLM-извлечение концептовКакие концепты и отношения извлекает LLMТокены, API
EmbeddingsКто похож по смыслу даже на разном словареGPU/API, реиндекс

Начинать стоит с первых двух — они самые дешёвые и самые объяснимые.

Ограничения

Ограничения

Что учитывать

Технические границы метода, которые важно знать до внедрения.

Синонимы не ловятся — «ИИ-агент» и «ИИ-робот» для TF-IDF — разные сущности.

Метод работает со словами как символами, не с понятиями. Спасает ручной словарь синонимов или объединение с embeddings-слоем.

Чувствительность к размеру документа — Очень короткие заметки дают шумные векторы — мало характерных слов, веса нестабильны.

Помогают n-граммы и «скользящие окна» для длинных документов, но короткие тексты остаются слабым местом.

Смешивание языков — Не смешивайте русский и английский в одном векторе без обдумывания.

Либо делайте два словаря, либо используйте единый препроцессинг. Иначе стоп-слова одного языка становятся «характерными» для другого.

Объём словаря — Для больших корпусов словарь растёт быстро.

Ограничивайте max_features в TfidfVectorizer, иначе матрица разрастается и замедляет расчёт сходства.

Perplexity-байас в тематическом корпусе — Если вы пишете почти всё про одну тему, доминирующее слово этой темы становится «общим» и выпадает из сильных маркеров.

Спасает sub-domain TF-IDF по рубрикам — отдельный snapshot для каждого раздела.

Антипаттерны

Антипаттерны

Чего не делать

Ошибочные ходы, которые ломают результат TF-IDF.

Пропустить лемматизацию для русского — «Агент», «агенты», «агентов» без лемматизации — три разных слова с тремя разными весами.

Это самая частая ошибка: TF-IDF для русского контента работает в разы хуже без приведения к начальной форме.

Не вычистить «горячие» стоп-слова — Частицы вроде «это», «очень», «как» в русском легко выбиваются в топ, если вы не вычистили их из корпуса.

Стандартные списки стоп-слов не всегда покрывают разговорные частицы.

Сразу запускать embeddings — Современный рефлекс «подключите LLM и векторную базу» работает, но часто это перебор.

На малых и средних корпусах TF-IDF даёт 80% пользы за 1% сложности. Сначала TF-IDF — embeddings только когда точно знаете, что не хватает глубокого смысла.

Смешать всё в один вектор — Если корпус содержит и короткие заметки, и длинные статьи — веса будут несопоставимы.

Либо нормализуйте по длине, либо разделяйте на группы по размеру.

Чеклист

Чеклист

Проверка перед запуском

Конкретные действия, которые нужно выполнить до внедрения TF-IDF в продакшен.

Если вам нужны «похожие материалы» или простой семантический поиск по своему архиву — начните с TF-IDF. Скорее всего, больше ничего добавлять не понадобится ещё долго.

Корпус собран — Тексты экспортированы из источника (Notion, Markdown-файлы, база данных) и доступны как единый массив raw_docs.

Без этого шага расчёт невозможен.

Препроцессинг с лемматизацией — Функция preprocess() написана под ваш язык: нижний регистр, чистка разметки, стоп-слова, лемматизация.

Для русского — pymorphy3 или аналог. Проверьте на 5-10 документах глазами.

Параметры TF-IDF выставлены — ngram_range=(1,2), min_df=2, max_df=0.5 — стартовая конфигурация.

Для вашего корпуса значения могут отличаться. Проверьте размер словаря — если он огромный, ограничьте max_features.

Snapshot сохранён — Vectorizer и tfidf-матрица сброшены в файл (joblib.dump) и лежат в репозитории или на диске.

Дата сборки зафиксирована. Без сохранения пересчёт при каждом запуске убивает смысл «снимка».

Top-5 проверены глазами — Для каждого документа посчитаны top-5 ближайших.

Вы прогнали глазами — результаты имеют смысл, а не случайный шум. Если шум — проблема в препроцессинге, не в алгоритме.

Регулярный rebuild запланирован — Пересборка snapshot по расписанию или по хуку «новая публикация».

Без регулярного обновления снимок устаревает и перестаёт отражать реальный состав корпуса.