Диаграммы рассеяния в семантическом SEO: от эмбеддингов к тематическим картам

В предыдущих статьях мы уже рассматривали методы семантической SEO, включая концепцию векторных вложений, кластеризацию сайта на основе эмбеддингов, составление тематических карт как базового документа для дальнейшей работы над структурой и контентом. В этой статье рассмотрим метод оценки семантики сайта с помощью диаграмм рассеяния.

Диаграммы рассеяния в семантическом SEO: от эмбеддингов к тематическим картам

Почему ключевые слова больше не главное

Классика SEO опирается на частотность запросов. Инструменты вроде Яндекс Вордстат, Google Keyword Planner, Ahrefs или Semrush ранжируют слова по количеству ежемесячных показов — и на этом строится вся стратегия: выбираются высокочастотные запросы, под них пишутся тексты, считается плотность вхождений. Такой подход работает в конкурентных потребительских нишах, где запросов много и статистика репрезентативна.

Но что делать, если вы работаете со сложным проектом — узкий B2B, медицинские устройства, промышленная автоматизация или специализированное научное оборудование? Вордстат показывает ноль, конкурентов в выдаче не видно, а клиенты есть — и они находят сайт по странным фразам, которые ни один классический инструмент не предложит. Более того, современные поисковые системы всё реже опираются на точное совпадение слов: Google с внедрением BERT (2019), MUM (2021) и SGE/AI Overviews (2023–2024), а Яндекс с YATI (2020) и нейропоиском научились понимать смысл запроса, а не только набор слов. Это значит, что страница может ранжироваться по запросу, слов которого в ней нет вовсе — если семантика совпадает.

Разберём метод, который позволяет увидеть семантическую структуру вашего сайта глазами машинного обучения — диаграммы рассеяния (scatter plots) на основе векторных представлений (эмбеддингов) текста.

Вы научитесь:

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

Для кого: SEO-специалисты, владеющие Python на уровне базовых скриптов и знакомые с понятиями «вектор», «кластеризация», «расстояние». Если вы никогда не работали с Python, но хотите понять метод концептуально — читайте описания и пропускайте блоки кода; логика от этого не потеряется. Для тех, кто предпочитает no-code решения, в конце статьи приведены альтернативные инструменты.

Часть 1. От текста к точке: эмбеддинги и снижение размерности

1.1 Почему слова — плохие координаты

Представьте, что каждая страница — это набор слов. Можно измерить близость страниц по пересечению слов. Классические подходы к этому:

TF-IDF (Term Frequency — Inverse Document Frequency) — каждое слово получает вес, зависящий от его частоты на конкретной странице и редкости во всём корпусе. Если слово «уплотнение» встречается часто на одной странице, но редко на остальных — оно получит высокий вес. Две страницы сравниваются по косинусу угла между их TF-IDF-векторами.

Jaccard similarity — доля общих слов (или n-грамм) от объединения всех слов двух документов. Метрика простая и интуитивная, но грубая.

BM25 — усовершенствованная версия TF-IDF, используемая в поисковых движках (Elasticsearch, Lucene). Учитывает длину документа и насыщение термов, но по-прежнему оперирует точными словоформами.

Все эти методы объединяет одно фундаментальное ограничение: они не видят синонимов и смысловых связей. «Насос» и «помпа» будут разнесены далеко, хотя означают одно и то же. «Торцевое уплотнение» и «mechanical seal» — тем более. Контекстуальная близость («подшипник» часто упоминается рядом с «валом» и «смазкой») полностью игнорируется.

Эмбеддинги (векторные представления) решают эту проблему. Модель (например, sentence-transformers/all-MiniLM-L6-v2 или intfloat/e5-large-v2) превращает любой текст в плотный вектор фиксированной длины — обычно 384, 768 или 1024 числа с плавающей запятой. Эти числа кодируют семантику: близкие по смыслу тексты получают близкие векторы (по косинусному расстоянию).

Как это работает «под капотом»: в основе большинства современных эмбеддеров лежит архитектура Transformer, обученная на огромных текстовых корпусах. Во время обучения модель учится предсказывать контекст слов или определять, связаны ли два предложения по смыслу. В результате модель формирует внутреннее представление, в котором семантически близкие тексты оказываются рядом в многомерном пространстве. Это не словарь синонимов — это непрерывное пространство значений, где «насос КМ для кислых сред» ближе к «помпа химостойкая для агрессивных жидкостей», чем к «правила утилизации бытовых отходов».

from sentence_transformers import SentenceTransformer import numpy as np model = SentenceTransformer('all-MiniLM-L6-v2') pages = [ "Насос КМ 80-50-200 для кислых сред", "Уплотнение торцевое для агрессивных жидкостей", "Правила утилизации бытовых отходов" ] embeddings = model.encode(pages) # форма (3, 384) # Проверим косинусное сходство между парами from sklearn.metrics.pairwise import cosine_similarity sim_matrix = cosine_similarity(embeddings) print(sim_matrix) # Первые две страницы будут ближе друг к другу, чем к третьей

1.2 Краткая история эмбеддингов: от Word2Vec до современных моделей

Чтобы понимать, почему мы используем именно sentence-transformers, полезно знать эволюцию подхода.

Word2Vec (2013, Google) — первая массово известная модель плотных векторных представлений слов. Каждое слово получает один вектор, обученный на предсказании контекста. Ограничение: одно слово = один вектор, без учёта полисемии («замок» — здание или запирающее устройство).

GloVe (2014, Stanford) — аналогичная идея, но обучение на матрице ко-встречаемости слов. Векторы хорошо улавливают аналогии (king − man + woman ≈ queen).

FastText (2016, Facebook) — расширение Word2Vec, работающее с подсловными единицами (символьными n-граммами). Может обрабатывать слова, не встречавшиеся при обучении, что критично для технических ниш с терминологией.

ELMo (2018, Allen AI) — первые контекстуальные эмбеддинги: одно и то же слово получает разные векторы в зависимости от окружения.

BERT (2018, Google) — революция. Двунаправленный Transformer, который читает текст целиком, а не слева направо. Породил целое семейство моделей. Однако BERT сам по себе не даёт хороших эмбеддингов предложений — для этого нужна дополнительная настройка.

Sentence-BERT (SBERT, 2019) — адаптация BERT для получения эмбеддингов предложений и текстов, оптимизированных для сравнения по косинусному расстоянию. Именно на этом подходе основана библиотека sentence-transformers, которую мы используем.

Современные модели (2023–2025): E5 (Microsoft), BGE (BAAI), GTE (Alibaba), Nomic Embed, а также мультиязычные варианты вроде multilingual-e5-large — значительно превосходят ранние SBERT-модели по качеству, особенно на технических и мультиязычных текстах.

