1. 概述
Java REST Client 有兩種風格:
- Java Low Level REST Client :用於Elasticsearch的官方低級客戶端。它允許通過http與Elasticsearch集群通信。將請求編排和響應反編排留給用戶自己處理。它兼容所有的Elasticsearch版本。(PS:學過WebService的話,對編排與反編排這個概念應該不陌生。可以理解為對請求參數的封裝,以及對響應結果的解析)
- Java High Level REST Client :用於Elasticsearch的官方高級客戶端。它是基於低級客戶端的,它提供很多API,並負責請求的編排與響應的反編排。(PS:就好比是,一個是傳自己拼接好的字符串,並且自己解析返回的結果;而另一個是傳對象,返回的結果也已經封裝好了,直接是對象,更加規范了參數的名稱以及格式,更加面對對象一點)
(PS:所謂低級與高級,我覺得一個很形象的比喻是,面向過程編程與面向對象編程)
在 Elasticsearch 7.0 中不建議使用TransportClient,並且在8.0中會完全刪除TransportClient。因此,官方更建議我們用Java High Level REST Client,它執行HTTP請求,而不是序列號的Java請求。既然如此,這里就直接用高級了。
2. Java High Level REST Client (高級REST客戶端)
2.1. Maven倉庫
<dependency> <groupId>org.elasticsearch.client</groupId> <artifactId>elasticsearch-rest-high-level-client</artifactId> <version>6.5.4</version> </dependency>
2.2. 依賴
- org.elasticsearch.client:elasticsearch-rest-client
- org.elasticsearch:elasticsearch
2.3. 初始化
RestHighLevelClient client = new RestHighLevelClient( RestClient.builder( new HttpHost("localhost", 9200, "http"), new HttpHost("localhost", 9201, "http")));
高級客戶端內部會創建低級客戶端用於基於提供的builder執行請求。低級客戶端維護一個連接池,並啟動一些線程,因此當你用完以后應該關閉高級客戶端,並且在內部它將會關閉低級客戶端,以釋放這些資源。關閉客戶端可以使用close()方法:
client.close();
2.4. 文檔API
2.4.1. 添加文檔
IndexRequest
IndexRequest request = new IndexRequest("posts", "doc", "1"); String jsonString = "{\"user\":\"kimchy\",\"postDate\":\"2013-01-30\",\"message\":\"trying out Elasticsearch\"}"; request.source(jsonString, XContentType.JSON);
提供文檔source的方式還有很多,比如:
通過Map的方式提供文檔source

通過XContentBuilder方式提供source

通過Object的方式(鍵值對)提供source

可選參數

同步執行

異步執行
你也可以異步執行 IndexRequest,為此你需要指定一個監聽器來處理這個異步響應結果:

一個典型的監聽器看起來是這樣的:

IndexResponse

如果有版本沖突,將會拋出ElasticsearchException

同樣的異常也有可能發生在當opType設置為create的時候,且相同索引、相同類型、相同ID的文檔已經存在時。例如:

2.4.2. 查看文檔
Get Request

可選參數

同步執行

異步執行

Get Response

當索引不存在,或者指定的文檔的版本不存在時,響應狀態嗎是404,並且拋出ElasticsearchException


2.4.3. 文檔是否存在

2.4.4. 刪除文檔
Delete Request

可選參數
同添加
2.5. 搜索API
Search Request
基本格式是這樣的:

大多數查詢參數被添加到 SearchSourceBuilder
可選參數

SearchSourceBuilder
控制檢索行為的大部分選項都可以在SearchSourceBuilder中設置。下面是一個常見選項的例子:

在這個例子中,我們首先創建了一個SearchSourceBuilder對象,並且帶着默認選項。然后設置了一個term查詢,接着設置檢索的位置和數量,最后設置超時時間
在設置完這些選項以后,我們只需要把SearchSourceBuilder加入到SearchRequest中即可

構建Query
用QueryBuilder來創建Serarch Query。QueryBuilder支持Elasticsearch DSL中每一種Query
例如:

還可以通過QueryBuilders工具類來創建QueryBuilder對象,例如:

無論是用哪種方式創建,最后一定要把QueryBuilder添加到SearchSourceBuilder中

排序
SearchSourceBuilder 可以添加一個或多個 SortBuilder
SortBuilder有四種實現:FieldSortBuilder、GeoDistanceSortBuilder、ScoreSortBuilder、ScriptSortBuilder

聚集函數

同步執行

異步執行

從查詢響應中取出文檔

