目录
一、简介
ElasticSearch是一个基于Lucene的搜索服务器,它提供了一个分布式多用户能力的全文搜索引擎,基于RESTful-web接口。ElasticSearch是面向文档的,这意味着它可以存储整个对象或文档,同时索引每个文档的内容使之可以被搜索(每个字段都拥有一个倒排索引),在ElasticSearch中,你可以对文档进行索引、搜索、排序、过滤,这正是它能够执行复杂的全文搜索的原因之一。
上面是官话,选择使用es的一个很大原因就是pg库出现了查询性能瓶颈,由于历史原因,sql需要进行大量的关联查询且耗时较慢,因此需要启用es来存储一张业务宽表,冗余部分数据,数据更新时通过mq同步到es,然后将之前的查询操作全部转往es即可。
二、基本概念
这里记录下ES中常常出现的几个概念
- 节点(node):一个ES实例就是一个节点,一个机器可以有多个实例
- 索引(index):即一系列文档的集合,是相关文档存储的地方,一个es节点不建议建太多索引,否则搜索性能会受到影响
- 分片(shard):ES是分布式搜索引擎,每个索引有一个或多个分片,索引的数据被分配到各个分片上,相当于一桶水,用了N个杯子装;分片的存在有助于横向扩展,N个分片会被尽可能平均地分配在不同的节点上;每个分片都是一个最小工作单元,承载部分数据,lucene实例,完整的建立索引和处理请求的能力
- 副本(replica):可以理解为备份分片,相应的就有主分片,每个分片可以有多个副本;主分片和备分片不会出现在同一节点,默认情况下一个索引创建5个分片一个备份(即5主+5备=10个分片)
- 类型(type):在ElasticSearch中,一个索引对象可以存储很多不同用途的对象,而文档类型就可以让我们轻易地区分单个索引中的不同对象,每个文档可以有不同的结构,但要注意不同的文档类型不能为相同的属性设置不同的类型(例如在同一索引中的所有文档类型中,一个叫title的字段必须具有相同的类型);ES6已经不推荐单索引多类型结构,但依然保持兼容,到了ES7时已经完全不支持。
- 文档(documents):每个类型可以包含多个文档,每个文档包含多个字段(field)
ES存在两个端口号,9200和9300,9200作为http协议,主要用于外部通讯,一般我们使用的时候均为9200;9300作为tcp协议,jar之间就是通过tcp协议通讯,ES集群之间也是通过9300进行通讯。
关于字段的数据类型这里则不作记录,只扯一句,es5.0以后,string字段被拆分成两种新的数据类型:
- text:根据分词器进行分词,根据分词后的内容建立倒排索引
- keyword:不分词,直接根据字符串内容建立倒排索引
如果你在建立索引的时候未指定字段的数据类型,es会对该字段进行动态映射,即入库前result字段为数值型,es则映射为long,若后续result变为字符串,es会抛错
三、整合
由于项目需要,这里选取的es版本为6.7.2,记录与springboot的基本整合及使用
首先引入pom文件:
<dependency>
<groupId>org.elasticsearch.client</groupId>
<artifactId>elasticsearch-rest-high-level-client</artifactId>
<version>6.7.2</version>
</dependency>
<dependency>
<groupId>org.elasticsearch</groupId>
<artifactId>elasticsearch</artifactId>
<version>6.7.2</version>
</dependency>
然后在配置文件中加入es的基本配置信息
es.ip=
es.port=9200
es.enabled=true
es.index.name=
es.authentication.active=false // 是否需要密码校验
es.user.name=
es.user.password=
es客户端启动代码
private RestHighLevelClient client;
private BulkProcessor bulkProcessor;
@PostConstruct
public void establishElasticConnection() throws ServiceException {
if (!appEnv.isEsEnabled()){
return;
}
try {
if (appEnv.isEsAuthActive()){
final CredentialsProvider credentialsProvider =
new BasicCredentialsProvider();
credentialsProvider.setCredentials(AuthScope.ANY,
new UsernamePasswordCredentials(appEnv.getEsUserName(), appEnv.getEsPassword()));
RestClientBuilder builder = RestClient.builder(
new HttpHost(appEnv.getElasticSearchIp(), appEnv.getElasticSearchPort()))
.setHttpClientConfigCallback(new RestClientBuilder.HttpClientConfigCallback() {
@Override
public HttpAsyncClientBuilder customizeHttpClient(
HttpAsyncClientBuilder httpClientBuilder) {
return httpClientBuilder
.setDefaultCredentialsProvider(credentialsProvider);
}
});
client = new RestHighLevelClient(builder);
} else {
client = new RestHighLevelClient(
RestClient.builder(
new HttpHost(appEnv.getElasticSearchIp(), appEnv.getElasticSearchPort(), "http")));
}
// 初始化批量处理器
initBulkProcessor();
GetIndexRequest getIndexRequest = new GetIndexRequest(appEnv.getEsIndexName());
boolean result = client.indices().exists(getIndexRequest, RequestOptions.DEFAULT);
if (!result) {
// 指定索引结构并创建
createIndexStructure(appEnv.getEsIndexName());
}
} catch (ElasticsearchException ex){
throw new ServiceException(ErrorCode.INTERNAL_SERVER_ERROR, new String[] {ex.getMessage()});
} catch (Exception ex){
logger.error("Fail to establish ES connection, error:{}", ex.getMessage());
}
}
private void createIndexStructure(String indexName){
CreateIndexRequest request = new CreateIndexRequest(indexName);
request.settings(Settings.builder()
.put("index.number_of_shards", 1)
.put("index.number_of_replicas", 1)
);
String[] booleanFields = Constant.ES_BOOLEAN_FIELDS;
String[] keywordFields = Constant.ES_KEYWORD_TYPE_FIELDS;
String[] textFields = Constant.ES_TEXT_TYPE_FIELDS;
Map<String, Object> properties = new HashMap<>();
Map<String, Object> mapping = new HashMap<>();
assembleBooleanFields(booleanFields, properties);
assembleKeywordFields(keywordFields, properties);
assembleTextFields(textFields, properties);
mapping.put("properties", properties);
request.mapping(indexName, mapping);
try {
CreateIndexResponse createIndexResponse = client.indices().create(request, RequestOptions.DEFAULT);
if (!createIndexResponse.isAcknowledged()) {
logger.error("Aux costTracking Item Index Creation Failed, createIndexResponse:{}", createIndexResponse.toString());
}
} catch (IOException ioEx){
logger.info("Aux costTracking Item Index Creation Failed");
}
}
// 标识该字段既是text又是keyword,es的默认映射也是这种类型
private void assembleTextFields(String[] stringFields, Map<String, Object> properties){
for (String fieldName : stringFields) {
Map<String, Object> fieldMeta = new HashMap<>();
Map<String, Object> Fields = new HashMap<>();
Map<String, Object> KeyWord = new HashMap<>();
KeyWord.put(Constant.ES_KEYWORD_TYPE, Constant.ES_KEYWORD_KEYWORD);
KeyWord.put(Constant.ES_KEYWORD_IGNORE_ABOVE, 256);
Fields.put(Constant.ES_KEYWORD_KEYWORD, KeyWord);
fieldMeta.put(Constant.ES_KEYWORD_TYPE, Constant.ES_KEYWORD_TEXT);
fieldMeta.put(Constant.ES_KEYWORD_FIELDS, Fields);
properties.put(fieldName, fieldMeta);
}
}
// 批量处理器
private void initBulkProcessor() {
BulkProcessor.Listener listener = new BulkProcessor.Listener() {
@Override
public void beforeBulk(long executionId, BulkRequest request) {
logger.info("Try to bulk {} data", request.numberOfActions());
}
@Override
public void afterBulk(long executionId, BulkRequest request, BulkResponse response) {
logger.info("{} data bulk success", request.numberOfActions());
}
@Override
public void afterBulk(long executionId, BulkRequest request, Throwable failure) {
logger.error("{} data bulk failed, reason:{}", request.numberOfActions(), failure);
}
};
BiConsumer<BulkRequest, ActionListener<BulkResponse>> bulkConsumer =
(request, bulkListener) ->
client.bulkAsync(request, RequestOptions.DEFAULT, bulkListener);
bulkProcessor = BulkProcessor.builder(bulkConsumer, listener)
.setBulkActions(10000) // 每添加1w条数据执行一次操作
.setBulkSize(new ByteSizeValue(5, ByteSizeUnit.MB)) // 每达到5m请求size执行一次操作
.setFlushInterval(TimeValue.timeValueSeconds(5)) // 每5s执行一次操作
.setConcurrentRequests(1) // 允许并发
.setBackoffPolicy(BackoffPolicy.exponentialBackoff(TimeValue.timeValueMillis(100), 3)) // 100ms后执行,最大请求3次
.build();
}
上面是采用api的方式指定字段的数据类型,如果大部分索引的数据结构均相似,则可以采用索引模板的方式创建,这里不再演示,上面的映射类型如下:
"properties": {
"DstIp": {
"type": "text",
"fields": {
"keyword": {
"type": "keyword",
"ignore_above": 256
}
}
},
关于es的增删改查api则参考文档即可,当需要批量处理时建议使用上面的 BulkProcessor ,而不是 BulkRequest,关于查询时所用到的部分逻辑处理下面会介绍
四、es组合查询
这也是我们选择使用es的最大原因之一:优秀的搜索性能。这里只介绍es的组合查询:bool查询。bool查询对应lucene中的BooleanQuary,它由一个或多个字句组成,每个子句都有特定的类型,先介绍组合查询的四种逻辑组合:
- must:返回的文档必须满足must子句的条件,并且参与计算分值
- filter:返回的文档必须满足filter子句的条件。但是不会像must一样,参与计算分值
- should:返回的文档可能满足should子句的条件。在一个bool查询中,如果没有must或者filter,有一个或者多个should子句,那么只要满足一个就可以返回,minimum_should_match参数定义了至少满足几个子句
- must_nout:返回的文档必须不满足must_not定义的条件
使用上面四种组合基本可以满足任何复杂查询,如果一个查询既有filter又有should,那么至少包含一个should子句,下面介绍具体的查询方式:(下文括号中的类名对应es的api)
- match(matchQuery):匹配查询,在执行查询时,会对搜索词进行分词,只要文档中有匹配到任何一个分词结果则认为匹配
- match_phrase(matchPhraseQuery):短语匹配查询,搜索词不会被分词,而是直接以一个短语的形式查询,只要字段的分词结果顺序连接在一起与搜索词匹配则认为满足
- term(termQuery):精确查询,不会对搜索词进行分词,而是作为一个整体与目标字段进行匹配,若完全匹配,则可查询到
- terms(termsQuery):查找多个精确值,term查询对于查找单个值非常有用,但通常我们可能想搜索多个值,terms是包含操作,满足其一即可
- range(rangeQuery):范围查询,一般用于数字和日期字段
- exists(existsQuery):非空查询,es在对文档进行序列化时,空值默认不进行序列化,如果想查询某个字段不为空的数据,则可以使用exists进行查询
- wildcard(wildcardQuery):模糊查询,类似关系数据库中的like,尽量使用match或match_phrase匹配(有索引)
termQuery与matchPhraseQuery的区别:前者不会考虑分词内容,只会按照搜索词完全匹配字段内容,若相等则满足;后者会将对应字段分词后的各情况进行组合,若能组合成搜索词且间隔为0,则认为满足
五、图形化界面
平常开发建议直接使用chrome的head插件即可,简单易用,形如:
或者使用es的配套工具kibana,mac下可直接通过brew进行下载安装:(注意kibana的版本与es的版本需要匹配,我这里是6.7.0的kibana)
brew install kibana
注意kibana是依赖node.js的,kibana默认连接的es地址是本地,可以进到kibana的安装目录修改其配置文件,然后直接在命令行输入kibana即可启动,启动成功如下:
然后访问localhost:5601即可
不同版本的kibana界面还都不一样,因此只要你知道基本的api查询就行
六、es部分RESTful-api
1、删除索引:curl -XDELETE http://xxx:9200/aux_tracking_item-team4
支持正则,如 curl -XDELETE http://xxx:9200/*_tracking_item-team4
2、查看某个字段数据的分词结果:GET /${index}/${type}/${id}/_termvectors?fields=${fields_name}
3、测试分词接口:post _analyze
{
"analyzer": "standard",
"text": "hello world"
}