1.3 Проклятие размерности: зачем нужны UMAP и t-SNE

Вектор размерности 384 нельзя нарисовать на плоскости — у нас нет 384 глаз. Но для анализа нам нужна визуализация. Снижение размерности проецирует 384-мерные (или 768-, или 1024-мерные) точки на 2D-плоскость, максимально сохраняя расстояния между ними.

Основные методы:

PCA (Principal Component Analysis) — линейная проекция, ищущая направления максимальной дисперсии. Быстр, детерминирован (результат не зависит от случайного seed), хорошо работает для предварительной разведки. Однако PCA ищет линейные проекции, а семантические данные часто имеют нелинейную структуру — «рукава», «острова», «мосты» кластеров, которые PCA расплющивает.

t-SNE (t-distributed Stochastic Neighbor Embedding, 2008) — нелинейный метод, оптимизированный для сохранения локальной структуры. Хорошо разделяет компактные кластеры, визуально убедителен. Недостатки: медленный на больших наборах (>10 000 точек), результат сильно зависит от гиперпараметра perplexity, не сохраняет глобальные расстояния.

UMAP (Uniform Manifold Approximation and Projection, 2018) — более современный метод, основанный на теории топологии (упрощённо — сохраняет форму «облака» данных). Быстрее t-SNE в 10–100 раз, лучше сохраняет глобальную структуру (взаимное расположение кластеров), имеет более интуитивные гиперпараметры. На данный момент UMAP — стандарт де-факто для визуализации эмбеддингов.

import umap reducer = umap.UMAP(n_components=2, random_state=42) embeddings_2d = reducer.fit_transform(embeddings) # форма (n, 2)

Важное предупреждение: UMAP и t-SNE не сохраняют глобальные расстояния в полной мере. Две далёкие точки на графике действительно далеки в исходном пространстве, но обратное может быть неверным — близкие на графике могут оказаться не очень близки в оригинале. Это следствие неизбежных потерь при проекции 384 измерений на два. Для визуального анализа используйте график как качественный инструмент (обнаружение паттернов, кластеров, выбросов), а для количественных расчётов (порог перелинковки, точное расстояние между страницами) — метрики в исходном пространстве (косинусное расстояние между полными эмбеддингами).

1.4 Диаграмма рассеяния: что мы видим?

После проецирования мы получаем набор точек на плоскости. Каждая точка — одна страница (или фрагмент текста). Координаты осей X и Y абстрактны — они не имеют самостоятельной интерпретации (это не «тема» и не «сложность»). Но относительное расположение имеет чёткий смысл:

Точки рядом → семантически похожие страницы. Они говорят об одном и том же или очень близких вещах.

Точки далеко → разные темы. Чем больше расстояние, тем сильнее расхождение в смыслах.

Сгущения → кластеры — группы страниц, посвящённых одной узкой теме. В здоровом сайте кластеры соответствуют логическим разделам.

Выбросы → страницы, не вписывающиеся в общую структуру. Это может быть страница «О компании» (уникальная по содержанию), устаревший контент, случайно опубликованный черновик, или страница, которая семантически дублирует другой раздел и нуждается в переработке или удалении.

Мосты между кластерами → страницы на границе двух тем. Они могут быть ценными связующими узлами в архитектуре сайта.

Пример диаграммы рассеяния, полученной с помощью SFSS. Грубо, но для общего аудита сайта и первичного определения проблемных мест – годится.
Пример диаграммы рассеяния, полученной с помощью SFSS. Грубо, но для общего аудита сайта и первичного определения проблемных мест – годится.

Важно: не привязывайтесь к конкретным координатам. Если при повторном запуске UMAP кластер «переехал» из правого нижнего угла в левый верхний — это нормально. Смотрите на топологию: какие точки рядом, какие далеко, где сгущения, где пустоты.

Часть 2. Строим диаграмму для реального сайта

2.1 Подготовка данных

Что мы векторизуем?

Целевые страницы, которые несут самостоятельную семантическую ценность для бизнеса, его клиентов и поисковых систем:

  • Категории товаров
  • Карточки продуктов
  • Страницы услуг
  • Статьи блога
  • Посадочные страницы (landing pages)
  • Страницы FAQ (если они содержательны)
  • Кейсы / портфолио с описаниями (если они важны в тематике)

Что исключаем?

Служебные страницы и сквозные блоки создают шум и «стягивают» эмбеддинги к общему знаменателю:

  • Навигационные элементы (меню, сайдбары)
  • Футер (особенно если он содержит длинный текст «о компании»)
  • Страницы контактов, политики конфиденциальности, cookie-соглашений
  • Пагинация (page/2/, page/3/ — они дублируют тему основной страницы)
  • Страницы тегов и авторов (если на них нет уникального контента)

Какой текст брать со страницы?

Для каждой страницы соберите:

  • Заголовок H1 — основной сигнал о теме страницы.
  • Основной текст — контент в теге <main> или <article>, за вычетом общих блоков.
  • Тайтл (обязательно) и мета-описание (опционально) — может содержать краткую суммаризацию, полезную для коротких страниц.
  • Заголовки H2–H3 — структурные маркеры подтем страницы.

Если страница слишком длинная (более 2000 символов, или точнее — более 256 токенов для конкретной модели), разбейте её на смысловые фрагменты (по абзацам или заголовкам H2). Тогда одна точка = один фрагмент. Это позволит увидеть внутреннюю структуру длинной статьи — иногда одна статья содержит три разные подтемы, и это видно только при фрагментации.

Примечание. Как минимум, один раз стоит попробовать извлечь и векторизовать контент как есть, без разделения на основную зону и сквозные блоки. Зачем? – Посмотреть, как видит ваш сайт поисковый робот, и насколько размывается бесконечными мега-меню и коммерческими блоками зона main content.

Сбор данных: варианты

Вариант 1: CSV-экспорт из краулера. Инструменты Screaming Frog, A-Parser, SiteAnalyzer позволяют экспортировать URL, H1, мета-описание и даже основной текст в CSV.

Вариант 2: Собственный краулер на Python. Используйте requests + BeautifulSoup или Scrapy для обхода сайта.

Вариант 3: API CMS. Если сайт на WordPress, Bitrix, 1C-Битрикс — вытащите контент через REST API.

Простой скрипт, извлекающий только основной контент страниц, находя их через sitemap.xml. В итоге вы получите csv, где есть две колонки: URL и content. Примечание. Можно доработать скрипт так, чтобы извлекался ещё title, отдельно – подзаголовки, и что-то можно добавить в исключения.