3. 示例
3.1. 准備數據
3.1.1. 安裝IK分詞器插件
./bin/elasticsearch-plugin install https://github.com/medcl/elasticsearch-analysis-ik/releases/download/v6.5.4/elasticsearch-analysis-ik-6.5.4.zip

3.1.2. 創建索引
curl -X PUT "localhost:9200/book" -H 'Content-Type: application/json' -d' { "mappings":{ "_doc":{ "properties":{ "id":{ "type":"integer" }, "name":{ "type":"text", "analyzer":"ik_max_word", "search_analyzer":"ik_max_word" }, "author":{ "type":"text", "analyzer":"ik_max_word", "search_analyzer":"ik_max_word" }, "category":{ "type":"integer" }, "price":{ "type":"double" }, "status":{ "type":"short" }, "sellReason":{ "type":"text", "analyzer":"ik_max_word", "search_analyzer":"ik_max_word" }, "sellTime":{ "type":"date", "format":"yyyy-MM-dd" } } } } } '
3.1.3. 數據預覽

3.2. 示例代碼
3.2.1. 完整的pom.xml
<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.1.1.RELEASE</version> <relativePath/> <!-- lookup parent from repository --> </parent> <groupId>com.cjs.example</groupId> <artifactId>elasticsearch-demo</artifactId> <version>0.0.1-SNAPSHOT</version> <name>elasticsearch-demo</name> <description></description> <properties> <java.version>1.8</java.version> </properties> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-thymeleaf</artifactId> </dependency> <dependency> <groupId>org.elasticsearch.client</groupId> <artifactId>elasticsearch-rest-high-level-client</artifactId> <version>6.5.4</version> </dependency> <dependency> <groupId>org.apache.commons</groupId> <artifactId>commons-lang3</artifactId> <version>3.8</version> </dependency> <dependency> <groupId>com.alibaba</groupId> <artifactId>fastjson</artifactId> <version>1.2.54</version> </dependency> <dependency> <groupId>ch.qos.logback</groupId> <artifactId>logback-core</artifactId> <version>1.2.3</version> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <optional>true</optional> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> </dependencies> <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> </plugins> </build> </project>
3.2.2. 配置
package com.cjs.example.elasticsearch.config; import org.apache.http.HttpHost; import org.elasticsearch.client.RestClient; import org.elasticsearch.client.RestHighLevelClient; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; /** * @author ChengJianSheng * @date 2019-01-07 */ @Configuration public class ElasticsearchClientConfig { @Bean public RestHighLevelClient restHighLevelClient() { RestHighLevelClient client = new RestHighLevelClient( RestClient.builder( new HttpHost("localhost", 9200, "http"))); return client; } }
3.2.3. domain
package com.cjs.example.elasticsearch.domain.model; import lombok.Data; import java.io.Serializable; /** * 圖書 * @author ChengJianSheng * @date 2019-01-07 */ @Data public class BookModel implements Serializable { private Integer id; // 圖書ID private String name; // 圖書名稱 private String author; // 作者 private Integer category; // 圖書分類 private Double price; // 圖書價格 private String sellReason; // 上架理由 private String sellTime; // 上架時間 private Integer status; // 狀態(1:可售,0:不可售) }
3.2.4. Controller
package com.cjs.example.elasticsearch.controller; import com.alibaba.fastjson.JSON; import com.cjs.example.elasticsearch.domain.common.BaseResult; import com.cjs.example.elasticsearch.domain.common.Page; import com.cjs.example.elasticsearch.domain.model.BookModel; import com.cjs.example.elasticsearch.domain.vo.BookRequestVO; import com.cjs.example.elasticsearch.service.BookService; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.*; /** * 文檔操作 * @author ChengJianSheng * @date 2019-01-07 */ @Slf4j @RestController @RequestMapping("/book") public class BookController { @Autowired private BookService bookService; /** * 列表分頁查詢 */ @GetMapping("/list") public BaseResult list(BookRequestVO bookRequestVO) { Page<BookModel> page = bookService.list(bookRequestVO); if (null == page) { return BaseResult.error(); } return BaseResult.ok(page); } /** * 查看文檔 */ @GetMapping("/detail") public BaseResult detail(Integer id) { if (null == id) { return BaseResult.error("ID不能為空"); } BookModel book = bookService.detail(id); return BaseResult.ok(book); } /** * 添加文檔 */ @PostMapping("/add") public BaseResult add(@RequestBody BookModel bookModel) { bookService.save(bookModel); log.info("插入文檔成功!請求參數: {}", JSON.toJSONString(bookModel)); return BaseResult.ok(); } /** * 修改文檔 */ @PostMapping("/update") public BaseResult update(@RequestBody BookModel bookModel) { Integer id = bookModel.getId(); if (null == id) { return BaseResult.error("ID不能為空"); } BookModel book = bookService.detail(id); if (null == book) { return BaseResult.error("記錄不存在"); } bookService.update(bookModel); log.info("更新文檔成功!請求參數: {}", JSON.toJSONString(bookModel)); return BaseResult.ok(); } /** * 刪除文檔 */ @GetMapping("/delete") public BaseResult delete(Integer id) { if (null == id) { return BaseResult.error("ID不能為空"); } bookService.delete(id); return BaseResult.ok(); } }
3.2.5. Service
package com.cjs.example.elasticsearch.service.impl; import com.alibaba.fastjson.JSON; import com.cjs.example.elasticsearch.domain.common.Page; import com.cjs.example.elasticsearch.domain.model.BookModel; import com.cjs.example.elasticsearch.domain.vo.BookRequestVO; import com.cjs.example.elasticsearch.service.BookService; import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.StringUtils; import org.elasticsearch.action.ActionListener; import org.elasticsearch.action.DocWriteResponse; import org.elasticsearch.action.delete.DeleteRequest; import org.elasticsearch.action.delete.DeleteResponse; import org.elasticsearch.action.get.GetRequest; import org.elasticsearch.action.get.GetResponse; import org.elasticsearch.action.index.IndexRequest; import org.elasticsearch.action.index.IndexResponse; import org.elasticsearch.action.search.SearchRequest; import org.elasticsearch.action.search.SearchResponse; import org.elasticsearch.action.support.replication.ReplicationResponse; import org.elasticsearch.action.update.UpdateRequest; import org.elasticsearch.action.update.UpdateResponse; import org.elasticsearch.client.RequestOptions; import org.elasticsearch.client.RestHighLevelClient; import org.elasticsearch.common.unit.TimeValue; import org.elasticsearch.index.query.BoolQueryBuilder; import org.elasticsearch.index.query.QueryBuilders; import org.elasticsearch.rest.RestStatus; import org.elasticsearch.search.SearchHit; import org.elasticsearch.search.SearchHits; import org.elasticsearch.search.builder.SearchSourceBuilder; import org.elasticsearch.search.sort.FieldSortBuilder; import org.elasticsearch.search.sort.SortOrder; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import org.springframework.util.CollectionUtils; import java.io.IOException; import java.util.*; import java.util.stream.Collectors; /** * @author ChengJianSheng * @date 2019-01-07 */ @Slf4j @Service public class BookServiceImpl implements BookService { private static final String INDEX_NAME = "book"; private static final String INDEX_TYPE = "_doc"; @Autowired private RestHighLevelClient client; @Override public Page<BookModel> list(BookRequestVO bookRequestVO) { int pageNo = bookRequestVO.getPageNo(); int pageSize = bookRequestVO.getPageSize(); SearchSourceBuilder sourceBuilder = new SearchSourceBuilder(); sourceBuilder.from(pageNo - 1); sourceBuilder.size(pageSize); sourceBuilder.sort(new FieldSortBuilder("id").order(SortOrder.ASC)); // sourceBuilder.query(QueryBuilders.matchAllQuery()); BoolQueryBuilder boolQueryBuilder = QueryBuilders.boolQuery(); if (StringUtils.isNotBlank(bookRequestVO.getName())) { boolQueryBuilder.must(QueryBuilders.matchQuery("name", bookRequestVO.getName())); } if (StringUtils.isNotBlank(bookRequestVO.getAuthor())) { boolQueryBuilder.must(QueryBuilders.matchQuery("author", bookRequestVO.getAuthor())); } if (null != bookRequestVO.getStatus()) { boolQueryBuilder.must(QueryBuilders.termQuery("status", bookRequestVO.getStatus())); } if (StringUtils.isNotBlank(bookRequestVO.getSellTime())) { boolQueryBuilder.must(QueryBuilders.termQuery("sellTime", bookRequestVO.getSellTime())); } if (StringUtils.isNotBlank(bookRequestVO.getCategories())) { String[] categoryArr = bookRequestVO.getCategories().split(","); List<Integer> categoryList = Arrays.asList(categoryArr).stream().map(e->Integer.valueOf(e)).collect(Collectors.toList()); BoolQueryBuilder categoryBoolQueryBuilder = QueryBuilders.boolQuery(); for (Integer category : categoryList) { categoryBoolQueryBuilder.should(QueryBuilders.termQuery("category", category)); } boolQueryBuilder.must(categoryBoolQueryBuilder); } sourceBuilder.query(boolQueryBuilder); SearchRequest searchRequest = new SearchRequest(); searchRequest.indices(INDEX_NAME); searchRequest.source(sourceBuilder); try { SearchResponse searchResponse = client.search(searchRequest, RequestOptions.DEFAULT); RestStatus restStatus = searchResponse.status(); if (restStatus != RestStatus.OK) { return null; } List<BookModel> list = new ArrayList<>(); SearchHits searchHits = searchResponse.getHits(); for (SearchHit hit : searchHits.getHits()) { String source = hit.getSourceAsString(); BookModel book = JSON.parseObject(source, BookModel.class); list.add(book); } long totalHits = searchHits.getTotalHits(); Page<BookModel> page = new Page<>(pageNo, pageSize, totalHits, list); TimeValue took = searchResponse.getTook(); log.info("查詢成功!請求參數: {}, 用時{}毫秒", searchRequest.source().toString(), took.millis()); return page; } catch (IOException e) { log.error("查詢失敗!原因: {}", e.getMessage(), e); } return null; } @Override public void save(BookModel bookModel) { Map<String, Object> jsonMap = new HashMap<>(); jsonMap.put("id", bookModel.getId()); jsonMap.put("name", bookModel.getName()); jsonMap.put("author", bookModel.getAuthor()); jsonMap.put("category", bookModel.getCategory()); jsonMap.put("price", bookModel.getPrice()); jsonMap.put("sellTime", bookModel.getSellTime()); jsonMap.put("sellReason", bookModel.getSellReason()); jsonMap.put("status", bookModel.getStatus()); IndexRequest indexRequest = new IndexRequest(INDEX_NAME, INDEX_TYPE, String.valueOf(bookModel.getId())); indexRequest.source(jsonMap); client.indexAsync(indexRequest, RequestOptions.DEFAULT, new ActionListener<IndexResponse>() { @Override public void onResponse(IndexResponse indexResponse) { String index = indexResponse.getIndex(); String type = indexResponse.getType(); String id = indexResponse.getId(); long version = indexResponse.getVersion(); log.info("Index: {}, Type: {}, Id: {}, Version: {}", index, type, id, version); if (indexResponse.getResult() == DocWriteResponse.Result.CREATED) { log.info("寫入文檔"); } else if (indexResponse.getResult() == DocWriteResponse.Result.UPDATED) { log.info("修改文檔"); } ReplicationResponse.ShardInfo shardInfo = indexResponse.getShardInfo(); if (shardInfo.getTotal() != shardInfo.getSuccessful()) { log.warn("部分分片寫入成功"); } if (shardInfo.getFailed() > 0) { for (ReplicationResponse.ShardInfo.Failure failure : shardInfo.getFailures()) { String reason = failure.reason(); log.warn("失敗原因: {}", reason); } } } @Override public void onFailure(Exception e) { log.error(e.getMessage(), e); } }); } @Override public void update(BookModel bookModel) { Map<String, Object> jsonMap = new HashMap<>(); jsonMap.put("sellReason", bookModel.getSellReason()); UpdateRequest request = new UpdateRequest(INDEX_NAME, INDEX_TYPE, String.valueOf(bookModel.getId())); request.doc(jsonMap); try { UpdateResponse updateResponse = client.update(request, RequestOptions.DEFAULT); } catch (IOException e) { log.error("更新失敗!原因: {}", e.getMessage(), e); } } @Override public void delete(int id) { DeleteRequest request = new DeleteRequest(INDEX_NAME, INDEX_TYPE, String.valueOf(id)); try { DeleteResponse deleteResponse = client.delete(request, RequestOptions.DEFAULT); if (deleteResponse.status() == RestStatus.OK) { log.info("刪除成功!id: {}", id); } } catch (IOException e) { log.error("刪除失敗!原因: {}", e.getMessage(), e); } } @Override public BookModel detail(int id) { GetRequest getRequest = new GetRequest(INDEX_NAME, INDEX_TYPE, String.valueOf(id)); try { GetResponse getResponse = client.get(getRequest, RequestOptions.DEFAULT); if (getResponse.isExists()) { String source = getResponse.getSourceAsString(); BookModel book = JSON.parseObject(source, BookModel.class); return book; } } catch (IOException e) { log.error("查看失敗!原因: {}", e.getMessage(), e); } return null; } }
3.2.6. 頁面
<!DOCTYPE html> <html lang="zh"> <head> <meta charset="UTF-8"> <title>圖書列表</title> <link rel="stylesheet" href="/bootstrap-4/css/bootstrap.min.css"> <link rel="stylesheet" href="/bootstrap-table/bootstrap-table.css"> <script src="jquery-3.3.1.min.js"></script> <script src="/bootstrap-4/js/bootstrap.min.js"></script> <script src="/bootstrap-table/bootstrap-table.js"></script> <script src="/bootstrap-table/locale/bootstrap-table-zh-CN.js"></script> <script> $(function(){ $('#table').bootstrapTable({ url: '/book/list', method: 'get', sidePagination: 'server', responseHandler: function(res) { // 加載服務器數據之前的處理程序,可以用來格式化數據。參數:res為從服務器請求到的數據。 var result = {}; result.total = res.data.totalCount; result.rows = res.data.pageList; return result; }, pagination: true, pageSize: 3, // 初始PageSize queryParams: function(params) { var req = { pageSize: params.limit, pageNo: params.offset + 1 }; return req; }, striped: true, search: true, columns: [{ field: 'id', title: 'ID' }, { field: 'name', title: '名稱' }, { field: 'author', title: '作者' }, { field: 'price', title: '單價' }, { field: 'sellTime', title: '上架時間' }, { field: 'status', title: '狀態', formatter: function(value) { if (value == 1) { return '<span style="color: green">可售</span>'; } else { return '<span style="color: red">不可售</span>'; } } }, { field: 'category', title: '分類', formatter: function(value) { if (value == 10010) { return '中國當代小說'; } else if (value == 10011) { return '武俠小說'; } else if (value == 10012) { return '愛情小說'; } else if (value == 10013) { return '中國當代隨筆'; } } }, { field: 'sellReason', title: '上架理由' }, { title: '操作', formatter: function() { return '<a href="#">修改</a> <a href="#">刪除</a>'; } } ] }); }); </script> </head> <body> <div class="table-responsive" style="padding: 10px 30px"> <table id="table" class="table text-nowrap"></table> </div> </body> </html>
3.3. 演示

重點演示幾個查詢

返回結果:
{
"code": 200,
"success": true,
"msg": "SUCCESS",
"data": {
"pageNumber": 1,
"pageSize": 10,
"totalCount": 2,
"pageList": [
{
"id": 2,
"name": "倚天屠龍記(全四冊)",
"author": "金庸",
"category": 10011,
"price": 70.4,
"sellReason": "武林至尊,寶刀屠龍,號令天下,莫敢不從。",
"sellTime": "2018-11-11",
"status": 1
},
{
"id": 3,
"name": "神雕俠侶",
"author": "金庸",
"category": 10011,
"price": 70,
"sellReason": "風陵渡口初相遇,一見楊過誤終身",
"sellTime": "2018-11-11",
"status": 1
}
]
}
}
上面的查詢對應的Elasticsearch DSL是這樣的:
{
"from":0,
"size":10,
"query":{
"bool":{
"must":[
{
"match":{
"author":{
"query":"金庸",
"operator":"OR",
"prefix_length":0,
"max_expansions":50,
"fuzzy_transpositions":true,
"lenient":false,
"zero_terms_query":"NONE",
"auto_generate_synonyms_phrase_query":true,
"boost":1
}
}
},
{
"term":{
"status":{
"value":1,
"boost":1
}
}
},
{
"bool":{
"should":[
{
"term":{
"category":{
"value":10010,
"boost":1
}
}
},
{
"term":{
"category":{
"value":10011,
"boost":1
}
}
},
{
"term":{
"category":{
"value":10012,
"boost":1
}
}
}
],
"adjust_pure_negative":true,
"boost":1
}
}
],
"adjust_pure_negative":true,
"boost":1
}
},
"sort":[
{
"id":{
"order":"asc"
}
}
]
}
3.4. 工程結構

4. 參考
https://github.com/medcl/elasticsearch-analysis-ik
https://www.elastic.co/guide/en/elasticsearch/client/java-rest/current/java-rest-high.html
https://www.elastic.co/guide/en/elasticsearch/client/java-rest/current/java-rest-high-search.html
https://bootstrap-table.wenzhixin.net.cn/documentation/
5. 其它相關
