База знаний

Semantic snapshot и TF-IDF — как искать смысловые связи без LLM и embeddings

Простыми словами про TF-IDF и semantic snapshot: как построить «kарту смыслов» для корпуса текстов без LLM и векторных embeddings, и как это живёт рядом с ручными тегами и графом сайта.

Опубликовано

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

📌
Коротко: вы не «понимаете смысл» в строгом смысле слова. Вы строите для каждого документа отпечаток из «самых характерных слов» (TF-IDF) и сравниваете документы по этим отпечаткам. Semantic snapshot — это результат такого расчёта по всему корпусу: срез «кто похож на кого» на конкретный момент времени.

Зачем это нужно

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

Реальные задачи, где TF-IDF и semantic snapshot работают:

  • Подбор «По теме» в конце статьи.
  • «Похожие материалы» на странице.
  • Нахождение дублей и перекрывающихся текстов.
  • Подсказки «эти теги выглядят как один кластер».
  • Быстрый семантический поиск по своему архиву.
  • Карта «какие темы у меня действительно есть» вместо «какие я хочу».
💡
Ключевое правило: начинайте с TF-IDF. И только когда вы точно знаете, что вам не хватает именно «глубокого смысла» (а не лучшей разметки), — переходите к embeddings.

Идея на пальцах

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

  1. Часто встречаются внутри документа.
  2. Редко встречаются во всём остальном корпусе.

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

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

На выходе каждый документ превращается в вектор «слово → вес». Два документа «похожи», если их векторы близки (обычно по cosine similarity).

💡
Слово «вектор» здесь не должно пугать. Это просто список «вот слова, вот веса». Никакой магии.

TF-IDF против embeddings — в чём разница

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

Что из этого следует:

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

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

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

Этот snapshot содержит:

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

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


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

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

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

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

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

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).


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

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

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

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

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

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

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

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

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

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

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


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

На сайте уже есть ручной граф (теги, graph tags, внутренние ссылки) и есть «автоматический» взгляд через Graphify. TF-IDF/semantic snapshot — это третий, самый дешёвый слой: он не «понимает» текст, но отлично видит «что на самом деле часто говорят» в корпусе.

Notion image

Каждый слой решает своё:

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

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


Ограничения и грабли

  • Синонимы не ловятся. «ИИ-агент» и «ИИ-робот» для TF-IDF — разные сущности. Спасает ручной словарь «маппингов» или объединение с embeddings.
  • Чувствительность к размеру дока. Очень короткие заметки дают шумные векторы. Помогают n-граммы и «скользящие окна» для длинных.
  • Языки. Не смешивайте русский и английский в одном векторе без обдумывания — либо делайте два словаря, либо используйте единый препроцессинг.
  • Объём словаря. Для больших корпусов словарь растёт быстро. Ограничивайте max_features.
  • «Горячие» стоп-слова. Частицы вроде «это», «очень», «как» в русском легко выбиваются в топ, если вы не вычистили их.
  • Сильный perplexity-баиас в личном корпусе. Если вы пишете почти всё о контент-пайплайнах — слово «пайплайн» будет «общим». И выпадет из сильных маркеров, хотя именно он описывает ваш корпус лучше всего. Спасает sub-domain TF-IDF по рубрикам.
⚖️
Trade-off. TF-IDF «прозрачен и дёшев», embeddings «глубже, но дороже и темнее». На практике хорошая стратегия — держать TF-IDF как «официальный» слой для »По теме»/поиска, а embeddings — как «дополнительный» для сложных запросов.

Чеклист «как внедрить за один день»

Собрать корпус (экспорт из Notion / Markdown-файлы / база).
Написать препроцессинг с лемматизацией под ваш язык.
Построить TF-IDF с ngram_range=(1,2), min_df=2, max_df=0.5.
Сохранить snapshot (vectorizer + матрица) в репо/на диск.
Посчитать top-5 ближайших для каждого дока. Глазами проверить, имеют ли результаты смысл.
Встроить в блок «По теме» (или в виджет «похожие»).
Добавить регулярный rebuild по расписанию или по хуку «новая публикация».
Раз в месяц смотреть «кандидаты в темы» и «скрытые дубли».

Ссылки


По теме

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