相信大家在做大数据量文本检索的时候都会想到使用elasticsearch(https://github.com/elastic/elasticsearch)
关于es的一些特性在网上有很多资料,我这里就不重复了,最近因为工作调动的关系,重新使用到es,之前使用的大多是ik分词等,甚至只是拿来当做一个存储层(结合Flink CDC),如果熟读过官网会发现其实有很多特性,特别在当前(2023.05)已经是8.x的版本https://www.elastic.co/guide/en/elastic-stack/current/overview.html
下面我们来使用一下基础的查询,版本7.16
先说es中一个重要的概念,相关性,相似度计算得出检索的文本匹配,其中有两个概念,TF检索词频和IDF反向文档频率
TF:检索的词出现频率越高,相关性越高
IDF:包含检索词的频率越高,这个检索的相关性比重越低,什么意思呢?是不是跟TF有冲突?其实是为了一种情况,如果某个词在所有文档都出现了,那这个词可以说对本次检索意义不大了,所以用这个方式约束词频出现太高的情况。另外字段长度指的是被检索的,长度,相当于在es中存储的数据字段长度,如果越长的情况,相关性就会越低
(还有个关键词是boost,如果有使用过kibana的都会发现在查询语句中会有这个参数,这个参数其实是权重的意思,可以通过这个字段提高查询的权重)
介绍一下分词器的原理:
分词器一般是利用了tokenizer,把数据切割成很多个tokens,比如:我们是21世纪的新人类,那么不同分词器就会把他拆分成若干个tokens,如:我们,我们是,们是,是21世纪,等等,另外分词也有各种算法,比如我们常用的ik、ngram这些插件等,另外还有哑巴算法等(这些直接对词语进行分词操作的),另外这个只作用于text类型上,像keyword都是全值匹配,毕竟它这个字段就命名为关键词嘛
好了正式介绍基础查询:
term/terms:使用term查询如果是字段类型是keyword的话就是等值,如果是text则会进行分词(具体要看分词settings是字段或者查询)
GET INDEX_NAME/_search
{
"query" : {
"term" : {
"FIELD" : {
"VALUE" : ""
}
}
}
}
GET INDEX_NAME/_search
{
"query" : {
"terms" : {
"FIELD" : [
"VALUE1",
"VALUE2"
]
}
}
}
terms_set:和terms相似,但是可以让查询的数据包含个数的方式,使用minimum_should_match_field可以让查询的数据最少包含个数
GET INDEX_NAME/_search
{
"query": {
"terms_set": {
"programming_languages": {
"terms": ["c++","java","php"],
"minimum_should_match_field": "required_match"
}
}
}
}
exists:返回改字段不为空的文档,如null或[]
GET INDEX_NAME/_search
{
"query": {
"exists": {
"field": "FIELD"
}
}
}
fuzzy:模糊查询,相当于可以运行查询的字段不太一样也可能查询出来,比如:box→fox,box→ox,box→bxo,这个去取决于,配置的编辑距离,即可容忍不一样的字段个数
GET INDEX_NAME/_search
{
"query": {
"fuzzy": {
"FIELD": "xxx",
"fuzziness" : 2
}
}
}
prefix:很好理解,前缀查询,以下例子,包含xxx前缀的数据
GET INDEX_NAME/_search
{
"query": {
"prefix": {
"FIELD": "xxx"
}
}
}
range:范围查询,下面是对age这个字段,大于等于10或者小于等于20的返回查询
GET INDEX_NAME/_search
{
"query": {
"range": {
"age": {
"gte": 10,
"lte": 20
}
}
}
}
regexp:正则查询
GET INDEX_NAME/_search
{
"query": {
"regexp": {
"FIELD": "xx.*yy"
}
}
}
wildcard:通配符查询,支持两种*和?,*是匹配多个,?是单个,跟mysql的like很像,所以可以举一反三,不建议在开头使用*和?,会影响性能
GET INDEX_NAME/_search
{
"query": {
"wildcard": {
"name": {
"value": "xxx*"
}
}
}
}
interval:根据特定排序,这个有点像复杂事件,可以查询出来的数据让一些内容在前后左右,有特定顺序(这种查询要看场景,我并没有使用过,这里仅演示)以下查询会出现带有jay chou的字符,如果把ordered去掉,那么chou jay也会被查询出来,其他参数max_gaps影响这两个值最大距离(从字段是字面意思)
GET INDEX_NAME/_search
{
"query": {
"intervals": {
"name": {
"match": {
"query": "jay chou",
"max_gaps": 0,
"ordered": true
}
}
}
}
}
match:就是匹配,可以使用到分词
GET INDEX_NAME/_search
{
"query": {
"match": {
"name": "jay chou"
}
}
}
match_bool_prefix:等价于多个term
GET INDEX_NAME/_search
{
"query": {
"match_bool_prefix": {
"name": "jay chou"
}
}
}
GET INDEX_NAME/_search
{
"query": {
"bool": {
"should": [
{
"term": {
"name": {
"value": "jay"
}
}
},
{
"prefix": {
"name": {
"value": "chou"
}
}
}
]
}
}
}
match_pharse:先解析检测的词,在搜索包含的,以下会不会查出jay chou中间有其他词的数据,当然也可以放宽增加slop参数,则可以让jay chou中间有slop个数据,比如1就可以查出jay xx chou
GET INDEX_NAME/_search
{
"query": {
"match_phrase": {
"name": "jay chou"
}
}
}
match_phrase_prefix:相当于match_bool_prefix + match_phrase,先解析分词,再和match_phrase匹配
GET INDEX_NAME/_search
{
"query": {
"match_phrase_prefix": {
"name": "jay chou"
}
}
}
multi_match:顾名思义,可就是多个匹配,一个检索词可以匹配多个字段,同时可以结合and、or等操作丰富查询的结果,
GET INDEX_NAME/_search
{
"query": {
"multi_match": {
"query": "jay",
"fields": [
"name",
"info"
]
}
}
}
commom:查询会把查询语句分成两个部分,较为重要的分为一个部分,不那么重要的为一个部分,分别用low_freq_operator
、high_freq_operator
以及minimum_should_match
来控制这些语句的表现,在进行查询之前需要指定一个区分高频和低频词的分界点,也就是cutoff_frequency
,它既可以是小数比如0.001
代表该字段所有的token的集合里面出现的频率也可以是大于1
的整数代表这个词出现的次数。当token的频率高于这一个阈值的时候,他就会被当作高频词
GET INDEX_NAME/_search
{
"query": {
"common": {
"body": {
"query": "nelly the elephant as a cartoon",
"cutoff_frequency": 0.001,
"low_freq_operator": "and"
}
}
}
相当于
GET INDEX_NAME/_search
{
"query": {
"bool": {
"must": [
{"term": {"body": "nelly"}},
{"term": {"body": "elephant"}},
{"term": {"body": "cartoon"}}
],
"should": [
{"term": {"body": "the"}},
{"term": {"body": "as"}},
{"term": {"body": "a"}}
]
}
}
}
query_string/simple_query_string:输入一个查询语句,返回和这个查询语句匹配的所有的文档,这个我们用的比较少,这个查询语句不是简单的检索词,而是包含特定语法的的搜索语句,里面包含操作符比如AND
和OR
,在进行查询之前会被一个语法解析器解析,转化成可以执行的搜索语句进行搜索。用户可以生成一个特别复杂的查询语句,里面可能包含通配符、多字段匹配等等。
通过上面的学习之后,我进行的一个实战
需求描述:需要通过用户输入的一些关键词,这个关键词分为三个部分,暂时描述为A、B、C,三个部分在elasticsearch中已经有现成的数据,但是由于架构较为古老,使用的还是springboot1.x的版本,而elasticsearch的版本为7.x,无法使用springboot-data-elasticsearch的封装,目前需要做一个匹配,输入的A和B,需要从头逐字匹配,即:输入jaychou,那么需要匹配jaychou、jaycho、jaych、jayc,业务要求最少四个,最多不超过二十个,分别ABC三个字段
思考:首先思考逐字匹配,那么如果考虑到search_analyzer进行拆词的话,之前只使用过ik,ik的几种分词不满足,考虑是否有其他分词方式,于是通过互联网的查询找到elasticsearch官方有个edge_ngram的分词方式刚好满足需求(可以自行了解一下,分词方式是例如:我们是21世纪新人类,在规定最大和最小边界的情况下,我,我们是,我们是2,我们是21,我们是21世等),另外由于无法使用springboot-data的包,所有创建索引、分词器等都是手写(坑...),@multiField无法使用,如果手写一个,由于嵌套,维护成本可能比较高,于是新增一个字段(即单独多一个edge分词的字段)
按照刚刚的思考,创建了新的索引字段,使用edge分词,输入分词效果还不错,但是随之出现另一个问题,大家都知道_score,这是一个评分系数,是根据es匹配的算法得出的结果,使用上述分词之后有一个情况,由于需要逐匹配,那么包含越多的情况应该分数越高,但是由于不改写评分算法的情况下,如果数据中出现jay的分数可能会比jayc,为什么呢,很好理解,jay的那条数据全等了,但是按照需求,应该jayc分数应该更高
解决方案:
经过查阅和思考,最终方案,把查询的数据用算法截取为jay、jayc、jaych、jaycho、jaychou,然后全部丢进should条件中,使用prefix的方式进行前缀查询,再通过对_score进行sort排序,这样由于业务限制,prefix最多不超过16个条件,同时也不会出现jay全等分数比包含jayc的数据分数高的情况
elasticsearch是当前大热的文本检索框架,很好解决了大数据量检索的问题,我记得在之前的公司做订单中心这个功能时了解过,京东的订单中心也是使用elasticsearch来实现的,并对部署和使用进行了深层次的优化来满足京东庞大的订单量,总之不管存储、检索功能,elasticsearch表现都极好,目前也在飞速迭代,期待未来的一个形态。
参考文章:
https://blog.csdn.net/paditang/article/details/79098830
https://zhuanlan.zhihu.com/p/137575167?utm_source=wechat_session