Привет, меня зовут Ирина, я тестировщик в продуктовой команде iSpring.
В этой статье я на реальном примере интеграции OpenSearch в LMS iSpring Learn расскажу, как протестировать полнотекстовый поиск, сохранив баланс между качеством и трудозатратами. Мы не только разберём базовые проверки, но и погрузимся в тестирование стемминга, релевантности, работы в распределённой системе и отказоустойчивости. Материал будет полезен тестировщикам и разработчикам, которые хотят понять, что скрывается за фразой «протестировать поиск».
Введение
Можно найти много информации о том, как настроить OpenSearch/Elasticsearch, однако крайне мало готовых решений, объясняющих, как эффективно протестировать интеграцию с новым сервисом, какие аспекты требуют особой проработки, и на что стоит обращать внимание при проверке работы поиска.
Я поделюсь опытом интеграции OpenSearch с web-продуктом, чтобы другим тестировщикам было проще избежать возможных ошибок и сэкономить время на этапе проверок.
Контекст интеграции — с чем работали?
Модуль, в который был интегрирован OpenSearch, представляет собой базу знаний. Наша база знаний — хранилище с несколькими уровнями вложенности (пространства, папки) и разными типами материалов: статьи (лонгриды), документы и ссылки.

Часть 1: От SQL-запросов к OpenSearch — зачем мы это делали
Проблема «старого» поиска
Раньше поиск по хранилищу был реализован с помощью простых SQL-запросов, которые имели следующие ограничения:
Отсутствие поиска внутри статей — поиск производился только по заголовкам и описанию.
Отсутствие поиска по нечёткому соответствию, что требовало от пользователей запоминания точных фраз.
Старый поиск был неоптимально реализован на базе MySQL и имел ограничения по скорости работы с большими массивами данных.
Основная задача «умного» поиска
Обеспечить полнотекстовый поиск по всем типам контента (пространства, папки, статьи, ссылки, документы) с учётом структуры статей (24 типа блоков – таблицы, цитаты, интерактивные блоки).
Среди всех доступных решений выбрали именно OpenSearch, потому что:
а) он позволял не писать свой движок поиска.
б) OpenSearch достаточно гибкий.
в) Он больше подходил нам по юридическим аспектам (связанным с лицензиями).
OpenSearch позволил реализовать полнотекстовый поиск по частям слов и фраз, а не только по точным совпадениям. Дополнительно нужно было подружить сторонний сервис с особенностями нашего продукта и научить его отличать статусы статьи (чтобы черновики не индексировались), учитывать зоны видимости для различных групп пользователей и департаментов. И самое сложное – настроить поддержку до 32 языков, включая сложные языковые системы.
Часть 2: «Кухня» OpenSearch для тестировщика
Если вы только знакомитесь с «кухней» OpenSearch, то кратко я расскажу, с чем мы имеем дело. Если уже знакомы, смело можно перескочить пару абзацев.
Основная сложность для нас, как для QA, заключалась в смене парадигмы: мы перешли от реляционной SQL-базы к нереляционной поисковой системе OpenSearch. Чтобы говорить на одном языке с разработчиками и эффективно строить стратегию тестирования, важно было сначала чётко понять базовые сущности OpenSearch.
Для наглядности я сопоставила их с привычными SQL-концепциями в таблице ниже. Это не идеальная аналогия, но она отлично помогает сориентироваться.
Концепция в SQL |
Эквивалент в OpenSearch |
Что это значит для QA |
База данных |
Индекс |
Главный контейнер для данных. В нашем случае – около сотни индексов на проде. |
Таблица | ||
Строка |
Документ |
JSON-объект, который мы индексируем и по которому ищем. Каждый документ имеет уникальный ID. |
Колонка/поле |
Поле |
Пара ключ-значение в документе с определённым типом данных (строка/ число/массив и тд) |
Схема таблицы/БД |
Маппинг |
«Схема» индекса. Определяет структуру и типы данных каждого поля в документах индекса. Задаётся при создании индекса либо автоматически обновляется при записи новых документов. |
Теперь кратко о том, как происходит процесс индексации и поиска:
Токенизация: Предварительная обработка текста перед индексацией. Текст разбивается на лексемы (токены): слова, числа, символы. Например, «Hello world» → [Hello, world].
Нормализация: Приведение токенов к нижнему регистру, удаление стоп-слов («и», «в», «на»).
Стемминг: Приведение слова к основе: например, «тестирования» → «тестирован». Это позволяет поиску находить разные формы одного и того же слова.
Далее формируется список всех уникальных слов, данные о позициях этих слов в документе и перечень документов, в которых они встречаются.
Ниже привожу примеры запросов, которые позволяют оценить, как работает поиск.
Для выполнения запросов понадобится развёрнутый и настроенный сервис, а также данные авторизации.
1. Запросом /_cat/indices можно посмотреть, сколько индексов содержится в кластере и в каком они состоянии:
GET https://localhost:9200 /_cat/indices
Response
health: green //все сегменты работают в штатном режиме;
status: open //Индекс открыт и доступен для чтения и записи;
index: {{index_id}} //Имя индекса, задается пользователем;
uuid: {{uuid}} //Уникальный идентификатор индекса, генерируемый системой при создании;
pri: 1 //Количество первичных шардов на одном узле;
rep: 1 //Количество реплик(копий);
docs.count: 1873 //Количество документов, содержащихся в данном индексе;
docs.deleted: 132 //Количество удалённых документов, которые всё ещё занимают физическое пространство;
store.size: 7mb //Общий размер хранилища;
pri.store.size: 3.4mb //Размер физического хранилища, занятого только первичными шардами (без учёта реплик);
2. Чтобы понять, как слова разбиваются на токены, можно воспользоваться запросом /_analyze.
POST https://localhost:9200/_analyze
{
"analyzer": "standard",
"text": "Больше! Нужно больше кофе!"
}
RESPONSE
"tokens": [
{"token": "больше","start_offset": 0,"end_offset": 6, "type": "<ALPHANUM>","position": 0},
{"token": "нужно","start_offset": 8,"end_offset": 13,"type": "<ALPHANUM>","position": 1},
{"token": "больше","start_offset": 14,"end_offset": 20,"type": "<ALPHANUM>","position": 2},
{"token": "кофе","start_offset": 21,"end_offset": 25,"type": "<ALPHANUM>","position": 3}
]
Пояснение
где:
Start_offset: Начальная позиция токена (номер первого символа токена).
End_offset: Конечная позиция токена (позиция символа сразу после конца токена)
Type: категория или класс токена. <ALPHANUM> (число+буквы), <NUM> (числа), <HANGUL> (корейские символы), <PUNCT> (пунктуация).
3. Если нужно узнать, как происходит стемминг, то есть как слово будет храниться в базе в виде основы, в этом же запросе можно изменить тело запроса и добавить, например, стеммер для английского языка porter_stem.
POST https://localhost:9200/_analyze
{
"tokenizer": "standard",
"filter": ["lowercase", "porter_stem"],
"text": "testing"
}
RESPONSE
{
"tokens": [
{
"token": "test",
"start_offset": 0,
"end_offset": 7,
"type": "<ALPHANUM>",
"position": 0
}
]
}
4. Получение списка установленных фильтров.
Этот запрос вернёт информацию обо всех загруженных модулях и расширениях, включая фильтры, токенизаторы и анализаторы.
GET https://localhost:9200 /_cat/plugins?v=true&format=json
RESPONSE
...
{
"name": "opensearch-3",
"component": "analysis-stempel",
"version": "3.2.0"
}
...
Пояснение
где:
analysis-stempel – плагин для обработки морфологии польского языка.
Таким образом, запросы analyze и cat/plugins помогут ближе познакомиться с тем, как устроен процесс индексации, «заглянуть под капот» и понять, почему один запрос находит документ, а другой – нет.
Часть 3: Тестирование целостности данных и индексации
Зачастую невозможно начать тестирование сразу с UI. Но благодаря API можно проверить, что данные попадают в OpenSearch корректно.
Перед началом тестирования нужно определить, какие события отправляются в сервис OpenSearch, а какие – нет. К примеру, создание черновика статьи в нашем продукте не отправляет данные на индексацию, а вот публикация статьи – да.
Также необходимо проверить, что система не создаёт дубликатов при повторной отправке запроса и чистит данные при удалении источника.
Самый быстрый способ проверить, что событие создания/копирования сущностей нужным образом обрабатывается поиском, – это отправка запроса /_count:
GET htps://localhost:9200/_count
RESPONSE
{
"count": 114308,
"_shards": {
"total": 60,
"successful": 60,
"skipped": 0,
"failed": 0
}
}
Проверяли, что при публикации статьи count увеличивается на 1. При удалении → уменьшается. При перемещении → не меняется. При создании черновика → не меняется.
Затем мы углубились в проблемы, которые не видны на поверхности.
Проблема №1: уникальность ID текстовых блоков
В нашем продукте внутри статей может помещаться внушительный объём текстовой информации. Весь контент внутри статей разделён на блоки с разными свойствами: это может быть таблица или текст на фоне картинки, блок с кодом, интерактивный блок теста и многое другое. И так как наш поиск завязан на ID блоков (именно через них происходит процесс превращения страницы в токены), нам пришлось уделить вопросу уникальности ID больше внимания.
Суть: Изначально ID текстового блока в статье был 5-значным. При индексации сотен статей в рамках одного аккаунта мы получали миллионы таких блоков. Вероятность коллизии для идентификаторов, состоящих из 5 символов, (16^5 ≈1 млн) становилась неприемлемо высокой. Мы могли потерять данные.
Решение и тестирование: Мы перешли на 32-значные UUID. Это потребовало не просто сменить тип поля, а провести большое тестирование на регресс.
Какие выводы сделали: При планировании всегда выделяйте достаточно времени на регрессионные тесты.
Проблема №2: шардирование БД приложения
Суть: В продакшене база данных нашего приложения была разбита на несколько шардов. В тестовой среде — всего один. Весь код, включая задачи первичной индексации, был написан и протестирован в этой среде.
Инцидент: При первом запуске на продакшне таск индексации не смог корректно обойти все шарды БД. Данные из некоторых шардов не попали в OpenSearch. Поиск для части клиентов не работал.
Какие выводы сделали:
Тестируйте на схожей инфраструктуре. Мы настроили стенд, максимально повторяющий продакшн, с несколькими шардами БД.
Пишите интеграционные тесты, которые проверяют не просто факт отправки события, а его прохождение по всей цепочке: от БД приложения до появления документа в OpenSearch.
Тестируйте отказоустойчивость. Мы заранее предусмотрели и протестировали автоматическое переключение на старый поиск при проблемах с OpenSearch. Это спасло пользовательский опыт в критической ситуации.
Проблема №3: очистка данных
Тестировщику важно помнить не только о комфортной и точной работе пользователя с поиском, но и о том, чтобы хранилище (индекс) не переполнялось неиспользуемыми данными. Важным моментом была организация чистки индексов после удаления материалов или целых аккаунтов.
Суть: В ходе проверок обнаружилось, что после полного удаления аккаунта из базы данных, в Opensearch отдавался весь его проиндексированный контент. Разработчикам предстояло доработать механизм чистки индекса при удалении аккаунта из базы продукта.
Нашли тоже запросом через API:
POST https://localhost::9200/{{index_id}}/_search
{
"size": 10000,
"query": {
"match": {
"accountId": "{{account_id}}"
}
}
}
Пояснение
где account_id – идентификатор удалённого из базы аккаунта.
В ответе получили полный список индексированных сущностей, что недопустимо.

