ElasticSearch进阶高级映射、DSL、聚合、智能搜索建议

470 阅读23分钟

高级映射

地理坐标点数据类型

  • 地理坐标点 地理坐标点是指地球表面可以用经纬度描述的一个点。 地理坐标点可以用来计算两个坐标间的距离,还 可以判断一个坐标是否在一个区域中。地理坐标点需要显式声明对应字段类型为 geo_point
PUT /food_poi
{
  "mappings": {
    "properties": {
      "name": {
        "type": "text"
      },
      "location": {
        "type": "geo_point"
      }
    }
  }
}
  • 经纬度坐标格 如上例, location 字段被声明为 geo_point 后,我们就可以索引包含了经纬度信息的文档了。 经纬度信息的形式可以是字符串、数组或者对象
# 字符串形式
PUT /food_poi/_doc/1
{
  "name": "兰州拉面",
  "location": "33.333,33.333"
}
# 对象形式
PUT /food_poi/_doc/2
{
  "name": "黄焖鸡",
  "location": {
    "lat": 66.666,
    "lon": 66.666
  }
}
# 数组形式
PUT /food_poi/_doc/3
{
  "name": "沙县",
  "location": [
    88.888,
    88.888
  ]
}

lon:经度 lat:纬度

注意

  1. 字符串形式以半角逗号分割,如 "lat,lon"
  2. 对象形式显式命名为 latlon
  3. 数组形式表示为 [lon,lat]
  • 通过地理坐标点过滤 有四种地理坐标点相关的过滤器 可以用来选中或者排除文档
过滤器作用
geo_bounding_box找出落在指定矩形框中的点
geo_distance找出与指定位置在给定距离内的点
geo_distance_range找出与指定点距离在给定最小距离和最大距离之间的点
geo_polygon找出落在多边形中的点。 这个过滤器使用代价很大 。当你觉得自己 需要使用它,最好先看看 geo-shapes

geo_bounding_box这是目前为止最有效的地理坐标过滤器了,因为它计算起来非常简单。你指定一个矩形的顶部 , 底部 ,左边界和右边界,然后过滤器只需判断坐标的经度是否在左右边界之间,纬度是否在上下边界之间

  • 过滤举例
    • geo_bounding_box
    GET /food_poi/_search
      {
        "query": {
          "bool": {
            "must": {
              "match_all": {}
            },
            "filter": {
              "geo_bounding_box": {
                "location": {
                  "top_left": {
                    "lat": 35.00,
                    "lon": 33.00
                  },
                  "bottom_right": {
                    "lat": 33.00,
                    "lon": 70.00
                  }
                }
              }
            }
          }
        }
      }
    
    • geo_distance
      • 过滤仅包含与地理位置相距特定距离内的匹配的文档。假设以下映射和索引文档 然后可以使用 geo_distance过滤器执行以下查询
      GET /food_poi/_search
      {
        "query": {
          "bool": {
            "must": {
              "match_all": {}
            },
            "filter": {
              "geo_distance": {
                "distance": "2000km",
                "location": {
                  "lat": 31,
                  "lon": 31
                }
              }
            }
          }
        }
      }
      

动态映射

Elasticsearch在遇到文档中以前未遇到的字段,可以使用dynamic mapping(动态映射机制)来确定 字段的数据类型并自动把新的字段添加到类型映射。Elastic的动态映射机制可以进行开关控制,通过设置mappings的dynamic属性,dynamic有如下设置项

  • true:遇到陌生字段就执行dynamic mapping处理机制
  • false:遇到陌生字段就忽略
  • strict:遇到陌生字段就报错

设置strict

# 设置为报错
PUT /user
{
  "mappings": {
    "dynamic": "strict",
    "properties": {
      "name": {
        "type": "text"
      },
      "address": {
        "type": "object",
        "dynamic": true
      }
    }
  }
}

#执行
PUT /user/_doc/1
{
  "name": "zhangsan",
  "age": "20",
  "address": {
    "province": "beijing",
    "city": "beijing"
  }
}

提示报错 image.png

设置true

PUT /user
{
  "mappings": {
    "dynamic": true,
    "properties": {
      "name": {
        "type": "text"
      },
      "address": {
        "type": "object",
        "dynamic": true
      }
    }
  }
}

#执行
PUT /user/_doc/1
{
  "name": "zhangsan",
  "age": "20",
  "address": {
    "province": "beijing",
    "city": "beijing"
  }
}

自动生成age字段 image.png

自定义动态映射

如果你想在运行时增加新的字段,你可能会启用动态映射。 然而有时候动态映射规则可能不太智能。幸运的是,我们可以通过设置去自定义这些规则,以便更好的适用于你的数据。

日期检测

当Elasticsearch遇到一个新的字符串字段时,它会检测这个字段是否包含一个可识别的日期,比如2014-01-01如果它像日期,这个字段就会被作为date类型添加。否则它会被作为string类型添加。有些时候这个行为可能导致一些问题。