import requests import pandas as pd from bs4 import BeautifulSoup import time DEFAULT_HEADERS = { 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36' } def extract_text_from_html(html, base_url=''): """ Извлекает основной текстовый контент из HTML. Удаляет шум (скрипты, стили, навигацию). """ soup = BeautifulSoup(html, 'html.parser') # Удаляем шумовые элементы for tag in soup(['script', 'style', 'nav', 'footer', 'header', 'aside', 'form', 'noscript', 'iframe']): tag.decompose() # Пытаемся найти основной контент main = soup.select_one('main') or soup.select_one('article') or soup.select_one('.content') if main is None: main = soup.body if main is None: return '' # Извлекаем H1 h1 = soup.find('h1') h1_text = h1.get_text(strip=True) if h1 else '' # Основной текст body_text = main.get_text(separator=' ', strip=True) return f"{h1_text}. {body_text}" def extract_all_urls_from_sitemap(sitemap_url, headers=None): """ Рекурсивно извлекает все URL из sitemap.xml, поддерживая вложенные карты. Возвращает список URL для парсинга. """ if headers is None: headers = DEFAULT_HEADERS try: resp = requests.get(sitemap_url, timeout=30, headers=headers) resp.raise_for_status() except Exception as e: print(f"Ошибка загрузки sitemap {sitemap_url}: {e}") return [] # Парсим XML soup = BeautifulSoup(resp.content, 'xml') # Проверяем, является ли текущий файл индексом (содержит вложенные карты) nested_sitemaps = soup.find_all('sitemap') if nested_sitemaps: # Если это индексный sitemap, рекурсивно обрабатываем каждый вложенный all_urls = [] for sitemap in nested_sitemaps: loc_tag = sitemap.find('loc') if loc_tag and loc_tag.text: nested_url = loc_tag.text print(f"Обработка вложенной карты: {nested_url}") all_urls.extend(extract_all_urls_from_sitemap(nested_url, headers)) return all_urls else: # Если это обычный sitemap, извлекаем все URL urls = [loc.text for loc in soup.find_all('loc')] print(f"Найдено {len(urls)} URL в {sitemap_url}") return urls def crawl_sitemap(sitemap_url, max_pages=500, headers=None, delay=0.5): """ Основная функция для парсинга контента. """ if headers is None: headers = DEFAULT_HEADERS # Получаем все URL для парсинга из sitemap all_page_urls = extract_all_urls_from_sitemap(sitemap_url, headers) # Ограничиваем количество страниц page_urls_to_parse = all_page_urls[:max_pages] print(f"Всего найдено URL: {len(all_page_urls)}. Будет обработано: {len(page_urls_to_parse)}") pages = [] for idx, url in enumerate(page_urls_to_parse, 1): try: print(f"Обработка [{idx}/{len(page_urls_to_parse)}]: {url}") resp = requests.get(url, timeout=15, headers=headers) if resp.status_code == 200: content_type = resp.headers.get('content-type', '') if 'text/html' in content_type: text = extract_text_from_html(resp.text, url) if len(text) > 100: pages.append({'url': url, 'content': text}) else: print(f" → Контент слишком короткий ({len(text)} символов)") else: print(f" → Не HTML (Content-Type: {content_type}), пропущено") else: print(f" → HTTP {resp.status_code}, пропущено") except Exception as e: print(f" → Ошибка: {e}") time.sleep(delay) print(f"Завершено. Собрано {len(pages)} страниц из {len(page_urls_to_parse)}") return pd.DataFrame(pages) if __name__ == "__main__": # Используем главный индексный sitemap сайта ВАШ-САЙТ.ru SITEMAP_URL = "https://ВАШ-САЙТ.ru/sitemap.xml" df = crawl_sitemap(SITEMAP_URL, max_pages=500) if not df.empty: df.to_csv("ВАШ-САЙТ_content.csv", index=False, encoding="utf-8-sig") print("Результат сохранён в ВАШ-САЙТ_content.csv") else: print("Нет данных для сохранения")

2.2 Векторизация и снижение размерности

import numpy as np import pandas as pd from sentence_transformers import SentenceTransformer import umap import plotly.express as px # 1. Загрузка модели (мультиязычная, хорошо работает с русским и английским) model = SentenceTransformer('intfloat/multilingual-e5-large') # 2. Загрузка данных (ожидаются колонки: url, content) df = pd.read_csv('pages.csv') # Для E5 моделей рекомендуется префикс "passage:" для документов. # Используем URL как дополнительный контекст + содержимое (первые 1500 символов) texts = [f"passage: {row['url']}. {row['content'][:1500]}" for _, row in df.iterrows()] # 3. Получение эмбеддингов (L2-нормализация включена) embeddings = model.encode(texts, show_progress_bar=True, batch_size=32, normalize_embeddings=True) # 4. Снижение размерности до 2D с помощью UMAP reducer = umap.UMAP( n_neighbors=15, min_dist=0.1, metric='cosine', random_state=42, n_epochs=500 ) embeddings_2d = reducer.fit_transform(embeddings) # 5. Добавление координат в DataFrame df['x'] = embeddings_2d[:, 0] df['y'] = embeddings_2d[:, 1] # 6. Интерактивная визуализация (Plotly) fig = px.scatter( df, x='x', y='y', hover_data=['url'], # показываем URL при наведении title='Семантическая карта сайта', width=1200, height=800 ) fig.update_traces(marker=dict(size=6, opacity=0.7)) fig.update_layout( xaxis_title='', yaxis_title='', xaxis=dict(showticklabels=False), yaxis=dict(showticklabels=False) ) fig.show() # 7. Сохранение эмбеддингов и данных с координатами np.save('embeddings.npy', embeddings) df.to_csv('pages_with_coords.csv', index=False) # сохраняет URL, content, x, y

Настройки UMAP, важные для SEO-анализа:

n_neighbors=15 — сколько ближайших соседей учитывать при построении локальной структуры. Значение 5–10 даёт больше локальных деталей, но сильнее разрывает кластеры (полезно для крупных сайтов с чёткими подкатегориями). Значение 30–50 показывает глобальную картину, но мелкие кластеры сливаются (полезно для первичной разведки).

min_dist=0.1 — минимальное расстояние между точками на плоскости. Меньшие значения (0.0–0.05) создают более плотную упаковку — хорошо для поиска подкластеров. Большие значения (0.3–0.5) «раздвигают» точки, делая картинку менее загруженной.

metric='cosine' — обязательно, так как эмбеддинги нормализованы и стандартная мера близости в NLP — именно косинусное расстояние, а не евклидово.

Вот такая визуализация получается на этом этапе. Теперь у нас есть диаграмма с адресами и координатами и csv, содержащая URL, контент и координаты
Вот такая визуализация получается на этом этапе. Теперь у нас есть диаграмма с адресами и координатами и csv, содержащая URL, контент и координаты

2.3 Добавляем кластеризацию на график

Визуальная интуиция полезна, но субъективна — разные аналитики увидят разное количество кластеров. Добавим объективную цветовую маркировку с помощью алгоритма HDBSCAN (Hierarchical Density-Based Spatial Clustering of Applications with Noise).

Почему HDBSCAN, а не K-Means? K-Means требует заранее указать количество кластеров k — а мы его не знаем. Кроме того, K-Means создаёт сферические кластеры одинакового размера, что не соответствует реальной семантической структуре. HDBSCAN автоматически определяет количество кластеров, находит группы произвольной формы и, что критически важно, маркирует шум — страницы, которые не принадлежат ни одному кластеру.

import hdbscan clusterer = hdbscan.HDBSCAN( min_cluster_size=5, # минимум страниц для формирования кластера min_samples=2, # чем больше, тем «строже» (меньше кластеров, больше шума) metric='euclidean', # в 2D-пространстве UMAP используем евклидово расстояние cluster_selection_method='eom' # 'eom' — для иерархических кластеров, 'leaf' — для плоских ) df['cluster'] = clusterer.fit_predict(embeddings_2d) # -1 = шум/выбросы # Визуализация fig = px.scatter( df, x='x', y='y', color=df['cluster'].astype(str), # строка для дискретной палитры hover_data=['url', 'title'], title='Семантические кластеры сайта (HDBSCAN)', width=1200, height=800, color_discrete_sequence=px.colors.qualitative.Set2 ) fig.show() # Статистика print(f"Найдено кластеров: {df['cluster'].nunique() - (1 if -1 in df['cluster'].values else 0)}") print(f"Страниц-выбросов: {(df['cluster'] == -1).sum()}")

Интерпретация кластеров:

Кластеры с номерами 0, 1, 2… — тематические группы. Просмотрите заголовки страниц внутри каждого кластера, чтобы дать ему человеческое название («Насосы КМ», «Уплотнения», «Доставка и оплата»).

Кластер -1 — выбросы. Это страницы, которые не похожи ни на одну группу достаточно сильно. Среди них могут быть как уникальные важные страницы (например, «О компании», «Вакансии»), так и мусор (дубли, устаревшие акции, пустые шаблоны). Каждый выброс нужно проверить вручную.

Альтернатива: кластеризация в исходном пространстве

Можно кластеризовать не в 2D (после UMAP), а в исходном пространстве эмбеддингов (384/768/1024 измерения). Это точнее, так как UMAP вносит искажения, но результат сложнее визуализировать. На практике хороший компромисс — кластеризовать в исходном пространстве, а визуализировать в 2D, раскрашивая точки по кластерам из оригинала.

# Кластеризация в исходном пространстве (точнее, но требует подбора параметров) clusterer_hd = hdbscan.HDBSCAN( min_cluster_size=5, metric='euclidean', # для нормализованных эмбеддингов евклидово ≈ косинусному cluster_selection_method='eom' ) df['cluster_hd'] = clusterer_hd.fit_predict(embeddings)

2.4 Автоматическая генерация названий кластеров

Чтобы не просматривать все страницы вручную, можно автоматически извлечь ключевые термины для каждого кластера с помощью TF-IDF:

from sklearn.feature_extraction.text import TfidfVectorizer def name_clusters(df, text_column='content', cluster_column='cluster', top_n=5): """Извлекает ключевые слова для каждого кластера""" cluster_names = {} for cluster_id in sorted(df[cluster_column].unique()): if cluster_id == -1: cluster_names[-1] = "Выбросы" continue cluster_texts = df[df[cluster_column] == cluster_id][text_column].tolist() tfidf = TfidfVectorizer(max_features=1000, stop_words=None, max_df=0.9) tfidf_matrix = tfidf.fit_transform(cluster_texts) # Средний TF-IDF по кластеру mean_tfidf = tfidf_matrix.mean(axis=0).A1 top_indices = mean_tfidf.argsort()[-top_n:][::-1] top_words = [tfidf.get_feature_names_out()[i] for i in top_indices] cluster_names[cluster_id] = ', '.join(top_words) return cluster_names names = name_clusters(df) for cid, name in names.items(): print(f"Кластер {cid}: {name}")

Часть 3. Диагностика семантического здоровья сайта

3.1 Паттерны дисперсии: что видит аналитик

Глядя на диаграмму рассеяния, опытный аналитик быстро определяет «типаж» сайта по характеру распределения точек. Выделим основные паттерны.

А) Низкая дисперсия — «чёрная дыра»

Признаки: все точки сконцентрированы в радиусе 20% от общего размаха графика, нет видимых подгрупп. Все страницы выглядят как одно бесформенное пятно.

Низкая дисперсия: когда все ваши товарные карточки примерно ни о чём и отличаются только полем "цена".
Низкая дисперсия: когда все ваши товарные карточки примерно ни о чём и отличаются только полем "цена".

Диагноз: контент слишком однороден. Типичные причины: шаблонные короткие описания товаров, различающиеся только артикулом; автоматически сгенерированные тексты из одного шаблона; или модель эмбеддингов не различает вашу специфику (например, общая англоязычная модель плохо разбирается в русскоязычных инженерных терминах).

Лечение: увеличить длину и уникальность текстов. Если карточки товаров содержат только «Насос КМ 80-50-200. Цена по запросу» — добавьте описание назначения, области применения, технические характеристики, особенности эксплуатации. Попробуйте сменить эмбеддер на специализированный для вашего языка и предметной области. Для русскоязычных технических текстов хорошие результаты показывают deepvk/USER-bge-m3 и intfloat/multilingual-e5-large.

Б) Нормальная иерархическая дисперсия — «здоровый сад»

