1.简介
Elasticsearch 是一个分布式、高扩展、高实时的搜索与数据分析引擎。它的底层是开源库Apache Lucene。为了解决Lucene复杂性,Elasticsearch应运而生。可以理解为Elasticsearch是对Lucene的封装。封装之后提供 RESTful API 来更友好的帮助我们实现存储和检索。
当然,Elasticsearch 不仅仅是 Lucene,并且也不仅仅只是一个全文搜索引擎。 它可以被下面这样准确地形容:
- 一个分布式的实时文档存储,每个字段可以被索引与搜索;
- 一个分布式实时分析搜索引擎;
- 能胜任上百个服务节点的扩展,并支持 PB 级别的结构化或者非结构化数据。
2.概念
为了更好的去了解elasticSearch之前,需要知道以下几个概念,这些概念能够更好的帮助你去了解elasticSearch,elasticSearch为什么可以高实时的搜索雨数据分析。本文主要讲述elasticSearch 7.*版本,每个大的版本概念略有差异。
2.1 全文搜索(Full-text Search)
全文检索是指计算机索引程序通过扫描文章中的每一个词,对每一个词建立一个索引,指明该词在文章中出现的次数和位置,当用户查询时,检索程序就根据事先建立的索引进行查找,并将查找的结果反馈给用户的检索方式。
2.2 倒排索引(Inverted Index)
倒排索引源于实际应用中需要根据属性的值来查找记录,也常被称为反向索引、置入档案或反向档案,是一种索引方法。该索引表中的每一项都包括一个属性值和具有该属性值的各记录的地址。由于不是由记录来确定属性值,而是由属性值来确定记录的位置,因而称为倒排索引(inverted index)。Elasticsearch能够实现快速、高效的搜索功能,正是基于倒排索引原理。有倒排索引必有正排索引,通俗地来讲,正排索引是通过key找value,倒排索引则是通过value找key。
可能看完之后,你还是不够清楚什么是倒排索引,这里结合一个简单的实例进行阐述。假设有个user索引,它有四个字段:分别是name,gender,age,address。画出来的话,大概是下面这个样子,跟关系型数据库一样。
id | name | age | adders |
1 | 张三 | 21 | 北京 |
2 | 李四 | 32 | 北京 |
3 | 王五 | 22 | 上海 |
Term(单词):一段文本经过分析器分析以后就会输出一串单词,这一个一个的就叫做Term(直译为:单词)
Term Dictionary(单词字典):顾名思义,它里面维护的是Term,可以理解为Term的集合
Term Index(单词索引):为了更快的找到某个单词,我们为单词建立索引
Posting List(倒排列表):倒排列表记录了出现过某个单词的所有文档的文档列表及单词在该文档中出现的位置信息,每条记录称为一个倒排项(Posting)。根据倒排列表,即可获知哪些文档包含某个单词。
(PS:实际的倒排列表中并不只是存了文档ID这么简单,还有一些其它的信息,比如:词频(Term出现的次数)、偏移量(offset)等,可以想象成是Python中的元组,或者Java中的对象)
(PS:如果类比现代汉语词典的话,那么Term就相当于词语,Term Dictionary相当于汉语词典本身,Term Index相当于词典的目录索引)
每个文档(可以看作关系型数据库中的表)都有一个ID,如果插入的时候没有指定的话,Elasticsearch会自动生成一个。
上面的例子,Elasticsearch建立的索引大致如下:
name字段:
term | post |
张三 | 1 |
李四 | 2 |
王五 | 3 |
age字段:
term | post |
21 | 1 |
22 | 3 |
32 | 2 |
addres字段:
term | post |
北京 | [1,2] |
上海 | 3 |
Elasticsearch分别为每个字段都建立了一个倒排索引。Post是一个数组,存储了所有符合某个Term的文档ID。只要知道文档ID,就能快速找到文档。可是,要怎样通过我们给定的关键词快速找到这个Term呢?当然是建索引了,为Terms建立索引,最好的就是B-Tree索引
关于倒排索引还有更多的内容,涉及到压缩、存储等相关内容,这里不在一一赘述,小编对这一块也没有很多的真知灼见,后期共同去努力学习、研究。
2.3 节点 & 集群(Node & Cluster)
Elasticsearch 本质上是一个分布式数据库,允许多台服务器协同工作,每台服务器可以运行多个Elasticsearch实例。单个Elasticsearch实例称为一个节点(Node),一组节点构成一个集群(Cluster)。
2.4 索引(Index)
Elasticsearch 数据管理的顶层单位就叫做 Index(索引),相当于关系型数据库里的表的概念。另外,每个Index的名字必须是小写。
2.5 文档(Document)
Index里面单条的记录称为 Document(文档)。许多条 Document 构成了一个 Index。Document 使用 JSON 格式表示。同一个 Index 里面的 Document,不要求有相同的结构(scheme),但是最好保持相同,这样有利于提高搜索效率。
2.6 文档元数据(Document metadata)
文档元数据为_index, _id, 这三者可以唯一表示一个文档,_index表示文档在哪存放,_id为文档的唯一标识。
2.7 字段(Fields)
每个Document都类似一个JSON结构,它包含了许多字段,每个字段都有其对应的值,多个字段组成了一个 Document,类似关系型数据库数据表中的字段。可以通过类比更好的让你了解。
索引(indices)--------------------------------数据库
文档(Document)----------------Row 行
字段(Field)-------------------Columns 列
3.应用场景
Elasticsearch作为主要的后端搜索支持,因为它具备快速的搜索能力、百万数据毫秒级响应,并提供持久存储、高可用、统计等。Elasticsearch不支持事务等特性,不易与实际业务进行之际结合。
日志数据分析,logstash采集日志,ES进行复杂的数据分析(ELK技术,elasticsearch+logstash+kibana)。
其实elasticsearch更多主要应用于数据量大、响应速度快、事务性不高的情况。
4.实战一
基于kibana操作Elasticsearch脚本。
Elasticsearch提供了多种交互使用方式,小编常用Kibana(可以把它类比为Navicat)对Elasticsearch 进行操作及Java API进行操作,实际代码业务中常用Java api,本章节主要介绍通过Kibana的方式对Elasticsearch进行操作,以及工作实践中常用的方法 。
4.1创建索引
"number_of_shards": 1 分片数
"number_of_replicas": 1 副本数
PUT people
{
"settings": {
"number_of_shards": 1,
"number_of_replicas": 1
},
"properties": {
"name": {
"type": "text",
"analyzer": "ik_max_word"
},
"age": {
"type": "integer",
"index": "false"
},
"sex": {
"type": "keyword"
}
}
}
4.2 删除索引
DELETE people
4.3 查询索引
类似mysql的desc table_name
GET people/_mapping
按照主键Id查询索引数据
GET people/_doc/id_value
4.4 统计数据条数
类似mysql的select count(*) from table_name
GET people/_count
4.5 添加数据
POST people/_doc/1 { "name" : "小明", "age" : 18, "sex": "男" }
4.6 更新数据
POST people/_doc/1 { "name" : "小红", "age" : 18, "sex": "女" }
4.7 删除数据
通过查询id进行删除即可
DELETE people/_doc/1
4.8 分页查询数据
不要使用from,size进行深度分页,会有性能问题
GET people/_search { "query": { "match_all": {} }, "from": 1, "size": 2 }
4.9 匹配查询
查询name是小王2的字段 默认情况为or (即包含小王字段的也会查询到)
GET people/_search { "query": { "match": { "name": "小王2" } } }
如果我们只想要小王2 则通过更改关联关系(此时我们只能查询到小王2)
GET people/_search { "query": { "match": { "name":{"query": "小王2","operator": "and"} } } }
如果满足其中的50% 岂可视为查询正确 小王和小王2都会查询出来
如果设置为100% 则只会查出小王2
GET people/_search { "query": { "match": { "name":{"query": "小王2","minimum_should_match": "50%"} } } }
4.10 多字段查询
我们在sex 和name中查询带 “男”字 的信息
GET people/_search { "query": { "multi_match": { "query": "男" , "fields": ["name","sex"] } } }
4.11 精确查找
term 查询被用于精确值 匹配,这些精确值可能是数字、时间、布尔或者那些未分词的字符串,terms 就是查询多个
GET people/_search { "query": { "terms": { "sex": [ "男","女" ] } }
4.12 过滤查询
只查询name字段
GET people/_search { "query": { "terms": { "sex": [ "男","女" ] } },"_source": "name" }
不需要带有age 的字段
GET people/_search { "query": { "terms": { "sex": [ "男","女" ] } },"_source": { "excludes": "age" } }
只需要性别和名字
GET people/_search { "query": { "terms": { "sex": [ "男","女" ] } },"_source": { "includes": ["name","sex"] } }
4.13 排序查询
姓名包含:张三 按照年龄排序
GET people/_search { "query": { "match": { "name": "张三" } }, "sort": [ { "age": { "order": "asc" } } ] }
查询年龄大于20岁的所有人
GET people/_search { "query": { "bool": { "filter": { "range": { "age": { "gt": 20 } } } } } }
4.14 多字段查询
查询name和address字段包含单词三的数据
GET people/_search { "query": { "multi_match": { "query": "三", "fields": ["name","address"] } } }
4.15 范围查询
查询年龄大于20小于30的人
GET people/_search { "query": { "range": { "age": { "gt": 20, "lt": 30 } } } }
- term查询时,不会分词,直接匹配倒排索引
- match查询时会进行分词
- term查询不会走分词器,但是会匹配倒排索引,所以查询的结构就跟分词器如何分词有关系,比如name字段赋值为Oppo,这时使用term查询Oppo不会查询出文档,这时因为es默认是用的standard分词器,它在分词后会将单词转成小写输出,所以使用oppo查不出文档,使用小写oppo可以查出来。
4.16 exists 查询和 missing 查询
查找那些指定字段中有值 (exists) 或无值 (missing) 的文档
指定name字段有值:
GET people/_search { "query": { "bool": { "filter": { "exists": { "field": "name" } } } } }
name字段无值:
GET people/_search { "query": { "bool": { "filter": { "missing": { "field": "name" } } } } }
4.17 match_phrase查询
短语查询,精确匹配,查询张三会匹配name字段包含张三短语的,而不会进行分词查询,也不会查询出包含张 其他词 三这样的文档
GET /ad/phone/_search { "query": { "match_phrase": { "ad": "a red" } } }
5.实战二
基于Elasticsearch API进行业务数据处理。
Elasticsearch提供了多种交互使用方式,本章节主要介绍通过Elasticsearch API进行操作.
5.1 新增或者更新
该方法主要是判断索引是否存在,存在更新,不存在新增。
public String index(String index, String id, Map<String, Object> map) throws IOException {
IndexRequest indexRequest = new IndexRequest(index);
indexRequest.source(map);
indexRequest.id(id);
IndexResponse indexResponse = restHighLevelClient.index(indexRequest, RequestOptions.DEFAULT);
return indexResponse.getResult().getLowercase();
}
5.2 批量插入
public void bulkInsert(String index, String idKeyStr, List<Map<String, Object>> list) throws IOException {
if (StringUtils.isBlank(index) || StringUtils.isBlank(idKeyStr) || CollectionUtils.isEmpty(list)) {
return;
}
BulkRequest bulkRequest = new BulkRequest();
list.forEach(map -> {
IndexRequest indexRequest = new IndexRequest(index);
indexRequest.source(map);
indexRequest.id(map.get(idKeyStr).toString());
bulkRequest.add(indexRequest);
});
restHighLevelClient.bulk(bulkRequest, RequestOptions.DEFAULT);
}
5.3 删除
public long deleteByQuery(String index, String fieldName, String value) throws IOException {
if (StringUtils.isBlank(index) || StringUtils.isBlank(fieldName) || StringUtils.isBlank(value)) {
return -1;
}
DeleteByQueryRequest deleteByQueryRequest = new DeleteByQueryRequest(index);
//设置版本冲突时继续执行
deleteByQueryRequest.setConflicts("proceed");
// 设置查询条件
deleteByQueryRequest.setQuery(new TermQueryBuilder(fieldName, value));
deleteByQueryRequest.setMaxDocs(60);
deleteByQueryRequest.setBatchSize(1000);
// 并行
deleteByQueryRequest.setSlices(2);
// 使用滚动参数来控制“搜索上下文”存活的时间
deleteByQueryRequest.setScroll(TimeValue.timeValueMinutes(10));
// 超时
deleteByQueryRequest.setTimeout(TimeValue.timeValueMinutes(2));
// 刷新索引
deleteByQueryRequest.setRefresh(true);
BulkByScrollResponse response = restHighLevelClient.deleteByQuery(deleteByQueryRequest, RequestOptions.DEFAULT);
return response.getStatus().getUpdated();
}
5.4 多字段匹配查询
public String getByMultiFieldNames(String index, Map<String, Object> fieldMap) throws IOException {
if (StringUtils.isBlank(index) || MapUtils.isEmpty(fieldMap)) {
return null;
}
SearchRequest searchRequest = new SearchRequest(index);
SearchSourceBuilder sourceBuilder = new SearchSourceBuilder();
BoolQueryBuilder boolQueryBuilder = QueryBuilders.boolQuery();
//循环传入搜索参数
fieldMap.forEach((key, value) -> {
boolQueryBuilder.must(QueryBuilders.termQuery(key, value));
});
sourceBuilder.query(boolQueryBuilder);
searchRequest.source(sourceBuilder);
SearchResponse searchResponse = restHighLevelClient.search(searchRequest, RequestOptions.DEFAULT);
return handleSearchResponse2Json(searchResponse);
}
5.5 根据id 查询
public String getByIndexAndId(String index, String id) throws IOException {
if (StringUtils.isBlank(index) || StringUtils.isBlank(id)) {
return null;
}
GetRequest getRequest = new GetRequest(index, id);
GetResponse getResponse = restHighLevelClient.get(getRequest, RequestOptions.DEFAULT);
return getResponse.getSourceAsString();
}
5.6 统计
public long getCountByIndex(String index) throws IOException {
if (StringUtils.isBlank(index)) {
return 0;
}
CountRequest countRequest = new CountRequest(index);
CountResponse count = restHighLevelClient.count(countRequest, RequestOptions.DEFAULT);
return count.getCount();
}
5.7 判断文档是否存在
public boolean isExists(String index, String id) throws IOException {
if (StringUtils.isBlank(index) || StringUtils.isBlank(id)) {
return Boolean.FALSE;
}
GetRequest getRequest = new GetRequest(index, id);
//禁用fetching _source
getRequest.fetchSourceContext(new FetchSourceContext(false));
// 关闭 存储字段
getRequest.storedFields("_none_");
return restHighLevelClient.exists(getRequest, RequestOptions.DEFAULT);
}
5.8 分页查询(模糊,准确)
public SearchElasticSearchResult getDataByPage(BatchDwdDataPage batchDwdDataPage) throws IOException {
String configindexName = batchDwdDataPage.getConfigName();
//按需返回指定字段
if (StringUtils.isBlank(configindexName)) {
return null;
}
SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder();
BoolQueryBuilder queryBuilder = QueryBuilders.boolQuery();
if ((BusinessConstant.DATA_CENTER_SYNC_BATCH_DWD_ES_CONFIG).equals(configindexName)) {
if (StringUtils.isNotBlank(batchDwdDataPage.getIndexName())) {
String indexName = batchDwdDataPage.getIndexName();
queryBuilder.must(QueryBuilders.wildcardQuery(BusinessConstant.INDEX_NAME, "*" + indexName + "*"));
}
} else if ((BusinessConstant.DATA_CENTER_SYNC_BATCH_DB_CONFIG).equals(configindexName)) {
if (StringUtils.isNotBlank(batchDwdDataPage.getTargetDbName())) {
String targetDbName = batchDwdDataPage.getTargetDbName();
queryBuilder.must(QueryBuilders.wildcardQuery(BusinessConstant.TARGET_DBNAME, "*" + targetDbName + "*"));
}
if (StringUtils.isNotBlank(batchDwdDataPage.getTargetTbName())) {
String targetTbName = batchDwdDataPage.getTargetTbName();
queryBuilder.must(QueryBuilders.wildcardQuery(BusinessConstant.TARGET_TBNAME, "*" + targetTbName + "*"));
}
}
if (StringUtils.isNotBlank(batchDwdDataPage.getSourceDataType())) {
String sourceDataType = batchDwdDataPage.getSourceDataType();
queryBuilder.must(QueryBuilders.wildcardQuery(BusinessConstant.SOURCE_DATA_TYPE, "*" + sourceDataType + "*"));
}
if (StringUtils.isNotBlank(batchDwdDataPage.getSourceDbName())) {
String sourceDbName = batchDwdDataPage.getSourceDbName();
queryBuilder.must(QueryBuilders.wildcardQuery(BusinessConstant.SOURCE_DBNAME, "*" + sourceDbName + "*"));
}
if (StringUtils.isNotBlank(batchDwdDataPage.getSourceTbName())) {
String sourceTbName = batchDwdDataPage.getSourceTbName();
queryBuilder.must(QueryBuilders.wildcardQuery(BusinessConstant.SOURCE_TBNAME, "*" + sourceTbName + "*"));
}
if (StringUtils.isNotBlank(batchDwdDataPage.getSourceUrl())) {
String sourceUrl = batchDwdDataPage.getSourceUrl();
queryBuilder.must(QueryBuilders.wildcardQuery(BusinessConstant.SOURCE_DBURL, "*" + sourceUrl + "*"));
}
searchSourceBuilder.query(queryBuilder);
Integer pageSize = batchDwdDataPage.getPageSize() == null ? 10 : batchDwdDataPage.getPageSize();
Integer pageNo = batchDwdDataPage.getPageNum() == null ? 1 : (batchDwdDataPage.getPageNum() - 1) * pageSize;
searchSourceBuilder.from(pageNo).size(pageSize);
searchSourceBuilder.trackTotalHits(true);
SearchRequest searchRequest = new SearchRequest();
searchRequest.indices(configindexName);
searchRequest.source(searchSourceBuilder);
SearchResponse searchResponse = restHighLevelClient.search(searchRequest, RequestOptions.DEFAULT);
SearchHit[] hits = searchResponse.getHits().getHits();
if (hits.length == 0) {
return null;
}
List<Map<String, Object>> dataList = new ArrayList<>(hits.length);
for (int i = 0; i < hits.length; i++) {
dataList.add(hits[i].getSourceAsMap());
}
SearchElasticSearchResult searchElasticSearchResult = new SearchElasticSearchResult();
searchElasticSearchResult.setDataInfo(dataList);
searchElasticSearchResult.setPageSize(pageSize);
searchElasticSearchResult.setPageNom(pageNo);
searchElasticSearchResult.setTotal(searchResponse.getHits().getTotalHits().value);
return searchElasticSearchResult;
}
5.9 局部更新
public String upsert(String index,String id, Map<String,Object> map) throws IOException{
UpdateRequest updateRequest = new UpdateRequest(index,id);
updateRequest.doc(map);
updateRequest.retryOnConflict(30);
IndexRequest indexRequest = new IndexRequest(index);
indexRequest.source(map);
indexRequest.id(id);
updateRequest.upsert(indexRequest);
UpdateResponse updateResponse=null;
updateResponse = restHighLevelClient.update(updateRequest, RequestOptions.DEFAULT);
return updateResponse.getResult().getLowercase();
}
5.10 判断nested格式数据是否存在
/**
* desription 判断nested类型数据是否存在
* @param indexName 索引名称
* @param fieldName 字段名称
* @param value 字段值
* @param path nested类型的父类名称
* @param fieldMap nested参数集合
* @return
* @throws IOException
*/
public boolean isExistByNestedRepeat(String indexName,String fieldName,String value,String path,Map<String,Object> fieldMap) {
boolean flag = false;
try {
SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder();
BoolQueryBuilder queryBuilder = QueryBuilders.boolQuery();
SearchRequest searchRequest = new SearchRequest(indexName);
BoolQueryBuilder queryBuilderByCountry = QueryBuilders.boolQuery();
for(Map.Entry entry:fieldMap.entrySet()){
queryBuilderByCountry.must(QueryBuilders.termQuery(path+"."+entry.getKey().toString(),entry.getValue().toString()));
}
queryBuilder.must(QueryBuilders.nestedQuery(path,queryBuilderByCountry, ScoreMode.None));
queryBuilder.must(QueryBuilders.termQuery(fieldName,value));
searchSourceBuilder.query(queryBuilder);
searchRequest.source(searchSourceBuilder);
SearchResponse searchResponse= restHighLevelClient.search(searchRequest, RequestOptions.DEFAULT);
long size = searchResponse.getHits().getTotalHits().value;
if (size>0){
flag=true;
}
} catch (IOException e) {
e.printStackTrace();
}
return flag;
}
5.11 nested格式数据局部更新
/**
* description: nested部分更新(只匹配更新传入的字段,多余字段不更新)
* @author wsy
* @param index 索引
* @param id (es id _id)
* @param map 待更新的信息
* @param nestedName nested名称
* @return void
* @date 2020/8/28 8:19 下午
**/
public String updateNestedById(String index,String id, Map<String,Object> map,String nestedName,String nestedIdName ) throws Exception{
UpdateRequest updateRequest = new UpdateRequest(index,id);
Map<String, Object> parameters = Collections.singletonMap("map",map);
updateRequest.script(NestedUtils.getUpdateNestedScript(map,nestedName,nestedIdName));
updateRequest.retryOnConflict(MagicConstant.MagicValue.RETRY_ON_CONFLICT_SIZE);
UpdateResponse updateResponse = restHighLevelClient.update(updateRequest, RequestOptions.DEFAULT);
return updateResponse.getResult().getLowercase();
}
5.12 nested添加
/**
* description: nested添加
* @author wsy
* @param index 索引
* @param map 待更新的信息
* @param nestedName nested名称
* @return void
* @date 2020/8/28 8:19 下午
**/
public String addNestedTypeData(String index,String value,String nestedName,Map map) throws IOException{
UpdateRequest request = new UpdateRequest( index, value);
request.script(NestedUtils.getAddNestedScript(map,nestedName));
request.retryOnConflict(MagicConstant.MagicValue.RETRY_ON_CONFLICT_SIZE);
UpdateResponse updateResponse= restHighLevelClient.update(request, RequestOptions.DEFAULT);
return updateResponse.getResult().getLowercase();
5.13 nested 删除
/**
* description: nested删除
* @author wsy
* @param index 索引
* @param id (es id _id)
* @param map 待更新的信息
* @param nestedName nested名称
* @return void
* @date 2020/8/28 8:19 下午
**/
public String deleteNestedById(String index,String id, Map<String,Object> map,String nestedName,String nestedIdName ) throws Exception{
//log.info("index:"+index+",id:"+id+",map:"+JSONObject.toJSONString(map).toString()+",nestedName:"+nestedName+",nestedIdName:"+nestedIdName);
UpdateRequest updateRequest = new UpdateRequest(index,id);
updateRequest.script(NestedUtils.getDeleteNestedScript(map,nestedName,nestedIdName));
updateRequest.retryOnConflict(MagicConstant.MagicValue.RETRY_ON_CONFLICT_SIZE);
UpdateResponse updateResponse = restHighLevelClient.update(updateRequest, RequestOptions.DEFAULT);
return updateResponse.getResult().getLowercase();
}
5.14 转换工具
/**
* description: 将SearchResponse 取出数据 转换成json
* @author wsy
* @param searchResponse
* @return java.lang.String
* @date 2020/9/30 2:33 下午
**/
private String handleSearchResponse2Json(SearchResponse searchResponse){
SearchHit[] hits = searchResponse.getHits().getHits();
if(hits.length == 0){
return null;
}
List<Map<String,Object>> dataList = new ArrayList<>(hits.length);
for(int i=0; i< hits.length; i++){
dataList.add(hits[i].getSourceAsMap());
}
return JSONObject.toJSONString(dataList);
}
以上是小编通过查找资料以及个人工作过程中的积累整理的于ElasticeSearch资料,希望能够为初学者带来一定的帮助,针对于ElasticeSearch而言,这些内容只是冰山一角,还需要不断的去学习和认知,通过实际过程中的运用,ElasticeSearch在并发查询高,数据量大的业务场景中起到了关键性作用。更好的支撑服务的正常运行。
更多技术相关内容请扫码关注吾爱Java公众号:
让我们一起学习成长。