想象下,你有如下这样的一个文档: { "note": "2014-01-01" } 假设这是第一次识别 note 字段,它会被添加为 date 字段。但是如果下一个文档像这样: { "note": "Logged out" } 这显然不是一个日期,但为时已晚。这个字段已经是一个日期类型,这个 不合法的日期 将会造成一个 异常。日期检测可以通过在根对象上设置 date_detection 为 false 来关闭

PUT /date_index
{
  "mappings": {
    "date_detection": false
  }
}

使用这个映射,字符串将始终作为 string 类型。如果需要一个 date 字段,必须手动添加。 Elasticsearch 判断字符串为日期的规则可以通过 dynamic_date_formats setting 来设置。

PUT /date_index
{
  "mappings": {
    "dynamic_date_formats": "MM/dd/yyyy"
  }
}

如果string格式是MM/dd/yyyy则会设置为date类型,如果不是则是string

dynamic_templates

使用dynamic_templates可以完全控制新生成字段的映射,甚至可以通过字段名称或数据类型来应用不同的映射。每个模板都有一个名称,你可以用来描述这个模板的用途,一个 mapping 来指定映射应该怎样使用,以及至少一个参数 (如 match) 来定义这个模板适用于哪个字段。模板按照顺序来检测;第一个匹配的模板会被启用。例如,我们给string类型字段定义两个模板:

  • es :以 _es 结尾的字段名需要使用 spanish 分词器。
  • cn :所有其他字段使用ik分词器。 我们将 es 模板放在第一位,因为它比匹配所有字符串字段的cn模板更特殊:
PUT /templates_index
{
  "mappings": {
    "dynamic_templates": [
      {
        "es": {
          "match": "*_es",
          "match_mapping_type": "string",
          "mapping": {
            "type": "text",
            "analyzer": "spanish"
          }
        }
      },
      {
        "cn": {
          "match": "*",
          "match_mapping_type": "string",
          "mapping": {
            "type": "text",
            "analyzer": "ik_max_word"
          }
        }
      }
    ]
  }
}

match_mapping_type允许你应用模板到特定类型的字段上,就像有标准动态映射规则检测的一样(例如string或long)

测试代码:

PUT /templates_index/_doc/1
{
  "name_es": "testes",
  "name": "test长江大桥"
}

image.png match参数值匹配字段名称,path_match参数会匹配对象完整路径 比如people.*.age会匹配如下字段

{
    "people":{
        "man":{
            "age":20
        }
     }
}

DSL

Query DSL | Elasticsearch Guide [8.1] | Elastic Elasticsearch提供了基于JSON的完整查询DSL(Domain Specific Language 特定域的语言)来定义查询。将查询DSL视为查询的AST(抽象语法树),它由两种子句组成:

  • 叶子查询子句
    • 叶子查询子句在特定域中寻找特定的值,如match,term或range查询。复合查询子句
  • 复合查询子句
    • 包装其他叶子查询或复合查询,并用于以逻辑方式组合多个查询(例如bool或dis_max查询),或更改其行为(例如constant_score查询)。

基本语法

POST /索引库名/_search
{
  "query": {
    "查询类型": {
      "查询条件": "查询条件值"
    }
  }
}

这里的query代表一个查询对象,里面可以有不同的查询属性

  • 查询类型:例如:match_all,match,term,range等等
  • 查询条件:查询条件会根据类型的不同,写法也有差异,后面详细讲解

查询所有

POST /food_poi/_search
{
  "query": {
    "match_all": {}
  }
}
  • query查询对象
  • match_all查询所有 image.png 结果解析
字段含义
took查询花费时间,单位是毫秒
time_out是否超时
_shards分片信息
hits搜索结果总览对象
第一层hits含义
字段含义
total搜索到的总条数
max_score所有结果中文档得分的最高分
hits搜索结果的文档对象数组,每个元素是一条搜索到的文档信息
第二层hits含义
字段含义
_index索引库
_type文档类型
_id文档id
_score文档得分
_source文档的源数据

全文索引

全文搜索能够搜索已分析的文本字段,如电子邮件正文,商品描述等。使用索引期间应用于字段的同一分析器处理查询字符串。全文搜索的分类很多几个典型的如下:

匹配搜索(match query)

全文查询的标准查询,它可以对一个字段进行模糊、短语查询。match queries接收 text/numerics/dates, 对它们进行分词分析, 再组织成一个boolean查询。可通过operator指定bool组合操作(or、and默认是or)。

#测试数据
PUT /study_test
{
  "settings": {},
  "mappings": {
    "properties": {
      "name": {
        "type": "text",
        "analyzer": "ik_max_word"
      },
      "price": {
        "type": "float"
      }
    }
  }
}

POST /study_test/_doc/
{
  "name": "elastic入门",
  "price": 88
}