Признаки: несколько компактных групп (5–15), между ними заметные зазоры, внутри групп есть разброс (подгруппы второго уровня). Выбросов немного (менее 10–15% от общего числа).

Нормальная дисперсия: всё хорошо. И это подозрительно!
Нормальная дисперсия: всё хорошо. И это подозрительно!

Диагноз: структура сайта логична, эмбеддинги работают корректно. Кластеры соответствуют реальным разделам сайта (проверьте!). Можно переходить к поиску пробелов и оптимизации.

Что делать дальше: сопоставить кластеры с разделами навигации сайта. Если они совпадают — отлично. Если не совпадают (например, эмбеддинги разделили «Товары» на три семантических кластера, а в меню это один раздел) — рассмотрите реструктуризацию.

В) Высокая дисперсия — «рассыпавшееся ожерелье»

Признаки: точки разбросаны равномерно, нет явных кластеров. HDBSCAN относит большинство точек в шум (-1).

Высокая дисперсия: контент ни о чём и обо всём
Высокая дисперсия: контент ни о чём и обо всём

Диагноз: либо страницы слишком разные и не объединены общей темой (сайт-каталог «всего подряд», агрегатор), либо параметры UMAP/HDBSCAN подобраны неправильно (слишком высокое n_neighbors у UMAP, слишком большой min_cluster_size у HDBSCAN).

