В этой статье я хочу рассказать о некоторых основных шагах по запуску и запуску узла Elasticsearch, а также о том, как подключать, индексировать и искать данные в Elasticsearch из приложения Java с использованием библиотеки Spring Data Elasticsearch.
Я также описываю некоторые общие задачи, например Как проиндексировать поле, чтобы мы могли выполнять поиск и заказ по одному и тому же полю. Как построить запрос для поиска и фильтрации по нескольким полям.
Всякий раз, когда мне нужен новый проект Spring boot, я захожу в start.spring.io чтобы сгенерировать его. Вот моя установка:
После выбора желаемого имени проекта и зависимостей нажмите кнопку Создать, затем извлеките загруженный zip-файл. В этом примере каталог проекта – spring-data-elasticsearch-example
Чтобы начать, мне нужно запустить Elasticsearch, чтобы мое приложение могло подключаться к нему. Вот несколько вариантов:
Вариант 1: Запустите узел Elasticsearch с помощью docker-compose
Поскольку у меня уже есть docker и docker-compose, установленные на моем mac ( Рабочий стол Docker ). Я создаю файл elasticsearch.yml
и запускаю контейнер Elasticsearch с помощью docker-compose:
Создать elasticsearch.yml
в src/main/docker
cd spring-data-elasticsearch-example mkdir -p src/main/docker && touch src/main/docker/elasticsearch.yml
Со следующим содержанием:
version: '2' services: my-elasticsearch: image: docker.elastic.co/elasticsearch/elasticsearch:7.9.2 container_name: my-elasticsearch # volumes: # - ~/data/my-elasticsearch/:/usr/share/elasticsearch/data/ ports: - 9200:9200 environment: - 'ES_JAVA_OPTS=-Xms1024m -Xmx1024m' - 'discovery.type=single-node'
Затем запустите контейнер Elasticsearch
docker-compose -f src/main/docker/elasticsearch.yml up -d
Elasticsearch теперь запущен и работает по адресу http://localhost:9200/
Более подробную информацию можно найти в Elasticsearch documentation/|
Вариант 2: Использование Elasticsearch SaaS, такого как bonsai
Bonsai предлагает бесплатный план хобби, который идеально подходит для целей обучения/оценки.
Просто зарегистрируйте учетную запись на bonsai и перейдите в Доступ> Учетные данные , чтобы получить URL для подключения на следующем шаге. Подобно решению docker-compose, я могу получить доступ к URL в браузере, чтобы убедиться, что он работает правильно
Учитывая, что у меня есть следующие примеры объектов: Книга и Автор. Я хотел бы проиндексировать все книги и их авторов в индексе под названием “книги”.
Создайте объекты и определите, как мы хотим индексировать
@Getter @Setter @Accessors(chain = true) @EqualsAndHashCode @ToString @Document(indexName="books") public class Book { @Id private String id; @MultiField( mainField = @Field(type = FieldType.Text, fielddata = true), otherFields = { @InnerField(suffix = "raw", type = FieldType.Keyword) } ) private String name; @Field(type = FieldType.Text) private String summary; @Field(type = FieldType.Double) private Double price; @Field(type = FieldType.Object) private Listauthors; }
В Book entity я использую различные аннотации, такие как @Document, @id, @Field, @MultiField, чтобы указать Elasticsearch, как я хочу, чтобы эта сущность и свойства были проиндексированы.
@Document(IndexName="books")
указывает, что я хочу сохранить книги в индексе с именемbooks
. По умолчанию сопоставление индексов будет создано при запуске Java-приложения. Аннотация @Document имеет множество атрибутов. Более подробную информацию можно найти в официальной документации- сегменты: количество сегментов для индекса.
- реплики: количество реплик для индекса.
- createIndex: Настройка того, следует ли создавать индекс при начальной загрузке репозитория. Значение по умолчанию равно true.
@Идентификатор
: используется для идентификации, это помогает запрашивать книгу по идентификатору или обновлять существующую книгу в Elasticsearch.@Field
: используется для указания типа данных, содержащихся в поле, таких как строки или логические значения, и их предполагаемого использования. Список типов данных можно найти в mapping-types documentation. Кроме того, можно настроить analyzer , search Analyzer и normalizer . В Elasticsearch стандартный анализатор является анализатором по умолчанию.
Вот как свойства книги индексируются с использованием приведенных выше аннотаций:
сводка
имеет текстовый тип, ацена
имеет двойной тип.имя
индексируется как для текстовых полей, так и для полей ключевых слов с помощью аннотации@MultiField
. ОсновнойТекстовое
поле анализируется для полнотекстового поиска, в то время как@InnerField
raw
являетсяКлючевым словом
, которое будет оставлено как есть в Elasticsearch и может использоваться для сортировки (или фильтрации в случае имени автора ниже). Посколькуraw
является внутренним полем, мы можем получить к нему доступ какname.raw
.authors
индексируется как вложенный объект JSON.
Сущность-автор
@Getter @Setter @Accessors(chain = true) @EqualsAndHashCode @ToString public class Author { @Id private String id; @MultiField( mainField = @Field(type = FieldType.Text, fielddata = true), otherFields = { @InnerField(suffix = "raw", type = FieldType.Keyword) } ) private String name; }
Сущность Author может содержать другие вложенные объекты, например Контакты, но для этого поста в блоге я оставляю все как есть.
Подобно названию книги, Имя автора
также индексируется как Текст
(основное поле) и ключевое слово (внутреннее поле raw
), чтобы мы могли выполнять поиск по author.name
поле, а также фильтровать и сортировать по автору.имя.необработанное
поле.
Настройте Высокоуровневый клиент REST
Следующим шагом является настройка подключения к моему запущенному Elasticsearch и создание хранилища, чтобы я мог индексировать и искать книги.
@Configuration @EnableElasticsearchRepositories( basePackages = "dev.vuongdang.springdataelasticsearchexample.repository" ) public class ElasticSearchConfig extends AbstractElasticsearchConfiguration { @Override @Bean public RestHighLevelClient elasticsearchClient() { final ClientConfiguration clientConfiguration = ClientConfiguration.builder() .connectedTo("localhost:9200") .build(); return RestClients.create(clientConfiguration).rest(); } }
Требуется настроить конфигурацию клиента, однако в приложении обычно используются абстракции более высокого уровня Репозитории Elasticsearch
.
@EnableElasticsearchRepositories
активирует поддержку репозитория для всех интерфейсов репозиториев, определенных в пакете dev.vuongdang.springdataelasticsearchexample.repository
. Благодаря Spring Data Elasticsearch я могу определить интерфейс, и реализация будет обработана автоматически.
В приведенной выше конфигурации клиента можно задать параметры для SSL, тайм-аутов подключения и сокета, заголовков и других параметров. Например:
ClientConfiguration clientConfiguration = ClientConfiguration.builder() .connectedTo("localhost:9200") .useSsl() .withConnectTimeout(Duration.ofSeconds(5)) .withSocketTimeout(Duration.ofSeconds(3)) .withBasicAuth(username, password);
Приведенная выше конфигурация клиента подключается к localhost:9200
. В случае, если вы используете app.bonsai.io
конфигурация может выглядеть следующим образом:
final ClientConfiguration clientConfiguration = ClientConfiguration.builder() .connectedTo("sass-testing-1537538524.eu-central-1.bonsaisearch.net:443") .usingSsl() .withBasicAuth("", " ") .build();
Создайте репозиторий
/** * Define the repository interface. The implementation is done by Spring Data Elasticsearch */ public interface BookSearchRepository extends ElasticsearchRepository{ List findByAuthorsNameContaining(String name); }
Давайте создадим тест, чтобы проиллюстрировать, как использовать репозиторий. Я создал 3 книги, а затем убедился, что эти операции выполняются правильно: сохранить, найти по идентификатору и найти по имени автора.
@SpringBootTest class BookServiceTest { @Autowired private BookService bookService; @Autowired private BookSearchRepository bookSearchRepository; @Autowired private ElasticsearchOperations elasticsearchOperations; public static final String BOOK_ID_1 = "1"; public static final String BOOK_ID_2 = "2"; public static final String BOOK_ID_3 = "3"; private Book book1; private Book book2; private Book book3; @BeforeEach public void beforeEach() { // Delete and recreate index IndexOperations indexOperations = elasticsearchOperations.indexOps(Book.class); indexOperations.delete(); indexOperations.create(); indexOperations.putMapping(indexOperations.createMapping()); // add 2 books to elasticsearch Author markTwain = new Author().setId("1").setName("Mark Twain"); book1 = bookSearchRepository .save(new Book().setId(BOOK_ID_1).setName("The Mysterious Stranger") .setAuthors(singletonList(markTwain)) .setSummary("This is a fiction book")); book2 = bookSearchRepository .save(new Book().setId(BOOK_ID_2).setName("The Innocents Abroad") .setAuthors(singletonList(markTwain)) .setSummary("This is a special book") ); book3 = bookSearchRepository .save(new Book().setId(BOOK_ID_3).setName("The Other Side of the Sky").setAuthors( Arrays.asList(new Author().setId("2").setName("Amie Kaufman"), new Author().setId("3").setName("Meagan Spooner")))); } /** * Read books by id and ensure data are saved properly */ @Test void findById() { assertEquals(book1, bookSearchRepository.findById(BOOK_ID_1).orElse(null)); assertEquals(book2, bookSearchRepository.findById(BOOK_ID_2).orElse(null)); assertEquals(book3, bookSearchRepository.findById(BOOK_ID_3).orElse(null)); } @Test public void query() { Listbooks = bookSearchRepository.findByAuthorsNameContaining("Mark"); assertEquals(2, books.size()); assertEquals(book1, books.get(0)); assertEquals(book2, books.get(1)); } }
В методе beforeEach
я воссоздаю индекс и вставляю 3 книги, чтобы убедиться, что в каждом тесте у меня есть свежие новые данные.
Вы можете перейти на localhost:9200/books/_search , чтобы просмотреть все проиндексированные книги.
Или перейдите по адресу localhost:9200/books/_mapping , чтобы просмотреть подробное отображение каждого поля.
В Хранилище Поиска книг
, я могу назвать метод, и он будет автоматически преобразован в запрос Elasticsearch JSON. Другой способ – определить запрос JSON с помощью @Query
аннотация. Например:
@Query("{\"match\": {\"name\": {\"query\": \"?0\"}}}") PagefindByName(String name, Pageable pageable);
Они отлично подходят для простых запросов, но на практике мы обычно предоставляем конечному пользователю поле поиска, некоторые фильтры и сортировку. Для достижения этой цели я предпочитаю использовать встроенный конструктор запросов из-за его гибкости.
Давайте создадим Книжный сервис, чтобы проиллюстрировать, как использовать конструктор запросов для формирования сложного запроса для поиска по нескольким полям, фильтрации и сортировки
@Service public class BookService { @Getter @Setter @Accessors(chain = true) @ToString public static class BookSearchInput { private String searchText; private BookFilter filter; } @Getter @Setter @Accessors(chain = true) @ToString public static class BookFilter { private String authorName; } @Autowired private ElasticsearchOperations operations; public SearchPagesearchBooks(BookSearchInput searchInput, Pageable pageable) { // query QueryBuilder queryBuilder; if(searchInput == null || isEmpty(searchInput.getSearchText())) { // search text is empty, match all results queryBuilder = QueryBuilders.matchAllQuery(); } else { // search text is available, match the search text in name, summary, and authors.name queryBuilder = QueryBuilders.multiMatchQuery(searchInput.getSearchText()) .field("name", 3) .field("summary") .field("authors.name") .fuzziness(Fuzziness.ONE) //fuzziness means the edit distance: the number of one-character changes that need to be made to one string to make it the same as another string .prefixLength(2);//The prefix_length parameter is used to improve performance. In this case, we require that the first three characters should match exactly, which reduces the number of possible combinations.; } // filter by author name BoolQueryBuilder filterBuilder = boolQuery(); if(searchInput.getFilter() != null && isNotEmpty(searchInput.getFilter().getAuthorName())){ filterBuilder.must(termQuery("authors.name.raw", searchInput.getFilter().getAuthorName())); } NativeSearchQuery query = new NativeSearchQueryBuilder().withQuery(queryBuilder) .withFilter(filterBuilder) .withPageable(pageable) .build(); SearchHits hits = operations.search(query, Book.class); return SearchHitSupport.searchPageFor(hits, query.getPageable()); } }
Чтобы протестировать этот BookService #searchBook, я добавляю еще один метод тестирования в BookServiceTest выше
@Test void searchBook() { // Define page request: return the first 10 results. Sort by book's name ASC Pageable pageable = PageRequest.of(0, 10, Direction.ASC, "name.raw"); // Case 1: search all books: should return 3 books assertEquals(3, bookService.searchBooks(new BookSearchInput(), pageable) .getTotalElements()); // Case 2: filter books by author Mark Twain: Should return [book2, book1] SearchPagebooksByAuthor = bookService.searchBooks( new BookSearchInput().setFilter(new BookFilter().setAuthorName("Mark Twain")), pageable); // sort by book name asc assertEquals(2, booksByAuthor.getTotalElements()); Iterator > iterator = booksByAuthor.iterator(); assertEquals(book2, iterator.next().getContent()); // The Innocents Abroad assertEquals(book1, iterator.next().getContent()); // The Mysterious Stranger // Case 3: search by text 'special': Should return book 2 because it has summary containing 'special' // one typo in the search text: (specila) is accepted thanks to `fuziness` SearchPage specialBook = bookService .searchBooks(new BookSearchInput().setSearchText("specila"), pageable);// book 2 assertEquals(1, specialBook.getTotalElements()); assertEquals(book2, specialBook.getContent().iterator().next().getContent()); // The Innocents Abroad }
Пожалуйста, обратите внимание, что в Случае 3
выше в тексте поиска есть одна опечатка – это special
вместо special. Но это работает так, как ожидалось, потому что я установил .нечеткость (Нечеткость. ОДИН)
в построителе запросов.
Я нахожу полезным регистрировать запрос JSON в среде разработки, чтобы убедиться, что мы создаем правильные запросы. Этот журнал можно включить в application.properties
logging.level.org.springframework.data.elasticsearch.client.WIRE=trace
Теперь, когда я запускаю метод search Box
test, я вижу запрос Elasticsearch в файле журнала, как показано ниже:
{ "from": 0, "size": 10, "query": { "multi_match": { "query": "special", "fields": [ "authors.name^1.0", "name^3.0", "summary^1.0" ], "type": "best_fields", "operator": "OR", "slop": 0, "fuzziness": "1", "prefix_length": 2, "max_expansions": 50, "zero_terms_query": "NONE", "auto_generate_synonyms_phrase_query": true, "fuzzy_transpositions": true, "boost": 1.0 } }, "post_filter": { "bool": { "adjust_pure_negative": true, "boost": 1.0 } }, "version": true, "sort": [ { "name.raw": { "order": "asc" } } ] }
В этом сообщении в блоге я освещаю следующие темы:
- Запустите и запустите ElasticSearch
- Настройте проект Spring boot для работы с Elasticsearch
- Индексировать объект POJO
- Создание индекса и сопоставления в эластичном поиске
- Поиск, фильтрация и сортировка с помощью Spring Data Elasticsearch
Пример исходного кода можно найти здесь в Github
Оригинал: “https://dev.to/vuongddang/getting-started-with-spring-data-elasticsearch-198h”