Spring Data Elasticsearch 시작

본고에서 저는 Elasticsearch 노드를 시작하고 실행하는 방법과 Spring 데이터인 Elasticsearch 라이브러리를 사용하여 자바 응용 프로그램에서 연결하고 인덱스하며 Elasticsearch의 데이터를 검색하는 기본적인 절차를 소개할 것입니다.
같은 필드를 검색하고 정렬할 수 있도록 색인 필드를 만드는 것과 같은 일반적인 작업도 설명합니다.여러 필드에서 검색하고 필터링하기 위해 조회를 만드는 방법

새 Springboot 프로젝트 만들기


새로운 Springboot 프로젝트가 필요할 때마다 start.spring.io 생성합니다.이것은 내 설정입니다.

필요한 항목 이름과 의존 항목을 선택한 후 생성을 누르고 다운로드한 zip 파일을 추출합니다.이 예에서 프로젝트 디렉토리는 spring-data-elasticsearch-example

Elasticsearch가 시작되고 실행되었는지 확인합니다.


우선, 응용 프로그램이 연결될 수 있도록 Elasticsearch를 실행해야 합니다.
간단한 선택은 docker compose를 사용하여 하나의 Elasticsearch 노드를 시작하는 것이다
왜냐하면 나는 이미 나의 맥에 docker와dockercomposeDocker Desktop를 설치했기 때문이다.elasticsearch.yml 파일을 만들고 docker compose로 Elasticsearch 용기를 시작합니다.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/
자세한 내용은 Elasticsearchdocumentation에서 확인할 수 있습니다.

ElasticSearch의 인덱스 데이터


예를 들어 Book과 Author의 예가 있습니다.나는 모든 책과 작가를 책이라는 색인에 편입시키고 싶다.

엔티티 작성 및 색인 방식 정의