POST /study_test/_doc/
{
  "name": "mysql入门2",
  "price": 66
}

POST /study_test/_doc/
{
  "name": "elastic进阶",
  "price": 100
}
  • or关系 match类型查询,会把查询条件进行分词,然后进行查询,多个词条之间是or的关系
POST /study_test/_search
{
  "query": {
    "match": {
      "name": "elastic进阶"
    }
  }
}

image.png 结果查出来了elastic的入门和进阶,看一下分词结果:

image.png 由此可以看出分词之间的关系是or。

  • and关系 某些情况下,我们需要更精确查找,我们希望这个关系变成and,可以这样做:
POST /study_test/_search
{
  "query": {
    "match": {
      "name": {
        "query": "elastic进阶",
        "operator": "and"
      }
    }
  }
}

结果如下,只查出来了一条数据 image.png

短语搜索(match phrase query)

match_phrase查询用来对一个字段进行短语查询,可以指定 analyzer、slop移动因子

  • 不设置slop image.png 可以看到查不出结果
  • 设置slop image.png 可以看到查出了一条结果

slop是移动因子设置之后可以移动多少个词匹配,所以这和分词器也有很大的关系。

query_string查询

Query String提供了无需指定某字段而对文档全文进行匹配查询的一个高级查询,同时可以指定在 哪些字段上进行匹配。

案例如下

# 默认
GET /study_test/_search
{
  "query": {
    "query_string": {
      "query": "66"
    }
  }
}
#指定字段
GET /study_test/_search
{
  "query": {
    "query_string": {
      "query": "66",
      "default_field": "name"
    }
  }
}
# 逻辑查询
GET /study_test/_search
{
  "query": {
    "query_string": {
      "query": "elastic OR 入门",
      "default_field": "name"
    }
  }
}
GET /study_test/_search
{
  "query": {
    "query_string": {
      "query": "elastic AND 入门",
      "default_field": "name"
    }
  }
}
# 模糊查询
# ~模糊偏移量
GET /study_test/_search
{
  "query": {
    "query_string": {
      "query": "elastic~1",
      "default_field": "name"
    }
  }
}
# 多字段支持
GET /study_test/_search
{
  "query": {
    "query_string": {
      "query": "66",
      "fields": [
        "name",
        "price"
      ]
    }
  }
}

多字段匹配搜索(multi match query)

如果你需要在多个字段上进行文本搜索,可用multi_match。multi_match在 match的基础上支持对多个字段进行文本查询。

#普通写法,类似query_string
GET /study_test/_search
{
  "query": {
    "multi_match": {
      "query": "66",
      "fields": [
        "name",
        "price"
      ]
    }
  }
}

#可使用通配符
GET /study_test/_search
{
  "query": {
    "multi_match": {
      "query": "66",
      "fields": [
        "name",
        "pri*"
      ]
    }
  }
}

词条级搜索(term-level queries)

可以使用term-level queries根据结构化数据中的精确值查找文档。 与全文查询不同,term-level queries不分析搜索词。相反,词条与存储在字段级别中的术语完全匹配。

首先要造一组数据:

PUT /book
{
  "settings": {},
  "mappings": {
    "properties": {
      "description": {
        "type": "text",
        "analyzer": "ik_max_word"
      },
      "name": {
        "type": "text",
        "analyzer": "ik_max_word"
      },
      "price": {
        "type": "float"
      },
      "timestamp": {
        "type": "date",
        "format": "yyyy-MM-dd HH:mm:ss||yyyy-MM-dd||epoch_millis"
      }
    }
  }
}

PUT /book/_doc/1
{
  "name": "lucene",
  "description": "Lucene Core is a Java library providing powerful indexing and search features, as well as spellchecking, hit highlighting and advanced analysis/tokenization capabilities. The PyLucene sub project provides Python bindings for Lucene Core. ",
  "price": 100.45,
  "timestamp": "2022-04-08 19:11:35"
}

PUT /book/_doc/2
{
  "name": "solr",
  "description": "Solr is highly scalable, providing fully fault tolerant distributed indexing, search and analytics. It exposes Lucenes features through easy to use JSON/HTTP interfaces ornative clients for Java and other languages.",
  "price": 320.45,
  "timestamp": "2022-04-06 19:11:35"
}

PUT /book/_doc/3
{
  "name": "Hadoop",
  "description": "The Apache Hadoop software library is a framework that allows for the distributed processing of large data sets across clusters of computers using simple programming models.",
  "price": 620.45,
  "timestamp": "2022-04-07 19:11:35"
}
PUT /book/_doc/4
{
  "name": "ElasticSearch",
  "description": "Elasticsearch是一个基于Lucene的搜索服务器。它提供了一个分布式多用户能力的全文搜索引擎,基于RESTful web接口。Elasticsearch是用Java语言开发的,并作为Apache许可条款下的开放源码发布,是一种流行的企业级搜索引擎。Elasticsearch用于云算中,能够达到实时搜索,稳定,可靠,快速,安装使用方便。官方客户端在Java、.NET(C#)、PHP、Python、ApacheGroovy、Ruby和许多其他语言中都是可用的。根据DB-Engines的排名显示,Elasticsearch是最受欢迎的企业搜索引擎,其次是Apache Solr,也是基于Lucene。",
  "price": 999.99,
  "timestamp": "2022-04-05 19:11:35"
}

