Elasticsearch実践入門:全文検索アーキテクチャとSpring Boot連携ガイド

全文検索の基礎とElasticsearchのアーキテクチャ

1. 全文検索と逆索引の概念

全文検索は、非構造化テキストデータの中から特定のキーワードやフレーズを高速に抽出する技術です。従来のリレーショナルデータベースでは LIKE '%キーワード%' を用いた部分一致検索が一般的ですが、このアプローチはインデックスを効率的に活用できず、データ規模が増加するにつれてフルスキャンが生じてパフォーマンスが著しく劣化します。

検索エンジンが採用するのは逆索引(Inverted Index)構造です。処理フローは以下の通りです:

  • テキストの前処理:トークン化(分かち書き)、大文字小文字の正規化、ストップワードの除去など
  • 逆索引の構築:各トークンがどのドキュメントに、どの位置に出現するかをマッピングしたデータ構造を生成
  • クエリ評価:検索語をトークン化し、逆索引から該当ドキュメントIDの集合を高速に取出し、関連性スコアを計算

逆索引の論理構造を簡易的に表すと以下のようになります:

トークンドキュメントIDリスト出現頻度・位置情報
分散システムdoc_01, doc_05, doc_12頻度: 高 / 位置: [3, 15, 22]
インデックスdoc_01, doc_04, doc_09, doc_15頻度: 中 / 位置: [1, 8, 14, 30]
クラウドdoc_03, doc_11頻度: 低 / 位置: [5, 19]

2. Elasticsearchの特徴と技術選定基準

ElasticsearchはLuceneベースで構築された分散型検索・分析エンジンであり、Java仮想マシン上で動作します。近リアルタイムのデータ反映、水平スケーラビリティ、柔軟なクエリDSL、RESTful APIが主要な特徴です。

他のデータストアとの主要な比較指標は以下の通りです:

評価項目ElasticsearchApache SolrMongoDBMySQL/PostgreSQL
用途カテゴリ検索・ログ分析・メトリクス検索・集計分析ドキュメントストアトランザクション処理
スケーリング方式ネイティブ分散(シャード/レプリカ)ZooKeeper連携などシャーディング/レプリケーションパーティショニング/クラスタ
データモデルJSON(スキマ/弱スキーマ)XML/JSON/SolrDocBSON(ドキュメント)リレーショナルテーブル
一貫性モデル最終的整合性最終的整合性ACID(複数ドキュメント)強一貫性
得意分野テキスト検索、フルテキスト、可視化複雑なファセット分析柔軟なスキーマ、高い書き込み性能複雑なJOIN、厳密な整合性

大規模なログ処理、検索機能の実装、時系列データの可視化が必要なアーキテクチャではElasticsearchが最適解となる場合が多く、厳密なトランザクションが必要な決済や在庫管理にはRDBMSが推奨されます。

3. 実行環境の構築と設定

Windows環境での起動準備

公式アーカイブから圧縮ファイルを解凍し、bin/elasticsearch.batを実行します。環境変数 ES_JAVA_HOMEES_HOME の設定により、JDKの参照パスとインストールルートを明示できます。起動後、ブラウザで http://localhost:9200 にアクセスし、JSONレスポンスが返却されれば完了です。

Linux環境での本番準備

root権限での実行はセキュリティ上推奨されないため、専用ユーザーを作成し、ファイル所有権を移動します。

adduser elastic_user
passwd elastic_user
chown -R elastic_user:elastic_user /opt/elasticsearch

~/.bashrc 環境変数を設定し、elasticsearch.yml に以下の基本設定を追記します。

network.host: 0.0.0.0
discovery.type: single-node
xpack.security.enabled: false

JVMヒープメモリは jvm.options-Xms-Xmx を等値に設定し、物理メモリの50%未満、かつ32GB未満に抑えることがベストプラクティスです。システムリミット調整には limits.confsysctl.conf の修正が必要です。

Kibanaとアナライザの導入

Kibanaは検索クエリの検証や可視化に利用するUIツールです。ポート5601で起動し、Dev ToolsコンソールからAPIテストを行います。日本語検索ではIK Analyzerが標準的に利用されます。バージョン差異が生じた場合は、プラグインディレクトリ内の plugin-descriptor.properties を手修正し、ES起動前に再起動を忘れずに行います。

アナライザの動作確認は以下のリクエストで可能です。

POST _analyze
{
  "analyzer": "ik_max_word",
  "text": "マイクロサービスアーキテクチャの設計パターン"
}

4. コアデータモデルの理解

Elasticsearchのデータ階層は以下の要素で構成されます:

  • ノード(Node):単一のESプロセス。マスター候補、データ、インジェストなどの役割を担います。
  • インデックス(Index):論理的なドキュメント集合。RDBMSのデータベースまたはテーブル群に相当します。
  • ドキュメント(Document):JSON形式の最小データ単位。_id_source_seq_no_primary_term などのシステムメタデータを保有します。
  • シャードとレプリカ:物理的なデータ分割単位。障害耐性と読み込みスループットの向上に寄与します。

7.x以降ではType概念が削除され、_doc がパス固定化されています。バージョン管理は _seq_no_primary_term による楽観的ロック方式に統一されています。

5. インデックス操作とマッピング制御

インデックス生成時には、シャード数やデフォルトアナライザを静的に定義できます。一度決定した主シャード数やフィールド型は変更不可能なため、本番環境では明示的なマッピング定義が必須です。

PUT /product_catalog
{
  "settings": {
    "number_of_shards": 3,
    "number_of_replicas": 1,
    "index": {
      "analysis.analyzer.default.type": "ik_max_word"
    }
  },
  "mappings": {
    "properties": {
      "title": { "type": "text" },
      "sku_code": { "type": "keyword" },
      "price": { "type": "scaled_float", "scaling_factor": 100 },
      "tags": { "type": "keyword" }
    }
  }
}

