ElasticSearch 高级查询语法

ES的Query DSL查询语法很多,如何选择合适的语法,同学们需要理解以下几点:

  • 需求: 精确值还是全文?
  • 分词器会影响查询结果,不同的字段可以指定不同的分词器
  • Elasticsearch 默认会以文档的相关度算分进行排序

ES倒排索引

当数据写入 ES 时,数据将会通过分词被切分为不同的term,ES 将 term 与其对应的文档列表建立一种映射关系,这种结构就是倒排索引。如下图所示:

为了进一步提升索引的效率,ES 在 term 的基础上利用 term 的前缀或者后缀构建了 term index, 用于对 term 本身进行索引,ES 实际的索引结构如下图所示:

这样当我们去搜索某个关键词时,ES 首先根据它的前缀或者后缀迅速缩小关键词的在 term dictionary 中的范围,大大减少了磁盘IO的次数。

  • 单词词典(Term Dictionary) :记录所有文档的单词,记录单词到倒排列表的关联关系
  • 倒排列表(Posting List)-记录了单词对应的文档结合,由倒排索引项组成
  • 倒排索引项(Posting):
    • 文档ID
    • 词频TF–该单词在文档中出现的次数,用于相关性评分
    • 位置(Position)-单词在文档中分词的位置。用于短语搜索(match phrase query)
    • 偏移(Offset)-记录单词的开始结束位置,实现高亮显示

Elasticsearch 的JSON文档中的每个字段,都有自己的倒排索引。
可以指定对某些字段不做索引:

  • 优点︰节省存储空间
  • 缺点: 字段无法被搜索

ES高级查询Query DSL

ES中提供了一种强大的检索数据方式,这种检索方式称之为Query DSL(Domain Specified Language) , Query DSL是利用Rest API传递JSON格式的请求体(RequestBody)数据与ES进行交互,这种方式的丰富查询语法让ES检索变得更强大,更简洁。
官方文档

示例数据
#指定ik分词器
PUT /es_db
{
  "settings" : {
      "index" : {
          "analysis.analyzer.default.type": "ik_max_word"
      }
  }
}

# 创建文档,指定id
PUT /es_db/_doc/1
{
"name": "张三",
"sex": 1,
"age": 25,
"address": "广州天河公园",
"remark": "java developer"
}
PUT /es_db/_doc/2
{
"name": "李四",
"sex": 1,
"age": 28,
"address": "广州荔湾大厦",
"remark": "java assistant"
}

PUT /es_db/_doc/3
{
"name": "王五",
"sex": 0,
"age": 26,
"address": "广州白云山公园",
"remark": "php developer"
}

PUT /es_db/_doc/4
{
"name": "赵六",
"sex": 0,
"age": 22,
"address": "长沙橘子洲",
"remark": "python assistant"
}

PUT /es_db/_doc/5
{
"name": "张龙",
"sex": 0,
"age": 19,
"address": "长沙麓谷企业广场",
"remark": "java architect assistant"
}	
	
PUT /es_db/_doc/6
{
"name": "赵虎",
"sex": 1,
"age": 32,
"address": "长沙麓谷兴工国际产业园",
"remark": "java architect"
}

查询所有 match_all

使用match_all,默认只会返回10条数据。
原因:_search查询默认采用的是分页查询,每页记录数size的默认值为10。如果想显示更多数据,指定size

  • 返回指定条数size
    • size 关键字: 指定查询结果中返回指定条数。默认返回值10条
GET /es_db/_search
{
  "query": {
    "match_all": {}
  },
  "size": 100
}

注意: 查询结果的窗口太大 from + size的结果必须小于或等于10000
# 可以采用scroll api更高效的请求大量数据集
# 查询结果的窗口的限制可以通过参数index.max_result_window进行设置。

PUT /es_db/_settings
{ 
  "index.max_result_window" :"20000"
}
#修改现有所有的索引,但新增的索引,还是默认的10000
PUT /_all/_settings
{ 
  "index.max_result_window" :"20000"
}

#查看所有索引中的index.max_result_window值
GET /_all/_settings/index.max_result_window

注意:参数index.max_result_window主要用来限制单次查询满足查询条件的结果窗口的大小,窗口大小由from + size共同决定。不能简单理解成查询返回给调用方的数据量。这样做主要是为了限制内存的消耗。
比如:from为1000000,size为10,逻辑意义是从满足条件的数据中取1000000到(1000000 + 10)的记录。这时ES一定要先将(1000000 + 10)的记录(即result_window)加载到内存中,再进行分页取值的操作。尽管最后我们只取了10条数据返回给客户端,但ES进程执行查询操作的过程中确需要将(1000000 + 10)的记录都加载到内存中,可想而知对内存的消耗有多大。这也是ES中不推荐采用(from + size)方式进行深度分页的原因。
同理,from为0,size为1000000时,ES进程执行查询操作的过程中确需要将1000000 条记录都加载到内存中再返回给调用方,也会对ES内存造成很大压力。