Лечение: проверьте выборку страниц — возможно, вы включили несвязанные разделы. Попробуйте анализировать разделы по отдельности. Уменьшите n_neighbors до 5–10, уменьшите min_cluster_size до 3.

Г) Два-три гигантских кластера — «слипшиеся комки»

Признаки: все точки собраны в 2–3 огромных плотных скопления, внутренняя структура которых неразличима.

Диагноз: кластеризация слишком грубая, или контент разделов настолько однороден внутри себя, что алгоритм не видит подтем.

Лечение: увеличьте min_cluster_size или используйте cluster_selection_method='leaf' в HDBSCAN для более детального дробления. Альтернативно — проведите рекурсивную кластеризацию: возьмите каждый крупный кластер и кластеризуйте его отдельно.

Д) «Гантель» — два кластера, соединённые мостом

Признаки: два чётких кластера соединены цепочкой точек.

Диагноз: на сайте есть два основных направления (например, «Оборудование» и «Услуги по обслуживанию»), а мост — это страницы, связывающие оба направления (например, «Сервисное обслуживание оборудования Х»).

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

3.2 Обнаружение семантических дублей

Прежде чем искать пробелы, убедитесь, что у вас нет избыточного контента. Семантические дубли — страницы, которые говорят почти одно и то же, но с разных URL — конкурируют друг с другом за выдачу (каннибализация) и размывают ссылочный вес.

from sklearn.metrics.pairwise import cosine_similarity # Матрица попарных сходств в исходном пространстве эмбеддингов sim_matrix = cosine_similarity(embeddings) # Найти пары с высоким сходством (порог > 0.92 — почти дубли) THRESHOLD = 0.92 duplicates = [] for i in range(len(sim_matrix)): for j in range(i + 1, len(sim_matrix)): if sim_matrix[i][j] > THRESHOLD: duplicates.append({ 'page_a': df.iloc[i]['url'], 'title_a': df.iloc[i]['title'], 'page_b': df.iloc[j]['url'], 'title_b': df.iloc[j]['title'], 'similarity': round(sim_matrix[i][j], 3) }) dup_df = pd.DataFrame(duplicates).sort_values('similarity', ascending=False) print(f"Найдено потенциальных дублей: {len(dup_df)}") dup_df.head(20)

Что делать с дублями:

  • Если обе страницы нужны (например, две похожие, но всё-таки разные модели насоса) — дифференцируйте контент, подчеркните различия.
  • Если одна из них не нужна — объедините контент на лучшей странице, настройте 301-редирект с худшей.
  • Если это один и тот же товар по разным URL — canonical.

3.3 Поиск семантических пробелов в контенте

Это ключевой метод для ниш без поисковой статистики.

Идея: внутри одного кластера (по мнению HDBSCAN) две точки могут быть довольно далеко друг от друга. Если между ними нет других точек — значит, между соответствующими темами нет промежуточного контента. Это и есть семантический пробел (content gap).

Это более корректно делать в исходном пространстве эмбеддингов, а не в 2D, чтобы избежать артефактов UMAP:

from sklearn.metrics.pairwise import cosine_distances # Для каждого кластера ищем максимально далёкие пары gaps = [] for cluster_id in df[df['cluster'] != -1]['cluster'].unique(): cluster_mask = df['cluster'] == cluster_id cluster_indices = df[cluster_mask].index.tolist() if len(cluster_indices) < 3: continue # Расстояния в ИСХОДНОМ пространстве (не в 2D!) cluster_embeddings = embeddings[cluster_indices] dist_matrix = cosine_distances(cluster_embeddings) # Находим пару с максимальным расстоянием i_local, j_local = np.unravel_index(np.argmax(dist_matrix), dist_matrix.shape) gap_distance = dist_matrix[i_local, j_local] if gap_distance > 0.5: # порог подбирается экспериментально (0.4–0.7) page_a = df.iloc[cluster_indices[i_local]] page_b = df.iloc[cluster_indices[j_local]] gaps.append({ 'cluster': cluster_id, 'page_a': page_a['title'], 'page_b': page_b['title'], 'url_a': page_a['url'], 'url_b': page_b['url'], 'distance': round(gap_distance, 3) }) gap_df = pd.DataFrame(gaps).sort_values('distance', ascending=False) print("Семантические пробелы:") gap_df.head(10)

Что делать с найденными парами: создайте страницу, которая связывает эти две темы. Например, если одна страница — «Насос КМ для воды», другая — «Уплотнения для кислот», пробелом может быть «Уплотнения для насоса КМ в кислых средах» или «Как выбрать насос КМ для агрессивных жидкостей».

Продвинутый метод: k-distance graph

from sklearn.neighbors import NearestNeighbors nn = NearestNeighbors(n_neighbors=4, metric='cosine') nn.fit(embeddings) distances, indices = nn.kneighbors(embeddings) # Среднее расстояние до 3 ближайших соседей для каждой страницы df['avg_neighbor_dist'] = distances[:, 1:].mean(axis=1) # Страницы с аномально большим расстоянием до соседей — кандидаты на «одинокие» темы isolation_threshold = df['avg_neighbor_dist'].quantile(0.9) isolated = df[df['avg_neighbor_dist'] > isolation_threshold] print("Наиболее изолированные страницы (возможные пробелы вокруг них):") print(isolated[['url', 'title', 'avg_neighbor_dist']].to_string())

3.4 Оценка полноты покрытия темы: сравнение с конкурентами

Один из самых мощных приёмов — совместная визуализация семантических карт вашего сайта и конкурентов.

Алгоритм:

  • Соберите страницы 2–3 конкурентов теми же методами.
  • Объедините все тексты (ваши + конкурентские) в один набор.
  • Векторизуйте всё вместе одной моделью (это критично — эмбеддинги от разных запусков несопоставимы).
  • Снизьте размерность всего набора одним UMAP.
  • Раскрасьте точки по принадлежности к сайту.
