多くの日常的なプロジェクトにおいて、検索機能は最も頻繁にアクセスされるページの一つです。現在の検索機能はデータベースに基づくあいまい検索によって実装されており、多くの問題点があります。
まず第一に、検索効率が低いことです。
データベースのあいまい検索はインデックスを利用しないため、データ量が大きくなると検索性能が著しく低下します。検索エンジンに切り替えることで、速度は大幅に向上します。
注意すべき点は、データベースのあいまい検索はテーブルのデータ量が増えるにつれて、検索性能の低下が非常に顕著になる一方、検索エンジンの性能はデータが増えてもあまり低下しないことです。
次に、機能が限定的であることです。
データベースのあいまい検索機能は限定的で、検索条件が非常に厳格です。ユーザーが検索したキーワードを正確に含んでいる必要があります。一方、検索エンジンでは、ユーザー入力に誤字がある場合や、ピンイン検索、類義語検索をしても正しくデータに一致させることができます。
以上のことから、大量データの検索や複雑な検索要件に直面する場合、専用の検索エンジンを使用することをお勧めします。
現在、世界の検索技術ランキングは以下の通りです:
一位は今日学習するelasticsearchです。
elasticsearchは非常に強力なオープンソース検索エンジンであり、多くの機能をサポートしています。例えば:
コード検索
商品検索
ソリューション検索
地図検索
今日の学習を通じて、以下の学習目標を達成してください:
- 逆転インデックスの原理を理解する
- IKアナライザーを使用する方法を理解する
- インデックスマッピングのプロパティの意味を理解する
- インデックスとマッピングを作成できる
- ドキュメントのCRUDを実装できる
1.elasticsearchの基本
Elasticsearchの公式サイトは以下の通りです:
本章では、Elasticsearchの基本的な原理と基礎概念について初歩的に理解しましょう。
1.1.認識とインストール
Elasticsearchはelastic社が開発した検索エンジン技術で、elastic技術スタックの一部です。完全な技術スタックには以下のものが含まれます:
- Elasticsearch:データの保存、計算、検索に使用
- Logstash/Beats:データ収集に使用
- Kibana:データ可視化に使用
この技術スタック全体はELKと呼ばれ、ログ収集、システム監視、状態分析などによく使用されます:
この技術スタックの中核は、保存、検索、計算を行うElasticsearchです。したがって、次に学習する核心もElasticsearchです。
インストールする内容は2つの部分に分かれます:
- elasticsearch:保存、検索、演算
- kibana:グラフィカル表示
まずElasticsearchは言うまでもなく、コアのデータ保存、検索、分析機能を提供します。
次にKibanaですが、ElasticsearchはRestfulスタイルのAPIを提供しており、あらゆる操作はHTTPリクエストを送信することで実行できます。ただし、HTTPリクエストの方法、パス、リクエストパラメータの形式には厳格な規範があります。これらの規範は確かに覚えきれないので、Kibanaというサービスを利用する必要があります。
Kibanaはelastic社が提供する、Elasticsearchを操作するためのビジュアルコンソールです。その機能は非常に強力で、以下のものを含みます:
- Elasticsearchデータの検索、表示
- Elasticsearchデータの統計、集約、およびグラフィカルレポート、チャートの作成
- Elasticsearchクラスタ状態の監視
- 開発コンソール(DevTools)も提供しており、ElasticsearchのRestful APIインターフェースに構文ヒントを提供しています
1.1.1.elasticsearchのインストール
以下のDockerコマンドを使用して、シングルノード版のelasticsearchをインストールできます:
docker run -d \
--name es \
-e "ES_JAVA_OPTS=-Xms512m -Xmx512m" \
-e "discovery.type=single-node" \
-v es-data:/usr/share/elasticsearch/data \
-v es-plugins:/usr/share/elasticsearch/plugins \
--privileged \
--network hm-net \
-p 9200:9200 \
-p 9300:9300 \
elasticsearch:7.12.1
注意点として、ここではelasticsearchの7.12.1バージョンを使用しています。8以上のバージョンではJavaAPIの変更が大きく、企業での応用は広くありません。企業で広く応用されているのは8以下のバージョンです。
ミラーのプルが困難な場合は、このミラータールファイルを直接使用できます(すべての資料は記事の末尾で一度に取得できます):
インストールが完了したら、9200ポートにアクセスすると、Elasticsearchサービスの基本情報が表示されます:
1.1.2.Kibanaのインストール
以下のDockerコマンドを使用して、Kibanaをデプロイできます:
docker run -d \
--name kibana \
-e ELASTICSEARCH_HOSTS=http://es:9200 \
--network=hm-net \
-p 5601:5601 \
kibana:7.12.1
ミラーのプルが困難な場合は、事前資料で提供されたミラータールファイルを直接インポートできます:
インストールが完了したら、5601ポートに直接アクセスすると、コンソールページが表示されます:
Explore on my ownを選択した後、メインページに入ります:
次にDev toolsを選択し、開発ツールページに入ります:
1.2.逆転インデックス
elasticsearchが高性能な検索を実現できるのは、まさに基盤となる逆転インデックス技術のおかげです。では、逆転インデックスとは何でしょうか?
逆転インデックスの概念は、MySQLのような順方向インデックスに基づいています。
1.2.1.順方向インデックス
まず、順方向インデックスを復習しましょう。
例えばtb_goodsという名前のテーブルがあるとします:
| **id** | **title** | **price** |
|---|---|---|
| 1 | 小米手机 | 3499 |
| 2 | 华为手机 | 4999 |
| 3 | 华为小米充电器 | 49 |
| 4 | 小米手环 | 49 |
| ... | ... | ... |
idフィールドにはすでにインデックスが作成されています。インデックスの基盤はB+木構造を使用しているため、idに基づいて検索する速度は非常に速いです。しかし、titleのような他のフィールドはリーフノードにのみ存在します。
したがって、titleに基づいて検索するには、木の各リーフノードを順番にたどり、titleデータが条件に合致するか判断する必要があります。
例えば、ユーザーのSQL文が以下のようになっている場合:
select * from tb_goods where title like '%手机%';
検索の概略の流れは以下の通りです:
説明:
- 1)検索条件が
like '%手机%'であることが検出され、titleに手机が含まれるデータを見つける必要がある - 2)各行データ(各リーフノード)を順番にたどる。例えば、最初に
idが1のデータを取得する - 3)データの
titleフィールド値が条件に合致するか判断する - 4)合致すれば結果セットに追加し、合致しなければ破棄する
- 5)ステップ1に戻る
以上のことから、idに基づく正確な一致の場合、インデックスを使用でき、検索効率が高いです。しかし、検索条件があいまい一致の場合、インデックスが有効にならないため、インデックス検索が全表スキャンに退化し、効率が悪くなります。
したがって、順方向インデックスはインデックスフィールドに基づく正確な検索には適していますが、部分用語に基づくあいまい一致には適していません。
一方、逆転インデックスはまさに部分用語に基づくあいまい一致の問題を解決します。
1.2.2.逆転インデックス
逆転インデックスには非常に重要な2つの概念があります:
- ドキュメント(
Document):検索するためのデータで、その各データが一つのドキュメントです。例えば、一つのウェブページ、一つの商品情報 - 用語(
Term):ドキュメントデータやユーザー検索データを、あるアルゴリズムで分割し、意味を持つ単語として得られたもの。例:「私は中国人です」は「私」、「は」、「中国人」、「中国」、「国人」のようないくつかの用語に分割できます
逆転インデックスの作成は、順方向インデックスの一特別な処理と応用であり、その流れは以下の通りです:
- 各ドキュメントのデータを分割アルゴリズムを使用して意味に基づき分割し、個々の用語にする
- 表を作成し、各行データには用語、用語が含まれるドキュメントid、位置などの情報を含める
- 用語の一意性があるため、用語に順方向インデックスを作成できる
このように、用語をインデックスとする表が作られたものが逆転インデックス表です。両者の比較は以下の通りです:
順方向インデックス
| **id(インデックス)** | **title** | **price** |
|---|---|---|
| 1 | 小米手机 | 3499 |
| 2 | 华为手机 | 4999 |
| 3 | 华为小米充电器 | 49 |
| 4 | 小米手环 | 49 |
| ... | ... | ... |
逆転インデックス
| **用語(インデックス)** | **ドキュメントid** |
|---|---|
| 小米 | 1,3,4 |
| 手机 | 1,2 |
| 华为 | 2,3 |
| 充电器 | 3 |
| 手环 | 4 |
逆転インデックスの検索フローは以下の通りです(「华为手机」を検索する例)、図:
フロー説明:
1)ユーザーが条件"华为手机"を入力して検索します。
2)ユーザー入力条件を分割し、用語を取得します:华为、手机。
3)用語を逆転インデックスで検索します(用語にインデックスがあるため、検索効率が非常に高い)、用語を含むドキュメントid:1、2、3が取得できます。
4)ドキュメントidを順方向インデックスで検索し、具体的なドキュメントを取得します(idにもインデックスがあるため、検索効率も非常に高いです)!
逆転インデックスを先に検索し、次に順方向インデックスを検索しますが、用語もドキュメントidもインデックスが作成されているため、検索速度は非常に速いです!全表スキャンは不要です。
1.2.3.順方向と逆転
なぜ一つは順方向インデックスと呼ばれ、もう一つは逆転インデックスと呼ばれるのでしょうか?
- 順方向インデックスは最も伝統的なもので、idに基づくインデックス方式です。しかし、用語に基づいて検索するには、まず各ドキュメントを逐次取得し、ドキュメントに必要な用語が含まれているか判断する必要があります。これはドキュメントから用語を探すプロセスです。
- 逆転インデックスはその逆で、まずユーザーが検索する用語を見つけ、用語に基づいて用語を含むドキュメントのidを取得し、次にidに基づいてドキュメントを取得します。これは用語からドキュメントを探すプロセスです。
ちょうど逆ではないでしょうか?
では、両者の方式の長所と短所は何でしょうか?
順方向インデックス:
-
長所:
-
複数のフィールドにインデックスを作成できる
-
インデックスフィールドに基づいて検索、ソートする速度が非常に速い
-
短所:
-
非インデックスフィールド、またはインデックスフィールドの部分用語に基づいて検索する場合、全表スキャンしかできない。
逆転インデックス:
-
長所:
-
用語に基づいて検索、あいまい検索する場合、速度が非常に速い
-
短所:
-
用語にのみインデックスを作成でき、フィールドには作成できない
-
フィールドに基づいてソートできない
1.3.基礎概念
elasticsearchには多くの独自の概念があり、MySQLとは少し異なりますが、類似点もあります。
1.3.1.ドキュメントとフィールド
elasticsearchは**ドキュメント(Document)**に基づいて保存されるもので、データベースの一つの商品データ、一つの注文情報などです。ドキュメントデータはJSON形式にシリアライズされた後、elasticsearchに保存されます:
したがって、元のデータベースの一行のデータがESの一つのJSONドキュメントであり、データベースの各行データには多くの列が含まれており、これらの列はJSONドキュメントの**フィールド(Field)**に変換されます。
1.3.2.インデックスとマッピング
ビジネスの発展に伴い、esに保存するドキュメントも増えていきます。例えば、商品ドキュメント、ユーザードキュメント、注文ドキュメントなどです:
すべてのドキュメントを散在させて保存すると、非常に混乱し、管理も不便です。
したがって、同じタイプのドキュメントを一箇所に集中して管理する必要があります。これを**インデックス(Index)**と呼びます。例えば:
商品インデックス
ユーザーインデックス
注文インデックス
- すべてのユーザードキュメントは、ユーザーのインデックスとしてまとめることができます。
- すべての商品ドキュメントは、商品のインデックスとしてまとめることができます。
- すべての注文ドキュメントは、注文のインデックスとしてまとめることができます。
したがって、インデックスをデータベースのテーブルと考えることができます。
データベースのテーブルには制約情報があり、テーブルの構造、フィールド名、タイプなどを定義するために使用されます。そのため、インデックスには**マッピング(mapping)**があり、これはインデックス内のドキュメントのフィールド制約情報で、テーブルの構造制約に似ています。
1.3.3.mysqlとelasticsearch
mysqlとelasticsearchの概念を統一的に比較してみましょう:
| **MySQL** | **Elasticsearch** | **説明** |
|---|---|---|
| Table | Index | インデックス(index)、はドキュメントのコレクションで、データベースのテーブル(table)に似ている |
| Row | Document | ドキュメント(Document)、はデータの各レコードで、データベースの行(Row)に似ており、ドキュメントはすべてJSON形式 |
| Column | Field | フィールド(Field)、はJSONドキュメント内のフィールドで、データベースの列(Column)に似ている |
| Schema | Mapping | Mapping(マッピング)はインデックス内のドキュメントの制約で、例えばフィールドタイプ制約。データベースのテーブル構造(Schema)に似ている |
| SQL | DSL | DSLはelasticsearchが提供するJSONスタイルのリクエスト文で、elasticsearchを操作し、CRUDを実現するために使用される |
図:
では、elasticsearchを学習すればmysqlは不要になるのでしょうか?
そうではありません。両者はそれぞれが得意な分野を持っています:
- Mysql:トランザクションタイプの操作に長けており、データのセキュリティと一貫性を確保できます
- Elasticsearch:大量データの検索、分析、計算に長けています
したがって、企業では両者を組み合わせて使用することが多いです:
- セキュリティが高い書き込み操作はmysqlで実現
- 検索性能が高い検索要件はelasticsearchで実現
- その後、何らかの方法でデータを同期し、一貫性を保証します
1.4.IKアナライザー
Elasticsearchの鍵は逆転インデックスであり、逆転インデックスはドキュメント内容の分割に依存しています。そして分割には効率的かつ正確な分割アルゴリズムが必要であり、IKアナライザーはそのような中国語分割アルゴリズムです。
1.4.1.IKアナライザーのインストール
方法1:オンラインインストール
以下のコマンドを実行するだけでインストールできます:
docker exec -it es ./bin/elasticsearch-plugin install https://github.com/medcl/elasticsearch-analysis-ik/releases/download/v7.12.1/elasticsearch-analysis-ik-7.12.1.zip
次にesコンテナを再起動します:
docker restart es
方法2:オフラインインストール
ネットワーク速度が遅い場合は、オフラインインストールも選択できます。
まず、以前にインストールしたElasticsearchコンテナのpluginsデータボリュームディレクトリを確認します:
docker volume inspect es-plugins
結果は以下の通りです:
[
{
"CreatedAt": "2024-11-06T10:06:34+08:00",
"Driver": "local",
"Labels": null,
"Mountpoint": "/var/lib/docker/volumes/es-plugins/_data",
"Name": "es-plugins",
"Options": null,
"Scope": "local"
}
]
elasticsearchのプラグインが/var/lib/docker/volumes/es-plugins/_dataディレクトリにマウントされていることがわかります。IKアナライザーをこのディレクトリにアップロードする必要があります。
記事末尾の資料には7.12.1バージョンのikアナライザーアーカイブファイルが提供されており、それを解圧する必要があります:
次に、仮想機の/var/lib/docker/volumes/es-plugins/_dataディレクトリにアップロードします:
最後に、esコンテナを再起動します:
docker restart es
1.4.2.IKアナライザーの使用
IKアナライザーには2つのモードが含まれます:
ik_smart:スマートセマンティック分割ik_max_word:最細粒度分割
KibanaのDevToolsでアナライザーをテストします。まず、Elasticsearch公式の標準アナライザーをテストします:
POST /_analyze
{
"analyzer": "standard",
"text": "黑马程序员学习java太棒了"
}
結果は以下の通りです:
{
"tokens" : [
{
"token" : "黑",
"start_offset" : 0,
"end_offset" : 1,
"type" : "<IDEOGRAPHIC>",
"position" : 0
},
{
"token" : "马",
"start_offset" : 1,
"end_offset" : 2,
"type" : "<IDEOGRAPHIC>",
"position" : 1
},
{
"token" : "程",
"start_offset" : 2,
"end_offset" : 3,
"type" : "<IDEOGRAPHIC>",
"position" : 2
},
{
"token" : "序",
"start_offset" : 3,
"end_offset" : 4,
"type" : "<IDEOGRAPHIC>",
"position" : 3
},
{
"token" : "员",
"start_offset" : 4,
"end_offset" : 5,
"type" : "<IDEOGRAPHIC>",
"position" : 4
},
{
"token" : "学",
"start_offset" : 5,
"end_offset" : 6,
"type" : "<IDEOGRAPHIC>",
"position" : 5
},
{
"token" : "习",
"start_offset" : 6,
"end_offset" : 7,
"type" : "<IDEOGRAPHIC>",
"position" : 6
},
{
"token" : "java",
"start_offset" : 7,
"end_offset" : 11,
"type" : "<ALPHANUM>",
"position" : 7
},
{
"token" : "太",
"start_offset" : 11,
"end_offset" : 12,
"type" : "<IDEOGRAPHIC>",
"position" : 8
},
{
"token" : "棒",
"start_offset" : 12,
"end_offset" : 13,
"type" : "<IDEOGRAPHIC>",
"position" : 9
},
{
"token" : "了",
"start_offset" : 13,
"end_offset" : 14,
"type" : "<IDEOGRAPHIC>",
"position" : 10
}
]
}
わかるように、標準アナライザーは1字1用語しかできず、中国語を正しく分割できません。
次にIKアナライザーをテストします:
POST /_analyze
{
"analyzer": "ik_smart",
"text": "黑马程序员学习java太棒了"
}
実行結果は以下の通りです:
{
"tokens" : [
{
"token" : "黑马",
"start_offset" : 0,
"end_offset" : 2,
"type" : "CN_WORD",
"position" : 0
},
{
"token" : "程序员",
"start_offset" : 2,
"end_offset" : 5,
"type" : "CN_WORD",
"position" : 1
},
{
"token" : "学习",
"start_offset" : 5,
"end_offset" : 7,
"type" : "CN_WORD",
"position" : 2
},
{
"token" : "java",
"start_offset" : 7,
"end_offset" : 11,
"type" : "ENGLISH",
"position" : 3
},
{
"token" : "太棒了",
"start_offset" : 11,
"end_offset" : 14,
"type" : "CN_WORD",
"position" : 4
}
]
}
1.4.3.拡張辞書
インターネットの発展に伴い、「造語運動」もますます活発になっています。多くの新しい言葉が生まれ、元の語彙リストには存在しません。例:「泰裤辣」、「传智播客」など。
IKアナライザーはこれらの語彙を分割できません。テストしてみましょう:
POST /_analyze
{
"analyzer": "ik_max_word",
"text": "传智播客开设大学,真的泰裤辣!"
}
結果:
{
"tokens" : [
{
"token" : "传",
"start_offset" : 0,
"end_offset" : 1,
"type" : "CN_CHAR",
"position" : 0
},
{
"token" : "智",
"start_offset" : 1,
"end_offset" : 2,
"type" : "CN_CHAR",
"position" : 1
},
{
"token" : "播",
"start_offset" : 2,
"end_offset" : 3,
"type" : "CN_CHAR",
"position" : 2
},
{
"token" : "客",
"start_offset" : 3,
"end_offset" : 4,
"type" : "CN_CHAR",
"position" : 3
},
{
"token" : "开设",
"start_offset" : 4,
"end_offset" : 6,
"type" : "CN_WORD",
"position" : 4
},
{
"token" : "大学",
"start_offset" : 6,
"end_offset" : 8,
"type" : "CN_WORD",
"position" : 5
},
{
"token" : "真的",
"start_offset" : 9,
"end_offset" : 11,
"type" : "CN_WORD",
"position" : 6
},
{
"token" : "泰",
"start_offset" : 11,
"end_offset" : 12,
"type" : "CN_CHAR",
"position" : 7
},
{
"token" : "裤",
"start_offset" : 12,
"end_offset" : 13,
"type" : "CN_CHAR",
"position" : 8
},
{
"token" : "辣",
"start_offset" : 13,
"end_offset" : 14,
"type" : "CN_CHAR",
"position" : 9
}
]
}
わかるように、「传智播客」と「泰裤辣」は正しく分割できません。
したがって、正しく分割するには、IKアナライザーの辞書も絶えず更新する必要があります。IKアナライザーは語彙を拡張する機能を提供しています。
1)IKアナライザーコンフィグディレクトリを開きます:
注意点として、オンラインインストールの場合、デフォルトではconfigディレクトリはありません。事前資料で提供されたikのconfigを対応するディレクトリにアップロードする必要があります。
2)IKAnalyzer.cfg.xml設定ファイルに以下の内容を追加します:
<?xml version="1.0" encoding="UTF-8"?>
<properties>
<comment>IK Analyzer 拡張設定</comment>
<!--ユーザーはここで自分の拡張辞書を設定できます *** 拡張辞書を追加-->
<entry key="ext_dict">ext.dic</entry>
</properties>
3)IKアナライザーコンフィグディレクトリにext.dicを新しく作成します。configディレクトリの下にある設定ファイルをコピーして変更することもできます:
传智播客
泰裤辣
4)elasticsearchを再起動します:
docker restart es
# ログを確認
docker logs -f elasticsearch
再度テストすると、「传智播客」と「泰裤辣」が正しく分割されていることがわかります:
{
"tokens" : [
{
"token" : "传智播客",
"start_offset" : 0,
"end_offset" : 4,
"type" : "CN_WORD",
"position" : 0
},
{
"token" : "开设",
"start_offset" : 4,
"end_offset" : 6,
"type" : "CN_WORD",
"position" : 1
},
{
"token" : "大学",
"start_offset" : 6,
"end_offset" : 8,
"type" : "CN_WORD",
"position" : 2
},
{
"token" : "真的",
"start_offset" : 9,
"end_offset" : 11,
"type" : "CN_WORD",
"position" : 3
},
{
"token" : "泰裤辣",
"start_offset" : 11,
"end_offset" : 14,
"type" : "CN_WORD",
"position" : 4
}
]
}
1.4.4.まとめ
アナライザーの役割は何ですか?
- 逆転インデックスを作成する際に、ドキュメントを分割する
- ユーザーが検索する際に入力された内容を分割する
IKアナライザーにはいくつかのモードがありますか?
ik_smart:スマート分割、粗粒度ik_max_word:最細分割、細粒度
IKアナライザーは語彙を拡張しますか?語彙を停止しますか?
- configディレクトリの
IkAnalyzer.cfg.xmlファイルを使用して拡張辞書と停止辞書を追加する - 辞書に拡張語彙または停止語彙を追加する
2.インデックス操作
Indexはデータベースのテーブルに似ており、Mappingマッピングはテーブルの構造に似ています。esにデータを保存するには、まずIndexとMappingを作成する必要があります
2.1.Mappingマッピングプロパティ
Mappingはインデックス内のドキュメントの制約であり、一般的なMappingプロパティには以下のものが含まれます:
-
type:フィールドデータタイプ、一般的なシンプルタイプには以下のものがあります: -
文字列:
text(分割可能なテキスト)、keyword(正確な値、例:ブランド、国、IPアドレス) -
数値:
long、integer、short、byte、double、float、 -
ブール値:
boolean -
日付:
date -
オブジェクト:
object -
index:インデックスを作成するかどうか、デフォルトはtrue -
analyzer:どのアナライザーを使用するか -
properties:フィールドのサブフィールド
以下のJSONドキュメントを例に挙げます:
{
"age": 21,
"weight": 52.1,
"isMarried": false,
"info": "黑马程序员Java讲师",
"email": "zy@itcast.cn",
"score": [99.1, 99.5, 98.9],
"name": {
"firstName": "云",
"lastName": "赵"
}
}
各フィールドに対応するマッピング(Mapping):
| **フィールド名** | **フィールドタイプ** | **タイプ説明** | **検索に** **参加** | **分割に** **参加** | **アナライザー** |
|---|---|---|---|---|---|
| age | integer |
整数 | - | —— | |
| weight | float |
浮動小数点数 | - | —— | |
| isMarried | boolean |
ブール値 | - | —— | |
| info | text |
文字列だが分割が必要 | - | - | IK |
keyword |
文字列だが分割しない | —— | |||
| score | float |
配列内の要素タイプのみ参照 | - | —— | |
| name | firstName | keyword |
文字列だが分割しない | - | |
| lastName | keyword |
文字列だが分割しない | - | —— |
2.2.インデックスのCRUD
ElasticsearchはRestfulスタイルのAPIを採用しているため、そのリクエスト方法とパスは比較的規範的であり、リクエストパラメータもJSONスタイルです。
KibanaのDevToolsに基づいてリクエストを作成してテストします。構文ヒントがあるため、非常に便利です。
2.2.1.インデックスとマッピングの作成
基本構文:
- リクエスト方法:
PUT - リクエストパス:
/インデックス名、カスタマイズ可能 - リクエストパラメータ:
mappingマッピング
形式:
PUT /インデックス名
{
"mappings": {
"properties": {
"フィールド名":{
"type": "text",
"analyzer": "ik_smart"
},
"フィールド名2":{
"type": "keyword",
"index": "false"
},
"フィールド名3":{
"properties": {
"サブフィールド": {
"type": "keyword"
}
}
},
// ...略
}
}
}
例:
# PUT /heima
{
"mappings": {
"properties": {
"info":{
"type": "text",
"analyzer": "ik_smart"
},
"email":{
"type": "keyword",
"index": "false"
},
"name":{
"properties": {
"firstName": {
"type": "keyword"
}
}
}
}
}
}
2.2.2.インデックスのクエリ
基本構文:
- リクエスト方法:GET
- リクエストパス:/インデックス名
- リクエストパラメータ:なし
形式:
GET /インデックス名
例:
GET /heima
2.2.3.インデックスの変更
逆転インデックス構造はそれほど複雑ではありませんが、データ構造が変更された場合(例えばアナライザーを変更した場合)、逆転インデックスを再作成する必要があり、これはまさに災害です。したがって、インデックスは一度作成されると、mappingを変更できません。
mappingに既存のフィールドを変更することはできませんが、mappingに新しいフィールドを追加することは許可されています。なぜなら、逆転インデックスに影響を与えないからです。したがって、インデックスを変更できるのは、インデックスに新しいフィールドを追加すること、またはインデックスの基本属性を更新することです。
構文説明:
PUT /インデックス名/_mapping
{
"properties": {
"新しいフィールド名":{
"type": "integer"
}
}
}
例:
PUT /heima/_mapping
{
"properties": {
"age":{
"type": "integer"
}
}
}
2.2.4.インデックスの削除
構文:
- リクエスト方法:DELETE
- リクエストパス:/インデックス名
- リクエストパラメータ:なし
形式:
DELETE /インデックス名
例:
DELETE /heima
2.2.5.まとめ
インデックス操作には以下のものがあります:
- インデックスの作成:PUT /インデックス名
- インデックスのクエリ:GET /インデックス名
- インデックスの削除:DELETE /インデックス名
- インデックスの変更、フィールドの追加:PUT /インデックス名/_mapping
わかるように、インデックスの操作は基本的にRestfulスタイルに従っており、したがってAPIインターフェースは非常に統一されており、記憶しやすいです。
3.ドキュメント操作
インデックスが準備できたので、次にインデックスにデータを追加できます。
Elasticsearchのデータは実際にはJSONスタイルのドキュメントです。ドキュメントを操作する自然に追加、削除、更新、検索などの一般的な操作があり、それぞれ学習します。
3.1.ドキュメントの追加
構文:
POST /インデックス名/_doc/ドキュメントid
{
"フィールド1": "値1",
"フィールド2": "値2",
"フィールド3": {
"サブプロパティ1": "値3",
"サブプロパティ2": "値4"
},
}
例:
POST /heima/_doc/1
{
"info": "黑马程序员Java讲师",
"email": "zy@itcast.cn",
"name": {
"firstName": "云",
"lastName": "赵"
}
}
応答:
3.2.ドキュメントのクエリ
restスタイルに基づき、追加はpost、クエリはgetになります。ただし、クエリには通常条件が必要です。ここではドキュメントidを指定します。
構文:
GET /{インデックス名}/_doc/{id}
例:
GET /heima/_doc/1
結果の確認:
3.3.ドキュメントの削除
削除にはDELETEリクエストを使用し、同様にidに基づいて削除する必要があります:
構文:
DELETE /{インデックス名}/_doc/id値
例:
DELETE /heima/_doc/1
結果:
3.4.ドキュメントの更新
更新には2つの方法があります:
- 全量更新:元のドキュメントを直接上書き
- 部分更新:ドキュメント内の一部フィールドを更新
3.4.1.全量更新
全量更新は元のドキュメントを上書きするもので、その本質は2つの操作です:
- 指定されたidに基づいてドキュメントを削除
- 同じidのドキュメントを新規追加
注意:idに基づいて削除する際にidが存在しない場合、2番目の新規追加も実行され、更新から新規追加に変わります。
構文:
PUT /{インデックス名}/_doc/ドキュメントid
{
"フィールド1": "値1",
"フィールド2": "値2",
// ... 略
}
例:
PUT /heima/_doc/1
{
"info": "黑马程序员高级Java讲师",
"email": "zy@itcast.cn",
"name": {
"firstName": "云",
"lastName": "赵"
}
}
idが1のドキュメントはすでに削除されているため、最初の実行時のフィードバックはcreatedです:
したがって、2回目を実行すると、フィードバックはupdatedになります:
3.4.2.部分更新
部分更新は、指定されたidに一致するドキュメント内の一部フィールドのみを更新します。
構文:
POST /{インデックス名}/_update/ドキュメントid
{
"doc": {
"フィールド名": "新しい値",
}
}
例:
POST /heima/_update/1
{
"doc": {
"email": "ZhaoYun@itcast.cn"
}
}
実行結果:
3.5.バッチ処理
バッチ処理はPOSTリクエストを使用し、基本構文は以下の通りです:
POST _bulk
{ "index" : { "_index" : "test", "_id" : "1" } }
{ "field1" : "value1" }
{ "delete" : { "_index" : "test", "_id" : "2" } }
{ "create" : { "_index" : "test", "_id" : "3" } }
{ "field1" : "value3" }
{ "update" : {"_id" : "1", "_index" : "test"} }
{ "doc" : {"field2" : "value2"} }
-
indexは追加操作を表します -
_index:インデックス名を指定 -
_id:操作するドキュメントidを指定 -
{ "field1" : "value1" }:追加するドキュメント内容 -
deleteは削除操作を表します -
_index:インデックス名を指定 -
_id:操作するドキュメントidを指定 -
updateは更新操作を表します -
_index:インデックス名を指定 -
_id:操作するドキュメントidを指定 -
{ "doc" : {"field2" : "value2"} }:更新するドキュメントフィールド
例、バッチ追加:
POST /_bulk
{"index": {"_index":"heima", "_id": "3"}}
{"info": "黑马程序员C++讲师", "email": "ww@itcast.cn", "name":{"firstName": "五", "lastName":"王"}}
{"index": {"_index":"heima", "_id": "4"}}
{"info": "黑马程序员前端讲师", "email": "zhangsan@itcast.cn", "name":{"firstName": "三", "lastName":"张"}}
バッチ削除:
POST /_bulk
{"delete":{"_index":"heima", "_id": "3"}}
{"delete":{"_index":"heima", "_id": "4"}}
3.6.まとめ
ドキュメント操作には以下のものがあります:
-
ドキュメントの作成:
POST /{インデックス名}/_doc/ドキュメントid { jsonドキュメント } -
ドキュメントのクエリ:
GET /{インデックス名}/_doc/ドキュメントid -
ドキュメントの削除:
DELETE /{インデックス名}/_doc/ドキュメントid -
ドキュメントの更新:
-
全量更新:
PUT /{インデックス名}/_doc/ドキュメントid { jsonドキュメント } -
部分更新:
POST /{インデックス名}/_update/ドキュメントid { "doc": {フィールド}}
5.RestClientによるドキュメント操作
インデックスが準備できたので、ドキュメントを操作できます。インデックス操作と分離するため、再度テストクラスを作成し、2つのことを行います:
- RestHighLevelClientの初期化
- 私たちの商品データはデータベースにあり、IHotelServiceを使用してクエリする必要があるため、このインターフェースを注入します
package com.hmall.item.es;
import com.hmall.item.service.IItemService;
import org.apache.http.HttpHost;
import org.elasticsearch.client.RestClient;
import org.elasticsearch.client.RestHighLevelClient;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import java.io.IOException;
@SpringBootTest(properties = "spring.profiles.active=local")
public class DocumentTest {
private RestHighLevelClient client;
@Autowired
private IItemService itemService;
@BeforeEach
void setUp() {
this.client = new RestHighLevelClient(RestClient.builder(
HttpHost.create("http://192.168.150.101:9200")
));
}
@AfterEach
void tearDown() throws IOException {
this.client.close();
}
}
5.1.ドキュメントの追加
データベースの商品情報をelasticsearchにインポートする必要があります。偽のデータではなく、実際の商品データを使用します。
5.1.1.エンティティクラス
インデックス構造とデータベース構造にはまだいくつかの差異があるため、インデックス構造に対応するエンティティを定義する必要があります。
hm-serviceモジュールのcom.hmall.item.domain.dtoパッケージに新しいDTOを定義します:
package com.hmall.item.domain.po;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import java.time.LocalDateTime;
@Data
@ApiModel(description = "インデックスエンティティ")
public class ItemDoc{
@ApiModelProperty("商品id")
private String id;
@ApiModelProperty("商品名")
private String name;
@ApiModelProperty("価格(分)")
private Integer price;
@ApiModelProperty("商品画像")
private String image;
@ApiModelProperty("カテゴリー名")
private String category;
@ApiModelProperty("ブランド名")
private String brand;
@ApiModelProperty("売上数")
private Integer sold;
@ApiModelProperty("コメント数")
private Integer commentCount;
@ApiModelProperty("広告かどうか、true/false")
private Boolean isAD;
@ApiModelProperty("更新日時")
private LocalDateTime updateTime;
}
5.1.2.API構文
ドキュメント追加のリクエスト構文は以下の通りです:
POST /{インデックス名}/_doc/1
{
"name": "Jack",
"age": 21
}
対応するJavaAPIは以下の通りです:
インデックス操作のAPIと非常によく似ており、同じように3つのステップがあります:
- 1)Requestオブジェクトを作成します。ここでは
IndexRequestで、ドキュメントを追加するとは逆転インデックスを作成するプロセスです - 2)リクエストパラメータを準備します。この例ではJsonドキュメントです
- 3)リクエストを送信します
変化した点は、ここで直接client.xxx()APIを使用し、もはやclient.indices()を必要としないことです。
5.1.3.完全なコード
商品データをインポートするには、APIテンプレート「3ステップ」を参照するだけでなく、いくつかの準備作業も必要です:
- 商品データはデータベースから取得するため、まず
Itemオブジェクトをクエリします ItemオブジェクトをItemDocオブジェクトに変換しますItemDTOをJSON形式にシリアライズします
したがって、コード全体のステップは以下の通りです:
- 1)idに基づいて商品データ
Itemをクエリします - 2)
Itemをドキュメントタイプにカプセル化します - 3)
ItemDocをJSONに変換します - 4)IndexRequestを作成し、インデックス名とidを指定します
- 5)リクエストパラメータ、つまりJSONドキュメントを準備します
- 6)リクエストを送信します
item-serviceのDocumentTestテストクラスに、単体テストを作成します:
@Test
void testAddDocument() throws IOException {
// 1.idに基づいて商品データをクエリ
Item item = itemService.getById(100002644680L);
// 2.ドキュメントタイプに変換
ItemDoc itemDoc = BeanUtil.copyProperties(item, ItemDoc.class);
// 3.ItemDTOをjsonに変換
String doc = JSONUtil.toJsonStr(itemDoc);
// 1.Requestオブジェクトを準備
IndexRequest request = new IndexRequest("items").id(itemDoc.getId());
// 2.Jsonドキュメントを準備
request.source(doc, XContentType.JSON);
// 3.リクエストを送信
client.index(request, RequestOptions.DEFAULT);
}
5.2.ドキュメントのクエリ
idに基づいてドキュメントをクエリする例を取り上げます
5.2.1.構文説明
クエリのリクエスト文は以下の通りです:
GET /{インデックス名}/_doc/{id}
以前のフローと同様に、コードはおおよそ2ステップです:
- Requestオブジェクトを作成
- リクエストを送信、結果を取得
しかし、クエリの目的は結果を得ることです。ItemDTOに解析し、さらに結果の解析を追加する必要があります。例示コードは以下の通りです:
わかるように、応答結果はJSONであり、ドキュメントは_sourceプロパティに含まれています。したがって、解析は_sourceを取得し、Javaオブジェクトに逆シリアライズすることです。
他のコードは以前と同様で、フローは以下の通りです:
- 1)Requestオブジェクトを準備します。今回はクエリなので、
GetRequestです - 2)リクエストを送信し、結果を取得します。クエリなので、ここで
client.get()メソッドを呼び出します - 3)結果を解析します。つまりJSONを逆シリアライズします
5.2.2.完全なコード
DELETE /hotel/_doc/{id}
item-serviceのDocumentTestテストクラスに、単体テストを作成します:
@Test
void testGetDocumentById() throws IOException {
// 1.Requestオブジェクトを準備
GetRequest request = new GetRequest("items").id("100002644680");
// 2.リクエストを送信
GetResponse response = client.get(request, RequestOptions.DEFAULT);
// 3.応答結果のsourceを取得
String json = response.getSourceAsString();
ItemDoc itemDoc = JSONUtil.toBean(json, ItemDoc.class);
System.out.println("itemDoc= " + ItemDoc);
}
5.3.ドキュメントの削除
削除のリクエスト文は以下の通りです:
DELETE /hotel/_doc/{id}
クエリと比較すると、リクエスト方法がDELETEからGETに変わっただけです。Javaコードはおそらく同じく2ステップです:
- 1)Requestオブジェクトを準備します。削除なので今回は
DeleteRequestオブジェクトです。インデックス名とidを指定する必要があります - 2)リクエストを送信します。削除なので、
client.delete()メソッドです
item-serviceのDocumentTestテストクラスに、単体テストを作成します:
@Test
void testDeleteDocument() throws IOException {
// 1.Requestを準備、2つのパラメータ、最初はインデックス名、2番目はドキュメントid
DeleteRequest request = new DeleteRequest("item", "100002644680");
// 2.リクエストを送信
client.delete(request, RequestOptions.DEFAULT);
}
5.4.ドキュメントの更新
更新には2つの方法があります:
- 全量更新:本質的にidに基づいて削除し、再度追加する
- 部分更新:ドキュメント内の指定フィールド値を更新
RestClientのAPIでは、全量更新は追加のAPIと完全に一致し、判断基準はIDです:
- 追加時にIDが既に存在する場合、更新します
- 追加時にIDが存在しない場合、新規追加します
ここでは繰り返しません。主に部分更新のAPIに注目しましょう。
5.4.1.構文説明
部分更新のリクエスト構文は以下の通りです:
POST /{インデックス名}/_update/{id}
{
"doc": {
"フィールド名": "フィールド値",
"フィールド名": "フィールド値"
}
}
コード例は図の通りです:
以前と同様に、3つのステップです:
- 1)
Requestオブジェクトを準備します。今回は更新なので、UpdateRequestです - 2)パラメータを準備します。つまりJSONドキュメントで、更新するフィールドが含まれます
- 3)ドキュメントを更新します。ここで
client.update()メソッドを呼び出します
5.4.2.完全なコード
item-serviceのDocumentTestテストクラスに、単体テストを作成します:
@Test
void testUpdateDocument() throws IOException {
// 1.Requestを準備
UpdateRequest request = new UpdateRequest("items", "100002644680");
// 2.リクエストパラメータを準備
request.doc(
"price", 58800,
"commentCount", 1
);
// 3.リクエストを送信
client.update(request, RequestOptions.DEFAULT);
}
5.5.ドキュメントのバッチインポート
以前のケースでは、すべて単一のドキュメントを操作していました。しかし、データベースの商品データは実際には数十万件に達し、一部のプロジェクトでは数百万件に達する可能性があります。
これらのデータをインデックスにインポートする場合、逐次インポートすることはできず、バッチ処理ソリューションを採用する必要があります。一般的なソリューションには以下のものがあります:
-
Logstashを使用したバッチインポート
-
Logstashのインストールが必要
-
データの再加工能力が比較的弱い
-
コーディング不要だが、Logstashインポート設定の作成を学習する必要あり
-
JavaAPIを使用したバッチインポート
-
コーディングが必要だが、JavaAPIに基づいているため、学習コストが低い
-
より柔軟で、データを任意に再加工処理してからインデックスに書き込める
次に、JavaAPIを使用してドキュメントのバッチインポートを実現する方法を学習します。
5.5.1.構文説明
バッチ処理は、前に説明したドキュメントのCRUDステップと基本的に一致しています:
- Requestを作成しますが、今回は
BulkRequestを使用します - リクエストパラメータを準備
- リクエストを送信します。今回は
client.bulk()メソッドを使用します
BulkRequest自体は実際にはリクエストパラメータを持っていません。その本質は、複数の通常のCRUDリクエストを組み合わせて一緒に送信することです。例:
- バッチドキュメント追加は、各ドキュメントに対して
IndexRequestリクエストを作成し、それをBulkRequestにカプセル化して一緒に送信します。 - バッチ削除は、N個の
DeleteRequestリクエストを作成し、それをBulkRequestにカプセル化して一緒に送信します
したがって、BulkRequestにはaddメソッドが提供されており、他のCRUDリクエストを追加するために使用されます:
わかるように、追加できるリクエストは以下のものです:
IndexRequest、つまり追加UpdateRequest、つまり更新DeleteRequest、つまり削除
したがって、Bulkに複数のIndexRequestを追加することは、バッチ追加機能です。例:
@Test
void testBulk() throws IOException {
// 1.Requestを作成
BulkRequest request = new BulkRequest();
// 2.リクエストパラメータを準備
request.add(new IndexRequest("items").id("1").source("json doc1", XContentType.JSON));
request.add(new IndexRequest("items").id("2").source("json doc2", XContentType.JSON));
// 3.リクエストを送信
client.bulk(request, RequestOptions.DEFAULT);
}
5.5.2.完全なコード
商品データをインポートする場合、商品数は数十万件に達するため、一度にすべてインポートすることはできません。ループ反復方式を採用し、各回1000件程度のデータをインポートすることをお勧めします。
item-serviceのDocumentTestテストクラスに、単体テストを作成します:
@Test
void testLoadItemDocs() throws IOException {
// ページングで商品データをクエリ
int pageNo = 1;
int size = 1000;
while (true) {
Page<Item> page = itemService.lambdaQuery().eq(Item::getStatus, 1).page(new Page<Item>(pageNo, size));
// 非nullチェック
List<Item> items = page.getRecords();
if (CollUtils.isEmpty(items)) {
return;
}
log.info("第{}ページのデータをロード、合計{}件", pageNo, items.size());
// 1.Requestを作成
BulkRequest request = new BulkRequest("items");
// 2.パラメータを準備、複数の追加リクエストを追加
for (Item item : items) {
// 2.1.ドキュメントタイプItemDTOに変換
ItemDoc itemDoc = BeanUtil.copyProperties(item, ItemDoc.class);
// 2.2.追加ドキュメントのRequestオブジェクトを作成
request.add(new IndexRequest()
.id(itemDoc.getId())
.source(JSONUtil.toJsonStr(itemDoc), XContentType.JSON));
}
// 3.リクエストを送信
client.bulk(request, RequestOptions.DEFAULT);
// ページング
pageNo++;
}
}
5.6.まとめ
ドキュメント操作の基本的なステップ:
-
RestHighLevelClientを初期化 -
XxxRequestを作成。
-
XXXは
Index、Get、Update、Delete、Bulk -
パラメータを準備(
Index、Update、Bulkの場合) -
リクエストを送信。
-
RestHighLevelClient#.xxx()メソッドを呼び出し、xxxはindex、get、update、delete、bulk -
結果を解析(
Getの場合)
全体の資料
リンク:https://pan.baidu.com/s/1z08p65lEyWvkd7IAQ2eDyQ 提取码:6666