分页查询form

from 关键字: 用来指定起始返回位置,和size关键字连用可实现分页效果

GET /es_db/_search
{
  "query": {
    "match_all": {}
  },
  "size": 5,
  "from": 0
}

深分页查询Scroll

改动index.max_result_window参数值的大小,只能解决一时的问题,当索引的数据量持续增长时,在查询全量数据时还是会出现问题。而且会增加ES服务器内存大结果集消耗完的风险。最佳实践还是根据异常提示中的采用scroll api更高效的请求大量数据集。

#查询命令中新增scroll=1m,说明采用游标查询,保持游标查询窗口一分钟。
#这里由于测试数据量不够,所以size值设置为2。
#实际使用中为了减少游标查询的次数,可以将值适当增大,比如设置为1000。
GET /es_db/_search?scroll=1m 
{
    "query": { "match_all": {}},
    "size":  2
}

结果中还会存在一个 _scroll_id

采用游标id查询:

# scroll_id 的值就是上一个请求中返回的 _scroll_id 的值
GET /_search/scroll
{
    "scroll": "1m", 
    "scroll_id" : "FGluY2x1ZGVfY29udGV4dF91dWlkDXF1ZXJ5QW5kRmV0Y2gBFmNwcVdjblRxUzVhZXlicG9HeU02bWcAAAAAAABmzRY2YlV3Z0o5VVNTdWJobkE5Z3MtXzJB"
}

多次根据 scroll_id 游标查询,直到没有数据返回则结束查询。采用游标查询索引全量数据,更安全高效,限制了单次对内存的消耗。

指定字段排序sort

注意:会让得分失效

GET /es_db/_search
{
  "query": {
    "match_all": {}
  },
  "sort": [
    {
      "age": "desc"
    }
  ]
}

#排序,分页
GET /es_db/_search
{
  "query": {
    "match_all": {}
  },
  "sort": [
    {
      "age": "desc"
    }
  ],
  "from": 10,
  "size": 5
}

返回指定字段 _source

GET /es_db/_search
{
  "query": {
    "match_all": {}
  },
  "_source": ["name","address"]
}

模糊匹配 match

match在匹配时会对所查找的关键词进行分词,然后按分词匹配查找
match支持以下参数:

  • query : 指定匹配的值
  • operator : 匹配条件类型
    • and : 条件分词后都要匹配
    • or : 条件分词后有一个匹配即可(默认)
  • minmum_should_match : 最低匹配度,即条件在倒排索引中最低的匹配度
#模糊匹配 match   分词后or的效果
GET /es_db/_search
{
  "query": {
    "match": {
      "address": "广州白云山公园"
    }
  }
}

# 分词后 and的效果
GET /es_db/_search
{
  "query": {
    "match": {
      "address": {
        "query": "广州白云山公园",
        "operator": "AND"
      }
    }
  }
}

在match中的应用: 当operator参数设置为or时,minnum_should_match参数用来控制匹配的分词的最少数量。

# 最少匹配广州,公园两个词
GET /es_db/_search
{
  "query": {
    "match": {
      "address": {
        "query": "广州公园",
        "minimum_should_match": 2
      }
    }
  }
}

短语查询 match_phrase

match_phrase查询分析文本并根据分析的文本创建一个短语查询。match_phrase 会将检索关键词分词。match_phrase的分词结果必须在被检索字段的分词中都包含,而且顺序必须相同,而且默认必须都是连续的。

GET /es_db/_search
{
  "query": {
    "match_phrase": {
      "address": "广州白云山"
    }
  }
}
有数据
GET /es_db/_search
{
  "query": {
    "match_phrase": {
      "address": "广州白云"
    }
  }
}
无数据

POST _analyze
{
    "analyzer":"ik_max_word",
    "text":"广州白云山"
}
# 分词结果
{
  "tokens" : [
    {
      "token" : "广州",
      "start_offset" : 0,
      "end_offset" : 2,
      "type" : "CN_WORD",
      "position" : 0
    },
    {
      "token" : "白云山",
      "start_offset" : 2,
      "end_offset" : 5,
      "type" : "CN_WORD",
      "position" : 1
    },
    {
      "token" : "白云",
      "start_offset" : 2,
      "end_offset" : 4,
      "type" : "CN_WORD",
      "position" : 2
    },
    {
      "token" : "云山",
      "start_offset" : 3,
      "end_offset" : 5,
      "type" : "CN_WORD",
      "position" : 3
    }
  ]
}
原因: 广州和白云不是相邻的词条,中间隔了个白云山,所以搜不出数据.

