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());
}
}