Перейти к содержимому

Поисковые движки

Поисковые движки — специализированные базы данных для полнотекстового поиска на основе инвертированного индекса (слово → список документов). Этапы: индексация (токенизация, нормализация, стемминг, удаление стоп-слов) и поиск с ранжированием (TF-IDF, BM25). Популярные системы: Elasticsearch (REST API, агрегации, Kibana, масштабирование), Solr, Typesense, Manticore. Типы запросов: match, bool, fuzzy (опечатки), wildcard, geo. Агрегации (группировки, гистограммы). Идеальные сценарии: поиск по сайту, логам (ELK), документам, геопоиск. Отличие от реляционных БД, настройка анализаторов, типичные ошибки.

Введение: Как Google находит за миллисекунды

Представьте, что вы ищете в интернете “как испечь хлеб”. Google просматривает миллиарды страниц и возвращает результат за доли секунды. Как это возможно? Неужели Google каждую миллисекунду сканирует весь интернет?

Конечно, нет. Вместо этого поисковые системы заранее строят специальную структуру данных — инвертированный индекс. Это похоже на алфавитный указатель в конце книги: слово “хлеб” встречается на страницах 10, 25, 103, и вы сразу переходите к ним, не листая всю книгу.

Поисковый движок (Search Engine) — это специализированная база данных для полнотекстового поиска. Она умеет находить документы по словам и фразам, ранжировать результаты по релевантности, поддерживать сложные поисковые запросы (AND, OR, NOT, поиск по фразе, поиск с ошибками, поиск по части слова).

Это не просто “еще один тип NoSQL”. Поисковые движки решают задачу, с которой плохо справляются реляционные базы: WHERE text LIKE '%word%' — медленно, не масштабируется, не ранжирует. Поисковые движки делают это быстро, на терабайтах данных, с учетом грамматики и синонимов.

Что такое поисковый движок

Поисковый движок (Search Engine / Search Database) — это система для индексации и поиска неструктурированных и полуструктурированных данных. Основные задачи:

  • Индексация: построение инвертированного индекса по тексту
  • Поиск: быстрый поиск слов и фраз
  • Ранжирование: сортировка результатов по релевантности
  • Аналитика: агрегации по результатам поиска (фасеты, гистограммы)

Ключевое отличие от реляционных БД

ХарактеристикаРеляционная БДПоисковый движок
Поиск по текстуLIKE '%word%' — полное сканированиеИнвертированный индекс — мгновенно
РанжированиеНет (только ORDER BY)Релевантность (TF-IDF, BM25)
ОпечаткиНетFuzzy search (расстояние Левенштейна)
МорфологияНетСтемминг, лемматизация
СинонимыНетДа
Язык запросовSQLСпециализированный (Query DSL)

Как работает поисковый движок

Этап 1: Индексация (Indexing)

Исходный документ → Анализ → Инвертированный индекс

Документ: "Быстрая коричневая лиса прыгает через ленивую собаку"

Шаг 1: Токенизация (разбивка на слова)
["Быстрая", "коричневая", "лиса", "прыгает", "через", "ленивую", "собаку"]

Шаг 2: Нормализация (нижний регистр)
["быстрая", "коричневая", "лиса", "прыгает", "через", "ленивую", "собаку"]

Шаг 3: Стемминг/лемматизация (приведение к основе)
["быстр", "коричнев", "лис", "прыга", "через", "ленив", "собак"]

Шаг 4: Удаление стоп-слов (слишком частые)
["быстр", "коричнев", "лис", "прыга", "ленив", "собак"]

Шаг 5: Построение инвертированного индекса
"быстр"    → [документ1, позиция1]
"коричнев" → [документ1, позиция2]
"лис"      → [документ1, позиция3]
"прыга"    → [документ1, позиция4]
"ленив"    → [документ1, позиция6]
"собак"    → [документ1, позиция7]

Этап 2: Поиск (Searching)

Запрос пользователя → Анализ (те же шаги) → Поиск в индексе → Ранжирование

Запрос: "лиса собака"

Анализ: ["лис", "собак"]

Поиск в индексе:
"лис"   → документ1
"собак" → документ1

Результат: документ1 (релевантность: 2 слова из 2)

Ранжирование: Как сортируются результаты

TF-IDF (Term Frequency — Inverse Document Frequency)

Классический алгоритм ранжирования, используемый в Lucene (основа Elasticsearch).

  • TF (Term Frequency): Как часто слово встречается в документе. Чем чаще, тем важнее.
  • IDF (Inverse Document Frequency): Насколько слово редкое во всей коллекции. Редкие слова важнее.