解决: 如何解决词条间隔的问题?可以借助slop参数,slop参数告诉match_phrase查询词条能够相隔多远时仍然将文档视为匹配。
#广州云山分词后相隔为2,可以匹配到结果
GET /es_db/_search
{
  "query": {
    "match_phrase": {
      "address": {
        "query": "广州云山",
        "slop": 2
      } 
    }
  }
}
注意:这种方式不灵活,相隔为3 又搜不出来了.

多字段查询 multi_match

可以根据字段类型,决定是否使用分词查询,得分最高的在前面

GET /es_db/_search
{
  "query": {
    "multi_match": {
      "query": "长沙张龙",
      "fields": [
        "address",
        "name"
      ]
    }
  }
}
注意:字段类型分词,将查询条件分词之后进行查询,如果该字段不分词就会将查询条件作为整体进行查询。

全字段搜索 query_string

允许我们在单个查询字符串中指定AND | OR | NOT条件,同时也和 multi_match query 一样,支持多字段搜索。和match类似,但是match需要指定字段名,query_string是在所有字段中搜索,范围更广泛。
注意: 查询字段分词就将查询条件分词查询,查询字段不分词将查询条件不分词查询

  • 未指定字段查询
GET /es_db/_search
{
  "query": {
    "query_string": {
      "query": "张三 OR 橘子洲"
    }
  }
}
  • 指定单个字段查询
#Query String
GET /es_db/_search
{
  "query": {
    "query_string": {
      "default_field": "address",
      "query": "白云山 OR 橘子洲"
    }
  }
}
  • 指定多个字段查询
GET /es_db/_search
{
"query": {
"query_string": {
"fields": ["name","address"],
"query": "张三 OR (广州 AND 王五)"
}
}
}

simple_query_string

类似Query String,但是会忽略错误的语法,同时只支持部分查询语法,不支持AND OR NOT,会当作字符串处理。支持部分逻辑:

  • + 替代AND
  • | 替代OR
  • - 替代NOT
#simple_query_string 默认的operator是OR
GET /es_db/_search
{
  "query": {
    "simple_query_string": {
      "fields": ["name","address"],
      "query": "广州公园",
      "default_operator": "AND"
    }
  }
}

GET /es_db/_search
{
  "query": {
    "simple_query_string": {
      "fields": ["name","address"],
      "query": "广州 + 公园"
    }
  }
}

关键词查询 Term

Term用来使用关键词查询(精确匹配),还可以用来查询没有被进行分词的数据类型。Term是表达语意的最小单位,搜索和利用统计语言模型进行自然语言处理都需要处理Term。match在匹配时会对所查找的关键词进行分词,然后按分词匹配查找,而term会直接对关键词进行查找。一般模糊查找的时候,多用match,而精确查找时可以使用term。

  • ES中默认使用分词器为标准分词器(StandardAnalyzer),标准分词器对于英文单词分词,对于中文单字分词。
  • 在ES的Mapping Type 中 keyword , date ,integer, long , double , boolean or ip 这些类型不分词,只有text类型分词。
#关键字查询 term
GET /es_db/_search
{
  "query":{
    "term": {
      "address": {
        "value": "广州白云"
      }
    }
  }
}

# 采用term精确查询, 查询字段映射类型为keyword
GET /es_db/_search
{
  "query":{
    "term": {
      "address.keyword": {
        "value": "广州白云山公园"
      }
    }
  }
}
在ES中,Term查询,对输入不做分词。会将输入作为一个整体,在倒排索引中查找准确的词项,并且使用相关度算分公式为每个包含该词项的文档进行相关度算分。

ES中的结构化搜索

