簡介
https://blog.csdn.net/heiyogl/article/details/103281714
基本概念
Elasticsearch也是基於Lucene的全文檢索庫,本質也是存儲數據,很多概念與MySQL類似的。
對比關係:
索引庫(indices) |
Databases 數據庫 |
類型(type) |
Table 數據表 |
文檔(Document) |
Row 行 |
字段(Field) |
Columns 列 |
常用術語
1.1:文檔Document:用戶存儲在Elasticsearch中的文檔;
1.2:索引 Index:由具有相同字段的文檔列表組成;
1.3:節點Node:一個Elasticsearch的運行實例,是集羣的構成單元;
1.4:集羣Cluster:由一個或多個節點組成,對外提供服務;
1.5:分片:每個索引都有多個分片,每個分片是一個Lucene索引;
1.6:備份:拷貝一份分片就完成了分片的備份;
詳細說明
- 索引庫(indices): indices是index的複數,代表許多的索引;
- 類型(type): 類型是模擬mysql中的table概念,一個索引庫下可以有不同類型的索引,比如商品索引,訂單索引,其數據格式不同。不過這會導致索引庫混亂,因此未來版本中會移除這個概念;
- 文檔(document): 存入索引庫原始的數據。比如每一條商品信息,就是一個文檔;
- 字段(field): 文檔中的屬性;
- 映射配置(mappings): 字段的數據類型、屬性、是否索引、是否存儲等特性。
在Elasticsearch有一些集羣相關的概念:
- 索引集(Indices,index的複數):邏輯上的完整索引
- 分片(shard):數據拆分後的各個部分
- 副本(replica):每個分片的複製
字段類型
- type:字段類型,是枚舉:FieldType,可以是text、long、short、date、integer、object等
- text:存儲數據時候,會自動分詞,並生成索引;
- keyword:存儲數據時候,不會分詞建立索引;
- Numerical:數值類型,分兩類
- 基本數據類型:long、interger、short、byte、double、float、half_float
- 浮點數的高精度類型:scaled_float
- 需要指定一個精度因子,比如10或100。elasticsearch會把真實值乘以這個因子後存儲,取出時再還原。
- Date:日期類型;
- elasticsearch可以對日期格式化爲字符串存儲,但是建議我們存儲爲毫秒值,存儲爲long,節省空間。
- index:是否索引,布爾類型,默認是true
- store:是否存儲,布爾類型,默認是false
- analyzer:分詞器名稱,這裏的ik_max_word即使用ik分詞器
注意事項
1、Elasticsearch本身就是分佈式的,因此即便你只有一個節點,Elasticsearch默認也會對你的數據進行分片和副本操作;當你向集羣添加新數據時,數據也會在新加入的節點中進行平衡;
2、ES創建索引時默認創建5個分片一個備份 , 分片的數量只能在創建索引時指定,備份可以動態修改;
3、索引命名規範:字母小寫,且不含中劃線。
ES windows 版 安裝部署
下載地址: https://www.elastic.co/cn/downloads/elasticsearch
文件名: elasticsearch-7.4.0-windows-x86_64.zip
Es 安裝包內本身內置了JDK,且內置的JDK版本是該Es所推薦使用的版本。
1、將文件 elasticsearch-7.4.0-windows-x86_64.zip 解壓到安裝目錄下;
2、配置es使用內置的jdk(而非電腦所配置的JAVA_HOME環境變量)
使用文本編輯器編輯 bin/elasticsearch-env.bat 直接添加了JAVA_HOME 的配置,具體如下:
rem 配置自己的jdk
set JAVA_HOME=E:/xxx/elastic/elasticsearch-7.4.0/jdk
3、雙擊打開解壓後 bin 目錄下的 elasticsearch.bat
瀏覽器打開地址: http://127.0.0.1:9200/ 看到如下界面說明啓動好了
ElasticSearch-head 安裝部署
ElasticSearch-head 是一個用來查看ES的運行狀態和數據的可視化工具,ElasticSearch-head依賴 node.js
1、node.js 安裝
下載地址: http://nodejs.cn/download/
文件名: node-v12.13.0-x64.msi
雙擊打開即可進入安裝環節,安裝完成後,打開cmd 命令,輸入 node --version 命令即可查看安裝的版本。
node.js 安裝完成後,切換到 node.js 的安裝目錄(cmd中輸入 npm config ls 命令可查看安裝目錄),運行命令安裝 grunt,命令如下: npm install -g grunt-cli
2、ElasticSearch-head 安裝
下載地址: https://github.com/mobz/elasticsearch-head
文件名: elasticsearch-head-master.zip
下載完成後,cmd 進入 elasticsearch-head-master 的解壓目錄,然後執行 npm install 命令(對改目錄下的相關文件解壓並安裝)。安裝完成後,輸入啓動命令 npm run start
瀏覽器地址輸入: http://127.0.0.1:9100/ 可查看 head 界面
Es默認不允許跨域鏈接,所以默認會顯示未連接的狀態。
在es > config 目錄下,打開 elasticsearch.yml 文件,並在末尾處添加配置:
http.cors.enabled: true
http.cors.allow-origin: "*"
保存,重啓es,且刷新elasticsearch-head 即可鏈接。
IK分詞器安裝
下載地址: https://github.com/medcl/elasticsearch-analysis-ik
文件名: elasticsearch-analysis-ik-master.zip
安裝步驟:
1、cmd 進入 elasticsearch-analysis-ik-master 解壓目錄;
2、執行 mvn clean package 命令進行打包,打包完後會多出一個 target 文件夾。打包zip文件路徑爲: target > releases > elasticsearch-analysis-ik-7.4.0.zip ;
3、在es安裝目錄下的 plugins 文件夾下創建 analysis-ik 文件夾,並將上一步打包好的 elasticsearch-analysis-ik-7.4.0.zip 解壓到此處,如下圖:
4、重啓 ES,可以啓動,則說明安裝成功。
使用Postman創建索引
創建一個空的索引 testindex (非結構化索引)
http://127.0.0.1:9200/testindex
通過 elasticsearch-head 可以看到該索引的mappings 是空的(非結構化索引)
對索引的mappings進行賦值,並轉換爲結構化索引
mapping類似於數據庫表的結構,主要用於
1.定義index下的字段名;
2.定義字段類型;
3.定義倒排索引相關的配置。
http://127.0.0.1:9200/testindex/_mapping
{"properties":{"name":{"type":"text"},"sex":{"type":"integer"}}}
通過 elasticsearch-head 可以看到該索引的mappings
SpringBoot 集成實戰
基於SpringBoot實現新增/修改、刪除、查詢、聚合統計的功能
1、pom.xml 添加依賴
<!-- elasticsearch 依賴包 -->
<dependency>
<groupId>org.elasticsearch</groupId>
<artifactId>elasticsearch</artifactId>
<version>7.2.0</version>
</dependency>
<dependency>
<groupId>org.elasticsearch.client</groupId>
<artifactId>elasticsearch-rest-high-level-client</artifactId>
<version>7.2.0</version>
</dependency>
2、application.properties 配置ES地址
elasticsearch.ip=127.0.0.1:9200
3、創建 ElasticsearchRestClient.java
@Configuration
public class ElasticsearchRestClient {
private static final int ADDRESS_LENGTH = 2;
private static final String HTTP_SCHEME = "http";
private static final String ARTICLE_INDEX_NAME = "article_index";
@Value("${elasticsearch.ip}")
String[] ipAddress;
private RestHighLevelClient highLevelClient;
@Bean
public RestClientBuilder restClientBuilder() {
HttpHost[] hosts = Arrays.stream(ipAddress)
.map(this::makeHttpHost)
.filter(Objects::nonNull)
.toArray(HttpHost[]::new);
return RestClient.builder(hosts);
}
@Bean(name = "highLevelClient")
public RestHighLevelClient highLevelClient(@Autowired RestClientBuilder restClientBuilder) {
highLevelClient = new RestHighLevelClient(restClientBuilder);
return highLevelClient;
}
private HttpHost makeHttpHost(String s) {
assert StringUtils.isNotEmpty(s);
String[] address = s.split(":");
if (address.length == ADDRESS_LENGTH) {
String ip = address[0];
int port = Integer.parseInt(address[1]);
return new HttpHost(ip, port, HTTP_SCHEME);
} else {
return null;
}
}
// 新增 或 修改文檔(若ID存在,則修改)
public void insertOrUpdateArticle(EsArticle article) throws IllegalArgumentException, IllegalAccessException{
try {
XContentBuilder builder = XContentFactory.jsonBuilder();
builder.startObject();
// 遍歷實體類
Class clas = article.getClass();
Field[] fields = clas.getDeclaredFields();
for (int i = 0; i < fields.length; i++) {
Field f = fields[i];
f.setAccessible(true);
// es字段賦值
if (f.getType().toString().equals("class java.sql.Timestamp")) { //若字段類型爲時間類型,則將其轉換爲 long 進行存儲
builder.field(f.getName(), ((Timestamp)f.get(article)).getTime());
} else {
builder.field(f.getName(), f.get(article));
}
}
builder.endObject();
IndexRequest request = new IndexRequest(ARTICLE_INDEX_NAME).source(builder);
request.id(article.getId()); // 通過指定 id 來實現:若存在則更新記錄,否則插入記錄
highLevelClient.index(request, RequestOptions.DEFAULT);
} catch (IOException e) {
e.printStackTrace();
}
}
// 批量新增 或 修改文檔(若ID存在,則修改)
public int insertOrUpdateBatchArticle(List<EsArticle> list) {
int result = -1;
try {
BulkRequest request = new BulkRequest();
for (EsArticle article : list) {
XContentBuilder builder = XContentFactory.jsonBuilder();
builder.startObject();
// 遍歷實體類
Class clas = article.getClass();
Field[] fields = clas.getDeclaredFields();
for (int i = 0; i < fields.length; i++) {
Field f = fields[i];
f.setAccessible(true);
// es字段賦值
if (f.getType().toString().equals("class java.sql.Timestamp")) { //若字段類型爲時間類型,則將其轉換爲 long 進行存儲
if (null == f.get(article)) {
builder.field(f.getName(), 0l);
} else {
builder.field(f.getName(), ((Timestamp)f.get(article)).getTime());
}
} else {
builder.field(f.getName(), f.get(article));
}
}
builder.endObject();
IndexRequest requestItem = new IndexRequest(ARTICLE_INDEX_NAME).source(builder);
requestItem.id(article.getId()); // 通過指定 id 來實現:若存在則更新記錄,否則插入記錄
request.add(requestItem);
}
highLevelClient.bulk(request, RequestOptions.DEFAULT);
result = list.size();
} catch (Exception e) {
e.printStackTrace();
}
return result;
}
// 批量刪除
public <T> void deleteBatchArticle(Collection<T> idList) {
BulkRequest request = new BulkRequest();
idList.forEach(item -> request.add(new DeleteRequest(ARTICLE_INDEX_NAME, item.toString())));
try {
highLevelClient.bulk(request, RequestOptions.DEFAULT);
} catch (Exception e) {
throw new RuntimeException(e);
}
}
/**
* 通過查詢條件實現分頁查詢
* @param queryParam 查詢條件(Map<String, Object>)
* @param pageIndex 分頁索引,從第 1 頁開始
* @param pageSize 每頁條數
* @param order 排序數組,[排序字段][排序方式(asc | desc)]
*/
public void queryArticle(Map<String, Object> queryParam, int pageIndex, int pageSize, String[][] order) {
List<Map<String, Object>> result = new ArrayList<Map<String, Object>>();
SearchRequest searchRequest = new SearchRequest(ARTICLE_INDEX_NAME);
SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder();
queryBuilder(pageIndex, pageSize, queryParam, ARTICLE_INDEX_NAME, order, searchRequest, searchSourceBuilder);
try {
SearchResponse response = highLevelClient.search(searchRequest, RequestOptions.DEFAULT);
for (SearchHit hit : response.getHits().getHits()) {
Map<String, Object> map = hit.getSourceAsMap();
map.put("id", hit.getId());
result.add(map);
// 取高亮結果
if (null != queryParam.get("content")) { // 如果查詢條件中有內容關鍵詞,則使用內容關鍵詞處理高亮
Map<String, HighlightField> highlightFields = hit.getHighlightFields();
HighlightField highlight = highlightFields.get("content");
Text[] fragments = highlight.fragments(); // 多值的字段會有多個值
String fragmentString = fragments[0].string();
System.out.println("高亮:" + fragmentString);
}
}
System.out.println("pageIndex:" + pageIndex);
System.out.println("pageSize:" + pageSize);
System.out.println(response.getHits().getTotalHits());
System.out.println(result.size());
for (Map<String, Object> map : result) {
System.out.println(map.get("title"));
System.out.println(map.get("content"));
System.out.println(map.get("id"));
}
} catch (IOException e) {
e.printStackTrace();
}
}
/**
* 構建查詢條件
* @param pageIndex 分頁索引,從第 1 頁開始
* @param pageSize 每頁條數
* @param query 查詢條件集合(Map<String, Object>)
* @param indexName 索引名稱
* @param order 排序數組,[排序字段][排序方式(asc | desc)]
* @param searchRequest
* @param searchSourceBuilder
*/
private void queryBuilder(Integer pageIndex, Integer pageSize, Map<String, Object> query, String indexName, String[][] order, SearchRequest searchRequest, SearchSourceBuilder searchSourceBuilder) {
if (query != null && !query.keySet().isEmpty()) {
if (pageIndex != null && pageSize != null) {
searchSourceBuilder.size(pageSize);
if (pageIndex <= 0) {
pageIndex = 0;
}
searchSourceBuilder.from((pageIndex - 1) * pageSize);
}
//1.創建QueryBuilder(即設置查詢條件)這兒創建的是組合查詢(也叫多條件查詢),後面會介紹更多的查詢方法
// 組合查詢BoolQueryBuilder -> must(QueryBuilders):AND ; mustNot(QueryBuilders):NOT; should(QueryBuilders):OR
BoolQueryBuilder boolBuilder = QueryBuilders.boolQuery();
// 構建查詢條件
query.keySet().forEach(
key -> {
boolBuilder.must(QueryBuilders.matchQuery(key, query.get(key)));// builder下有must、should以及mustNot 相當於sql中的and、or以及not
});
searchSourceBuilder.query(boolBuilder);
HighlightBuilder highlightBuilder = new HighlightBuilder();
HighlightBuilder.Field highlightTitle = new HighlightBuilder.Field("content").preTags("<strong>").postTags("</strong>");
highlightTitle.highlighterType("unified");
highlightBuilder.field(highlightTitle);
searchSourceBuilder.highlighter(highlightBuilder);
}
searchRequest.source(searchSourceBuilder);
// 排序
if (null != order && order.length > 0) {
for (int i = 0; i < order.length; i++) {
searchSourceBuilder.sort(new FieldSortBuilder(order[i][0]).order(SortOrder.fromString(order[i][1])));
}
}
}
/**
* 通過條件實現聚合查詢(統計)
* @param queryParam 查詢條件(Map<String, Object>)
* @param pageIndex 分頁索引,從第 1 頁開始
* @param pageSize 每頁條數
* @param order 排序數組,[排序字段][排序方式(asc | desc)]
* @param groupName 聚合查詢別名
* @param groupField 聚合字段
* @return
*/
public LinkedHashMap<String, Long> countArticle(Map<String, Object> queryParam, int pageIndex, int pageSize, String[][] order, String groupName, String groupField) {
SearchRequest searchRequest = new SearchRequest(ARTICLE_INDEX_NAME);
SearchSourceBuilder sourceBuilder = new SearchSourceBuilder();
// 構建聚合條件
TermsAggregationBuilder termsAggregationBuilder = AggregationBuilders.terms(groupName).field(groupField);
sourceBuilder.aggregation(termsAggregationBuilder);
sourceBuilder.timeout(new TimeValue(60, TimeUnit.SECONDS));
// 構建查詢條件
queryBuilder(pageIndex, pageSize, queryParam, ARTICLE_INDEX_NAME, order, searchRequest, sourceBuilder);
// 若不需要上述“構建查詢條件”,直接使用下文即可
// searchRequest.source(sourceBuilder);
try {
SearchResponse searchResponse = highLevelClient.search(searchRequest, RequestOptions.DEFAULT);
Aggregations aggregations = searchResponse.getAggregations();
Map<String, Aggregation> stringAggregationMap = aggregations.asMap();
ParsedStringTerms parsedLongTerms = (ParsedStringTerms) stringAggregationMap.get(groupName);
List<? extends Terms.Bucket> buckets = parsedLongTerms.getBuckets();
// 使用LinkedHashMap 確保結果有序輸出
LinkedHashMap<String, Long> map = new LinkedHashMap<String, Long>();
for (Terms.Bucket bucket : buckets) {
long docCount = bucket.getDocCount();//個數
String v = bucket.getKeyAsString(); //
map.put(v, docCount);
}
return map;
} catch (IOException e) {
e.printStackTrace();
}
return null;
}
/**
* 通過條件實現多字段聚合查詢(統計)
* @param queryParam 查詢條件(Map<String, Object>)
* @param pageIndex 分頁索引,從第 1 頁開始
* @param pageSize 每頁條數
* @param order 排序數組,[排序字段][排序方式(asc | desc)](暫不支持)
* @param groupField1 聚合字段1
* @param groupField2 聚合字段2
* @return
*/
public List<HashMap<String, Object>> countArticleMultiple(Map<String, Object> queryParam, int pageIndex, int pageSize, String[][] order, String groupField1, String groupField2) {
//搜索結果狀態信息
List<HashMap<String, Object>> result = new ArrayList<HashMap<String, Object>>();
try {
// 1、創建search請求
//SearchRequest searchRequest = new SearchRequest();
SearchRequest searchRequest = new SearchRequest(ARTICLE_INDEX_NAME);
// 2、用SearchSourceBuilder來構造查詢請求體 ,請仔細查看它的方法,構造各種查詢的方法都在這。
SearchSourceBuilder sourceBuilder = new SearchSourceBuilder();
sourceBuilder.size(0);
//加入聚合
//字段值項分組聚合
TermsAggregationBuilder aggregation = AggregationBuilders.terms("by_mutiple_" + groupField1 + "_" + groupField2)
.script(new Script("doc['" + groupField1 + "'] +'#'+doc['" + groupField2 + "']")) //(分組如果在字符串字段上,需要建立字段對應的.keyword字段(如:media_type.keyword),該字段支持聚合處理,直接用字符串字段會報錯。)
//.field("fngroup.keyword")
// .size(Integer.MAX_VALUE)
.size(500) // 開發環境,避免出現性能問題,設置返回buckets數量相對小一些;一次性返回buckets數量大於10000,需設置search.max_buckets參數,否則會報錯。
.order(BucketOrder.aggregation("count", true));
//計算每組的平均balance指標
aggregation.subAggregation(AggregationBuilders.count("count").field("sessionid"));
sourceBuilder.aggregation(aggregation);
// 構建查詢條件
queryBuilder(pageIndex, pageSize, queryParam, ARTICLE_INDEX_NAME, order, searchRequest, sourceBuilder);
//3、發送請求
SearchResponse searchResponse = highLevelClient.search(searchRequest,RequestOptions.DEFAULT);
int row = 0;
//4、處理響應
if(RestStatus.OK.equals(searchResponse.status())) {
// 獲取聚合結果
Aggregations aggregations = searchResponse.getAggregations();
Terms byAgeAggregation = aggregations.get("by_mutiple_" + groupField1 + "_" + groupField2);
for(Terms.Bucket buck : byAgeAggregation.getBuckets()) {
HashMap<String, Object> map=new HashMap<String, Object>();
String[] arr= buck.getKeyAsString().split("#");
map.put(groupField1, arr[0].replace("[","").replace("]",""));
map.put(groupField2, arr[1].replace("[","").replace("]",""));
map.put("count", buck.getDocCount()); // 獲取統計數
result.add(map);
}
}
} catch (IOException e) {
e.printStackTrace();
}
return result;
}
}
4、調用測試
// 新增記錄
public void create() {
EsArticle article = new EsArticle();
article.setTid("tidv");
article.setTitle("標題");
try {
client.insertOrUpdateArticle(article);
} catch (IllegalArgumentException | IllegalAccessException e) {
e.printStackTrace();
}
System.out.println("success ..");
}
// 查詢
public void query() {
Map<String, Object> queryParam = new HashMap<String, Object>();
queryParam.put("content", "深圳創業板");
client.queryArticle(queryParam, 1, 5, null);
System.out.println("success..");
}
// 分組統計
public void count() {
Map<String, Object> queryParam = new HashMap<String, Object>();
String[][] order = {{"tid", "desc"}};
LinkedHashMap<String, Long> resultMap = client.countArticle(queryParam, 1, 5, order, "by_tid", "tid");
int row = 1;
for (Entry<String, Long> entry : resultMap.entrySet()) {
System.out.println(row++ + " > " + entry.getKey() + ":" + entry.getValue());
}
System.out.println("success..");
}
// 多字段分組統計
public void countArticleMultiple() {
String[][] order = {{"media_type", "desc"}};
Map<String, Object> queryParam = new HashMap<String, Object>();
queryParam.put("content", "深圳創業板");
client.countArticleMultiple(queryParam, 1, 5, order, "tid", "media_type");
System.out.println("success..");
}
至此,就完成了從安裝部署到集成常用功能的實戰工作。
參考文獻:
https://blog.csdn.net/u014082714/article/details/89963046
https://blog.csdn.net/chen_2890/article/details/83895646
https://blog.csdn.net/weixin_37703281/article/details/90974279
https://www.jianshu.com/p/1fbfde2aefa5