@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<Author> authors;
}
Book entity에서 나는 @Document, @Field, @MultiField 등 다른 주석을 사용하여 이 실체와 속성을 색인하는 방법을 표시합니다.
  • @Document(indexName="books")는 책을 books라는 색인에 저장하고 싶다고 밝혔다.기본적으로 색인 매핑은 Java 응용 프로그램을 시작할 때 생성됩니다.@Document 메모에는 여러 속성이 있습니다.자세한 내용은 공식 웹 사이트 documentation 를 참조하십시오.
  • 조각: 색인 조각 수.
  • 사본: 인덱스의 사본 수입니다.
  • createIndex: 저장소 부트 시 인덱스를 생성할지 여부를 구성합니다.기본값은true입니다.
  • @Id: 표지 목적에 사용되며 id에 따라 서적을 조회하거나 Elasticsearch의 기존 서적을 업데이트하는 데 도움이 됩니다.
  • @Field: 문자열이나 부울 값 및 예상 용도와 같은 필드에 포함된 데이터 유형을 지정합니다.데이터 유형 목록은 mapping-types 문서에서 찾을 수 있습니다.
    이 밖에도 analyzer, searchAnalyzernormalizer 사용자 정의가 가능합니다.Elasticsearch에서 standard analyzer 은 기본 분석기입니다.
  • 다음은 상기 주석을 사용하여 책의 속성을 색인하는 방법입니다.
  • summary에는 텍스트 유형이, price에는 이중 유형이 있습니다.
  • name를 사용하여 텍스트 및 키워드 필드에 주석 인덱스를 사용합니다.마스터@MultiField 필드를 분석하여 전체 텍스트 검색을 수행하고 Text@InnerFieldraw으로 Elasticsearch에서 변경되지 않으며 정렬(또는 아래 저자 이름의 경우 필터링)에 사용할 수 있습니다.Keyword 는 내부 필드이므로 raw 로 액세스할 수 있습니다.
  • name.raw는 중첩된 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;
    }
    
    저자의 실체는 연락처와 같은 다른 끼워 넣는 대상을 포함할 수 있지만, 이 블로그 글에 대해 나는 원래의 모양을 유지하고 간단하다.
    책 제목과 유사하게authors 또한 Author's name (mainField) 와 키워드 (innerField Text 로 인덱스되어 raw 필드를 검색하고 author.name 필드를 필터링하고 정렬할 수 있습니다.

    고급 REST 클라이언트 구성


    다음 단계는 제가 실행하는 Elasticserach와의 연결을 설정하고 저장소를 만들어서 책을 색인하고 검색할 수 있도록 합니다.
    @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();
        }
    }
    
    
    ClientConfiguration을 구성해야 하지만 응용 프로그램에서는 일반적으로 더 높은 수준의 추상author.name.raw을 사용합니다.Elasticsearch Repositories 패키지에 정의된 모든 저장소 인터페이스에 대한 저장소 지원을 활성화합니다.Spring Data Elasticsearch를 사용하여 인터페이스를 정의하면 자동으로 처리됩니다.
    위의 ClienConfiguration은 SSL,connect,socket 시간 초과, 헤더 및 기타 매개 변수에 대한 옵션을 설정할 수 있습니다.예:
    ClientConfiguration clientConfiguration = ClientConfiguration.builder()
      .connectedTo("localhost:9200")                      
      .useSsl()                                                             
      .withConnectTimeout(Duration.ofSeconds(5))                            
      .withSocketTimeout(Duration.ofSeconds(3))                             
      .withBasicAuth(username, password);  
    
    위의 클라이언트 구성은 에 연결됩니다@EnableElasticsearchRepositories.dev.vuongdang.springdataelasticsearchexample.repository를 사용하는 경우 다음과 같이 구성할 수 있습니다.
    final ClientConfiguration clientConfiguration = ClientConfiguration.builder()
                    .connectedTo("sass-testing-1537538524.eu-central-1.bonsaisearch.net:443")
                    .usingSsl()
                    .withBasicAuth("<username>", "<password>")
                    .build();
    

    저장소 만들기


    /**
     * Define the repository interface. The implementation is done by Spring Data Elasticsearch
     */
    public interface BookSearchRepository extends ElasticsearchRepository<Book, String> {
    
        List<Book> findByAuthorsNameContaining(String name);
    }
    
    저장소를 사용하는 방법을 설명하는 테스트를 만듭니다.나는 세 권의 책을 만들고 이 동작들이 정상적으로 작동하는지 확인한다. 저장, Id 찾기, 저자 이름 찾기.
    @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<Book> books = bookSearchRepository.findByAuthorsNameContaining("Mark");
    
            assertEquals(2, books.size());
            assertEquals(book1, books.get(0));
            assertEquals(book2, books.get(1));
        }
    }
    
    
    localhost:9200 방법에서, 나는 매번 테스트에 새로운 데이터가 있는지 확인하기 위해 색인을 다시 만들고 책 세 권을 삽입했다.
    localhost:9200/books/_search로 이동하여 모든 색인 서적을 볼 수 있습니다.
    또는 localhost:9200/books/_mapping로 이동하여 각 필드의 세부 매핑을 확인합니다.

    검색 및 필터링

    app.bonsai.io 에서 Elasticsearch JSON 조회로 자동으로 해석되는 방법을 명명할 수 있습니다.다른 방법은 주석을 사용하여 JSON 질의를 정의하는 것입니다.예:
     @Query("{\"match\": {\"name\": {\"query\": \"?0\"}}}")
        Page<Book> findByName(String name, Pageable pageable);
    
    이러한 것들은 간단한 검색에 있어서는 매우 좋지만, 실천에서, 우리는 보통 최종 사용자에게 검색 필드, 필터, 정렬을 제공한다.이 점을 실현하기 위해서, 나는 내장된 검색 생성기를 사용하는 것을 더욱 좋아한다. 왜냐하면 그것은 유연성을 가지고 있기 때문이다.
    여러 필드에서 검색, 필터링, 정렬을 위해 검색 생성기를 사용하여 복잡한 검색을 만드는 방법을 설명하는 BookService를 만듭니다.
    @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<Book> 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<Book> hits = operations.search(query, Book.class);
    
            return SearchHitSupport.searchPageFor(hits, query.getPageable());
        }
    }
    
    
    이 Book Service #searchBook을 테스트하기 위해 위의 Book Service Test에 테스트 방법을 추가했습니다.
    @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<Book> booksByAuthor = bookService.searchBooks(
                    new BookSearchInput().setFilter(new BookFilter().setAuthorName("Mark Twain")),
                    pageable); // sort by book name asc
            assertEquals(2, booksByAuthor.getTotalElements());
    
            Iterator<SearchHit<Book>> 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<Book> specialBook = bookService
                    .searchBooks(new BookSearchInput().setSearchText("specila"), pageable);// book 2
            assertEquals(1, specialBook.getTotalElements());
    
            assertEquals(book2, specialBook.getContent().iterator().next().getContent()); // The Innocents Abroad
        }
    
    위의 beforeEach 에서 검색 텍스트에 맞춤법 오류가 있습니다. 이것은 BookSearchRepository 이지 특수한 것이 아닙니다.조회 생성기에 설치되어 있기 때문에 정상적으로 작동합니다. @Query.

    로그인 중


    나는 정확한 조회를 만들기 위해 개발 환경에서 JSON 조회를 기록하는 것이 매우 유용하다는 것을 발견했다.이 로그는 Case 3 에서 사용할 수 있습니다.
    logging.level.org.springframework.data.elasticsearch.client.WIRE=trace
    
    현재, 내가 specila 테스트 방법을 실행할 때, 로그 파일에서 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 시작 및 실행
  • Elasticsearch
  • 를 사용하도록 Springboot 항목 구성
  • 인덱스 POJO 객체
  • Elasticsaerch
  • 에서 색인 및 매핑 만들기
  • Spring 데이터 Elasticsearch
  • 를 사용하여 검색, 필터링 및 정렬
    샘플 소스 코드는 Github에서 찾을 수 있습니다here.

    좋은 웹페이지 즐겨찾기