Какие выводы сделали:
Всегда продумывать, что будет с собранными данными после того, как они стали неактуальными.
Часть 4: Интеллект поиска — тестирование релевантности и ранжирования
Здесь заканчивается функциональное тестирование и начинается тестирование «интеллекта» системы.
Система OpenSearch (и её предшественник Elasticsearch) использует концепции TF-IDF или BM25 для ранжирования результатов поиска.
При индексации документа OpenSearch строит обратный индекс (Inverted Index), содержащий статистику частоты каждого термина в документах и частоту самого термина по всему пулу документов. Затем, при выполнении запросов, система использует полученные данные для расчёта веса документов.
Разработчик может дополнительно управлять параметрами (например, включать boost для конкретных полей или регулировать влияние частотности термина на общую оценку документа).
Проще говоря, каждый поисковый термин оценивается в соответствии со следующими правилами:
Частота: поисковый термин, который встречается в документе чаще, будет иметь более высокую оценку.
Редкость: поисковый термин, который встречается в большем количестве документов, будет, как правило, оцениваться ниже. Например, между терминами «физика» и «орбиталь» OpenSearch отдаст предпочтение документам, которые содержат менее распространённое слово «орбиталь».
Нормализация длины: соответствие более длинному документу должно оцениваться ниже, чем соответствие короткому документу. Документ, содержащий полный словарь, будет соответствовать любому слову, но не будет достаточно релевантным ни одному конкретному слову.
Что мы настраивали:
Разработчики настраивали веса полей, чтобы заголовок (name) был важнее контента (textContent) и описания материала (description). Дополнительно в спеке был запрос на то, чтобы слова, находящиеся ближе к началу документа, были выше в выдаче.
Для тестирования мы создали специальный набор тестовых документов:
Короткий документ: «Вектор орбитали электрона».
Длинный документ: Большая научная статья, где слово «орбиталь» встречается 15 раз в разных блоках.
Документ-словарь: Глоссарий, где есть термин «орбиталь» среди тысяч других терминов.
Короткий документ, в котором слово «орбиталь» стоит в самом начале, но не в названии.
Документ с названием «Орбиталь: Путешествие в Мир Атомов»
Запрос: орбиталь
Ожидаемый порядок: [5, 4, 1, 2, 3].
Проверяется ценность документа по названию, по позиции токена, а также по длине документа. Короткий документ, целиком посвящённый термину, релевантнее длинного, где термин «размазан», и уж точно релевантнее словаря.
Кейс: Стоп-слова, которые собрались в предложение
Проблема: Мы обнаружили, что стандартный анализатор игнорирует стоп-слова. Это привело к забавному багу: название статьи «Сегодня и завтра» полностью игнорировалось поиском, так как состояло из стоп-слов.
Стоп-слова можно найти по ссылке в документации: russian_stop.
В тестировании мы поигрались с составлением из слов списка предложений (эти варианты не претендуют на грамматическую правильность и приведены здесь лишь для иллюстрации несовершенства реализации):

