ElasticsearchとSpring Bootによるドキュメントデータベース実装ガイド

Elasticsearchの基本概念

Elasticsearchは高度な全文検索能力を持つ分散型ドキュメントデータベースです。JSON形式をデフォルトのシリアライズ形式として使用し、各データをドキュメントとして扱います。

従来のリレーショナルデータベースとの比較

リレーショナルデータベース Elasticsearch
データベース (Database) インデックス (Index)
テーブル (Table) タイプ (Type)
行 (Row) ドキュメント (Document)
列 (Column) フィールド (Field)

Spring Bootとの統合

依存関係の設定

<dependency>
   <groupId>org.springframework.boot</groupId>
   <artifactId>spring-boot-starter-data-elasticsearch</artifactId>
</dependency>

エンティティクラスのアノテーション

Spring Data Elasticsearchでは、以下の主要なアノテーションを使用してドキュメントのマッピングを定義します:

  • @Document: クラスレベルで適用し、インデックス名、タイプ、シャード数、レプリカ数などを指定
  • @Id: 主キーフィールドをマーク
  • @Field: フィールドの型、インデックス設定、ストア設定、アナライザーなどを指定

書籍エンティティの実装例

@Document(indexName = "library", type = "books", shards = 3, replicas = 1)
public class Book {
    @Id
    private String isbn;
    
    @Field(type = FieldType.Text, analyzer = "kuromoji_analyzer")
    private String title;
    
    @Field(type = FieldType.Keyword)
    private String author;
    
    @Field(type = FieldType.Keyword)
    private String genre;
    
    @Field(type = FieldType.Double)
    private Double price;
    
    @Field(type = FieldType.Date, format = DateFormat.date_hour_minute_second)
    private Date publishDate;
    
    @Field(index = false, store = true)
    private String coverImageUrl;
}

インデックス操作

ElasticsearchTemplateを使用したインデックス管理

@Autowired
private ElasticsearchTemplate esTemplate;

@Test
public void manageIndexes() {
    // インデックスの作成
    esTemplate.createIndex(Book.class);
    
    // マッピングの設定
    esTemplate.putMapping(Book.class);
    
    // インデックス存在確認
    boolean exists = esTemplate.indexExists(Book.class);
    
    // インデックスの削除
    if (exists) {
        esTemplate.deleteIndex(Book.class);
    }
}

データ操作

リポジトリインターフェースの定義

public interface BookRepository extends ElasticsearchRepository<Book, String> {
    
    List<Book> findByTitleContaining(String title);
    
    List<Book> findByAuthor(String author);
    
    List<Book> findByGenreAndPriceLessThan(String genre, Double maxPrice);
    
    @Query("{\"bool\": {\"must\": [{\"match\": {\"title\": \"?0\"}}, {\"range\": {\"price\": {\"lte\": ?1}}]}}")
    List<Book> findByCustomQuery(String title, Double maxPrice);
}

CRUD操作の実装

@Service
public class BookService {
    
    @Autowired
    private BookRepository bookRepository;
    
    // 単一ドキュメントの保存
    public Book saveBook(Book book) {
        return bookRepository.save(book);
    }
    
    // 複数ドキュメントの一括保存
    public Iterable<Book> saveBooks(List<Book> books) {
        return bookRepository.saveAll(books);
    }
    
    // 全件検索(価格昇順)
    public List<Book> findAllBooksSortedByPrice() {
        return StreamSupport.stream(
            bookRepository.findAll(Sort.by("price").ascending()).spliterator(), false)
            .collect(Collectors.toList());
    }
    
    // 条件付き検索とページング
    public Page<Book> searchBooks(String keyword, Pageable pageable) {
        return bookRepository.findByTitleContaining(keyword, pageable);
    }
    
    // 集計クエリ
    public List<String> findPopularGenres() {
        NativeSearchQuery query = new NativeSearchQueryBuilder()
            .withQuery(QueryBuilders.matchAllQuery())
            .addAggregation(AggregationBuilders.terms("genres").field("genre"))
            .build();
        
        SearchHits<Book> searchHits = elasticsearchTemplate.search(query, Book.class);
        Terms terms = searchHits.getAggregations().get("genres");
        
        return terms.getBuckets().stream()
            .map(Bucket::getKeyAsString)
            .collect(Collectors.toList());
    }
}

高度な検索機能

@Component
public class BookSearcher {
    
    @Autowired
    private ElasticsearchRestTemplate template;
    
    // ブールクエリを使用した複合検索
    public List<Book> complexSearch(String title, String author, Double minPrice, Double maxPrice) {
        BoolQueryBuilder queryBuilder = QueryBuilders.boolQuery();
        
        if (title != null && !title.isEmpty()) {
            queryBuilder.must(QueryBuilders.matchQuery("title", title));
        }
        
        if (author != null && !author.isEmpty()) {
            queryBuilder.filter(QueryBuilders.termQuery("author", author));
        }
        
        if (minPrice != null || maxPrice != null) {
            RangeQueryBuilder rangeQuery = QueryBuilders.rangeQuery("price");
            if (minPrice != null) rangeQuery.gte(minPrice);
            if (maxPrice != null) rangeQuery.lte(maxPrice);
            queryBuilder.filter(rangeQuery);
        }
        
        NativeSearchQuery searchQuery = new NativeSearchQueryBuilder()
            .withQuery(queryBuilder)
            .withHighlightFields(
                new HighlightBuilder.Field("title").fragmentSize(150),
                new HighlightBuilder.Field("author").fragmentSize(100))
            .build();
        
        SearchHits<Book> hits = template.search(searchQuery, Book.class);
        return hits.stream().map(SearchHit::getContent).collect(Collectors.toList());
    }
}

タグ: Elasticsearch spring-boot document-database Java search-engine

5月11日 15:53 投稿