词条搜索(term query)

term查询用于查询指定字段包含某个词项的文档

POST /book/_search
{
  "query": {
    "term": {
      "name": "ElasticSearch"
    }
  }
}

结果如下,可以看到什么都没查出来。 image.png 原因:查看分词结果是小写的elasticsearch。 image.png 解决方案:

  1. 使用match
  2. 使用keyword,设置大小写不敏感
PUT /book
{
"settings": {
   "analysis": {
    "normalizer": {
      "my_normalizer": {
        "type": "custom",
        "filter": ["lowercase", "asciifolding"]
      }
    }
  }
},
"mappings": {
  "properties": {
    "description": {
      "type": "text",
      "analyzer": "ik_max_word"
    },
    "name": {
      "type": "keyword",
      "normalizer": "my_normalizer"
    },
    "price": {
      "type": "float"
    },
    "timestamp": {
      "type": "date",
      "format": "yyyy-MM-dd HH:mm:ss||yyyy-MM-dd||epoch_millis"
    }
  }
}
}

修改之后可以查到结果 image.png

词条集合搜索(terms query)

这个和全文索引的 query_string、多字段匹配差不多

GET /book/_search
{
  "query": {
    "terms": {
      "name": [
        "solr",
        "elasticsearch"
      ]
    }
  }
}

image.png

范围搜索(range query)

  • gte:大于等于
  • gt:大于
  • lte:小于等于
  • lt:小于
  • boost:查询权重
GET /book/_search
{
  "query": {
    "range": {
      "price": {
        "gte": 10,
        "lte": 200
      }
    }
  }
}

#日期过滤
GET /book/_search
{
  "query": {
    "range": {
      "timestamp": {
        "gte": "now-2d/d",
        "lt": "now/d"
      }
    }
  }
}
GET book/_search
{
  "query": {
    "range": {
      "timestamp": {
        "gte": "01/04/2022",
        "lte": "2023",
        "format": "dd/MM/yyyy||yyyy"
      }
    }
  }
}

不为空搜索(exists query)

查询指定字段值不为空的文档。相当SQL中的column is not null

GET /book/_search
{
  "query": {
    "exists": {
      "field": "price"
    }
  }
}

词项前缀搜索(prefix query)

GET /book/_search
{
  "query": {
    "prefix": {
      "name": "so"
    }
  }
}

通配符搜索(wildcard query)

GET /book/_search
{
  "query": {
    "wildcard": {
      "name": "so*r"
    }
  }
}

#name需要设置多个参数写法
GET /book/_search
{
  "query": {
    "wildcard": {
      "name": {
        "value": "lu*",
        "boost": 2
      }
    }
  }
}

正则搜索(regexp query)

regexp允许使用正则表达式进行term查询.注意regexp如果使用不正确,会给服务器带来很严重的性能压力。比如.*开头的查询,将会匹配所有的倒排索引中的关键字,这几乎相当于全表扫描,会很慢。因此如果可以的话,最好在使用正则前,加上匹配的前缀。

GET /book/_search
{
  "query": {
    "regexp": {
      "name": "s.*"
    }
  }
}

模糊搜索(fuzzy query)

GET /book/_search
{
  "query": {
    "fuzzy": {
      "name": "so"
    }
  }
}
GET /book/_search
{
  "query": {
    "fuzzy": {
      "name": {
        "value": "so",
        "boost": 1,
        "fuzziness": 2
      }
    }
  }
}

fuzziness:允许模糊的长度.只能是0,1,2

ids搜索(id集合查询)

GET /book/_search
{
  "query": {
    "ids": {
      "values": [
        "1",
        "3"
      ]
    }
  }
}

复合搜索(compound query)

constant_score query

用来包装另一个查询,将查询匹配的文档的评分设为一个常值

#得分不一样
GET /book/_search
{
  "query": {
    "term": {
      "description": "solr"
    }
  }
}

#得分一样
GET /book/_search
{
  "query": {
    "constant_score": {
      "filter": {
        "term": {
          "description": "solr"
        }
      },
      "boost": 1.2
    }
  }
}

布尔搜索(bool query)