マッピングパラメータの代表的な用途:

  • analyzer:書き込み時のトークン化ルールを指定
  • copy_to:複数フィールドの値を複合フィールドへ統合し、一括検索を可能にする
  • dynamic:未知フィールドの自動追加挙動(true / false / strict
  • doc_values:集計・ソート向けディスク構造(text 型は非対応)
  • norms:関連性スコアリングの重み付け情報(フィルタ用途では false で容量節約)

スキーマ変更が必要な場合は、新しいインデックスを生成し _reindex APIでデータを移行、エイリアス切り替えによってダウンタイムゼロの更新を実現します。

POST _reindex
{
  "source": { "index": "product_catalog_v1" },
  "dest": { "index": "product_catalog_v2" }
}

6. ドキュメント管理と検索クエリ

ドキュメントの登録は PUT(ID指定)または POST(自動採番)を用います。部分的な値更新には _update エンドポイントを使用し、既存ドキュメントを削除・再登録する全量更新とは処理コストが異なります。

POST /product_catalog/_update/1024
{
  "doc": { "price": 19800 }
}

検索DSLでは、match がアナライザを適用して部分一致検索を行い、term がトークン化を行わない完全一致検索を実行します。これらは text 型と keyword 型の使い分けに直結します。

GET /product_catalog/_search
{
  "query": {
    "bool": {
      "must": { "match": { "title": "サーバー 構築" } },
      "filter": { "range": { "price": { "gte": 5000, "lte": 30000 } } }
    }
  },
  "sort": [{ "price": "asc" }],
  "from": 0,
  "size": 10
}

高スループットなバッチ処理には _bulk APIが利用されます。アクション行とデータ行を交互に配置し、単一リクエスト内でCRUDを混合できます。

POST _bulk
{ "create": { "_index": "product_catalog", "_id": "2001" } }
{ "title": "ネットワーク機器", "sku_code": "NET-01", "price": 85000, "tags": ["IT","基盤"] }
{ "index": { "_index": "product_catalog", "_id": "2002" } }
{ "title": "ストレージ装置", "sku_code": "STO-03", "price": 120000, "tags": ["IT","データ"] }

複数のドキュメントをIDで抽出する場合は _mget を、異なるクエリを一度に実行する場合は _msearch を利用します。

7. Spring Boot 3との統合実装

Spring Data Elasticsearchは、インデックス操作とドキュメントCRUDを抽象化したテンプレートを提供します。依存関係を追加し、接続設定を定義します。

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

アプリケーション設定:

spring:
  elasticsearch:
    uris: http://localhost:9200
    connection-timeout: 3s
    socket-timeout: 30s

エンティティクラスでは @Document@Field 注釈でインデックス名と型定義をマッピングします。

@Data
@AllArgsConstructor
@NoArgsConstructor
@Document(indexName = "staff_records")
public class StaffRecord {
    @Id
    private String id;
    
    @Field(type = FieldType.Keyword)
    private String employeeCode;
    
    @Field(type = FieldType.Integer)
    private int age;
    
    @Field(type = FieldType.Text, analyzer = "ik_max_word")
    private String workplace;
    
    @Field(type = FieldType.Text)
    private String roleDescription;
}

リポジトリインタフェースを継承することで、基本メソッドが自動的に利用可能になります。

@Repository
public interface StaffSearchRepository extends ElasticsearchRepository<StaffRecord, String> {
    List<StaffRecord> findByEmployeeCode(String code);
}

テンプレートベースの高度な操作では、インデックス作成・削除・バッチインポートを以下のように実装できます。

@Slf4j
@SpringBootTest
class EsIntegrationVerification {

    @Autowired
    private ElasticsearchOperations esOperations;
    
    private static final String TARGET_INDEX = "staff_records";

    @Test
    void executeIndexManagement() {
        IndexOperations indexOps = esOperations.indexOps(StaffRecord.class);
        if (indexOps.exists()) {
            indexOps.delete();
        }
        indexOps.create();
        indexOps.putMapping(IndexCoordinates.of(TARGET_INDEX));
        log.info("インデックス再構築が完了しました");
    }

    @Test
    void performBatchDocumentIngestion() {
        List<StaffRecord> initialDataset = List.of(
            new StaffRecord("S001", "A-101", 29, "東京本社", "バックエンド開発"),
            new StaffRecord("S002", "B-205", 34, "大阪支社", "インフラ構成"),
            new StaffRecord("S003", "C-308", 27, "福岡拠点", "フロントエンド開発")
        );

        List<IndexQuery> bulkQueries = initialDataset.stream()
            .map(record -> {
                IndexQuery query = new IndexQuery();
                query.setId(record.getId());
                query.setSource(new Gson().toJson(record));
                return query;
            })
            .toList();

        esOperations.bulkIndex(bulkQueries, IndexCoordinates.of(TARGET_INDEX));
        log.info("バッチ登録処理が完了しました。対象件数: {}", bulkQueries.size());
    }

    @Test
    void validateSearchFiltering() {
        NativeQuery searchRequest = NativeQuery.builder()
            .withQuery(QueryBuilders.matchQuery("workplace", "東京"))
            .withPageable(PageRequest.of(0, 5))
            .build();

        SearchHits<StaffRecord> results = esOperations.search(searchRequest, StaffRecord.class);
        results.forEach(hit -> 
            log.info("取得データ: {}", hit.getContent())
        );
    }
}

上記構成により、データストアの接続抽象化と検索ロジックの分離が実現され、本番環境での運用保守性が向上します。

タグ: Elasticsearch 全文検索 SpringBoot3 逆索引 ElasticsearchRestTemplate

7月24日 01:53 投稿