2021/10/13
ElasticsearchをJavaAPIで触ってみた
目次
こんにちは!SMOOSYの開発を担当している周です。
SMOOSYを利用している学会がどんどん増えてきたなかで、請求検索機能を使う時に検索のスピードが遅いという声が出ました。そこで、解決策の一つとして使えそうか検索エンジンElasticsearchをJavaAPIで触ってみました。
Elasticsearchとは
Elastic社により開発されたオープンソースの全文検索エンジンです。Apache Luceneをベースとしており、インデックスから目的の単語を含むドキュメントを高速に検索することが可能です。簡単に言えば、検索に特化したクエリを実行することができるデータベースのようなものです。NoSQLのDBといっても良いと思います。世界で多く企業の検索サイトに使われています。アトラスでもConfitで利用しています。
環境準備
今回は以下の環境でElasticsearchを利用してみます。
-
- macOS 11.6
- Elasticsearch 7.8.0
- IntelliJ IDEA 2021.1.3
- java 11
インストール
Elasticsearchをインストールするには、まずJavaの実行環境をインストールする必要があります。Javaの実行環境をインストールしていない方はこちらを参考にしてください。
Elasticsearchのダウンロードと解凍
1 2 |
$ wget https://artifacts.elastic.co/downloads/elasticsearch/elasticsearch-7.8.0.zip $ unzip elasticsearch-7.8.0.zip |
Elasticsearchの実行
1 2 |
$ cd elasticsearch-7.8.0/ $ ./bin/elasticsearch -d |
接続確認
1 |
$ curl http://127.0.0.1:9200 |
上記のコマンドを実行し、下記のようなJSONのレスポンスが出たら、インストールは成功です。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
{ "name" : "C2104-03.local", "cluster_name" : "elasticsearch", "cluster_uuid" : "0Y2gcLcpQzqzlzEybF4hRA", "version" : { "number" : "7.8.0", "build_flavor" : "default", "build_type" : "tar", "build_hash" : "757314695644ea9a1dc2fecd26d1a43856725e65", "build_date" : "2020-06-14T19:35:50.234439Z", "build_snapshot" : false, "lucene_version" : "8.5.1", "minimum_wire_compatibility_version" : "6.8.0", "minimum_index_compatibility_version" : "6.0.0-beta1" }, "tagline" : "You Know, for Search" } |
要注意:Elasticsearchのバージョンアップによって、一部機能が使えなくなる場合があります。この記事の内容をやってみたい時、環境準備に記載しているバージョンをチェックした上で試してください。
さっそく始めましょう!
始める前に、Elasticsearchのインデックスとドキュメントとクエリを理解する必要があります。簡単に言うと、下記の表のような感じです。
Elasticsearch | リレーショナルデータベース |
---|---|
インデックス | データベース |
ドキュメント | レコード |
クエリ | SQL文 |
インデックスとドキュメントの概念は公式リファレンスの基本概念を参考にしてください。
pom.xml
pomファイルにライブラリ情報を下記のように記述してください。記述するとElasticsearchのClientが使えるようになります。
1 2 3 4 5 6 7 8 9 10 |
<dependency> <groupId>org.elasticsearch</groupId> <artifactId>elasticsearch</artifactId> <version>7.8.0</version> </dependency> <dependency> <groupId>org.elasticsearch.client</groupId> <artifactId>elasticsearch-rest-high-level-client</artifactId> <version>7.8.0</version> </dependency> |
インデックス
インデックスはリレーショナルデータベースのデータベースのようなものです。これから、インデックスに関する作成、検索、削除の処理をやってみます。
インデックスを作成する
userというインデックスを作成します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
public static void main(String[] args) throws IOException { // RESTクライアントを作成する RestHighLevelClient restHighLevelClient = new RestHighLevelClient( RestClient.builder(new HttpHost("localhost", 9200, "http")) ); // userインデックスを作成する CreateIndexRequest request = new CreateIndexRequest("user"); CreateIndexResponse createIndexResponse = restHighLevelClient.indices().create(request, RequestOptions.DEFAULT); // 作成したならtrue boolean acknowledged = createIndexResponse.isAcknowledged(); System.out.println(acknowledged); // RESTクライアントを閉じる restHighLevelClient.close(); } |
作成確認
1 |
$ curl http://127.0.0.1:9200/user |
レスポンス
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
{ "user": { "aliases": {}, "mappings": {}, "settings": { "index": { "creation_date": "1633138395805", "number_of_shards": "1", "number_of_replicas": "1", "uuid": "U5RdxqFhQsOJZK7lvxt7VA", "version": { "created": "7080099" }, "provided_name": "user" } } } } |
レスポンスの結果により、 userというインデックスが作成されたことが分かります。
インデックスを検索する
先程追加したuserのインデックスを検索してみます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
public static void main(String[] args) throws IOException { // RESTクライアントを作成する RestHighLevelClient restHighLevelClient = new RestHighLevelClient( RestClient.builder(new HttpHost("localhost", 9200, "http")) ); // userインデックスを検索する GetIndexRequest request = new GetIndexRequest("user"); GetIndexResponse getIndexResponse = restHighLevelClient.indices().get(request, RequestOptions.DEFAULT); // 検索結果を出力する System.out.println(getIndexResponse.getAliases()); System.out.println(getIndexResponse.getMappings()); System.out.println(getIndexResponse.getSettings()); // RESTクライアントを閉じる restHighLevelClient.close(); } |
consoleで出力した結果
1 2 3 |
{user=[]} {user=org.elasticsearch.cluster.metadata.MappingMetadata@976611f9} {user={"index.creation_date":"1633138395805","index.number_of_replicas":"1","index.number_of_shards":"1","index.provided_name":"user","index.uuid":"U5RdxqFhQsOJZK7lvxt7VA","index.version.created":"7080099"}} |
出力結果がCURLコマンドでのレスポンス内容と同じなので、インデックス検索ができていることが分かります。
インデックスを削除する
作成したインデックスを削除したいときは、下記の書き方で削除することができます。
1 2 3 4 5 6 7 8 9 10 11 12 13 |
public static void main(String[] args) throws IOException { // RESTクライアントを作成する RestHighLevelClient restHighLevelClient = new RestHighLevelClient( RestClient.builder(new HttpHost("localhost", 9200, "http")) ); // インデックスを削除する DeleteIndexRequest request = new DeleteIndexRequest("user"); AcknowledgedResponse delete = restHighLevelClient.indices().delete(request, RequestOptions.DEFAULT); // 削除結果:削除したならtrue System.out.println(delete.isAcknowledged()); // RESTクライアントを閉じる restHighLevelClient.close(); } |
ドキュメント
ドキュメントはリレーショナルデータベースの行のようなものです。これから、ドキュメントの作成、検索、更新、削除に関する操作をやってみます。
ドキュメントを作成する
userのインデックスに「id=1001、name=”周001”、sex=”男”、age=”30”」というドキュメントを作成します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 |
public static void main(String[] args) throws IOException { // RESTクライアントを作成する RestHighLevelClient restHighLevelClient = new RestHighLevelClient( RestClient.builder(new HttpHost("localhost", 9200, "http")) ); // データを作る IndexRequest request = new IndexRequest(); // idを指定する request.index("user").id("1001"); // idを指定しなければ // request.index("user") User user = new User(); user.setName("周001"); user.setSex("男"); user.setAge(30); // ESに挿入する時にJSONフォマットが必要なので、JSONに変更する ObjectMapper mapper = new ObjectMapper(); String asString = mapper.writeValueAsString(user); request.source(asString, XContentType.JSON); // ドキュメントを作成する IndexResponse response = restHighLevelClient.index(request, RequestOptions.DEFAULT); // 結果を出力する System.out.println(response.getResult()); // RESTクライアントを閉じる restHighLevelClient.close(); } |
作成確認
1 |
$ curl http://127.0.0.1:9200/user/_doc/1001 |
レスポンス
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
{ "_index": "user", "_type": "_doc", "_id": "1001", "_version": 2, "_seq_no": 1, "_primary_term": 1, "found": true, "_source": { "name": "周001", "sex": "男", "age": 30 } } |
レスポンス結果から、”_id”: “1001”のドキュメントが作成されたことが分かります。
ドキュメントを検索する
先程作成したドキュメントをidで検索します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
public static void main(String[] args) throws IOException { // RESTクライアントを作成する RestHighLevelClient restHighLevelClient = new RestHighLevelClient( RestClient.builder(new HttpHost("localhost", 9200, "http")) ); // idを指定する GetRequest request = new GetRequest(); request.index("user").id("1001"); // ドキュメントを検索する GetResponse response = restHighLevelClient.get(request, RequestOptions.DEFAULT); // 出力する System.out.println(response.getSourceAsString()); // RESTクライアントを閉じる restHighLevelClient.close(); } |
出力結果
1 |
{"name":"周001","sex":"女","age":30} |
結果により、idを通じてインデックスからドキュメントを取得したことがわかります。
ドキュメントを更新する
作成した「id=1001」のドキュメントで「sex」を「男」から「女」に変更します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
public static void main(String[] args) throws IOException { // RESTクライアントを作成する RestHighLevelClient restHighLevelClient = new RestHighLevelClient( RestClient.builder(new HttpHost("localhost", 9200, "http")) ); // データを修正する UpdateRequest request = new UpdateRequest(); request.index("user").id("1001"); // 性別の男を女に変更する request.doc(XContentType.JSON, "sex", "女"); // ドキュメントを更新する UpdateResponse response = restHighLevelClient.update(request, RequestOptions.DEFAULT); // 結果を出力する System.out.println(response.getResult()); // RESTクライアントを閉じる restHighLevelClient.close(); } |
作成確認
1 |
$ curl http://127.0.0.1:9200/user/_doc/1001 |
レスポンス
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
{ "_index": "user", "_type": "_doc", "_id": "1001", "_version": 3, "_seq_no": 3, "_primary_term": 1, "found": true, "_source": { "name": "周001", "sex": "女", "age": 30 } } |
レスポンス結果より、「sex」が「男」から「女」に変更されたことが分かります。
ドキュメントを削除する
作成した「id=1001」のドキュメントを削除します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
public static void main(String[] args) throws IOException { // RESTクライアントを作成する RestHighLevelClient restHighLevelClient = new RestHighLevelClient( RestClient.builder(new HttpHost("localhost", 9200, "http")) ); // idを指定する DeleteRequest request = new DeleteRequest(); request.index("user").id("1001"); // ドキュメントを削除する DeleteResponse response = restHighLevelClient.delete(request, RequestOptions.DEFAULT); // 結果を出力する System.out.println(response.toString()); // 关闭客户端 restHighLevelClient.close(); } |
出力結果
1 |
DeleteResponse[index=user,type=_doc,id=1001,version=4,result=deleted,shards=ShardInfo{total=2, successful=1, failures=[]}] |
DeleteResponseのresultでdeletedを返却していることから、ドキュメントが削除されたことがわかります。
ドキュメントを複数作成する
ドキュメントはデータベースの行のように、一度に複数作成することもできます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
public static void main(String[] args) throws IOException { // RESTクライアントを作成する RestHighLevelClient restHighLevelClient = new RestHighLevelClient( RestClient.builder(new HttpHost("localhost", 9200, "http")) ); // 複数データを設定する BulkRequest request = new BulkRequest(); request.add(new IndexRequest().index("user").id("1001").source(XContentType.JSON, "name", "zhangsan1", "age", 30,"sex", "男")); request.add(new IndexRequest().index("user").id("1002").source(XContentType.JSON, "name", "zhangsan2", "age", 40,"sex", "男")); request.add(new IndexRequest().index("user").id("1003").source(XContentType.JSON, "name", "zhangsan3", "age", 50,"sex", "女")); request.add(new IndexRequest().index("user").id("1004").source(XContentType.JSON, "name", "zhangsan", "age", 30,"sex", "男")); request.add(new IndexRequest().index("user").id("1005").source(XContentType.JSON, "name", "lisi", "age", 30,"sex", "男")); request.add(new IndexRequest().index("user").id("1006").source(XContentType.JSON, "name", "wangwu", "age", 40,"sex", "男")); request.add(new IndexRequest().index("user").id("1007").source(XContentType.JSON, "name", "wangwu1", "age", 50,"sex", "女")); request.add(new IndexRequest().index("user").id("1008").source(XContentType.JSON, "name", "wangwu2", "age", 30,"sex", "男")); request.add(new IndexRequest().index("user").id("1009").source(XContentType.JSON, "name", "wangwu3", "age", 20,"sex", "女")); request.add(new IndexRequest().index("user").id("1010").source(XContentType.JSON, "name", "wangwu4", "age", 30,"sex", "男")); // ドキュメントを複数に追加する BulkResponse bulk = restHighLevelClient.bulk(request, RequestOptions.DEFAULT); // 追加した数を出力する System.out.println(Arrays.stream(bulk.getItems()).count()); // RESTクライアントを閉じる restHighLevelClient.close(); } |
出力結果
1 |
10 |
作成された行数が10であるという結果により、ドキュメントが複数作成されたことがわかります。
クエリ
クエリはリレーショナルデータベースのSQL文のようなものです。これから、全てを検索、条件検索、ページング検索、あいまい検索、グルーピング検索の五つを例として、クエリを実行してみます。
全てを検索
SQLでの「SELECT * FROM USER」と同じです。userのインデックスから全てのドキュメントを検索します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
public static void main(String[] args) throws IOException { // RESTクライアントを作成する RestHighLevelClient restHighLevelClient = new RestHighLevelClient( RestClient.builder(new HttpHost("localhost", 9200, "http")) ); // インデックスを指定する SearchRequest request = new SearchRequest(); request.indices("user"); // 検索条件を指定する SearchSourceBuilder query = new SearchSourceBuilder().query(QueryBuilders.matchAllQuery()); request.source(query); // 検索する SearchResponse response = restHighLevelClient.search(request, RequestOptions.DEFAULT); // 出力する System.out.println(response.getHits().getTotalHits()); System.out.println(response.getTook()); response.getHits().forEach(hit -> System.out.println(hit.getSourceAsString())); // RESTクライアントを閉じる restHighLevelClient.close(); } |
出力結果
1 2 3 4 5 6 7 8 9 10 11 12 |
11 hits 123ms {"name":"周002","sex":"男","age":30} {"name":"zhangsan1","age":30,"sex":"男"} {"name":"zhangsan2","age":40,"sex":"男"} {"name":"zhangsan3","age":50,"sex":"女"} {"name":"zhangsan","age":30,"sex":"男"} {"name":"lisi","age":30,"sex":"男"} {"name":"wangwu","age":40,"sex":"男"} {"name":"wangwu1","age":50,"sex":"女"} {"name":"wangwu2","age":30,"sex":"男"} {"name":"wangwu3","age":20,"sex":"女"} |
ヒットした件数、検索にかかった時間、検索結果が出力されました。
条件検索
SQLでの「SELECT * FROM USER WHERE age = 30」と同じです。インデックスから「age=30」のドキュメントを検索します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
public static void main(String[] args) throws IOException { // RESTクライアントを作成する RestHighLevelClient restHighLevelClient = new RestHighLevelClient( RestClient.builder(new HttpHost("localhost", 9200, "http")) ); // インデックスを指定する SearchRequest request = new SearchRequest(); request.indices("user"); // 検索条件を指定する SearchSourceBuilder query = new SearchSourceBuilder().query(QueryBuilders.termQuery("age", 30)); request.source(query); // 検索する SearchResponse response = restHighLevelClient.search(request, RequestOptions.DEFAULT); // 出力する response.getHits().forEach(hit -> System.out.println(hit.getSourceAsString())); // RESTクライアントを閉じる restHighLevelClient.close(); } |
出力結果
1 2 3 4 5 6 |
{"name":"周002","sex":"男","age":30} {"name":"zhangsan1","age":30,"sex":"男"} {"name":"zhangsan","age":30,"sex":"男"} {"name":"lisi","age":30,"sex":"男"} {"name":"wangwu2","age":30,"sex":"男"} {"name":"wangwu4","age":30,"sex":"男"} |
年齢が30歳のレコードが出力されました。
ページング検索
SQLでの「SELECT * FROM USER limit 2 」と同じです。インデックスから取得したドキュメント件数の上限を設定して検索します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
public static void main(String[] args) throws IOException { // RESTクライアントを作成する RestHighLevelClient restHighLevelClient = new RestHighLevelClient( RestClient.builder(new HttpHost("localhost", 9200, "http")) ); // インデックスを指定する SearchRequest request = new SearchRequest(); request.indices("user"); // 検索条件を指定する SearchSourceBuilder query = new SearchSourceBuilder().query(QueryBuilders.matchAllQuery()); // 0から検索する query.from(0); // ページごとに2行を表示する query.size(2); request.source(query); // 検索する SearchResponse response = restHighLevelClient.search(request, RequestOptions.DEFAULT); // 出力する response.getHits().forEach(hit -> System.out.println(hit.getSourceAsString())); // RESTクライアントを閉じる restHighLevelClient.close(); } |
出力結果
1 2 |
{"name":"周002","sex":"男","age":30} {"name":"zhangsan1","age":30,"sex":"男"} |
あいまい検索
SQLでの「SELECT * FROM USER WHERE name LIKE “周%”」と同じです。インデックスからドキュメントを検索します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
public static void main(String[] args) throws IOException { // RESTクライアントを作成する RestHighLevelClient restHighLevelClient = new RestHighLevelClient( RestClient.builder(new HttpHost("localhost", 9200, "http")) ); // インデックスを指定する SearchRequest request = new SearchRequest(); request.indices("user"); // 検索条件を指定する SearchSourceBuilder builder = new SearchSourceBuilder(); // “周”だけ指定する FuzzyQueryBuilder fuzzyQueryBuilder = QueryBuilders.fuzzyQuery("name", "周").fuzziness(Fuzziness.ONE); builder.query(fuzzyQueryBuilder); request.source(builder); SearchResponse response = restHighLevelClient.search(request, RequestOptions.DEFAULT); response.getHits().forEach(hit -> System.out.println(hit.getSourceAsString())); // RESTクライアントを閉じる restHighLevelClient.close(); } |
出力結果
1 |
{"name":"周002","sex":"男","age":30} |
”周002”のデータを検索できました。
グルーピング検索
SQLでの「SELECT age, COUNT(*) FROM USER GROUP BY age」と同じです。インデックスからドキュメントを検索します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 |
public static void main(String[] args) throws IOException { // RESTクライアントを作成する RestHighLevelClient restHighLevelClient = new RestHighLevelClient( RestClient.builder(new HttpHost("localhost", 9200, "http")) ); // インデックスを指定する SearchRequest request = new SearchRequest(); request.indices("user"); // 検索条件を指定する SearchSourceBuilder builder = new SearchSourceBuilder(); // ageによって、グルーピングする AggregationBuilder aggregationBuilder = AggregationBuilders.terms("ageGroup").field("age"); builder.aggregation(aggregationBuilder); request.source(builder); // 検索する SearchResponse response = restHighLevelClient.search(request, RequestOptions.DEFAULT); // 結果を出力する Terms terms = response.getAggregations().get("ageGroup"); terms.getBuckets().forEach(bucket -> { int age = bucket.getKeyAsNumber().intValue(); long count = bucket.getDocCount(); System.out.println("年齢:" + age + "\t" + "数量:" + count); }); // RESTクライアントを閉じる restHighLevelClient.close(); } |
出力結果
1 2 3 4 |
年齢:30 数量:6 年齢:40 数量:2 年齢:50 数量:2 年齢:20 数量:1 |
最後に
以上でElasticsearchに関するインデックス、ドキュメント、クエリをJavaAPIを利用して操作する方法を簡単に紹介しました。Elasticsearchを触る時、いろいろ理解しないといけないことがありますが、リレーショナルデータベースと比較して、対応関係が理解できればそんなに複雑ではないかと思います。このようにアトラスでは、日々新しい技術にチャレンジしています。今後もより良いシステム開発のために、色々な技術を検証していきます!