当前位置 博文首页 > 小熊的进阶之路:Elasticsearch从0到千万级数据查询实践(非转载
https://www.elastic.co/cn/what-is/elasticsearch,es的起源,是因为程序员Shay Banon在使用Apache Lucene发现不太好用,然后手动改造升级的过程中发展起来的。(程序员就是需要有这种动力~)实际上es也是一个java应用,跑在jvm里面的
关系型数据库 | schema(库) | 表 | 每一行的数据 | 字段columns |
elasticsearch | index(索引) | document | 字段fields |
索引方式的区别,es主要是利用倒排索引(inverted index),这个翻译可能会让初次接触的人产生误解,误以为是倒着排序?其实不是这样,一般关系型数据库索引是把某个字段建立起一张索引表,传入这个字段的某个值,再去索引中判断是否有这个值,从而找到这个值所在数据(id)的位置。而倒排索引则是把这个值所在的文档id记录下来,当输入这个值的时候,直接查询出这个值所匹配的文档id,再取出id。所以我们在建立es索引的时候,有分词的概念,相当于可以把filed字段内容拆分,然后索引记录下来。例如“我爱中国”,可以拆分成“我”,“爱”,“中国”,“我爱中国”这五个词,同时记录下来这几个关键词的对应文档数据id是1,当我们查询“我”,“中国”时,都能查出这条数据。而如果使用关系型数据库去查包含“中国”这个关键字的数据的时候,则需要like前后通配全表扫描,无法快速找到关键词所在的数据行。
https://www.elastic.co/cn/start 在这个地址里面下载最新版本,目前是7.10.2(拖了一个月写完,我下载的时候是7.9.3- -!)
Windows版是一个压缩包文件,解压后(进入bin点开bat)即可使用。Linux版由于是直接在k8s里拉的镜像,这里就不做赘述。
启动完成之后访问:http://127.0.0.1:9200/,看见如下页面:You Know, for Search,就算启动成功啦。
像数据库一样,可视化界面有Navicat,SQLyog,MySql自带的Workbench。es也是需要一个可视化ui界面来方便我们操作的。这里选择的也是官方的的kibana:
https://www.elastic.co/cn/downloads/kibana :
请注意需要选择与es匹配的版本,如果版本不匹配,则会提示你:
或者是其他类似版本不匹配的错误。
安装完成后就可以打开kibana玩耍啦,由于我本地没有数据,拿的是7.6.2版本搭建的elk中kibana界面:
如果需要连接环境上的es,则可以在这里配置用户名和密码:
这个工具的搜索很方便,不需要指定查哪个字段的哪个值,直接在输入框搜索想要查询的字段即可。如果想看他对应的查询语句,点开F12打开控制台即可研究:
es的查询条件还是比较复杂的,但是在业务查询当中,一些比较简单的查询就可以满足大多数的通用分页查询了,除非是要开发报表查询,会复杂一些。
本地跑demo的话还是很容易的,这两个应用默认占用内存都不大,有需求可以自行调小一点:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-elasticsearch</artifactId>
</dependency>
其中“omsElasticsearchSettings”这一段的意思是像mybatis那样解析表达式,找到omsElasticsearchSettings这个bean的getSuffix方法获取前后缀。这样就可以实现动态的根据环境生成映射对应的索引
1 @Configuration
2 @AllArgsConstructor
3 public class ElasticsearchConfig {
5 private final Environment env;
7 @Bean
8 public ElasticsearchSettings omsElasticsearchSettings(){
9 return new ElasticsearchSettings().setSuffix(env.getActiveProfiles()[0]);
10 }
12 }
13
16 @Data
17 @Accessors(chain = true)
18 public class ElasticsearchSettings {
20 public String suffix;
22 }
在启动项目的时候,SpringData会检测配置中的es里是否存在对应索引,如不存在,则会根据@Document实体中配置的@Field字段来生成mapping文件:
生成的Mapping Demo如下:
PUT om_package_dev/?pretty
{
"settings": {
"number_of_shards" :1,
"number_of_replicas" : 1
},
"mappings": {
"properties": {
"_class": {
"type": "text",
"fields": {
"keyword": {
"type": "keyword",
"ignore_above": 256
}
}
},
"actualFreightCost": {
"type": "double"
},
"actualPackageCost": {
"type": "double"
}
}
}
}
建立一个@Repository像mybatis一样来做增删改查的映射封装:
底层是SpringData提供封装的统一方法:
保存数据的时候直接调用即可:
一般来说订单这些重要数据不会删除,要删除也是逻辑删除,所以删除接口基本不调用。直接更新逻辑删除值就好。更新也是调用这个:save/saveAll
查是Es的重头戏,我们打开org.elasticsearch.index.query.AbstractQueryBuilder查看实现类可以发现,继承这个抽象类的各种查询类有四五十个之多,不得不让人感叹es的查询强大,(与反人类,学习成本太高了)。
好消息是,如果业务场景不复杂,仅仅是想在分页查询上提高速度,那么只需要掌握一下几个类的用法即可:
我们封装了两个查询枚举,一个用来定义该实体是es查询条件实体@interface QueryEntity:
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface QueryEntity {
String[] dbOrders() default {};
String[] esOrders() default {};
String dbLogicField() default "";
String esLogicField() default "";
}
另外一个是用来定义字段,即使用es的哪个条件去查询@interface QueryField:
@Target({ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
public @interface QueryField {
String esField() default "";
String dbField() default "";
boolean like() default false;
boolean range() default false;
boolean require() default false;
boolean match() default false;
boolean commaSupported() default false;
boolean isBigDecimal() default false;
Class<?> searchTypeEnum() default void.class;
Class<?> sortTypeEnum() default void.class;
}
对应到实体上的用法demo就是:
这样可以支持区间查询,字段类型,对应es字段,从设计上规避了根据每个字段,调用每个拼接语句的上百个if/else噩梦。通过一个通用的查询工具类,来封装拼接这些查询条件QueryUtils:
@Slf4j
public class QueryUtils {
private static ConcurrentHashMap<Class<?>, HashMap<String, Field>> classFieldMap = new ConcurrentHashMap<>();
/**
* 构建查询
*
* @param obj
* @return 若为 null 说明该查询必定不会返回结果,无需查询 ES
*/
public static BoolQueryBuilder boolQuery(Object obj) {
if (obj == null) {
return null;
}
BoolQueryBuilder root = QueryBuilders.boolQuery();
if (!classFieldMap.containsKey(obj.getClass())) {
HashMap<String, Field> filedNameMap = new HashMap<>(obj.getClass().getDeclaredFields().length);
for (Field field : obj.getClass().getDeclaredFields()) {
filedNameMap.put(field.getName(), field);
}
classFieldMap.put(obj.getClass(), filedNameMap);
}
HashMap<String, Field> filedNameMap = classFieldMap.get(obj.getClass());
QueryEntity entitySetting = obj.getClass().getAnnotation(QueryEntity.class);
for (Field field : filedNameMap.values()) {
QueryField fieldSetting;
if ((fieldSetting = field.getAnnotation(QueryField.class)) == null) {
continue;
}
Object value = ReflectionUtil.getValue(field, obj);
if (isNullOrEmpty(value)) {
if (!fieldSetting.require()) {
continue;
}
return null;
}
String fieldName = getEsQueryFieldName(field, fieldSetting);
if (fieldSetting.range()) {
BoolQueryBuilder bool = QueryBuilders.boolQuery();
String[] arr = (String[]) value;
RangeQueryBuilder range = QueryBuilders.rangeQuery(fieldName);
if (arr.length != 2 || (StringUtils.isEmpty(arr[0]) && StringUtils.isEmpty(arr[1]))) {
continue;
}
if (!StringUtils.isEmpty(arr[0]) && StringUtils.isEmpty(arr[1])) {
bool.must(range.from(
fieldSetting.isBigDecimal() ? new BigDecimal(arr[0]) : DateUtil.parseAndGetTimestamp(arr[0])));
} else if (StringUtils.isEmpty(arr[0]) && !StringUtils.isEmpty(arr[1])) {
bool.must(range.to(fieldSetting.isBigDecimal() ? new BigDecimal(arr[1]) : DateUtil.parseAndGetTimestamp(arr[1])));
} else {
bool.must(range.from(fieldSetting.isBigDecimal() ? new BigDecimal(arr[0]) : DateUtil.parseAndGetTimestamp(arr[0])).
to(fieldSetting.isBigDecimal() ? new BigDecimal(arr[1]) : DateUtil.parseAndGetTimestamp(arr[1])));
}
root.must(bool);
} else if (field.getType() == List.class) {
assert value instanceof List<?>;
List<?> list = (List<?>) value;
if (CollectionUtils.isEmpty(list)) {
if (fieldSetting.require()) {
return null;
}
continue;
}
if (list.get(0) instanceof StoreListBO) {
BoolQueryBuilder bool1 = QueryBuilders.boolQuery();
for (Object store : list) {
StoreListBO bo = (StoreListBO) store;
BoolQueryBuilder bool2 = QueryBuilders.boolQuery();
if (!bo.getFlagAll()) {
bool2.must(QueryBuilders.termQuery("platformCode", bo.getPlatformCode()));
bool2.must(QueryBuilders.termsQuery("storeCode", bo.getStoreCodeList()));
}
bool1.should(bool2);
}
root.must(bool1);
} else {
root.must(QueryBuilders.termsQuery(fieldName, (List<?>) value));
}
} else if (fieldSetting.like()) {
root.must(QueryBuilders.wildcardQuery(fieldName, String.format("*%s*", value)));
} else if (fieldSetting.commaSupported()) {
root.must(QueryBuilders.termsQuery(fieldName, StringUtility.splitCommaString((String) value)));
} else if (fieldSetting.match()) {
if (fieldSetting.commaSupported()) {
root.must(QueryBuilders.multiMatchQuery(fieldName, StringUtility.splitCommaString((String) value)));
}