
В этой статье мы подробно рассмотрим все ключевые параметры OpenSearch, включая дашборды, документы, индексы, узлы, кластеры, шардирование, инвертированные индексы и сам процесс индексации. Понимание этих аспектов позволит максимально эффективно использовать OpenSearch для решения задач поиска и анализа данных в любых проектах.
Привет, Хабр! Меня зовут Евгений Ляшенко, я старший разработчик IBS. В эпоху, когда объемы данных растут с каждым днем, эффективный поиск информации становится критически важным для бизнеса и разработчиков. OpenSearch как мощный инструмент для полнотекстового поиска и аналитики предлагает гибкие решения для работы с большими массивами данных. Чтобы наглядно продемонстрировать его работу, я создал pet-проект с поиском по библиотеке книг и фильмов. Но сначала немного теории.
Что такое OpenSearch. История появления и определение
В истоках OpenSearch — драматичное противостояние двух ИТ-гигантов — Elastic NV и Amazon Web Services (AWS). Все началось в 2010 году с появлением Elasticsearch. Флагманский продукт Elastic NV, выпущенный под лицензией Apache 2.0 с открытым исходным кодом, быстро стал предпочитаемой корпоративной поисковой системой во всем мире. В пакете с движком для анализа логов Logstash и платформой аналитики и визуализации Kibana, Elasticsearch отлично подходил для мониторинга приложений, анализа журналов безопасности и отслеживания поведения пользователей.
Осознав потенциал решения, в 2015 году Amazon представила Amazon Elasticsearch Service — управляемый облачный сервис, позволяющий пользователям AWS развертывать масштабируемые кластеры Elasticsearch и управлять действиями с данными в облаке.
Elastic NV была, мягко говоря, не в восторге. Компания обвинила Amazon в нарушении прав на товарный знак и вводящем в заблуждение маркетинге, что привело к судебному иску в 2019 году. Спор достиг кульминации в 2021 году. В январе 2021 года Elastic NV перевела лицензирование Elasticsearch на Server Side Public License (SSPL) и Elastic License, ни одна из которых не является лицензией с открытым исходным кодом. Этот шаг был направлен на то, чтобы помешать компаниям, включая Amazon, предлагать Elasticsearch как услугу без партнерства с ее создателями.
Через несколько месяцев, в апреле 2021 года, AWS разветвила последнюю доступную версию Elasticsearch с открытым кодом (7.10.2), инициировав тем самым новый проект — OpenSearch. Так Amazon обошла изменения в лицензировании и продолжила предлагать своим облачным клиентам решения распределительной поисково-аналитической системы.
Собственно, и Elasticsearch, и AWS OpenSearch предназначены для обработки больших объемов данных и предоставления быстрых и надежных результатов поиска. Оба инструмента используют одну и ту же базовую библиотеку Apache Lucene и предлагают схожие возможности, такие как шардинг, репликация и распределенная архитектура, для обеспечения высокой производительности.
Однако у OpenSearch есть дополнительное преимущество. Будучи частью полностью управляемого сервиса, OpenSearch может использовать глобальную инфраструктуру AWS для повышения производительности, масштабируемости и надежности. Amazon предоставляет инструменты мониторинга производительности, автоматизированное резервное копирование и функции аварийного восстановления как часть сервиса OpenSearch.
Стек технологий OpenSearch
Помимо собственно поисковика, в стек OpenSearch входят Logstash (через плагин вывода) и OpenSearch Dashboards. В совокупности с битами (beats) — легковесными отправителями данных, такими как Filebeat, Metricbeat или Winlogbeat, — они образуют полный цикл управления логами, то есть сбор и систематизацию поиска.
Вот как выглядит схема взаимодействия:

Этому стеку можно найти и множество других применений, но в большой компании речь идет главным образом именно о логах. Логи обычно транспортируют с серверов, на которых они возникли, на сервер OpenSearch в виде сообщений. Чтобы гарантировать их доставку, в качестве транспорта обычно используют распределенный программный брокер сообщений Apache Kafka. На серверы, с которых можно собирать логи, мы устанавливаем биты. Они отправляют сообщения с логами в Kafka, Logstash берет эти сообщения из Kafka, обрабатывает и отправляет их в OpenSearch. Пользователь затем осуществляет просмотр и поиск по логам через интерфейс OpenSearch Dashboards.
Документ, индекс, ноды, кластеры и шарды
Перейдем к тому, как представлены данные в OpenSearch. Document — это единица, которая хранит информацию, то есть текст или структурированные данные. Когда мы ищем информацию, OpenSearch возвращает документы, соответствующие нашему запросу, в формате JSON. Например, в школьной реляционной базе данных документ может представлять одного учащегося как одну строку в таблице.
Index — это совокупность документов. Например, в базе данных школы индекс может представлять собой таблицу всех учеников.
OpenSearch разработан как распределенная поисковая система. Это означает, что он может работать на одном или нескольких узлах (nodes) — серверах, которые хранят данные и обрабатывают поисковые запросы.
Кластер (cluster) OpenSearch — это набор узлов. В каждом кластере есть выбранный узел, который организует операции на уровне кластера, такие как создание индекса. Узлы взаимодействуют друг с другом, поэтому, если запрос пользователя направлен на один узел, он отправляет запросы к другим узлам, собирает ответы и возвращает окончательный ответ.
OpenSearch разбивает индексы на сегменты, они же шарды (shards). Каждый сегмент хранит подмножество всех документов в индексе. Шарды используются для равномерного распределения нагрузки по узлам в кластере. Например, индекс размером 400 ГБ может быть слишком большим для обработки любым отдельным узлом в кластере, но, разделив его на 10 сегментов, по 40 ГБ каждый, OpenSearch может распределить шарды по 10 узлам и управлять каждым шардом по отдельности.
Инвертированный индекс
Индекс OpenSearch использует структуру данных, называемую инвертированным индексом. Inverted index сопоставляет слова с документами, в которых они встречаются.
Например, рассмотрим индекс, содержащий два документа:
Документ 1: «Красота в глазах смотрящего»,
Документ 2: «Красавица и чудовище».