«Сегодня мне наконец можно».
«Как сказать лучше про жизнь».
«Всего два нельзя или как быть человек».
OpenSearch, найди статьи!
Убедились, что поиск ничего не нашёл. Но предложения из стоп-слов явно являются не клиентским кейсом, а скорее тестерским, поэтому настройки поиска мы не меняли. Наша задача как QA — не только найти баг, но и понять его природу и оценить вероятность и критичность.
Часть 5: Автоматизация, нагрузка и итоги
Автоматизация
Мы использовали WDIO для UI-тестов, но самая ценная автоматизация была на уровне API.
Покрыли самый базовый сценарий: Создать статью через API → выполнить поисковый запрос UI → проверить по API наличие документа в результатах с набором требуемых полей в ответе → удалить статью → проверить отсутствие документа.
Такой незамысловатый кейс ускорил базовую проверку функционирования фичи, которая проходит за 2 секунды вместо 2 минут ручных тестов через интерфейс.
Нагрузочное тестирование
Наша цель: убедиться, что система не только функциональна, но и остаётся стабильной, отзывчивой и предсказуемой при росте числа пользователей и объёма данных.
При тестировании отправляли большое число запросов по созданию материалов, а также проверяли поисковые запросы, достающие из хранилища информацию. Следили за скоростью ответа и отсутствием зависания в очереди.
Команда нагрузочного тестирования симулировала работу десятков тысяч пользователей, используя скрипты, которые параллельно:
Создавали и модифицировали контент, генерируя поток событий индексации.
Выполняли разнообразные поисковые запросы (от простых однострочных до сложных фразовых), чтобы имитировать реальное использование.
Для контроля за состоянием сервиса мы завели дашборд в Grafana, где можно было контролировать нагрузку и наличие отправляемых на сервер запросов.