# Предположим, df_my — ваши страницы, df_comp1, df_comp2 — конкуренты df_my['source'] = 'Мой сайт' df_comp1['source'] = 'Конкурент А' df_comp2['source'] = 'Конкурент Б' df_all = pd.concat([df_my, df_comp1, df_comp2], ignore_index=True) all_texts = [f"passage: {row['title']}. {row['content'][:1500]}" for _, row in df_all.iterrows()] all_embeddings = model.encode(all_texts, show_progress_bar=True, normalize_embeddings=True) reducer_all = umap.UMAP(n_neighbors=15, min_dist=0.1, metric='cosine', random_state=42) all_2d = reducer_all.fit_transform(all_embeddings) df_all['x'] = all_2d[:, 0] df_all['y'] = all_2d[:, 1] fig = px.scatter( df_all, x='x', y='y', color='source', hover_data=['url', 'title'], title='Сравнение семантических профилей с конкурентами', width=1400, height=900, opacity=0.6 ) fig.show()

Что искать на такой карте:

  • Области, где есть только точки конкурентов, а ваших нет → темы, которые конкуренты покрывают, а вы нет.
  • Области, где есть только ваши точки → ваше уникальное позиционирование (или нерелевантный контент — нужно разобраться).
  • Области пересечения → прямая конкуренция; проверьте, чей контент глубже.

3.5 Анализ каннибализации

Каннибализация — ситуация, когда несколько ваших страниц конкурируют за одни и те же запросы. На диаграмме рассеяния это видно как неоправданное скопление точек в одной области.

Чтобы подтвердить каннибализацию, совместите данные эмбеддингов с данными из Google Search Console или Яндекс-Вебмастера:

# Загружаем данные Search Console (экспорт Performance → Pages) gsc = pd.read_csv('search_console_pages.csv') # url, clicks, impressions, position # Объединяем с нашими данными df_merged = df.merge(gsc, on='url', how='left') # Для каждого кластера проверяем: есть ли несколько страниц с одинаковыми запросами # но низким CTR и плохими позициями? for cluster_id in df_merged[df_merged['cluster'] != -1]['cluster'].unique(): cluster = df_merged[df_merged['cluster'] == cluster_id] if len(cluster) > 1: avg_position = cluster['position'].mean() if avg_position > 20: # Все страницы кластера на плохих позициях print(f"Возможная каннибализация в кластере {cluster_id}:") print(cluster[['url', 'title', 'position', 'clicks']].to_string()) print()

Часть 4. Проектирование тематических карт на основе диаграмм рассеяния

4.1 От кластеров к рубрикатору

Каждый кластер на графике — это кандидат в категорию верхнего уровня или в столповую (pillar) страницу. Внутри кластера видны подгруппы — это подкатегории или кластерные страницы (cluster content).

Модель «Хаб и спицы» (Pillar & Cluster):

  • Хаб (Pillar page) — большая обзорная страница по теме кластера. Содержит общую информацию, ссылки на все подтемы.
  • Спики (Cluster pages) — детальные страницы по узким подтемам. Ссылаются обратно на hub.

Практический алгоритм:

  • Выделите кластеры HDBSCAN с параметрами, дающими 5–15 кластеров (для небольшого сайта в 100–500 страниц).
  • Для каждого кластера выберите «центроидную» страницу — ближайшую к геометрическому центру кластера в исходном пространстве эмбеддингов. Она станет хабом (pillar page, портальной страницей). Если такой страницы нет (центроид попадает в пустоту), значит хаб нужно создать.
from sklearn.metrics.pairwise import cosine_distances hubs = {} for cluster_id in df[df['cluster'] != -1]['cluster'].unique(): cluster_mask = df['cluster'] == cluster_id cluster_indices = df[cluster_mask].index.tolist() cluster_embeddings = embeddings[cluster_indices] # Центроид кластера centroid = cluster_embeddings.mean(axis=0).reshape(1, -1) # Ближайшая к центроиду страница dists = cosine_distances(centroid, cluster_embeddings)[0] hub_local_idx = np.argmin(dists) hub_global_idx = cluster_indices[hub_local_idx] hubs[cluster_id] = { 'url': df.iloc[hub_global_idx]['url'], 'title': df.iloc[hub_global_idx]['title'], 'dist_to_centroid': round(dists[hub_local_idx], 3) } print(f"Кластер {cluster_id}: hub = {hubs[cluster_id]['title']}")
  • Для каждой точки внутри кластера определите, является ли она уже существующей страницей или это пробел (выявленный на предыдущем этапе).
  • Спроектируйте структуру URL: /тема/подтема/конкретная-страница. Кластер верхнего уровня задаёт первый сегмент пути, подкластер — второй.
  • Создайте таблицу контент-плана, где отметьте тип страницы, URL, приоритетность, актуальный статус

4.2 Рекурсивная кластеризация для крупных сайтов

Для сайтов с тысячами страниц один уровень кластеризации недостаточен. Примените рекурсивный подход:

  1. Кластеризация всего корпуса → 5–10 мегакластеров (разделы сайта).
  2. Для каждого мегакластера — повторная кластеризация → 3–8 подкластеров (подразделы).
  3. При необходимости — ещё один уровень.

Это даёт трёхуровневую иерархию, естественно ложащуюся на структуру каталога.

4.3 Динамические тематические карты с перелинковкой

Зная координаты страниц в семантическом пространстве, можно автоматически рекомендовать перелинковку. Правило: если две страницы находятся на расстоянии меньше порога в косинусной метрике исходных эмбеддингов, но между ними нет ссылки — добавьте взаимную перелинковку.

from sklearn.neighbors import NearestNeighbors # В исходном пространстве эмбеддингов (не в 2D!) nn = NearestNeighbors(n_neighbors=6, metric='cosine') nn.fit(embeddings) distances, indices = nn.kneighbors(embeddings) link_recommendations = [] for i, (dists, neighbors) in enumerate(zip(distances, indices)): for j, dist in zip(neighbors[1:], dists[1:]): # пропускаем саму страницу if dist < 0.3: # порог подбирается экспериментально link_recommendations.append({ 'from_url': df.iloc[i]['url'], 'from_title': df.iloc[i]['title'], 'to_url': df.iloc[j]['url'], 'to_title': df.iloc[j]['title'], 'distance': round(dist, 3) }) link_df = pd.DataFrame(link_recommendations).drop_duplicates( subset=['from_url', 'to_url'] ).sort_values('distance') print(f"Рекомендовано связей: {len(link_df)}") link_df.head(20)

Важные нюансы перелинковки:

  • Не создавайте слишком много ссылок с одной страницы — это размывает вес каждой ссылки. Рекомендуемый максимум — 5–8 контекстных ссылок в тексте страницы.
  • Приоритизируйте ссылки внутри одного кластера (они наиболее релевантны).
  • Ссылки между кластерами тоже полезны, но должны проходить через хабы.
  • Сверяйте рекомендации с реальной ссылочной структурой сайта (парсинг внутренних ссылок через Screaming Frog или собственный краулер), чтобы рекомендовать только новые связи.