Помимо идентификатора документа, OpenSearch сохраняет положение слова в документе для выполнения фразовых запросов, где слова должны располагаться рядом друг с другом.
Оценка релевантности
Когда мы ищем документ, OpenSearch сопоставляет слова в запросе со словами в документах. Например, по запросу «красота» OpenSearch вернет нам документы 1 и 2. Каждому документу присваивается Relevance — оценка релевантности, которая показывает, насколько хорошо документ соответствует запросу. Для расчета оценок релевантности документа OpenSearch использует алгоритм ранжирования BM25.
Отдельные слова в поисковом запросе называются поисковыми терминами. Каждый поисковый термин оценивается в соответствии со следующими правилами:
Частота: поисковый термин, который встречается в документе чаще, будет иметь более высокую оценку.
Редкость: поисковый термин, который встречается в большем количестве документов, будет, как правило, оцениваться ниже. Например, между терминами «мама» и «орбиталь» OpenSearch отдаст предпочтение документам, которые содержат менее распространенное слово «орбиталь».
Нормализация длины: соответствие более длинному документу должно оцениваться ниже, чем соответствие короткому документу. Документ, содержащий полный словарь, будет соответствовать любому слову, но не будет очень релевантным ни одному конкретному слову.
Демонстрация на примере проекта
Перейдем от теории к практике. Чтобы показать, как устроен OpenSearch, я написал проект для полнотекстового поиска по библиотеке фильмов и книг (вот ссылка на GitHub):
version: '2'
services:
opensearch:
build: .
container_name: marketplace_os
hostname: marketplace_os
ports:
- 9200:9200
- 9600:9600
environment:
- discovery.type=single-node
- "DISABLE_SECURITY_PLUGIN=true"
- "OPENSEARCH_JAVA_OPTS=-Xms512m -Xmx512m"
- network.host=0.0.0.0
healthcheck:
test: ["CMD-SHELL", "curl -f http://localhost:9200 || exit 1"]
interval: 10s
timeout: 5s
retries: 10
opensearch-dashboards:
image: opensearchproject/opensearch-dashboards:1.1.0
container_name: marketplace_os_dashboards
ports:
- 5601:5601
expose:
- "5601"
environment:
OPENSEARCH_HOSTS: '["http://opensearch:9200"]'
DISABLE_SECURITY_DASHBOARDS_PLUGIN: "true"
depends_on:
opensearch:
condition: service_healthy
В настройках я задал container_name и hostname, сделал маппинг полей из контейнера на свой локальный компьютер, в environment указал, что буду использовать только один узел, отключил плагин безопасности и сделал проверку доступности (healthcheck).
Для начала развернем сервер OpenSearch и OpenSearch Dashboards с помощью Docker Desktop (можно docker-compose.yml).
Далее запускаем само приложение. Оно написано на SpringBoot версии 3.1.2, Java 17, springdoc-openapi-starter-webmvc-ui 2.1.0, spring-data-opensearch-starter 1.2.0.
Теперь расскажу и покажу, как использовать API.
package org.opensearch.data.example.model;
import lombok.Getter;
import org.springframework.data.annotation.Id;
import org.springframework.data.elasticsearch.annotations.Document;
@Getter
@Document(indexName = "books")
public class Book {
@Id
private String id;
private String title;
private int publicationYear;
private String authorName;
private String isbn;
public void setTitle(String title) {
this.title = title;
}
public void setPublicationYear(int publicationYear) {
this.publicationYear = publicationYear;
}
public void setAuthorName(String authorName) {
this.authorName = authorName;
}
public void setIsbn(String isbn) {
this.isbn = isbn;
}
}
Чтобы задать индекс “books”, мы можем описать его с помощью аннотации document. И как раз увидеть, что OpenSearch использует под капотом Elasticsearch. Книги у нас будут храниться непосредственно в самом OpenSearch.
package org.opensearch.data.example.repository;
import org.opensearch.data.example.model.Book;
import org.springframework.data.elasticsearch.repository.ElasticsearchRepository;
import org.springframework.stereotype.Repository;
import java.util.List;
import java.util.Optional;
@Repository
public interface BookRepository extends ElasticsearchRepository<Book, String> {
List<Book> findByAuthorName(String authorName);
List<Book> findByTitleAndAuthorName(String title, String authorName);
Optional<Book> findByIsbn(String isbn);
}
На уровне репозитория мы также видим Elasticsearch, хотя формально и работаем с библиотекой OpenSearch.
Принцип работы довольно прост. Как и на любом уровне репозитория, мы можем расширяться от ElasticsearchRepository, прокидывая непосредственно саму модель и ее ключ к BookRepository.
В config мы просто указываем клиентскую конфигурацию с localhost, с которым мы будем работать.
package org.opensearch.data.example.config;
import org.opensearch.client.RestHighLevelClient;
import org.opensearch.data.client.orhlc.AbstractOpenSearchConfiguration;
import org.opensearch.data.client.orhlc.ClientConfiguration;
import org.opensearch.data.client.orhlc.RestClients;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class RestClientConfig extends AbstractOpenSearchConfiguration {
@Override
@Bean
public RestHighLevelClient opensearchClient() {
final ClientConfiguration clientConfiguration = ClientConfiguration.builder()
.connectedTo("localhost:9200")
.build();
return RestClients.create(clientConfiguration).rest();
}
}
В BookController используются все доступные методы, с помощью которых пользователь может подгружать книгу в библиотеку, забирать по определенным параметрам, то есть по запросу, по заголовку, по автору и т. д.
package org.opensearch.data.example.controller;
import jakarta.validation.Valid;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Positive;
import lombok.Getter;
import org.opensearch.data.example.metadata.PublicationYear;
import org.opensearch.data.example.model.Book;
import org.opensearch.data.example.service.BookService;
import org.opensearch.data.example.service.exception.BookNotFoundException;
import org.opensearch.data.example.service.exception.DuplicateIsbnException;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.*;
import java.util.List;
@RestController
@RequestMapping("/books")
public class BookController {
private final BookService bookService;
public BookController(BookService bookService) {
this.bookService = bookService;
}
@ResponseStatus(HttpStatus.OK)
@GetMapping(value = "/find_all")
public List<Book> getAllBooks() {
return bookService.getAll();
}
@ResponseStatus(HttpStatus.ACCEPTED)
@PostMapping
public Book createBook(@Valid @RequestBody BookDto book) throws DuplicateIsbnException {
return bookService.create(BookDto.transform(book));
}
@ResponseStatus(HttpStatus.OK)
@GetMapping(value = "/{isbn}")
public Book getBookByIsbn(@PathVariable String isbn) throws BookNotFoundException {
return bookService.getByIsbn(isbn).orElseThrow(() -> new BookNotFoundException("The given isbn is invalid"));
}
@ResponseStatus(HttpStatus.OK)
@GetMapping(value = "/query")
public List<Book> getBooksByAuthorAndTitle(@RequestParam(value = "title") String title, @RequestParam(value = "author") String author) {
return bookService.findByTitleAndAuthor(title, author);
}
@ResponseStatus(HttpStatus.OK)
@PutMapping(value = "/{id}")
public Book updateBook(@PathVariable String id, @RequestBody BookDto book) throws BookNotFoundException {
return bookService.update(id, BookDto.transform(book));
}
@ResponseStatus(HttpStatus.NO_CONTENT)
@DeleteMapping(value = "/{id}")
public void deleteBook(@PathVariable String id) {
bookService.deleteById(id);
}
@Getter
public static class BookDto {
@NotBlank
private String title;
@Positive
@PublicationYear
private Integer publicationYear;
@NotBlank
private String authorName;
@NotBlank
private String isbn;
static Book transform(BookDto bookDto) {
Book book = new Book();
book.setTitle(bookDto.title);
book.setPublicationYear(bookDto.publicationYear);
book.setAuthorName(bookDto.authorName);
book.setIsbn(bookDto.isbn);
return book;
}
public void setTitle(String title) {
this.title = title;
}
public void setPublicationYear(Integer publicationYear) {
this.publicationYear = publicationYear;
}
public void setAuthorName(String authorName) {
this.authorName = authorName;
}
public void setIsbn(String isbn) {
this.isbn = isbn;
}
}
}
Вот как все это выглядит в Swagger (http://localhost:8080/swagger-ui/index.html):

Теперь перейдем в сам OpenSearch Dashboards (http://localhost:5601/app/home#/).
В меню слева выбираем плагин Query Workbench и запускаем в нем запрос SHOW tables LIKE %
.
Таким образом мы выводим на экран все таблицы:

Возвращаемся на домашнюю страницу OpenSearch Dashboards и открываем консоль управления в разделе Manage your data:

Теперь используем запросы и посмотрим, что происходит в OpenSearch Dashboards. Начнем с метаинформации о кластере:

Состояние кластера, с указанием версии, узлов, индексов и маппинга к ним:

Посмотрим на индекс books. Создадим его вручную через метод PUT:

На данный момент наш новосозданный индекс пуст:

Зайдем в Swagger и подгрузим в индекс «Войну и мир» с помощью метода POST:

Книга успешно подрузилась и теперь хранится в самом OpenSearch:

Менять данные в OpenSearch нельзя, но это легко сделать в Swagger по ID документа. Например, заменим год публикации с 1869-го на 1870-й:

Год действительно заменился, а версия документа стала 2:

Перейдем к вставке списка документов. Я сгенерировал их наскоро с помощью ИИ DeepSeek:

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

А теперь перейдем к самому интересному — агрегации и визуализации данных. Выбираем в меню OpenSearch Dashboards вкладку Discover и создаем новый индекс, используя название books. Вот полный список книг:

Здесь мы можем отображать данные по разным полям. Например, по названию:

Для индекса книг я собрал такой тип визуализатора — по годам публикации:

Видим, что большинство книг в нашей библиотеке вышли в период с 1850-го по 1900 год:

Вторым (внешним) слоем пирога я добавил метрику по имени писателей:

Полезные вкладки для аналитиков находятся в правом верхнем углу. Вкладка Inspect выдает статистику в табличном виде:

Вкладка Share формирует ссылку на графики, а вкладка Reporting позволяет выгрузить файлы в .pdf или .png:

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