スプリングデータのエラスティックサーチ


この記事では、Spring Data EllipticSearchライブラリを使用して、エラスティックサーチノードを取得して実行する方法と、Javaアプリケーションからエラスティックサーチでデータを検索したり、インデックスを作成する方法についての基本的な手順を説明します.
私もいくつかの一般的なタスクについて説明します.クエリを複数のフィールドで検索してフィルターを作成する方法.

新しいスプリングブートプロジェクトを作成する


新しいスプリングブートプロジェクトを必要とするときはいつでもstart.spring.io を生成する.以下がセットアップです.

必要なプロジェクト名、および依存関係を選択した後、「生成」をクリックし、ダウンロードしたZIPファイルを展開します.この例では、プロジェクトディレクトリはspring-data-elasticsearch-example

確実なエラスティックサーチの実行


始めるためには、私のアプリケーションが接続できるように、エラスティックサーチを実行する必要があります.
つの単純なオプションは
私はすでに私のMacにインストールされているDockerとDockerを持っているのでDocker Desktop ). を作成するelasticsearch.yml ファイルを作成し、Dockerを使用してエラスティックサーチコンテナを起動します
クリエイト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'
エラスティックサーチコンテナを起動する
docker-compose -f src/main/docker/elasticsearch.yml up -d
FlashSearchは、現在、実行中ですhttp://localhost:9200/
詳細についてはdocumentation

インデックス検索データ


私には、以下のサンプル実体があります.私はすべての書籍や著者のインデックスをインデックスとして'本と呼ばれる.

エンティティを作成し、インデックスを作成する方法を定義します



@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;
}
ブックエンティティでは、@ document、@ field、@ multifieldのような別の注釈を使用して、その実体がどのようにしてどのように索引付けされているかを確認します.
  • @Document(indexName="books") 私はインデックスとして本を格納したいことを示しますbooks . 既定では、インデックスのマッピングがJavaアプリケーションを起動するように作成されます.@ドキュメント注釈には多くの属性があります.詳細は公式に見ることができますdocumentation
  • シャード:インデックスのシャードの数.
  • レプリカ:インデックスのレプリカの数.
  • CreateIndex :リポジトリブートストラップのインデックスを作成するかどうかを設定します.デフォルト値はtrueです.
  • @Id : アイデンティティの目的のために使用される、これはIDによって本を検索するか、エラスティック検索で既存の本を更新するのに役立ちます.
  • @Field : 文字列やブール値などのフィールドが含まれているデータの種類を指定するために使用します.データ型のリストはmapping-types ドキュメント.
    その上、analyzer , searchAnalyzer , and normalizer カスタマイズできます.弾性検索でstandard analyzer デフォルトのアナライザです.
  • ここでは、上記の注釈を使用してブックのプロパティをインデックス化する方法を示します.
  • summary テキスト型とprice ダブルタイプです.
  • name テキストフィールドとキーワードフィールドの両方にインデックスを付けます@MultiField 注釈.メインText その間、フィールドは全文検索のために分析されます@InnerField raw is Keyword これはエラスティックサーチのままで、ソートに使用することができます.以来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;
    }
    
    著者実体は他の入れ子になったオブジェクトe . g接触を含むことができます、しかし、このブログ柱のために、私はそれを単純に保ちます.
    本の名前と同様.Author's name もインデックスText (メインフィールド)およびキーワードraw ) だから我々は検索することができますauthor.name フィールド、フィルタ、ソートauthor.name.raw フィールド.

    クライアントの設定


    次のステップは、私の実行しているエラスティックスセラピーへの接続を設定し、リポジトリを作成して、インデックスを検索し、本を検索することです.
    @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 Repositories は通常、アプリケーションで使用されます.@EnableElasticsearchRepositories は、dev.vuongdang.springdataelasticsearchexample.repository パッケージ.Spring Data AnalySearchのおかげで、インターフェイスを定義でき、実装が自動的に処理されます.
    上記のclienconfigurationは、ssl、connectおよびsocket timeout、headers、および他のパラメータのオプションを設定することができます.例えば、
    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("<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);
    }
    
    リポジトリの使い方を説明するテストを作成しましょう.私は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<Book> 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 各フィールドの詳細なマッピングを参照してください.

    検索とフィルタ


    インBookSearchRepository , 私はメソッドを名前付けすることができます、そして、それは自動的に弾性検索JSON質問に解決されます.もう一つの方法はJSONクエリを定義することです@Query 注釈.例えば、
     @Query("{\"match\": {\"name\": {\"query\": \"?0\"}}}")
        Page<Book> 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<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());
        }
    }
    
    
    この本サービスをチェックするには、以上のテストメソッドを追加する
    @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
        }
    
    ご了承くださいCase 3 上に、検索テキストはspecila 代わりに.しかし、私がセットしたので、それは予想通りに働きます.fuzziness(Fuzziness.ONE) クエリビルダで.

    ログ記録


    JSONクエリを開発環境にログオンし、適切なクエリを作成するのに便利です.このログは、application.properties
    logging.level.org.springframework.data.elasticsearch.client.WIRE=trace
    
    今私が走るときsearchBook テストメソッドでは、以下のようにログファイルのエラスティックサーチクエリを見ることができます.
    {
        "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"
                }
            }
        ]
    }
    

    結論


    このブログ記事では、次のトピックを取り上げます
  • ドラッグアンドアップを取得
  • Springbootプロジェクトを設定します
  • ポアオブジェクト
  • インデックス作成とマッピング
  • スプリングデータを使用した検索、フィルタ、ソート
  • ソースコードの例を見つけるhere インジット