4.4 Визуализация как артефакт для клиента

Клиенты SEO не всегда могут объяснить, что и кому продают (и это практически норма в b2b), отлично понимают картинки. Интерактивная диаграмма рассеяния — мощный инструмент коммуникации на стадии согласовывания целей и стратегии продвижения:

  • Постройте scatter plot с цветовыми кластерами и подписями (Plotly позволяет экспортировать в HTML — клиент откроет в браузере).
  • Покажите пустые области — это «семантический долг» сайта.
  • Пометьте точки конкурентов (другим символом или прозрачностью) — это наглядно демонстрирует, где вы отстаёте.
  • Предложите план наполнения: каждая новая точка должна заполнять конкретную пустоту. Это легко превращается в контент-план с приоритетами.
  • Покажите эволюцию: наложите карты за разные периоды, чтобы продемонстрировать прогресс (появление новых точек в ранее пустых областях).
# Экспорт интерактивного HTML для клиента fig.write_html('semantic_map.html', include_plotlyjs='cdn')

Часть 5. Продвинутые техники

5.1 Совмещение с данными поисковой аналитики

Диаграмма рассеяния становится значительно мощнее, если обогатить её данными из Google Search Console, Яндекс.Вебмастера или систем аналитики:

# Размер точки = трафик, цвет = средняя позиция fig = px.scatter( df_merged, x='x', y='y', size='clicks', # больше кликов → крупнее точка color='position', # позиция в выдаче → цветовая шкала hover_data=['url', 'title', 'clicks', 'impressions', 'position'], title='Семантическая карта + поисковая аналитика', color_continuous_scale='RdYlGn_r', # зелёный = хорошая позиция, красный = плохая size_max=30 ) fig.show()

Это позволяет увидеть:

  • Кластеры с хорошими позициями, но низким трафиком → возможно, не хватает контента для привлечения длинного хвоста запросов.
  • Кластеры с плохими позициями → нужно усилить контент и/или ссылочный профиль.
  • Крупные точки (много трафика) в окружении мелких → «якорные» страницы, от которых нужно строить перелинковку.

5.2 Временная динамика: как сайт меняется

Проводите анализ регулярно (раз в 2–3 месяца) и сравнивайте карты. Для корректного сравнения используйте один и тот же UMAP-редюсер:

# Обучаем UMAP на первом снимке reducer = umap.UMAP(n_neighbors=15, min_dist=0.1, metric='cosine', random_state=42) reducer.fit(embeddings_v1) # Проецируем оба снимка одним редюсером coords_v1 = reducer.transform(embeddings_v1) coords_v2 = reducer.transform(embeddings_v2) # Новые точки в v2, которых не было в v1 — это созданный контент # Исчезнувшие точки — удалённые или переработанные страницы

5.3 Эмбеддинги запросов: не только страницы

Помимо страниц, можно векторизовать поисковые запросы (из Search Console, Вебмастера, логов поиска по сайту) и нанести их на ту же карту. Это покажет, насколько ваш контент соответствует тому, что ищут пользователи:

# Запросы из Search Console queries = gsc_queries['query'].tolist() # Для E5-моделей запросы кодируются с префиксом "query:" query_texts = [f"query: {q}" for q in queries] query_embeddings = model.encode(query_texts, normalize_embeddings=True) # Проецируем запросы тем же UMAP (обученным на страницах) query_2d = reducer.transform(query_embeddings) # Добавляем на график другим маркером import plotly.graph_objects as go fig = go.Figure() fig.add_trace(go.Scatter( x=df['x'], y=df['y'], mode='markers', name='Страницы', marker=dict(size=8, color='blue', opacity=0.6), text=df['title'], hoverinfo='text' )) fig.add_trace(go.Scatter( x=query_2d[:, 0], y=query_2d[:, 1], mode='markers', name='Запросы', marker=dict(size=4, color='red', symbol='diamond', opacity=0.4), text=queries, hoverinfo='text' )) fig.update_layout(title='Страницы vs Запросы', width=1400, height=900) fig.show()

Что искать:

  • Скопления красных точек (запросов) без синих (страниц) рядом → темы, по которым вас ищут, но контента нет.
  • Синие точки без красных → контент, который никто не ищет (или ищет по запросам, которых нет в вашей выборке).

Часть 6. Ограничения и подводные камни

6.1 Ложные разрывы из-за бедных эмбеддингов

Если страницы слишком короткие (менее 200–300 символов содержательного текста) или шаблонные, эмбеддинги будут почти одинаковыми — вы увидите «чёрную дыру» (низкую дисперсию) и получите ложноположительные разрывы или, наоборот, не обнаружите ни одного.

Решение: предварительно обогатить контент — добавить описания, характеристики, контекст использования, области применения. Если это невозможно быстро сделать на сайте — обогатите тексты для анализа (добавьте данные из внутренних баз, каталогов производителей, технических спецификаций).

6.2 Влияние повторяющихся блоков и шума

Если на всех страницах повторяется один и тот же блок («Заказать звонок. Мы работаем с 2000 года. Гарантия качества. Бесплатная доставка.»), эмбеддинги «схлопнутся» к этому общему знаменателю — и различия между страницами станут менее заметными.

Решение: перед векторизацией удаляйте повторяющиеся блоки. Варианты:

  • Структурно: вырезайте навигацию, футер, сайдбар, виджеты при парсинге (как в коде выше).
  • Алгоритмически: с помощью difflib.SequenceMatcher найдите текстовые фрагменты, повторяющиеся на >80% страниц, и удалите их.
  • Вручную: составьте список стоп-фраз специфичных для сайта.

6.3 Выбор модели эмбеддингов

Универсальные модели (all-MiniLM-L6-v2) хорошо работают для новостей и общих текстов, но для технических, юридических или медицинских ниш их может не хватать.

Рекомендации по выбору модели:

Для русскоязычных текстов:

  • intfloat/multilingual-e5-large — мультиязычная модель от Microsoft, хорошее качество на русском, требует префиксов «query:» / «passage:».
  • deepvk/USER-bge-m3 — адаптирована для русского языка.
  • sentence-transformers/paraphrase-multilingual-MiniLM-L12-v2 — компактная мультиязычная модель, хороша как базовый вариант.

Для англоязычных текстов:

  • BAAI/bge-large-en-v1.5 — одна из лучших англоязычных моделей.
  • intfloat/e5-large-v2 — отличная модель от Microsoft.

Мультиязычные (если сайт на нескольких языках):

  • intfloat/multilingual-e5-large
  • BAAI/bge-m3