查询用bool操作来组合多个查询字句为一个查询。

  • 可用的关键字:
    • must:必须满足
    • filter:必须满足,但执行的是filter上下文,不参与、不影响评分
    • should:或
    • must_not:必须不满足,在filter上下文中执行,不参与、不影响评分
  POST /book/_search
{
  "query": {
    "bool": {
      "must": {
        "match": {
          "description": "java"
        }
      },
      "filter": {
        "term": {
          "name": "solr"
        }
      },
      "must_not": {
        "range": {
          "price": {
            "gte": 200,
            "lte": 300
          }
        }
      },
      "minimum_should_match": 0,
      "boost": 1
    }
  }
}

minimum_should_match代表了最小匹配精度,如果设置minimum_should_match=1,那么should 语句中至少需要有一个条件满足。

排序

相关性评分排序

默认情况下,返回的结果是按照相关性进行排序的——最相关的文档排在最前。为了按照相关性来排序,需要将相关性表示为一个数值。在Elasticsearch中,相关性得分由一个浮点数进行表示,并在搜索结果中通过 _score参数返回,默认排序是 _score降序,按照相关性评分升序排序如下

#默认降序
POST /book/_search
{
  "query": {
    "match": {
      "description": "solr"
    }
  }
}
#设置升序
POST /book/_search
{
  "query": {
    "match": {
      "description": "solr"
    }
  },
  "sort": [
    {
      "_score": {
        "order": "asc"
      }
    }
  ]
}

字段值排序

POST /book/_search
{
  "query": {
    "match_all": {}
  },
  "sort": [
    {
      "price": {
        "order": "desc"
      }
    }
  ]
}

多级排序

假定我们想要结合使用price和_score(得分)进行查询,并且匹配的结果首先按照价格排序,然后按照相关性得分排序:

POST /book/_search
{
  "query": {
    "match_all": {}
  },
  "sort": [
    {
      "price": {
        "order": "desc"
      }
    },
    {
      "timestamp": {
        "order": "desc"
      }
    }
  ]
}

分页

Elasticsearch中实现分页的语法非常简单类似mysql:

POST /book/_search
{
  "query": {
    "match_all": {}
  },
  "size": 2,
  "from": 0
}
  • size:每页显示多少条
  • from:当前页起始索引, int start = (pageNum - 1) * size

高亮

Elasticsearch中实现高亮的语法比较简单:

#查询name并设置高亮字段name
POST /book/_search
{
  "query": {
    "match": {
      "name": "elasticsearch"
    }
  },
  "highlight": {
    "pre_tags": "<font color='pink'>",
    "post_tags": "</font>",
    "fields": [
      {
        "name": {}
      }
    ]
  }
}

image.png

#查询name并设置高亮字段name,description
POST /book/_search
{
  "query": {
    "match": {
      "name": "elasticsearch"
    }
  },
  "highlight": {
    "pre_tags": "<font color='pink'>",
    "post_tags": "</font>",
    "fields": [
      {
        "name": {}
      },
      {
        "description": {}
      }
    ]
  }
}

只查name就算设置其他字段高亮 其他字段也不会高亮 image.png

#全文索引
POST /book/_search
{
  "query": {
    "query_string": {
      "query": "elasticsearch"
    }
  },
  "highlight": {
    "pre_tags": "<font color='pink'>",
    "post_tags": "</font>",
    "fields": [
      {
        "name": {
          "pre_tags": "<font color='black'>",
          "post_tags": "</font>"
        }
      },
      {
        "description": {}
      }
    ]
  }
}
#置顶字段
POST /book/_search
{
  "query": {
    "bool": {
      "should": [{
        "match":{
          "name":"ElasticSearch"
        }
      },{
         "match":{
          "description":"elasticsearch"
        }
      }]
    }
  },
  "highlight": {
    "pre_tags": "<font color='pink'>",
    "post_tags": "</font>",
    "fields": [
      {
        "name": {
          "pre_tags": "<font color='black'>",
          "post_tags": "</font>"
        }
      },
      {
        "description": {}
      }
    ]
  }
}

结果如下,description高亮也正常 image.png 在使用match查询的同时,加上一个highlight属性:

  • pre_tags:前置标签
  • post_tags:后置标签
  • fields:需要高亮的字段
    • name:这里声明title字段需要高亮,后面可以为这个字段设置特有配置,也可以空

文档批量操作(bulk 和 mget)

mget 批量查询

单条查询GET /test_index/_doc/1,如果查询多个id的文档一条一条查询,网络开销太大。也可以使用ids搜索。

#mget批量查询
GET /_mget
{
  "docs": [
    {
      "_index": "book",
      "_id": 1
    },
    {
      "_index": "book",
      "_id": 2
    }
  ]
}

bulk 批量增删改

Bulk 操作解释将文档的增删改查一些列操作,通过一次请求全都做完。减少网络传输次数。

POST /_bulk
{"action": {"metadata"}}
{"data"}

实例:

POST /_bulk
{ "delete": { "_index": "book", "_id": "1" }}
{ "create": { "_index": "book", "_id": "5" }}
{ "name": "test","price":100.99 }
{ "update": { "_index": "book", "_id": "2"} }
{ "doc" : {"name" : "test_bulk"} }

功能:

  • delete:删除一个文档,只要1个json串就可以了删除的批量操作不需要请求体
  • create:相当于强制创建 PUT /index/type/id/_create
  • index:普通的put操作,可以是创建文档,也可以是全量替换文档
  • update:执行的是局部更新partial update操作
  • 格式:每个json不能换行。相邻json必须换行。
  • 隔离:每个操作互不影响。操作失败的行会返回其失败信息。 实际用法:bulk请求一次不要太大,否则一下积压到内存中,性能会下降。所以一次请求几千个操作、大小在几M正好。bulk会将要处理的数据载入内存中,所以数据量是有限的,最佳的数据两不是一个确定的数据,它取决于你的硬件,你的文档大小以及复杂性,你的索引以及搜索的负载。一般建议是1000-5000个文档,大小建议是5-15MB,默认不能超过100M,可以在es的配置文件(ES的config下的elasticsearch.yml)中配置。http.max_content_length: 10mb

Filter DSL

Elasticsearch中的所有的查询都会触发相关度得分的计算。对于那些我们不需要相关度得分的场景下,Elasticsearch以过滤器的形式提供了另一种查询功能,过滤器在概念上类似于查询,但是它们有非常快的执行速度,执行速度快主要有以下两个原因:

  • 过滤器不会计算相关度的得分,所以它们在计算上更快一些。
  • 过滤器可以被缓存到内存中,这使得在重复的搜索查询上,其要比相应的查询快出许多。 为了理解过滤器,可以将一个查询(像是match_all,match,bool等)和一个过滤器结合起来。我们以范围过滤器为例,它允许我们通过一个区间的值来过滤文档。这通常被用在数字和日期的过滤上。下面这个例子使用一个被过滤的查询,其返回price值是在200到1000之间(闭区间)的书。
POST /book/_search
{
  "query": {
    "bool": {
      "must": {
        "match_all": {}
      },
      "filter": {
        "range": {
          "price": {
            "gte": 200,
            "lte": 1000
          }
        }
      }
    }
  }
}

分解上面的例子,被过滤的查询包含一个match_all查询(查询部分)和一个过滤器(filter部分)。我们可以在查询部分中放入其他查询,在filter部分放入其它过滤器。在上面的应用场景中,由于所有的在这个范围之内的文档都是平等的(或者说相关度都是一样的),没有一个文档比另一个文档更相关,所以这个时候使用范围过滤器就非常合适了。通常情况下,要决定是使用过滤器还是使用查询,你就需要 问自己是否需要相关度得分。如果相关度是不重要的,使用过滤器,否则使用查询。查询和过滤器在概 念上类似于SELECT WHERE语句。

定位非法搜索及原因

在开发的时候,我们可能会写到上百行的查询语句,如果出错的话,找起来很麻烦,Elasticsearch提供了帮助开发人员定位不合法的查询的api _validate

GET /book/_validate/query?explain
{
  "query": {
    "match1": {
      "name": "test"
    }
  }
}

聚合分析

聚合介绍

聚合分析是数据库中重要的功能特性,完成对一个查询的数据集中数据的聚合计算。如:找出某字段(或计算表达式的结果)的最大值、最小值,计算和、平均值等。Elasticsearch作为搜索引擎兼数据库,同样提供了强大的聚合分析能力。

对一个数据集求最大、最小、和、平均值等指标的聚合,在ES中称为指标聚合metric而关系型数据库中除了有聚合函数外,还可以对查询出的数据进行分组group by,再在组上进行指标聚合。在ES中group by称为分桶,桶聚合bucketing

Elasticsearch聚合分析语法在查询请求体中以aggregations节点按如下语法定义聚合分析:

"aggregations" : {
  "<aggregation_name>" : { <!--聚合的名字 -->
    "<aggregation_type>" : { <!--聚合的类型 -->
      <aggregation_body> <!--聚合体:对哪些字段进行聚合 -->
    }
    [,"meta" : { [<meta_data_body>] } ]? <!--元 -->
    [,"aggregations" : { [<sub_aggregation>]+ } ]? <!--在聚合里面在定义子聚合 -->
  }
  [,"<aggregation_name_2>" : { ... } ]*<!--聚合的名字 -->
}

说明:aggregations 也可简写为 aggs

指标聚合

  • max min sum avg
#找出价格最贵的书
POST /book/_search
{
  "size": 0,
  "aggs": {
    "max_price": {
      "max": {
        "field": "price"
      }
    }
  }
}
  • 文档计数count
#价格大于100的书的个数
POST /book/_count
{
  "query": {
    "range": {
      "price": {
        "gt": 100
      }
    }
  }
}
  • value_count 统计某字段有值的文档数