TF = частота слова в документе
IDF = log(общее количество документов / количество документов со словом)
TF-IDF = TF * IDF

Пример:

  • Слово “кот” встречается 5 раз в документе (TF=5) и есть в 10% документов (IDF=log(10)=1) → вес = 5
  • Слово “и” встречается 100 раз (TF=100), но есть в 99% документов (IDF≈0.01) → вес ≈ 1

BM25 (Best Matching 25)

Современная версия TF-IDF, используется в Elasticsearch по умолчанию. Учитывает длину документа.

BM25 ≈ TF-IDF с насыщением (чтобы слишком частые слова не доминировали)

Практическое ранжирование в Elasticsearch

// Поиск с кастомным ранжированием
GET /articles/_search
{
  "query": {
    "bool": {
      "should": [
        { "match": { "title": "хлеб", "boost": 3 } },      // заголовок важнее
        { "match": { "content": "хлеб", "boost": 1 } }      // содержание
      ]
    }
  }
}

Популярные поисковые движки

Elasticsearch

Самый популярный поисковый движок. Построен на Apache Lucene, имеет REST API, горизонтальное масштабирование.

// Индексация документа
POST /articles/_doc/1
{
  "title": "Как испечь хлеб",
  "content": "Для выпечки хлеба нужны мука, вода, дрожжи и соль...",
  "author": "Иван",
  "published_at": "2024-01-15"
}

// Поиск
GET /articles/_search
{
  "query": {
    "match": {
      "content": "хлеб дрожжи"
    }
  },
  "sort": [
    { "published_at": "desc" }
  ],
  "aggs": {
    "by_author": {
      "terms": { "field": "author" }
    }
  }
}

Характеристики Elasticsearch:

  • Инвертированный индекс (Lucene)
  • Горизонтальное масштабирование (шардирование)
  • REST API (JSON over HTTP)
  • Агрегации (фасеты, гистограммы, метрики)
  • Поддержка 30+ языков
  • Визуализация (Kibana)

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

  • Поиск по логам (ELK Stack: Elasticsearch, Logstash, Kibana)
  • Поиск по каталогу товаров
  • Поиск по документам (статьи, база знаний)
  • Аналитика логов (метрики, дашборды)
  • Геопоиск (магазины рядом)

Apache Solr

Второй по популярности поисковый движок на Lucene. Более зрелый, чем Elasticsearch.

<!-- Solr запрос через HTTP -->
<select?q=хлеб+дрожжи&fq=author:Иван&sort=published_at desc&facet=true&facet.field=author/>

Характеристики Solr:

  • Lucene под капотом
  • XML/JSON интерфейс
  • Более богатые возможности настройки
  • Зрелые функции (ZooKeeper для кластеризации)

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

  • Большие проекты с устоявшейся инфраструктурой
  • Требуется тонкая настройка (чем Elasticsearch)
  • Уже используется Solr

Algolia / Typesense / Meilisearch

Современные API-first поисковые движки с упором на скорость и простоту.

// Typesense
const client = new Typesense.Client({
  nodes: [{ host: 'localhost', port: '8108', protocol: 'http' }],
  apiKey: 'xyz'
});

client.collections('products').documents().search({
  q: 'iphone',
  query_by: 'name,description',
  sort_by: 'price:asc'
});

Характеристики:

  • Очень быстрые (в память)
  • Простое API
  • Автодополнение, опечатки “из коробки”
  • Гибридное ранжирование

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

  • Поиск на сайте (интернет-магазин, блог)
  • Быстрый прототип
  • Не хочется администрировать Elasticsearch

Sphinx / Manticore Search

Поисковые движки с фокусом на интеграцию с реляционными БД.

-- Manticore Search (SQL-подобный)
SELECT * FROM products WHERE MATCH('хлеб дрожжи') AND price < 500;

Характеристики:

  • SQL-интерфейс
  • Интеграция с MySQL/PostgreSQL
  • Легковесный

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

  • Интеграция с существующей реляционной БД
  • Легковесное решение

Инвертированный индекс в деталях

Структура инвертированного индекса

Словарь (Dictionary)                 Постинг-листы (Posting Lists)
"быстр"    → [doc1:pos1, doc5:pos3, doc10:pos7]
"коричнев" → [doc1:pos2, doc8:pos1]
"лис"      → [doc1:pos3, doc3:pos5, doc7:pos2]
"прыга"    → [doc1:pos4, doc4:pos6]
"ленив"    → [doc1:pos6, doc6:pos3]
"собак"    → [doc1:pos7, doc2:pos4, doc9:pos5]