Как проверить, что модель подходит: возьмите 10 пар страниц, для которых вы точно знаете степень близости (5 похожих пар и 5 непохожих). Посчитайте косинусное сходство. Если модель корректно отделяет похожие от непохожих (сходство > 0.7 для похожих и < 0.4 для непохожих) — модель подходит. Если нет — пробуйте другую.

6.4 Стабильность UMAP

UMAP — стохастический алгоритм. При каждом запуске с разным random_state расположение точек на плоскости будет отличаться (хотя основные кластеры сохранятся). Это создаёт две проблемы:

  • Воспроизводимость: всегда фиксируйте random_state при анализе.
  • Валидация: запустите UMAP 5 раз с разными seed и убедитесь, что основные паттерны (количество кластеров, их относительное расположение) повторяются. Если при одном seed вы видите три кластера, а при другом — пять, значит, один из этих паттернов нестабилен.

6.5 Масштабирование: что делать с большими сайтами

Описанные выше методы хороши для сайтов малого и среднего объёма. Большие сайты требуют дополнительных ресурсов и других средств. Для сайтов с десятками тысяч страниц попробуйте следующее:

  • Эмбеддинги: используйте GPU (CUDA) для ускорения — model.encode(texts, device='cuda'). Батч-процессинг с batch_size=64–128.
  • UMAP: для >50 000 точек используйте umap.UMAP(low_memory=True) или cuml.UMAP (GPU-версия из библиотеки RAPIDS).
  • HDBSCAN: для больших данных — cuml.HDBSCAN (GPU).
  • Визуализация: Plotly может тормозить на >20 000 точках. Используйте datashader для растеризации или Plotly с scattergl вместо scatter.

6.6 Ложная интерпретация: корреляция ≠ каузация

Близость на диаграмме рассеяния означает семантическую похожесть текстов — и ничего более. Она не означает, что:

  • Страницы конкурируют за одни и те же запросы (для этого нужны данные Search Console).
  • Пользователи переходят с одной на другую (для этого нужна аналитика поведения).
  • Google считает их дублями (для этого нужен анализ индексации).

Диаграмма — инструмент генерации гипотез, а не доказательства. Каждую гипотезу нужно валидировать дополнительными данными.

Часть 7. Инструменты и альтернативные подходы

7.1 No-code и low-code альтернативы

SFSS тоже кое-что может, но пока количество алгоритмов кластеризации и других настроек больше про игру, чем про серьёзную семантическую аналитику
SFSS тоже кое-что может, но пока количество алгоритмов кластеризации и других настроек больше про игру, чем про серьёзную семантическую аналитику

Если вас органически отвращает Python, а длинные слова только расстраивают – вот инструменты для визуального семантического анализа:

  • Embedding Projector (TensorFlow) — бесплатный веб-инструмент для визуализации эмбеддингов с UMAP/t-SNE/PCA. Загрузите CSV с эмбеддингами.
  • Screaming Frog SEO Spider: начиная с версии 22, вы можете подключить модель извлечения векторов прямо при парсинге, а потом запустить визуализацию. Возможно, этого будет достаточно для ваших задач, хотя и менее информативно для дальнейшей работы.
  • Atlas (Nomic) — SaaS для визуализации больших коллекций эмбеддингов.
  • Google Colab — можно запустить весь код из этой статьи бесплатно в облаке (с GPU).
  • Готовые SEO-инструменты — некоторые SEO-платформы (например, MarketMuse, Surfer SEO, Clearscope) используют эмбеддинги внутри себя, хотя не дают прямого доступа к визуализации.

7.2 Когда всё это не нужно – вообще

  • Если на сайте менее 20 содержательных страниц — выборка слишком мала для статистически значимых паттернов. Анализ быстрее провести вручную.
  • Если сайт — одностраничник или лендинг — нечего кластеризовать.
  • Если задача — оптимизация отдельной страницы под конкретный запрос — здесь лучше работает классический on-page SEO (TF-IDF и BM25 конкурентного анализа, анализ SERP).
  • Если контент сайта — исключительно изображения или видео без текстового описания (хотя мультимодальные эмбеддинги типа CLIP могут помочь и тут).

Заключение: от визуализации к инженерии знаний

Диаграммы рассеяния на основе эмбеддингов превращают SEO из «угадай ключевое слово» в измеримую инженерию семантического пространства. Вы перестаёте гадать, какие страницы нужны — вы видите дыры в вашей карте знаний. Вы перестаёте полагаться на частотность запросов — вы работаете со смысловой структурой.

Этот подход особенно ценен для:

  • B2B-ниш с околонулевым поисковым объёмом, но платежеспособной аудиторией.
  • Технических и промышленных сайтов, где одну сущность называют десятью разными терминами.
  • Новых рынков, где статистика ещё не накоплена.
  • Крупных сайтов с тысячами страниц, где человек физически не способен охватить всю структуру.

Чек-лист внедрения:

  1. Собрать все значимые страницы сайта (минимум 50–100 для корректного анализа).
  2. Очистить текст от шаблонных блоков (навигация, футер, виджеты).
  3. Векторизовать — выбрать модель эмбеддингов, подходящую для языка и ниши.
  4. Снизить размерность — UMAP (n_neighbors=15, min_dist=0.1, metric='cosine').
  5. Кластеризовать — HDBSCAN (min_cluster_size от 3 до 10 в зависимости от объёма данных).
  6. Построить scatter plot с цветными кластерами и интерактивными подписями.
  7. Обнаружить и устранить семантические дубли (сходство > 0.92).
  8. Найти пары с максимальным внутрикластерным расстоянием → гипотезы о пробелах.
  9. Сравнить с конкурентами → найти уникальные возможности.
  10. Спроектировать hub-and-spoke структуру и перелинковку на основе данных.
  11. Создать новые страницы, заполняющие пробелы.
  12. Через 2–3 месяца повторить анализ и оценить динамику.

Не пытайтесь автоматизировать всё. Визуализация — инструмент для генерации гипотез, а не истина в последней инстанции.

Впрочем, это касается SEO в целом. Алгоритм не знает вашего бизнеса, не понимает ценности клиента и не чувствует рыночных трендов. Самые ценные инсайты рождаются, когда вы смотрите на график и вдруг замечаете: «Почему эти две точки так далеко, ведь они оба про уплотнения? Ах, в одном тексте сказано "сальник", а в другом "манжета" — нужно добавить синонимы в контент и связать страницы». Или: «Почему у конкурента есть целый кластер по теме "ремонт насосов", а у нас — пусто? Мы ведь тоже это делаем!»

Это и есть SEO на данных в действии — не замена интуиции, а её усиление данными.

4
1
1
24 комментария