#有价格的文档数
POST /book/_search?size=0
{
  "aggs": {
    "price_count": {
      "value_count": {
        "field": "price"
      }
    }
  }
}
  • cardinality值去重计数 基数
POST /book/_search?size=0
{
  "aggs": {
    "name_count": {
      "cardinality": {
        "field": "name"
      }
    },
    "price_count": {
      "cardinality": {
        "field": "price"
      }
    }
  }
}
  • stats 统计 count max min avg sum 5个值
#对价格字段聚合
POST /book/_search?size=0
{
  "aggs": {
    "price_stats": {
      "stats": {
        "field": "price"
      }
    }
  }
}
  • Extended stats 高级统计,比stats多4个统计结果:平方和、方差、标准差、平均值加/减两个标准差的区间
POST /book/_search?size=0
{
  "aggs": {
    "price_stats": {
      "extended_stats": {
        "field": "price"
      }
    }
  }
}
  • Percentiles 占比百分位对应的值统计
POST /book/_search?size=0
{
  "aggs": {
    "price_percents": {
      "percentiles": {
        "field": "price"
      }
    }
  }
}

#指定百分位
POST /book/_search?size=0
{
  "aggs": {
    "price_percents": {
      "percentiles": {
        "field": "price",
        "percents": [
          75,
          99,
          99.9
        ]
      }
    }
  }
}

百分位数计算

  • Percentiles rank 统计值小于等于指定值的文档占比 统计price小于100和200的文档的占比
POST /book/_search?size=0
{
  "aggs": {
    "gge_perc_rank": {
      "percentile_ranks": {
        "field": "price",
        "values": [
          100,
          200
        ]
      }
    }
  }
}

桶聚合

Bucket aggregations | Elasticsearch Guide [8.1] | Elastic

它执行的是对文档分组的操作(与sql中的group by类似),把满足相关特性的文档分到一个桶里,即桶分,输出结果往往是一个个包含多个文档的桶(一个桶就是一个group)

  • bucket:一个数据分组
  • metric:对一个数据分组执行的统计
#范围分组 聚合之后子聚合
POST /book/_search
{
  "size": 0,
  "aggs": {
    "group_by_price": {
      "range": {
        "field": "price",
        "ranges": [
          {
            "from": 0,
            "to": 200
          },
          {
            "from": 200,
            "to": 400
          },
          {
            "from": 400,
            "to": 1000
          }
        ]
      },
      "aggs": {
        #aggs名称
        "average_price": {
          #聚合操作
          "avg": {
            "field": "price"
          }
        },
        "max_price": {
          "max": {
            "field": "price"
          }
        }
      }
    }
  }
}

实现having效果

POST /book/_search
{
  "size": 0,
  "aggs": {
    "group_by_price": {
      "range": {
        "field": "price",
        "ranges": [
          {
            "from": 0,
            "to": 200
          },
          {
            "from": 200,
            "to": 400
          },
          {
            "from": 400,
            "to": 1000
          }
        ]
      },
      "aggs": {
        "average_price": {
          "avg": {
            "field": "price"
          }
        },
        "max_price": {
          "max": {
            "field": "price"
          }
        },
        "having": {
          "bucket_selector": {
            "buckets_path": {
              #aggs中定义的name
              "avg_price": "average_price",
              "max_price": "max_price"
            },
            "script": {
              "source": "params.avg_price >= 200 && params.max_price >= 300"
            }
          }
        }
      }
    }
  }
}

智能搜索建议

现代的搜索引擎,一般会具备"Suggest As You Type"功能,即在用户输入搜索的过程中,进行自动补全或者纠错。 通过协助用户输入更精准的关键词,提高后续全文搜索阶段文档匹配的程度。例如在百度上输入部分关键词,甚至输入拼写错误的关键词时,它依然能够提示出用户想要输入的内容: image.png 如果自己亲手去试一下,可以看到百度在用户刚开始输入的时候是自动补全的,而当输入到一定长度,如果因为单词拼写错误无法补全,就开始尝试提示相似的词。

那么类似的功能在Elasticsearch里如何实现呢?答案就在Suggesters API。Suggesters基本的运作原理是将输入的文本分解为token,然后在索引的字典里查找相似的term并返回。根据使用场景的不同, Elasticsearch里设计了4种类别的Suggester,分别是:

Term Suggester

准备一个叫做blogs的索引,配置一个text字段

#创建索引
PUT /blogs/
{
  "mappings": {
    "properties": {
      "body": {
        "type": "text"
      }
    }
  }
}