结构化数据:

  • 日期,布尔类型和数字都是结构化的
  • 文本也可以是结构化的。
    • 如彩色笔可以有离散的颜色集合:红(red) 、绿(green、蓝(blue)
    • 一个博客可能被标记了标签,例如,分布式(distributed)和搜索(search)
    • 电商网站上的商品都有UPC(通用产品码Universal Product Code)或其他的唯一

标识,它们都需要遵从严格规定的、结构化的格式。
应用场景:对bool,日期,数字,结构化的文本可以利用term做精确匹配

GET /es_db/_search
{
  "query": {
    "term": {
      "age": {
        "value": 28
      }
    }
  }
}

term处理多值字段,term查询是包含,不是等于

POST /employee/_bulk
{"index":{"_id":1}}
{"name":"小明","interest":["跑步","篮球"]}
{"index":{"_id":2}}
{"name":"小红","interest":["跳舞","画画"]}
{"index":{"_id":3}}
{"name":"小丽","interest":["跳舞","唱歌","跑步"]}

POST /employee/_search
{
  "query": {
    "term": {
      "interest.keyword": {
        "value": "跑步"
      }
    }
  }
}

前缀查询 prefix

它会对分词后的term进行前缀搜索。

  • 它不会分析要搜索字符串,传入的前缀就是想要查找的前缀
  • 默认状态下,前缀查询不做相关度分数计算,它只是将所有匹配的文档返回,然后赋予所有相关分数值为1。它的行为更像是一个过滤器而不是查询。两者实际的区别就是过滤器是可以被缓存的,而前缀查询不行。

prefix 的原理:需要遍历所有倒排索引,并比较每个 term 是否已所指定的前缀开头。

GET /es_db/_search
{
  "query": {
    "prefix": {
      "address": {
        "value": "广州"
      }
    }
  }
}

通配符查询 wildcard

通配符查询:工作原理和prefix相同,只不过它不是只比较开头,它能支持更为复杂的匹配模式。

GET /es_db/_search
{
  "query": {
    "wildcard": {
      "address": {
        "value": "*白*"
      }
    }
  }
}

范围查询 range

  • range:范围关键字
  • gte 大于等于
  • lte 小于等于
  • gt 大于
  • lt 小于
  • now 当前时间
POST /es_db/_search
{
  "query": {
    "range": {
      "age": {
        "gte": 25,
        "lte": 28
      }
    }
  }
}

日期 range

DELETE /product
POST /product/_bulk
{"index":{"_id":1}}
{"price":100,"date":"2021-01-01","productId":"XHDK-1293"}
{"index":{"_id":2}}
{"price":200,"date":"2022-01-01","productId":"KDKE-5421"}

GET /product/_mapping

GET /product/_search
{
  "query": {
    "range": {
      "date": {
        "gte": "now-2y"
      }
    }
  }
}

多 id 查询 ids

GET /es_db/_search
{
  "query": {
    "ids": {
      "values": [1,2]
    }
  }
}

模糊查询 fuzzy

在实际的搜索中,我们有时候会打错字,从而导致搜索不到。在Elasticsearch中,我们可以使用fuzziness属性来进行模糊查询,从而达到搜索有错别字的情形。
fuzzy 查询会用到两个很重要的参数,fuzziness,prefix_length

  • fuzziness:表示输入的关键字通过几次操作可以转变成为ES库里面的对应field的字段
    • 操作是指:新增一个字符,删除一个字符,修改一个字符,每次操作可以记做编辑距离为1,
    • 如中文集团到中威集团编辑距离就是1,只需要修改一个字符;
    • 该参数默认值为0,即不开启模糊查询。
    • 如果fuzziness值在这里设置成2,会把编辑距离为2的东东集团也查出来。
  • prefix_length:表示限制输入关键字和ES对应查询field的内容开头的第n个字符必须完全匹配,不允许错别字匹配
    • 如这里等于1,则表示开头的字必须匹配,不匹配则不返回
    • 默认值也是0
    • 加大prefix_length的值可以提高效率和准确率。
GET /es_db/_search
{
  "query": {
    "fuzzy": {
      "address": {
        "value": "白运山",
        "fuzziness": 1    
      }
    }
  }
}

GET /es_db/_search
{
  "query": {
    "match": {
      "address": {
        "query": "广洲",
        "fuzziness": 1
      }
    }
  }
}
注意: fuzzy 模糊查询 最大模糊错误 必须在0-2之间
- 搜索关键词长度为 2,不允许存在模糊
- 搜索关键词长度为3-5,允许1次模糊
- 搜索关键词长度大于5,允许最大2次模糊

高亮 highlight

highlight 关键字: 可以让符合条件的文档中的关键词高亮。

  • pre_tags 前缀标签
  • post_tags 后缀标签
  • tags_schema 设置为styled可以使用内置高亮样式
  • require_field_match 多字段高亮需要设置为false
示例代码
#指定ik分词器
PUT /products
{
  "settings" : {
      "index" : {
          "analysis.analyzer.default.type": "ik_max_word"
      }
  }
}

PUT /products/_doc/1
{
  "proId" : "2",
  "name" : "牛仔男外套",
  "desc" : "牛仔外套男装春季衣服男春装夹克修身休闲男生潮牌工装潮流头号青年春秋棒球服男 7705浅蓝常规 XL",
  "timestamp" : 1576313264451,
  "createTime" : "2019-12-13 12:56:56"
}

PUT /products/_doc/2
{
  "proId" : "6",
  "name" : "HLA海澜之家牛仔裤男",
  "desc" : "HLA海澜之家牛仔裤男2019时尚有型舒适HKNAD3E109A 牛仔蓝(A9)175/82A(32)",
  "timestamp" : 1576314265571,
  "createTime" : "2019-12-18 15:56:56"
}

测试
GET /products/_search
{
  "query": {
    "term": {
      "name": {
        "value": "牛仔"
      }
    }
  },
  "highlight": {
    "fields": {
      "*":{}
    }
  }
}

自定义高亮 html 标签
可以在 highlight 中使用 pre_tags 和 post_tags
GET /products/_search
{
  "query": {
    "term": {
      "name": {
        "value": "牛仔"
      }
    }
  },
  "highlight": {
    "post_tags": ["</span>"], 
    "pre_tags": ["<span style='color:red'>"],
    "fields": {
      "*":{}
    }
  }
}

多字段高亮
GET /products/_search
{
  "query": {
    "term": {
      "name": {
        "value": "牛仔"
      }
    }
  },
  "highlight": {
    "pre_tags": ["<font color='red'>"],
    "post_tags": ["<font/>"],
    "require_field_match": "false",
    "fields": {
      "name": {},
      "desc": {}
    }
  }
}

相关性和相关性算分

  • 搜索是用户和搜索引擎的对话,用户关心的是搜索结果的相关性
    • 是否可以找到所有相关的内容
    • 有多少不相关的内容被返回了
    • 文档的打分是否合理
    • 结合业务需求,平衡结果排名
  • 如何衡量相关性:
    • Precision(查准率)―尽可能返回较少的无关文档
    • Recall(查全率)–尽量返回较多的相关文档
    • Ranking -是否能够按照相关度进行排序

相关性(Relevance)

搜索的相关性算分,描述了一个文档和查询语句匹配的程度。ES 会对每个匹配查询条件的结果进行算分_score。打分的本质是排序,需要把最符合用户需求的文档排在前面。ES 5之前,默认的相关性算分采用TF-IDF,现在采用BM 25。

什么是TF-IDF

TF-IDF(term frequency–inverse document frequency)是一种用于信息检索与数据挖掘的常用加权技术。

  • TF-IDF被公认为是信息检索领域最重要的发明,除了在信息检索,在文献分类和其他相关领域有着非常广泛的应用。
  • IDF的概念,最早是剑桥大学的“斯巴克.琼斯”提出
    • 1972年——“关键词特殊性的统计解释和它在文献检索中的应用”,但是没有从理论上解释IDF应该是用log(全部文档数/检索词出现过的文档总数),而不是其他函数,也没有做进一步的研究
    • 1970,1980年代萨尔顿和罗宾逊,进行了进一步的证明和研究,并用 香农信息论 做了证明
  • 现代搜索引擎,对TF-IDF进行了大量细微的优化

Lucene 中的 TF-IDF 评分公式:

  • TF是词频(Term Frequency)

检索词在文档中出现的频率越高,相关性也越高。

  • IDF是逆向文本频率(Inverse Document Frequency)

每个检索词在索引中出现的频率,频率越高,相关性越低。

  • 字段长度归一值( field-length norm)

字段的长度是多少?字段越短,字段的权重越高。检索词出现在一个内容短的 title 要比同样的词出现在一个内容长的 content 字段权重更大。
以上三个因素——词频(term frequency)、逆向文档频率(inverse document frequency)和字段长度归一值(field-length norm)——是在索引时计算并存储的,最后将它们结合在一起计算单个词在特定文档中的权重。

BM25

BM25 就是对 TF-IDF 算法的改进,对于 TF-IDF 算法,TF(t) 部分的值越大,整个公式返回的值就会越大。BM25 就针对这点进行来优化,随着TF(t) 的逐步加大,该算法的返回值会趋于一个数值。

  • 从ES 5开始,默认算法改为BM 25
  • 和经典的TF-IDF相比,当TF无限增加时,BM 25算分会趋于一个数值

通过Explain API查看TF-IDF

PUT /test_score/_bulk
{"index":{"_id":1}}
{"content":"we use Elasticsearch to power the search"}
{"index":{"_id":2}}
{"content":"we like elasticsearch"}
{"index":{"_id":3}}
{"content":"Thre scoring of documents is caculated by the scoring formula"}
{"index":{"_id":4}}
{"content":"you know,for search"}

GET /test_score/_search
{
  "explain": true, 
  "query": {
    "match": {
      "content": "elasticsearch"
    }
  }
}

Boosting

Boosting是控制相关度的一种手段。
参数boost的含义:

  • 当boost > 1时,打分的权重相对性提升
  • 当0 < boost <1时,打分的权重相对性降低
  • 当boost <0时,贡献负分

返回匹配positive查询的文档并降低匹配negative查询的文档相似度分。这样就可以在不排除某些文档的前提下对文档进行查询,搜索结果中存在只不过相似度分数相比正常匹配的要低;

GET /test_score/_search
{
  "query": {
    "boosting": {
      "positive": {
        "term": {
          "content": "elasticsearch"
        }
      },
      "negative": {
         "term": {
            "content": "like"
          }
      },
      "negative_boost": 0.2
    }
  }
}

应用场景:希望包含了某项内容的结果不是不出现,而是排序靠后。

布尔查询bool Query

一个bool查询,是一个或者多个查询子句的组合,总共包括4种子句,其中2种会影响算分,2种不影响算分。

  • must: 相当于&& ,必须匹配,贡献算分
  • should: 相当于|| ,选择性匹配,贡献算分
  • must_not: 相当于! ,必须不能匹配,不贡献算分
  • filter: 必须匹配,不贡献算法

在Elasticsearch中,有Query和 Filter两种不同的Context

  • Query Context: 相关性算分
  • Filter Context: 不需要算分 ,可以利用Cache,获得更好的性能

相关性并不只是全文本检索的专利,也适用于yes | no 的子句,匹配的子句越多,相关性评分
越高。如果多条查询子句被合并为一条复合查询语句,比如 bool查询,则每个查询子句计算得出的评分会被合并到总的相关性评分中。

bool 查询语法

  • 子查询可以任意顺序出现
  • 可以嵌套多个查询
  • 如果你的bool查询中,没有must条件,should中必须至少满足一条查询
GET /es_db/_search
{
  "query": {
    "bool": {
      "must": {
        "match": {
          "remark": "java developer"
        }
      },
      "filter": {
        "term": {
          "sex": "1"
        }
      },
      "must_not": {
        "range": {
          "age": {
            "gte": 30
          }
        }
      },
      "should": [
        {
          "term": {
            "address.keyword": {
              "value": "广州天河公园"
            }
          }
        },
        {
          "term": {
            "address.keyword": {
              "value": "广州白云山公园"
            }
          }
        }
      ],
      "minimum_should_match": 1
    }
  }
}

如何解决结构化查询“包含而不是相等”的问题

POST /employee/_bulk
{"index":{"_id":1}}
{"name":"小明","interest":["跑步","篮球"]}
{"index":{"_id":2}}
{"name":"小红","interest":["跑步"]}
{"index":{"_id":3}}
{"name":"小丽","interest":["跳舞","唱歌","跑步"]}

POST /employee/_search
{
  "query": {
    "term": {
      "interest.keyword": {
        "value": "跑步"
      }
    }
  }
}

解决方案: 增加count字段,使用bool查询解决

  • 从业务角度,按需改进Elasticsearch数据模型
POST /employee/_bulk
{"index":{"_id":1}}
{"name":"小明","interest":["跑步","篮球"],"interest_count":2}
{"index":{"_id":2}}
{"name":"小红","interest":["跑步"],"interest_count":1}
{"index":{"_id":3}}
{"name":"小丽","interest":["跳舞","唱歌","跑步"],"interest_count":3}
  • 使用bool查询
# must 算分
POST /employee/_search
{
  "query": {
    "bool": {
      "must": [
        {
          "term": {
            "interest.keyword": {
              "value": "跑步"
            }
          }
        },
        {
          "term": {
            "interest_count": {
              "value": 1
            }
          }
        }
      ]
    }
  }
}
# filter不算分
POST /employee/_search
{
  "query": {
    "bool": {
      "filter": [
        {
          "term": {
            "interest.keyword": {
              "value": "跑步"
            }
          }
        },
        {
          "term": {
            "interest_count": {
              "value": 1
            }
          }
        }
      ]
    }
  }
}

利用bool嵌套实现should not逻辑

GET /es_db/_search
{
  "query": {
    "bool": {
      "must": {
        "match": {
          "remark": "java developer"
        }
      },
      "should": [
        {
          "bool": {
            "must_not": [
              {
                "term": {
                  "sex": 1
                }
              }
            ]
          }
        }
      ],
      "minimum_should_match": 1
    }
  }
}

Boosting Query

思考: 如何控制查询的相关性算分?

控制字段的Boosting

Boosting是控制相关的一种手段。可以通过指定字段的boost值影响查询结果

  • 参数boost的含义:
    • 当boost > 1时,打分的权重相对性提升
    • 当0 < boost <1时,打分的权重相对性降低
    • 当boost <0时,贡献负分
POST /blogs/_bulk
{"index":{"_id":1}}
{"title":"Apple iPad","content":"Apple iPad,Apple iPad"}
{"index":{"_id":2}}
{"title":"Apple iPad,Apple iPad","content":"Apple iPad"}

GET /blogs/_search
{
  "query": {
    "bool": {
      "should": [
        {
          "match": {
            "title": {
              "query": "apple,ipad",
              "boost": 1
            }
          }
        },
        {
          "match": {
            "content": {
              "query": "apple,ipad",
              "boost": 4
            }
          }
        }
      ]
    }
  }
}

案例:要求苹果公司的产品信息优先展示

POST /news/_bulk
{"index":{"_id":1}}
{"content":"Apple Mac"}
{"index":{"_id":2}}
{"content":"Apple iPad"}
{"index":{"_id":3}}
{"content":"Apple employee like Apple Pie and Apple Juice"}


GET /news/_search
{
  "query": {
    "bool": {
      "must": {
        "match": {
          "content": "apple"
        }
      }
    }
  }
}

利用must not排除不是苹果公司产品的文档

GET /news/_search
{
  "query": {
    "bool": {
      "must": {
        "match": {
          "content": "apple"
        }
      },
      "must_not": {
        "match":{
          "content": "pie"
        }
      }
    }
  }
}

利用negative_boost降低相关性

  • negative_boost 对 negative部分query生效
  • 计算评分时,boosting部分评分不修改,negative部分query乘以negative_boost值
  • negative_boost取值:0-1.0,举例:0.3

对某些返回结果不满意,但又不想排除掉( must_not),可以考虑boosting query的negative_boost。

GET /news/_search
{
  "query": {
    "boosting": {
      "positive": {
        "match": {
          "content": "apple"
        }
      },
      "negative": {
        "match": {
          "content": "pie"
        }
      },
      "negative_boost": 0.2
    }
  }
}

单字符串多字段查询

三种场景

  • 最佳字段(Best Fields)
    • 当字段之间相互竞争,又相互关联。例如,对于博客的 title和 body这样的字段,评分来自最匹配字段
  • 多数字段(Most Fields)
    • 处理英文内容时的一种常见的手段是,在主字段( English Analyzer),抽取词干,加入同义词,以
    • 匹配更多的文档。相同的文本,加入子字段(Standard Analyzer),以提供更加精确的匹配。其他字段作为匹配文档提高相关度的信号,匹配字段越多则越好。
  • 混合字段(Cross Field)
    • 对于某些实体,例如人名,地址,图书信息。需要在多个字段中确定信息,单个字段只能作为整体的一部分。希望在任何这些列出的字段中找到尽可能多的词

最佳字段查询Dis Max Query

将任何与任一查询匹配的文档作为结果返回,采用字段上最匹配的评分最终评分返回。

官方文档

PUT /blogs/_doc/1
{
    "title": "Quick brown rabbits",
    "body":  "Brown rabbits are commonly seen."
}

PUT /blogs/_doc/2
{
    "title": "Keeping pets healthy",
    "body":  "My quick brown fox eats rabbits on a regular basis."
}

POST /blogs/_search
{
    "query": {
        "bool": {
            "should": [
                { "match": { "title": "Brown fox" }},
                { "match": { "body":  "Brown fox" }}
            ]
        }
    }
}

bool should的算法过程:

  • 查询should语句中的两个查询
    • 加和两个查询的评分
    • 乘以匹配语句的总数
    • 除以所有语句的总数

上述例子中,title和body属于竞争关系,不应该讲分数简单叠加,而是应该找到单个最佳匹配的字段的评分。

使用最佳字段查询dis max query

POST blogs/_search
{
    "query": {
        "dis_max": {
            "queries": [
                { "match": { "title": "Brown fox" }},
                { "match": { "body":  "Brown fox" }}
            ]
        }
    }
}

可以通过tie_breaker参数调整

Tier Breaker是一个介于0-1之间的浮点数。0代表使用最佳匹配;1代表所有语句同等重要。

  • 获得最佳匹配语句的评分_score 。
  • 将其他匹配语句的评分与tie_breaker相乘
  • 对以上评分求和并规范化
POST /blogs/_search
{
    "query": {
        "dis_max": {
            "queries": [
                { "match": { "title": "Quick pets" }},
                { "match": { "body":  "Quick pets" }}
            ]
        }
    }
}


POST /blogs/_search
{
    "query": {
        "dis_max": {
            "queries": [
                { "match": { "title": "Quick pets" }},
                { "match": { "body":  "Quick pets" }}
            ],
            "tie_breaker": 0.2
        }
    }
}

Multi Match Query 最佳字段(Best Fields)搜索

Best Fields是默认类型,可以不用指定

POST /blogs/_search
{
  "query": {
    "multi_match": {
      "type": "best_fields",
      "query": "Quick pets",
      "fields": ["title","body"],
      "tie_breaker": 0.2
    }
  }
}

使用多数字段(Most Fields)搜索

DELETE /titles
PUT /titles
{
  "mappings": {
    "properties": {
      "title": {
        "type": "text",
        "analyzer": "english",
        "fields": {
          "std": {
            "type": "text",
            "analyzer": "standard"
          }
        }
      }
    }
  }
}

POST titles/_bulk
{ "index": { "_id": 1 }}
{ "title": "My dog barks" }
{ "index": { "_id": 2 }}
{ "title": "I see a lot of barking dogs on the road " }

# 结果与预期不匹配
GET /titles/_search
{
  "query": {
    "match": {
      "title": "barking dogs"
    }
  }
}

用广度匹配字段title包括尽可能多的文档——以提升召回率——同时又使用字段title.std 作为信号将相关度更高的文档置于结果顶部。

GET /titles/_search
{
  "query": {
    "multi_match": {
      "query": "barking dogs",
      "type": "most_fields",
      "fields": [
        "title",
        "title.std"
      ]
    }
  }
}

每个字段对于最终评分的贡献可以通过自定义值boost 来控制。比如,使title 字段更为重要,这样同时也降低了其他信号字段的作用:

#增加title的权重
GET /titles/_search
{
  "query": {
    "multi_match": {
      "query": "barking dogs",
      "type": "most_fields",
      "fields": [
        "title^10",
        "title.std"
      ]
    }
  }
}

跨字段(Cross Field)搜索

DELETE /address
PUT /address
{
    "settings" : {
        "index" : {
            "analysis.analyzer.default.type": "ik_max_word"
        }
    }
}

PUT /address/_bulk
{ "index": { "_id": "1"} }
{"province": "湖南","city": "长沙"}
{ "index": { "_id": "2"} }
{"province": "湖南","city": "常德"}
{ "index": { "_id": "3"} }
{"province": "广东","city": "广州"}
{ "index": { "_id": "4"} }
{"province": "湖南","city": "邵阳"}

#使用most_fields的方式结果不符合预期,不支持operator
GET /address/_search
{
  "query": {
    "multi_match": {
      "query": "湖南常德",
      "type": "most_fields",
      "fields": ["province","city"]
    }
  }
}

# 可以使用cross_fields,支持operator
#与copy_to相比,其中一个优势就是它可以在搜索时为单个字段提升权重。
GET /address/_search
{
  "query": {
    "multi_match": {
      "query": "湖南常德",
      "type": "cross_fields",
      "operator": "and", 
      "fields": ["province","city"]
    }
  }
}

可以用copy…to 解决,但是需要额外的存储空间

DELETE /address

PUT /address
{
  "mappings" : {
      "properties" : {
        "province" : {
          "type" : "keyword",
          "copy_to": "full_address"
        },
        "city" : {
          "type" : "text",
          "copy_to": "full_address"
        }
      }
    },
    "settings" : {
        "index" : {
            "analysis.analyzer.default.type": "ik_max_word"
        }
    }
}

PUT /address/_bulk
{ "index": { "_id": "1"} }
{"province": "湖南","city": "长沙"}
{ "index": { "_id": "2"} }
{"province": "湖南","city": "常德"}
{ "index": { "_id": "3"} }
{"province": "广东","city": "广州"}
{ "index": { "_id": "4"} }
{"province": "湖南","city": "邵阳"}

GET /address/_search
{
  "query": {
    "match": {
      "full_address": {
        "query": "湖南常德",
        "operator": "and"
      }
    }
  }
}

GET /address/_search
{
  "query": {
    "multi_match": {
      "query": "湖南常德",
      "type": "most_fields",
      "fields": ["province","city"]
    }
  }
}
  • 3
    点赞
  • 17
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值