ES分词搜索
ES的使用
- 前言
- 作者使用的版本
- 作者需求
- 简介
- ES简略介绍
- ik分词器简介
- 使用
- es的直接简单使用
- es的查询
- es在java中使用
- 备注说明
前言
作者使用的版本
- es: 7.17.27
- spring-boot-starter-data-elasticsearch: 7.14.2
作者需求
作者接到一个业务需求,我们系统有份数据被用来查询,进行搜索改造。我们的数据是可以分为两层,第一层是物品,物品含有物品名和物品别名。第二层是规格型号,一个物品可以配置多个规格型号,同一物品不同规格型号有不同的价格。
原先的搜索只支持对物品名或别名的模糊搜索。将带出该物品下所有的规格列表被用来选择。比如说原来数据库里有个物品叫做 蒙古大马
,如果使用蒙古的大马
进行搜索时搜索不到的,需要对这种搜索进行支持。
于是,作者选用了es作为文本搜索。作者只达到了可用的效果,并没有深究es的文档。底部会放置文档链接。
如上所述,本文并不对es做详细介绍。在上面说明中,作者已经碰到了问题,而es可以解决这个问题,所以本文只大概讲述es为什么能够解决我的问题
以及我如何使用es解决问题
。
简介
ES简略介绍
es为什么能够解决我的问题
,我想要的搜索场景是,可以从用户输入的文本串中提取
出有用的匹配信息,然后到我的数据库中对待匹配的信息
进行匹配。命中即认为该数据是用户需要的,用户输入文本可能与数据库中的某段信息的文字部分
是完全一致的,但是文字顺序
是被打乱的。mysql数据库支持like,所以可以将用户输入文字串分割成每个字,然后用or
去拼接查询。这种查询效率和使用方式可想而知有多么恐怖。但是es不需要这么麻烦的使用姿势,es支持match
,可以对文本进行匹配搜索。为什么MySQL这么麻烦,而es看起来很方便呢?因为他们主要的应用场景就不同,所以底层的数据结构就不同。首先看看他们对数据的索引方式
:
MySQL的索引方式很简单,一条记录留存,如果指定了需要索引某个字段,就会将这个字段的值
和对应记录的ID
拿出来,存储到B+
树中,二分搜索可以对有序数据进行快速查找,但是如果使用like '%x%'
;可以看到无法判断要匹配文本的首字母了,也就是无法使用索引了,这样数据量一旦上来,查询效率会飞快下降。
es的索引方式中文一直叫做倒排索引
,什么叫倒排索引,在mysql中,索引的是这个字段的全文本,然后用全文本去匹配我们的输入,如果匹配不到就错了,换个数据继续匹配。这个思路就是,我们需要所有数据的全文本,然后挨个匹配。可现在的场景是需要这么做,你只需要给我输入的文本内有效部分
的有关数据即可。所以我们首先需要考虑将输入信息进行分割
、提取
。然后用提取出来的每部分
数据参与匹配。那有个想法,如果我存的数据,也按照这种分割-提取
来提前建立一份索引呢?那就可以用提取到的部分输入直接去匹配数据库里提前存储好的数据的索引。匹配到一个部分即认为命中
,匹配到多个部分认为相关度更高
,最终得分
也就越高。这种将文档的文本打散进行索引再反过来查询文档的,被称作倒排索引
。
ik分词器简介
既然有个数据库可以支持倒排索引,那么接下来就看看怎么对文本分词
,首先一个问题是为什么要分词
,如果不分词,我输入一段话,每个字都是一个token
。那如果我认为有些字按照一定的顺序组合起来有固定的含义,他们就成了词,词就意味着通用,我们在存储和使用时都会按照这个顺序。即他们也可以做token
。这样一份文档的token
数量就会减少,这样可以显著提升查询的效率。
但是这也有个问题,比如上文的内蒙大马
。如果我用了分词器,ik的通用词库里有蒙古、大马
,我现在用马
去搜索,命中结果集为空,因为这段文本被索引为蒙古
、大马
,你用马
是匹配不到大马
这个索引的。字是默认组词使用的,如果有单字也是可以用使用的特殊情况,可以在词典里添加这个单字,需要注意词的顺序,最短的词放在上面,否则由于分词器是不贪心
的,已经匹配了一个比较好的索引,就不去再考虑生成一个差的索引了。词典添加后还要注意不要使用ik_smart
类型的分析器,下文会有说明。如果你认为你的场景每个字都需要考虑,那就简单了,不要分词
,以改兼赈、两难自解
;
es本身不支持中文分词,ik分词器可以支持中文的分词,所以需要ik分词器。ik分词有两种type
:
ik_smart
:对文本进行粗粒度分词,比如蓝瘦香菇这个词,我如果词典里有蓝瘦、香菇、蓝瘦香菇。他的分词结果蓝瘦香菇;显然,他的索引数量会更少,也会丢掉更多数据的查询。
ik_max_word
,进行最粗粒度的分词,会列举所有的可能(但不包括已经被组词的字
,所以单字的特殊情况可以将它作为词来解决)。
每次更新词库都需要重启,这太费劲了,还好ik支持热更新词库。只需要在配置文件配置remote_ext_dict
即可。但是,更新词典后只对之后的新数据有效,旧有数据需要重建索引
,
使用
es的直接简单使用
- 创建索引
PUT /index_name
{"mappings": {"properties": {"ref_id": { "type": "long" },"ref_type": { "type": "integer" },"goods_name": { "type": "text","analyzer":"my_ik_analyzer","search_analyzer":"ik_max_word"},"param_value": { "type": "text","analyzer":"my_ik_analyzer","search_analyzer":"ik_max_word"}}}
}
- 删除索引
DELETE /index_name
- 新增或更新文档
POST /index_name/_doc/doc_id # 最后一位参数可以指定生成的文档ID
{
"ref_id": 1,
"ref_type":1,
"goods_name": "狗;犬;野狗;导盲犬",
"param_value": "中华田园犬;美国狗;吃狗粮;奥利给"
}
- 分析测试
POST /index_name/_analyze?pretty
{
"analyzer": "ik_max_word",
"text":"内蒙大马"
}
- 查询
POST /bws_price_library/_search?pretty
{"query":
{"bool":{"must": [{ "multi_match": {"query":"大马","fields":["goods_name^2","param_value"] #字段名后面的^n可以指定得分权重。}},{"term":{"ref_type":"101"}}]}
}}
es的查询
es的查询很负责,所以能覆盖非常多的场景。作者上面涉及 准确查询term
、匹配查询match
,多字段匹配查询multi_match
、布尔嵌套查询bool
,只做了简单的示例,很容易触类旁通。具体的使用方式作者有时间会慢慢补充。
es在java中使用
- 保存
XXXXIndex index = new XXXXIndex();index.setId(BwsPriceConstant.SPECS_REF_TYPE_PART+"_"+id);index.setRefId(id);index.setRefType(BwsPriceConstant.SPECS_FEE_TYPE_PART);index.setGoodsName("name");index.setParamValue("value");IndexCoordinates indexCoordinates = elasticsearchRestTemplate.getIndexCoordinatesFor(XXXXIndex.class);elasticsearchRestTemplate.save(index,indexCoordinates);
- 删除
elasticsearchRestTemplate.delete(id, XXXXIndex.class);
- 更新
IndexCoordinates indexCoordinates = elasticsearchRestTemplate.getIndexCoordinatesFor(XXXXIndex.class);Document document = Document.create();document.setId(index.getId());document.putIfAbsent("ref_id",index.getRefId());document.putIfAbsent("ref_type",index.getRefType());document.putIfAbsent("goods_name",index.getGoodsName());document.putIfAbsent("param_value",index.getParamValue());elasticsearchRestTemplate.update(UpdateQuery.builder(index.getId()).withDocument(document).build(),indexCoordinates);
- 查询
Pageable pageable = PageRequest.of(reqVo.getCurrentPage()-1, reqVo.getPageSize());Map<String, Float> fields = new HashMap<>();fields.put("goods_name",3.0f);fields.put("param_value",1.0f);// 查询实体使用构造器模式,multiMatchQuery.fields方法可以指定查询权重,查看构造方法可以看到默认是都给了1的权重。这个权重也可以在构建索引的时候指定,不过查询时指定的话会更灵活一些NativeSearchQuery build = new NativeSearchQueryBuilder().withPageable(pageable).withQuery(new BoolQueryBuilder().must(new TermQueryBuilder("ref_type", reqVo.getFeeType())).must(QueryBuilders.multiMatchQuery(reqVo.getName(), "goods_name", "param_value").fields(fields))).build();SearchHits<XXXXIndex> search = elasticsearchRestTemplate.search(build, XXXXIndex.class);List<SearchHit<XXXXIndex>> searchHits = search.getSearchHits();// SearchHit中的Content就是指定的类型对象
备注说明
在使用时建议做好数据的手动同步,以防万一