Рубрики
Без рубрики

Начало работы с Spring Data Elasticsearch

В этой статье я хочу рассказать о некоторых основных шагах по запуску и запуску узла Elasticsearch… С тегами java, elasticsearch, docker.

В этой статье я хочу рассказать о некоторых основных шагах по запуску и запуску узла 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 List authors;
}

В 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() {
        List books = 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\"}}}")
    Page findByName(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 SearchPage searchBooks(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]
        SearchPage booksByAuthor = 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”