Используемые метрики
Search RPS (Request per second) — количество поисковых запросов в секунду на сервер. По-другому: интенсивность использования поиска.
Indexing RPS — количество индексирующих запросов в секунду на сервер. Показывает интенсивность операций обновления индекса (создание, изменение, удаление материалов). Эта метрика отражает «фоновую» нагрузку на систему.
Search Latency — время, которое требуется на отправку поискового запроса и полную загрузку ответа. Разбивается на 3 группы: 99, 95, 90 перцентилей (P99, P95, P90).
Indexing Latency — соответственно время, которое требуется на отправку индексирующих запросов и полную загрузку ответа.
Если P95 равен 7 сек, а P99 — 15 сек, это означает, что 95% пользователей получают ответ за 7 сек, но 4% (разница между P99 и P95) сталкиваются с задержкой в 15 сек.
Наш фокус был на P90 и P95, так как рост этих метрик — первый сигнал о том, что сервис перестаёт справляется с пиковой нагрузкой. Например, рост показателя P95 с 7 сек до 10 сек на панели Search Latency означает снижение производительности поиска на выдачу данных и первый сигнал о том, что сервис перестаёт справляется с нагрузкой. P99 часто показывает различные аномалии, но стабильно высокий P95 — это уже системная проблема.
Пример на наших данных:
90% запросов получают ответ от поиска за 4,5 сек и быстрее
95% запросов были выполнены за ≤ 7.5 сек и быстрее
99% всех поисковых запросов были выполнены за ≤ 9.5 сек
В ходе тестов мы внимательно следили за дашбордом.
Стабильность RPS: убеждались, что система может поддерживать высокий RPS продолжительное время без деградации.
Рост задержек: наблюдали, как увеличиваются P90 и P95 при росте RPS. Цель — найти «точку излома», после которой задержки начинают расти нелинейно.
Сервис создания контента, который генерировал события для индексации, начал отказывать раньше, чем OpenSearch исчерпал свои ресурсы. Он не выдерживал того же уровня параллелизма, что и наш поисковый кластер.
Этот результат нас скорее обрадовал, чем разочаровал. Он наглядно продемонстрировал, что архитектура обладает запасом прочности, а сам сервис поиска является отказоустойчивым и стабильным. Вопросов по его готовности к высоким нагрузкам у команды больше не возникло.
Заключение
Интеграция OpenSearch — это путь от тестирования функции к тестированию сложной распределённой системы. Нам пришлось научиться:
Мыслить данными: понимать, что попадает в индекс и в каком виде.
Тестировать «интеллект»: проверять не просто факт нахождения, а релевантность выдачи.
Предвидеть масштабирование: тестировать на инфраструктуре, близкой к продакшену.
В результате мы получили не просто «поиск», а мощный инструмент навигации по знаниям, который действительно помогает пользователям находить нужную информацию за секунды.
С какими сложностями при тестировании поиска сталкивались вы? Делитесь опытом в комментариях