# refresh即时刷新副本分片
POST _bulk/?refresh=true
{ "index" : { "_index" : "blogs" } }
{ "body": "Lucene is cool"}
{ "index" : { "_index" : "blogs" } }
{ "body": "Elasticsearch builds on top of lucene"}
{ "index" : { "_index" : "blogs" } }
{ "body": "Elasticsearch rocks"}
{ "index" : { "_index" : "blogs" } }
{ "body": "Elastic is the company behind ELK stack"}
{ "index" : { "_index" : "blogs" } }
{ "body": "elk rocks"}
{ "index" : { "_index" : "blogs"} }
{ "body": "elasticsearch is rock solid"}

分析一下刚刚输入的body有哪些进入了词典

POST _analyze
{
  "text": [
    "Lucene is cool",
    "Elasticsearch builds on top of lucene",
    "Elasticsearch rocks",
    "Elastic is the company behind ELK stack",
    "elk rocks",
    "elasticsearch is rock solid"
  ]
}

这些分出来的token都会成为词典里一个term,注意有些token会出现多次,因此在倒排索引里记录的词频会比较高,同时记录的还有这些token在原文档里的偏移量和相对位置信息。

执行一次suggester搜索看看效果:

#missing:搜不到才会给推荐结果
POST /blogs/_search
{
  "suggest": {
    "my-suggestion": {
      "text": "lucne rock",
      "term": {
        "suggest_mode": "missing",
        "field": "body"
      }
    }
  }
}

可以看到lucene写错了就给了一个正确的建议,rock没有建议因为词典里有。 image.png

  • missing 查不到才会给建议
  • popular 会给词频最高的建议 - always 7.x之后和popular无区别

两个term的相似性是如何判断的? ES使用了一种叫做Levenstein edit distance的算法,其核心思想就是一个词改动多少个字符就可以和另外一个词一致。动的越少,相似度越高。

Phrase Suggester

Phrase suggester在Term suggester的基础上,会考量多个term之间的关系,比如是否同时出现在索 引的原文里,相邻程度,以及词频等等。

执行一次suggester搜索看看效果:

#可设置高亮
POST /blogs/_search
{
  "suggest": {
    "my-suggestion": {
      "text": "lucne and elasticsear rock",
      "phrase": {
        "field": "body",
        "highlight": {
          "pre_tag": "<em>",
          "post_tag": "</em>"
        }
      }
    }
  }
}

执行可以看到有三个推荐。

  1. 修改了lucene和elasticsearch 打分0.004993905,同时出现在原文里,可信度最高。
  2. 修改了elasticsearch 打分0.0033391973
  3. 修改了lucene 打分0.0029183894 image.png

Completion Suggester

它主要针对的应用场景就是"Auto Completion"。 此场景下用户每输入一个字符的时候,就需要即时发送一次查询请求到后端查找匹配项,在用户输入速度较高的情况下对后端响应速度要求比较苛刻。因此实现上它和前面两个Suggester采用了不同的数据结构,索引并非通过倒排来完成,而是将analyze过的数据编码成FST和索引一起存放。对于一个open状态的索引,FST会被ES整个装载到内存里的,进行前缀查找速度极快。但是FST只能用于前缀查找,这也是Completion Suggester的局限所在。

为了使用Completion Suggester,字段的类型需要专门定义如下:

#type比较特殊,会使用fst
PUT /blogs_completion/
{
  "mappings": {
    "properties": {
      "body": {
        "type": "completion"
      }
    }
  }
}

#测试数据
POST _bulk/?refresh=true
{ "index" : { "_index" : "blogs_completion" } }
{ "body": "Lucene is cool"}
{ "index" : { "_index" : "blogs_completion" } }
{ "body": "Elasticsearch builds on top of lucene"}
{ "index" : { "_index" : "blogs_completion"} }
{ "body": "Elasticsearch rocks"}
{ "index" : { "_index" : "blogs_completion" } }
{ "body": "Elastic is the company behind ELK stack"}
{ "index" : { "_index" : "blogs_completion" } }
{ "body": "the elk stack rocks"}
{ "index" : { "_index" : "blogs_completion"} }
{ "body": "elasticsearch is rock solid"}

执行下面的查询:

POST /blogs_completion/_search?pretty
{
  "size": 0,
  "suggest": {
    "blog-suggest": {
      "prefix": "elastic i",
      "completion": {
        "field": "body"
      }
    }
  }
}

结果可以看出来出现了推荐 image.png 不同的分词器 分词转换不一样,最终也会影响匹配效果

mapping参数:
"preserve_separators": false, 这个设置为false,将忽略空格之类的分隔符
"preserve_position_increments": true,如果建议词第一个词是停用词,并且我们使用了过滤停用词的分析器,需要将此设置false。

实际应用开发过程中,需要根据数据特性和业务需要,灵活搭配analyzer和mapping参数,反复调试才可能获得理想的补全效果。

Context Suggester

  • Completion Suggester的扩展
  • 可以在搜索中加入更多的上下文信息,然后根据不同的上下文信息,对相同的输入,比如"star",提供不同的建议值,比如:
    • 咖啡相关:starbucks
    • 电影相关:star wars