Типы индексов

Индекс по словам (Term Index): Базовый. Слово → список документов.

Индекс с позициями (Positional Index): Слово → (документ, позиция). Позволяет искать фразы.

Индекс с частотами (Frequency Index): Слово → (документ, частота). Позволяет ранжировать по TF.

Индекс n-грамм (n-gram Index): Разбивает слова на части. Позволяет искать по части слова или с опечатками.

"хлеб" → 2-граммы: "хл", "ле", "еб"
Запрос "хлебный" → 2-граммы: "хл", "ле", "еб", "бн", "ны" → пересечение

Анализ текста (Text Analysis)

Компоненты анализатора

Tokenizer (Токенизатор): Разбивает текст на токены.

TokenizerПример
standard“Иван, Петр!” → [“Иван”, “Петр”]
whitespace“Иван Петр” → [“Иван”, “Петр”]
letter“Иван123” → [“Иван”]
keyword“Иван Петр” → [“Иван Петр”] (один токен)

Token Filter (Фильтры): Преобразуют токены.

ФильтрПример
lowercase“ИВАН” → “иван”
stop (стоп-слова)“и”, “в”, “на” → удалить
stemmer (стемминг)“бежал” → “беж”
synonym (синонимы)“iPhone” → “айфон”
ngram (n-граммы)“хлеб” → “хл”, “ле”, “еб”

Пример настройки анализатора в Elasticsearch

PUT /my_index
{
  "settings": {
    "analysis": {
      "analyzer": {
        "russian_analyzer": {
          "type": "custom",
          "tokenizer": "standard",
          "filter": ["lowercase", "russian_stop", "russian_stemmer"]
        }
      },
      "filter": {
        "russian_stop": {
          "type": "stop",
          "stopwords": "_russian_"
        },
        "russian_stemmer": {
          "type": "stemmer",
          "language": "russian"
        }
      }
    }
  },
  "mappings": {
    "properties": {
      "title": {
        "type": "text",
        "analyzer": "russian_analyzer"
      }
    }
  }
}

Типы запросов

Match (полнотекстовый поиск)

// Простой поиск (AND по умолчанию)
{ "match": { "content": "хлеб дрожжи" } }

// OR
{ "match": { "content": { "query": "хлеб дрожжи", "operator": "or" } } }

// Фраза (точный порядок слов)
{ "match_phrase": { "content": "как испечь хлеб" } }

Bool (комбинация условий)

{
  "bool": {
    "must": [                    // AND (должны совпадать)
      { "match": { "status": "published" } }
    ],
    "should": [                  // OR (повышают релевантность)
      { "match": { "title": "хлеб" } },
      { "match": { "content": "хлеб" } }
    ],
    "must_not": [                // NOT
      { "term": { "author": "спам" } }
    ],
    "filter": [                  // Фильтр (без влияния на релевантность)
      { "range": { "published_at": { "gte": "2024-01-01" } } }
    ]
  }
}

Fuzzy (поиск с опечатками)

// Расстояние Левенштейна = 2 (опечатки)
{ "match": { "name": { "query": "хлебны", "fuzziness": 2 } } }
// Найдет "хлебный", "хлебной", "хлебцы"...

Wildcard (поиск по маске)

{ "wildcard": { "name": "хлеб*" } }  // хлеб, хлебный, хлебопечка

Prefix (поиск по префиксу)

{ "prefix": { "name": "хлеб" } }

Range (диапазон)

{ "range": { "price": { "gte": 100, "lte": 1000 } } }

Geo (геопоиск)

{
  "geo_distance": {
    "distance": "10km",
    "location": { "lat": 55.75, "lon": 37.61 }
  }
}

Агрегации (Analytics)

Поисковые движки умеют не только искать, но и агрегировать результаты.

GET /sales/_search
{
  "query": { "match": { "product": "хлеб" } },
  "aggs": {
    "by_city": {                           // Группировка по городу
      "terms": { "field": "city" }
    },
    "by_price_range": {                    // Гистограмма по цене
      "histogram": { "field": "price", "interval": 100 }
    },
    "avg_price": {                         // Средняя цена
      "avg": { "field": "price" }
    },
    "sales_over_time": {                   // Временной ряд
      "date_histogram": {
        "field": "sold_at",
        "interval": "day"
      }
    }
  }
}

Результат:

{
  "aggregations": {
    "by_city": {
      "buckets": [
        { "key": "Москва", "doc_count": 150 },
        { "key": "СПб", "doc_count": 80 }
      ]
    },
    "avg_price": { "value": 45.5 }
  }
}

Когда использовать поисковые движки

Идеальные сценарии

СценарийПочему подходит
Поиск по сайту (интернет-магазин, блог, документация)Быстрый полнотекстовый поиск, автодополнение, опечатки
Поиск по логам (ELK Stack)Миллиарды логов, мгновенный поиск, агрегации
Поиск по документам (база знаний, юридические документы)Текстовые запросы, ранжирование по релевантности
Геопоиск (магазины рядом, точки интереса)Встроенная геопространственная поддержка
Аналитика логов (ошибки, метрики)Агрегации, временные ряды, дашборды
Рекомендательные системы (похожие товары)“More like this” запросы
Поиск с опечатками (пользователь может ошибиться)Fuzzy search “из коробки”

Сомнительные сценарии

СценарийПочему плохо подходит
Система транзакций (OLTP)Нет ACID, нет сложных транзакций
Хранилище “источник истины” (source of truth)Потеря данных при сбое (может быть)
Данные с жесткой схемой и связямиРеляционные БД подходят лучше
Очень маленькие объемы (сотни документов)Оверхед; можно использовать LIKE в SQL

Сравнение поисковых движков

ХарактеристикаElasticsearchSolrTypesenseManticore
ЯзыкJSON DSLXML/JSONJSONSQL
ИнтерфейсREST APIREST API / Admin UIREST APISQL / HTTP
МасштабированиеГоризонтальноеГоризонтальноеГоризонтальноеГоризонтальное
РанжированиеBM25 (настраиваемый)BM25 (настраиваемый)Ranker (правила)BM25
АгрегацииДаДаОграниченноДа
ВизуализацияKibanaBanana / GrafanaНетНет
СкоростьОчень высокаяВысокаяОчень высокая (in-memory)Высокая
СложностьСредняяВысокаяНизкаяСредняя
ПопулярностьОчень высокаяСредняяРастущаяНизкая

Распространенные ошибки

Ошибка 1: Использование Elasticsearch как основной БД

Хранение единственной копии данных в Elasticsearch, использование его как “источника истины”.

Как исправить: Elasticsearch — для поиска. Храните основную копию в реляционной или документной БД. Синхронизируйте данные через CDC или двойную запись.

Ошибка 2: Неправильный анализ текста (не тот язык)

Поиск по русским текстам с анализатором по умолчанию (standard). “бежал” не найдет “бежать”.

Как исправить: Настройте анализатор для вашего языка (stemmer, stop words).

Ошибка 3: Слишком много шард (shards)

Создание тысячи шард для небольшого объема данных (каждая шарда — накладные расходы).

Как исправить: 1 шарда на 10-50 ГБ данных. Для небольших проектов 1-5 шард достаточно.

Ошибка 4: Игнорирование реплик

Одна копия данных, потеря шарды — потеря данных.

Как исправить: Всегда используйте реплики (min 1 реплика, лучше 2).

Ошибка 5: Поиск по полю, которое не индексировано

// Поле не индексировано (index: false)
PUT /users/_mapping
{
  "properties": {
    "email": { "type": "text", "index": false }
  }
}

// Поиск по email не работает
GET /users/_search
{
  "query": { "match": { "email": "ivan@example.com" } }  // Ошибка!
}

Как исправить: Убедитесь, что поле имеет index: true (по умолчанию true).

Резюме для системного аналитика

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

  2. Elasticsearch — безусловный лидер. Самый популярный, богатая экосистема (Kibana, Logstash, Beats), мощный Query DSL, агрегации, масштабирование.

  3. Основные этапы работы: индексация (токенизация, нормализация, стемминг, построение индекса) → поиск (анализ запроса, поиск в индексе, ранжирование).

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

  5. Анализ текста критически важен. Неправильный токенизатор или отсутствие стемминга для русского языка сделают поиск бесполезным.

  6. Поисковые движки не заменяют реляционные БД. Они для поиска, не для ACID-транзакций. Основное хранилище — PostgreSQL/MongoDB, поисковый движок — для индексации и поиска.

Проверка знаний

Вопрос 1 из 4
Что такое поисковый движок как класс хранилища?
Когда поисковый движок особенно полезен?
Почему поисковый движок не всегда заменяет основную БД?
Что важно учитывать в поисковых системах?

Вопросы, где были ошибки