Elasticsearch作为分布式搜索引擎可以说应用非常广了,可以用于站内搜索,日志查询等功能。本文将着重介绍Elasticsearch的搜索与聚合功能。
Elasticsearch 安装 对于初学者来说Elasticsearch的安装建议采用docker的方式。这里推荐使用docker-compose的方式安装, 里面既包含了Elasticsearch还有Kibana和Cerabro, 可以一键安装到位了。
启动docker之后访问Kibana 地址为http://localhost:5601, 导入Kibana默认提供的三种数据, 然后就可以在Kibana的开发者工具中练习Elasticsearch搜索和聚合的语法了。
在介绍搜索 DSL (Domain Specific Language) 之前先介绍一下Elasticsearch的搜索算分规则。在ES5之前默认的相关性算分采用TF-IDF,现在采用BM25。
TF-IDF TF(Term Frequency): 检索词在一篇文档中出现的频率。检索词出现的次数除以文档的总字数。IDF (Inverse Document Frequency): 计算方式为 log(全部文档数/检索词出现过的文档总数)
TF-IDF计算公式:
TF(检索词1)* IDF(区块链) + TF(检索词2)* IDF(检索词2)....
本质就是加权求和
BM25 BM25的计算公式如下:
TF-IDF是一种早期的信息检索算法,它基于单词在文档中的频率(TF)和在所有文档中的逆文档频率(IDF)来计算相关性。然而,TF-IDF有一些已知的缺点。例如,它假设所有单词都是独立的,不考虑它们之间的关系。此外,TF-IDF对于长文档可能会有偏好,因为长文档可能包含更多的关键词。
BM25是一种更先进的相关性评分算法,它试图解决TF-IDF的一些问题。BM25考虑了单词的频率,但是对于高频词,它的增长速度会慢于TF-IDF,这可以防止某些单词过度影响评分。此外,BM25还考虑了文档的长度,避免了TF-IDF对长文档的偏好。
如果采用如下方式进行查询会发现返回结果为空,这是因为Elasticsearch 在建立索引的时候会默认对customer_first_name
字段进行分词, 分词之后Mary变成了mary因此无法完全匹配到。
GET /kibana_sample_data_ecommerce/_search
{
"query": {
"term": {
"customer_first_name": {
"value": "Mary"
}
}
}
}
如果改成如下语句就能完全匹配到了
GET /kibana_sample_data_ecommerce/_search
{
"query": {
"term": {
"customer_first_name.keyword": {
"value": "Mary"
}
}
}
}
这是如下图所示, text类型字段ES会默认创建一个keyword字段,通过这个字段去查询就能严格匹配到了。
Term查询并不会去做分词处理, 基于全文本的查询会。基于全文本的查找包括:Match Query / Match Phrase Query / Query String Query。查询的时候会对输入的查询进行分词,每个词逐个进行底层查询,最后将结果进行合并。并且为每个文档生成一个算分。下面例子中会先对“Low Spherecords”进行分词,比如结果是“low” 和“spherecords”, 然后再分别对这两个单词进行底层搜索。
POST /kibana_sample_data_ecommerce/_search
{
"query": {
"match": {
"manufacturer":{
"query": "Low Spherecords"
}
}
}
}
结构化搜索针对的是日期,布尔类型和数字这些类型。对于文本来说,可能是博客标签,电商网站商品的UPCs码或者其他标识。以日期格式为例可以通过range
进行范围查找
GET /kibana_sample_data_ecommerce/_search
{
"query": {
"range": {
"order_date": {
"gte": "now-4y"
}
}
}
}
bool 查询是一个或者多个子查询的组合。总有有must
,should
,must_not
和filter
四种查询子句。其中前面两种影响算分属于Query Context,后面两个不影响算分,属于Filter Context。下面是一个bool 查询的例子
GET /kibana_sample_data_ecommerce/_search
{
"query": {
"bool": {
"must": {
"term": {
"day_of_week" : "Monday"
}
},
"must_not": [
{
"range": {
"taxful_total_price": {
"lte": 90
}
}
}
],
"filter": {
"term": {
"currency": "EUR"
}
},
"should": [
{
"terms": {
"sku" : ["ZO0549605496", "ZO0299602996"]
}
}
]
}
}
}
子查询可以任意顺序出现,同时可以嵌套多个查询,如果在bool查询中没有must条件,should中必须至少满足一条查询。
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."
}
用如下两个语法去查询,采用第一种语法文档1排在文档2的前面,采用第二种语法结果正好相反。
POST /blogs/_search
{
"query": {
"bool": {
"should": [
{ "match": { "title": "Brown fox" }},
{ "match": { "body": "Brown fox" }}
]
}
}
}
POST blogs/_search
{
"query": {
"dis_max": {
"queries": [
{ "match": { "title": "Quick fox" }},
{ "match": { "body": "Quick fox" }}
]
}
}
}
原因是因为第一种should语法在算分的过程中会考虑整体语句匹配的总数。上述例子的中title和body字段是相互竞争的, 不应将分数简单的叠加,而是找到单个最佳匹配字段的评分。Disjunction Max Query 是将任何与任一查询匹配的文档作为结果返回。采用字段上最匹配的评分返回 当然第二种语法如果没有加上tie_breaker参数就可能出现超预期的效果。比如查询“Quick pets”的时候,因为两个文档中的字段匹配分数的最高都是一样的所以,文档1又出现在了文档2的前面。可以通过如下加上tie_breaker参数解决。加上后,其他匹配语句的评分会与tie_breaker相乘 ,然后再与最佳匹配的语句求和。
POST blogs/_search
{
"query": {
"dis_max": {
"queries": [
{ "match": { "title": "Quick pets" }},
{ "match": { "body": "Quick pets" }}
],
"tie_breaker": 0.7
}
}
}
Multi-Match提供了best_fields
,most_fields
, cross_fields
三种查询类型来应对不同的对字段查询场景。Multi-Match基本语法如下
GET /_search
{
"query": {
"multi_match" : {
"query": "this is a test",
"fields": [ "subject", "message" ]
}
}
}
下面是most_fields的例子,这个例子中, title字段使用english分词器, 然后插入两个文档
PUT /titles
{
"mappings": {
"properties": {
"title": {
"type": "text",
"analyzer": "english"
}
}
}
}
两篇文档
POST titles/_bulk
{ "index": { "_id": 1 }}
{ "title": "My dog barks" }
{ "index": { "_id": 2 }}
{ "title": "I see a lot of barking dogs on the road " }
使用下面的语法查询会发现文档1排在前面与期望不符,这是因为english分词器会把词性给抹掉掉了, barking 变成了bark , dogs变成了dog,而文档1语句更短所以排在了前面。
GET titles/_search
{
"query": {
"match": {
"title": "barking dogs"
}
}
}
解决方法是修改titles的设置,增加子字段并添加standard分词。在查询的时候使用most_fields类型进行搜索。
PUT /titles
{
"mappings": {
"properties": {
"title": {
"type": "text",
"analyzer": "english",
"fields": {"std": {"type": "text","analyzer": "standard"}}
}
}
GET /titles/_search
{
"query": {
"multi_match": {
"query": "barking dogs",
"type": "most_fields",
"fields": [ "title", "title.std" ]
}
}
}
GET /_search
{
"query": {
"multi_match" : {
"query": "Will Smith",
"type": "best_fields",
"fields": [ "first_name", "last_name" ],
"operator": "and"
}
}
}
上面采用best_fields
并不适合做跨字段的搜索。因为它的执行逻辑如下,是采用对每个field做operator的匹配。(+first_name:will +first_name:smith) | (+last_name:will +last_name:smith)所有的term必须在一个field中都匹配到。
而cross_field
可用于跨字段搜索。
GET /_search
{
"query": {
"multi_match" : {
"query": "Will Smith",
"type": "cross_fields",
"fields": [ "first_name", "last_name" ],
"operator": "and"
}
}
}
它的执行逻辑如下 +(first_name:will last_name:will) +(first_name:smith last_name:smith) 所有的term都至少在一个field中匹配到
Aggregation作为Search的一部分语法如下:
Metric Aggregation可以用来做一些单值或者多值分析。单值分析比如min, max avg, sum , cardinality。多值分析比如stats, extended stats, percentile, top hits。下面是单值分析的例子:
GET /kibana_sample_data_ecommerce/_search
{
"size": 0,
"aggs": {
"max_tax_total_price": {
"max": {
"field": "taxful_total_price"
}
}
}
}
Bucket aggregation 是按照一定规则把文档分配到不同的桶中,达到分类的目的。Terms Aggregation需要打开fieldata。keyword默认支持, text类型需要在mapping中打开然后才会按照分词之后的结果进行分类。如下这个例子中通过打开category的fieldata从而实现针对category做聚合。
PUT kibana_sample_data_ecommerce/_mapping
{
"properties" : {
"category":{
"type": "text",
"fielddata": true
}
}
}
GET /kibana_sample_data_ecommerce/_search
{
"size": 0,
"aggs": {
"category": {
"terms": {
"field": "category"
}
}
}
}
下面是嵌套聚合的例子,先根据星期进行分类,然后再根据total_quantity进行降序排列取前三个。
GET /kibana_sample_data_ecommerce/_search
{
"size": 0,
"aggs": {
"categories": {
"terms": {
"field": "day_of_week"
},
"aggs":{
"total_quantity":{
"top_hits": {
"size": 3,
"sort": [{"total_quantity":"desc"}]
}
}
}
}
}
}
Pipeline就是在一次聚合分析的基础之上再做一次聚合分析。比如下面的语法就是找出平均total_quantity
最少的那个星期。buckets_path
指定聚合路径,然后再去做一次min_bucket的计算。
GET /kibana_sample_data_ecommerce/_search
{
"size": 0,
"aggs": {
"categories": {
"terms": {
"field": "day_of_week"
},
"aggs":{
"avg_total_quantity":{
"avg": {
"field": "total_quantity"
}
}
}
},
"min_quantity":{
"min_bucket":{
"buckets_path":"categories>avg_total_quantity"
}
}
}
}
根据Pipeline的分析结果输出到原结果中的位置,可将Pipeline分为两大类:
show_term_doc_count_error
对结果进行分析,同时通过增加shard_size的大小来提高精准度。GET my_flights/_search
{
"size": 0,
"aggs": {
"weather": {
"terms": {
"field":"OriginWeather",
"size":1,
"shard_size":1,
"show_term_doc_count_error":true
}
}
}
}