20210404 3. 玩转 Elasticsearch 之高级应用 - 拉勾教育
玩转 Elasticsearch 之高级应用
映射高级
地理坐标点数据类型
-
地理坐标点
地理坐标点是指地球表面可以用经纬度描述的一个点。 地理坐标点可以用来计算两个坐标间的距离,还可以判断一个坐标是否在一个区域中。地理坐标点需要显式声明对应字段类型为
geo_point
:PUT /company-locations { "mappings": { "properties": { "name": { "type": "text" }, "location": { "type": "geo_point" } } } }
-
经纬度坐标格式
如上例,
location
字段被声明为geo_point
后,我们就可以索引包含了经纬度信息的文档了。 经纬度信息的形式可以是字符串、数组或者对象// 字符串形式 PUT /company-locations/_doc/1 { "name": "NetEase", "location": "40.715,74.011" }
// 对象形式 PUT /company-locations/_doc/2 { "name": "Sina", "location": { "lat": 40.722, "lon": 73.989 } }
// 数组形式 PUT /company-locations/_doc/3 { "name": "Baidu", "location": [ 73.983, 40.719 ] }
注意:
- 字符串形式以半角逗号分割,如
lat,lon
- 对象形式显式命名为
lat
和lon
- 数组形式表示为
[lon,lat]
- 字符串形式以半角逗号分割,如
通过地理坐标点过滤:有四种地理坐标点相关的过滤器,可以用来选中或者排除文档
过滤器 | 作用 |
---|---|
geo_bounding_box |
找出落在指定矩形框中的点 |
geo_distance |
找出与指定位置在给定距离内的点 |
geo_distance_range |
找出与指定点距离在给定最小距离和最大距离之间的点 |
geo_polygon |
找出落在多边形中的点。 这个过滤器使用代价很大 。当你觉得自己需要使用它,最好先看看 geo-shapes |
geo_bounding_box
查询:这是目前为止最有效的地理坐标过滤器了,因为它计算起来非常简单。 你指定一个矩形的顶部 ,底部 , 左边界和右边界,然后过滤器只需判断坐标的经度是否在左右边界之间,纬度是否在上下边界之间
GET /company-locations/_search
{
"query": {
"bool": {
"must": {
"match_all": {}
},
"filter": {
"geo_bounding_box": {
"location": {
"top_left": {
"lat": 40.73,
"lon": 71.12
},
"bottom_right": {
"lat": 40.01,
"lon": 74.1
}
}
}
}
}
}
}
location
这些坐标还可以用 bottom_left
和 top_right
来表示
geo_distance
:过滤仅包含与地理位置相距特定距离内的匹配的文档。
GET /company-locations/_search
{
"query": {
"bool": {
"must": {
"match_all": {}
},
"filter": {
"geo_distance": {
"distance": "200km",
"location": {
"lat": 40,
"lon": 70
}
}
}
}
}
}
动态映射
Elasticsearch 在遇到文档中以前未遇到的字段,可以使用 dynamic mapping (动态映射机制) 来确定字段的数据类型并自动把新的字段添加到类型映射。
Elasticsearch 的动态映射机制可以进行开关控制,通过设置 mappings 的 dynamic
属性, dynamic
有如下设置项:
true
:遇到陌生字段就执行 dynamic mapping 处理机制false
:遇到陌生字段就忽略strict
:遇到陌生字段就报错
// 设置为报错
PUT /user
{
"settings": {
"number_of_shards": 3,
"number_of_replicas": 0
},
"mappings": {
"dynamic": "strict",
"properties": {
"name": {
"type": "text"
},
"address": {
"type": "object",
"dynamic": true
}
}
}
}
// 插入以下文档,将会报错
// user索引层设置dynamic是strict,在user层内设置age将报错
// 在mappings层设置dynamic是ture,将动态映射生成字段
PUT /user/_doc/1
{
"name": "lisi",
"age": "20",
"address": {
"province": "beijing",
"city": "beijing"
}
}
PUT /user
{
"settings": {
"number_of_shards": 3,
"number_of_replicas": 0
},
"mappings": {
"dynamic": true,
"properties": {
"name": {
"type": "text"
},
"address": {
"type": "object",
"dynamic": true
}
}
}
}
自定义动态映射
如果你想在运行时增加新的字段,你可能会启用动态映射。 然而,有时候,动态映射规则可能不太智能。幸运的是,我们可以通过设置去自定义这些规则,以便更好的适用于你的数据。
日期检测
当 Elasticsearch 遇到一个新的字符串字段时,它会检测这个字段是否包含一个可识别的日期,比如 2014-01-01
,如果它像日期,这个字段就会被作为 date
类型添加。否则,它会被作为 string
类型添加。
有些时候这个行为可能导致一些问题。想象下,你有这样的一个文档:{ "note": "2014-01-01" }
假设这是第一次识别 note 字段,它会被添加为 date
字段。但是如果下一个文档像这样:{ "note": "Logged out" }
这显然不是一个日期,但为时已晚。这个字段已经是一个日期类型,这个 不合法的日期 将会造成一个
异常。
日期检测可以通过在根对象上设置 date_detection
为 false
来关闭
PUT /my_index/_doc/1
{
"note": "2014-01-01"
}
GET /my_index/_doc/1
GET /my_index/_mapping
PUT /my_index/_doc/1
{
"note": "Logged out"
}
// 报错
PUT /my_index
{
"mappings": {
"date_detection": false
}
}
使用这个映射,字符串将始终作为 string
类型。如果需要一个 date
字段,必须手动添加。 Elasticsearch 判断字符串为日期的规则可以通过 dynamic_date_formats
来设置。
PUT /my_index
{
"mappings": {
"dynamic_date_formats": "MM/dd/yyyy"
}
}
PUT /my_index/_doc/1
{
"note": "2014-01-01"
}
PUT /my_index/_doc/1
{
"note": "01/01/2014"
}
dynamic_templates
使用 dynamic_templates
可以完全控制新生成字段的映射,甚至可以通过字段名称或数据类型来应用不同的映射。每个模板都有一个名称,你可以用来描述这个模板的用途,一个 mapping
来指定映射应该怎样使用,以及至少一个参数 (如 match
) 来定义这个模板适用于哪个字段。
模板按照顺序来检测;第一个匹配的模板会被启用。例如,我们给 string
类型字段定义两个模板:
es
:以_es
结尾的字段名需要使用spanish
分词器。en
:所有其他字段使用english
分词器。
我们将 es
模板放在第一位,因为它比匹配所有字符串字段的 en
模板更特殊:
PUT /my_index2
{
"mappings": {
"dynamic_templates": [
{
"es": {
"match": "*_es",
"match_mapping_type": "string",
"mapping": {
"type": "text",
"analyzer": "spanish"
}
}
},
{
"en": {
"match": "*",
"match_mapping_type": "string",
"mapping": {
"type": "text",
"analyzer": "english"
}
}
}
]
}
}
PUT /my_index2/_doc/1
{
"name_es": "testes",
"name": "es"
}
GET /my_index2/_mapping
match_mapping_type
允许你应用模板到特定类型的字段上,就像有标准动态映射规则检测的一样(例如 string
或 long
)
match
参数只匹配字段名称,path_match
参数匹配字段在对象上的完整路径,所以 address.*.name
将匹配这样的字段:
{
"address": {
"city": {
"name": "New York"
}
}
}
Query DSL
Elasticsearch Reference [7.12] » Query DSL
Elasticsearch 提供了基于 JSON 的完整查询 DSL ( Domain Specific Language 特定域的语言)来定义查询。将查询 DSL 视为查询的 AST (抽象语法树),它由两种子句组成:
-
叶子查询子句
在特定域中寻找特定的值,如
match
,term
或range
查询。 -
复合查询子句
包装其他叶子查询或复合查询,并用于以逻辑方式组合多个查询(例如
bool
或dis_max
查询),或更改其行为(例如constant_score
查询)。
我们在使用 Elasticsearch 的时候,避免不了使用 DSL 语句去查询,就像使用关系型数据库的时候要学会 SQL 语法一样。如果我们学习好了 DSL 语法的使用,那么在日后使用和使用 Java Client 调用时候也会变得非常简单。
POST /索引库名/_search
{
"query": {
"查询类型": {
"查询条件": "查询条件值"
}
}
}
这里的 query
代表一个查询对象,里面可以有不同的查询属性:
-
查询类型:
例如:
match_all
,match
,term
,range
等等 -
查询条件:查询条件会根据类型的不同,写法也有差异,后面详细讲解
查询所有( match_all query )
POST /lagou-company-index/_search
{
"query": {
"match_all": {}
}
}
POST /my_index2/_search
{
"query": {
"match_all": {}
}
}
query
:代表查询对象match_all
:代表查询所有
返回结果字段说明:
took
:查询花费时间,单位是毫秒time_out
:是否超时_shards
:分片信息hits
:搜索结果总览对象total
:搜索到的总条数max_score
:所有结果中文档得分的最高分hits
:搜索结果的文档对象数组,每个元素是一条搜索到的文档信息_index
:索引库_type
:文档类型_id
:文档 id_score
:文档得分_source
:文档的源数据
全文搜索( full-text query )
全文搜索能够搜索已分析的文本字段,如电子邮件正文,商品描述等。使用索引期间应用于字段的同一分析器处理查询字符串。全文搜索的分类很多,几个典型的如下:
匹配搜索( match query )
全文查询的标准查询,它可以对一个字段进行模糊、短语查询。 match queries 接收 text
/ numerics
/ dates
,对它们进行分词分析,再组织成一个 boolean
查询。可通过 operator
指定 bool
组合操作(or
、and
,默认是 or
)。
现在,索引库中有 2 部手机, 1 台电视:
PUT /lagou-property
{
"settings": {},
"mappings": {
"properties": {
"title": {
"type": "text",
"analyzer": "ik_max_word"
},
"images": {
"type": "keyword"
},
"price": {
"type": "float"
}
}
}
}
POST /lagou-property/_doc/
{
"title": "小米电视4A",
"images": "http://image.lagou.com/12479122.jpg",
"price": 4288
}
POST /lagou-property/_doc/
{
"title": "小米手机",
"images": "http://image.lagou.com/12479622.jpg",
"price": 2699
}
POST /lagou-property/_doc/
{
"title": "华为手机",
"images": "http://image.lagou.com/12479922.jpg",
"price": 5699
}
or 关系
match
类型查询,会把查询条件进行分词,然后进行查询,多个词条之间是 or
的关系
POST /lagou-property/_search
{
"query": {
"match": {
"title": "小米电视4A"
}
}
}
在上面的案例中,不仅会查询到电视,而且与小米相关的都会查询到,多个词之间是 or
的关系。
and 关系
某些情况下,我们需要更精确查找,我们希望这个关系变成 and
,可以这样做:
POST /lagou-property/_search
{
"query": {
"match": {
"title": {
"query": "小米电视4A",
"operator": "and"
}
}
}
}
本例中,只有同时包含 小米
和 电视
的词条才会被搜索到。
短语搜索( match phrase query )
match_phrase
查询用来对一个字段进行短语查询,可以指定 analyzer
、slop
移动因子
GET /lagou-property/_search
{
"query": {
"match_phrase": {
"title": "小米电视"
}
}
}
// 结果:小米电视4A
GET /lagou-property/_search
{
"query": {
"match_phrase": {
"title": "小米 4A"
}
}
}
// 结果:无
GET /lagou-property/_search
{
"query": {
"match_phrase": {
"title": {
"query": "小米 4A",
"slop": 1
}
}
}
}
// 结果:小米电视4A
query_string 查询
Query String Query 提供了无需指定某字段而对文档全文进行匹配查询的一个高级查询,同时可以指定在哪些字段上进行匹配。
# 默认 和 指定字段
GET /lagou-property/_search
{
"query": {
"query_string": {
"query": "2699"
}
}
}
// 结果:小米手机
GET /lagou-property/_search
{
"query": {
"query_string": {
"query": "2699",
"default_field": "title"
}
}
}
// 结果:无
#逻辑查询
GET /lagou-property/_search
{
"query": {
"query_string": {
"query": "手机 OR 小米",
"default_field": "title"
}
}
}
// 结果:小米手机,华为手机,小米电视4A
GET /lagou-property/_search
{
"query": {
"query_string": {
"query": "手机 AND 小米",
"default_field": "title"
}
}
}
// 结果:小米手机
#模糊查询
GET /lagou-property/_search
{
"query": {
"query_string": {
"query": "大米~1",
"default_field": "title"
}
}
}
// 结果:小米手机,小米电视4A
#多字段支持
GET /lagou-property/_search
{
"query": {
"query_string": {
"query": "2699",
"fields": [
"title",
"price"
]
}
}
}
// 结果:小米手机
多字段匹配搜索( multi match query )
如果你需要在多个字段上进行文本搜索,可用 multi_match
。 multi_match
在 match
的基础上支持对多个字段进行文本查询
GET /lagou-property/_search
{
"query": {
"multi_match": {
"query": "2699",
"fields": [
"title",
"price"
]
}
}
}
还可以使用 *
匹配多个字段:
GET /lagou-property/_search
{
"query": {
"multi_match": {
"query": "http://image.lagou.com/12479622.jpg",
"fields": [
"title",
"ima*"
]
}
}
}
词条级搜索( term-level queries )
可以使用 term-level queries 根据结构化数据中的精确值查找文档。结构化数据的值包括日期范围、 IP 地址、价格或产品 ID 。
与全文查询不同, 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": "2020-08-21 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 or native clients for Java and other languages.",
"price": 320.45,
"timestamp": "2020-07-21 17: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": "2020-08-22 19:18:35"
}
PUT /book/_doc/4
{
"name": "ElasticSearch",
"description": "Elasticsearch是一个基于Lucene的搜索服务器。它提供了一个分布式多用户能力的全文搜索引擎,基于RESTful web接口。Elasticsearch是用Java语言开发的,并作为Apache许可条款下的开放源码发布,是一种流行的企业级搜索引擎。Elasticsearch用于云计算中,能够达到实时搜索,稳定,可靠,快速,安装使用方便。官方客户端在Java、.NET(C#)、PHP、Python、Apache Groovy、Ruby和许多其他语言中都是可用的。根据DB-Engines的排名显示,Elasticsearch是最受欢迎的企业搜索引擎,其次是Apache Solr,也是基于Lucene。",
"price": 999.99,
"timestamp": "2020-08-15 10:11:35"
}
词条搜索( term query )
term
查询用于查询指定字段包含某个词项的文档
POST /book/_search
{
"query": {
"term": {
"name": "solr"
}
}
}
词条集合搜索( terms query )
terms
查询用于查询指定字段包含某些词项的文档
GET /book/_search
{
"query": {
"terms": {
"name": [
"solr",
"elasticsearch"
]
}
}
}
因为 IK 分词器分词完成后,全部小写,所以这里的查询关键词必须全部小写
POST _analyze
{
"analyzer": "ik_max_word",
"text": "ElasticSearch"
}
{
"tokens" : [
{
"token" : "elasticsearch",
"start_offset" : 0,
"end_offset" : 13,
"type" : "ENGLISH",
"position" : 0
}
]
}
// 全文搜索和词条搜索的区别,全文搜索会使用相同分词器对搜索条件进行分词,词条搜索不分析搜索词,需要完全匹配
POST /book/_search
{
"query": {
"match": {
"name": "elasticsearch"
}
}
}
POST /book/_search
{
"query": {
"term": {
"name": "Elasticsearch"
}
}
}
范围搜索( range query )
gte
:大于等于gt
:大于lte
:小于等于lt
:小于boost
:查询权重 ,搜索条件的权重,boost
,可以将某个搜索条件的权重加大,此时当匹配这个搜索条件和匹配另一个搜索条件的 document ,计算 relevance score 时,匹配权重更大的搜索条件的 document , relevance score 会更高,当然也就会优先被返回回来
GET /book/_search
{
"query": {
"range": {
"price": {
"gte": 10,
"lte": 200,
"boost": 2
}
}
}
}
GET /book/_search
{
"query": {
"range": {
"timestamp": {
"gte": "now-500d/d",
"lt": "now/d"
}
}
}
}
GET book/_search
{
"query": {
"range": {
"timestamp": {
"gte": "18/08/2020",
"lte": "2021",
"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"
}
}
}
GET /book/_search
{
"query": {
"wildcard": {
"name": {
"value": "lu*",
"boost": 2
}
}
}
}
正则搜索( regexp query )
regexp 允许使用正则表达式进行 term 查询。注意 regexp 如果使用不正确,会给服务器带来很严重的性能压力。比如:*
开头的查询,将会匹配所有的倒排索引中的关键字,这几乎相当于全表扫描,会很慢。因此如果可以的话,最好在使用正则前,加上匹配的前缀。
GET /book/_search
{
"query": {
"regexp": {
"name": "s.*"
}
}
}
GET /book/_search
{
"query": {
"regexp": {
"name": {
"value": "s.*",
"boost": 1.2
}
}
}
}
模糊搜索( fuzzy query )
GET /book/_search
{
"query": {
"fuzzy": {
"name": "so"
}
}
}
// 结果:无
GET /book/_search
{
"query": {
"fuzzy": {
"name": {
"value": "so",
"boost": 1,
"fuzziness": 2
}
}
}
}
GET /book/_search
{
"query": {
"fuzzy": {
"name": {
"value": "sorl",
"boost": 1,
"fuzziness": 2
}
}
}
}
ids 搜索( id 集合查询)
GET /book/_search
{
"query": {
"ids": {
"type": "_doc",
"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 查询用 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": 1,
"lte": 300
}
}
},
"minimum_should_match": 1,
"boost": 1
}
}
}
minimum_should_match
代表了最小匹配精度,如果设置 minimum_should_match = 1
,那么 should
语句中至少需要有一个条件满足。例子中因为没有 should
条件,所以无法匹配到任何数据。
排序
相关性评分排序
默认情况下,返回的结果是按照 相关性 进行排序的——最相关的文档排在最前。
在本章的后面部分,我们会解释 相关性 意味着什么以及它是如何计算的, 不过让我们首先看看 sort
参数以及如
何使用它。
为了按照相关性来排序,需要将相关性表示为一个数值。在 Elasticsearch 中, 相关性得分 由一个浮点数进行表示,并在搜索结果中通过 _score
参数返回, 默认排序是 _score
降序,按照相关性评分升序排序如下
// 默认按照相关性 _score 降序排序
POST /book/_search
{
"query": {
"match": {
"description": "solr"
}
}
}
// 默认按照相关性 _score 升序排序
POST /book/_search
{
"query": {
"match": {
"description": "solr"
}
},
"sort": [
{
"_score": {
"order": "asc"
}
}
]
}
字段值排序
// 按照 price 降序排序
POST /book/_search
{
"query": {
"match_all": {}
},
"sort": [
{
"price": {
"order": "desc"
}
}
]
}
多级排序
假定我们想要结合使用 price
和 _score
(得分) 进行查询,并且匹配的结果首先按照价格排序,然后按照相关性得分排序:
// 先按照 price 降序,相同时按照 timestamp 降序
POST /book/_search
{
"query": {
"match_all": {}
},
"sort": [
{
"price": {
"order": "desc"
}
},
{
"timestamp": {
"order": "desc"
}
}
]
}
分页
POST /book/_search
{
"query": {
"match_all": {}
},
"size": 2,
"from": 0
}
// 先排序后分页
POST /book/_search
{
"query": {
"match_all": {}
},
"sort": [
{
"price": {
"order": "desc"
}
}
],
"size": 2,
"from": 2
}
size
:每页显示多少条from
:当前页起始索引,int start = (pageNum - 1) * size
高亮
返回值中会出现 highlight
字段
// name 被高亮标签包围
POST /book/_search
{
"query": {
"match": {
"name": "elasticsearch"
}
},
"highlight": {
"pre_tags": "<font color='pink'>",
"post_tags": "</font>",
"fields": [
{
"name": {}
}
]
}
}
// name 和 description 被高亮标签包围
POST /book/_search
{
"query": {
"match": {
"name": "elasticsearch"
}
},
"highlight": {
"pre_tags": "<font color='pink'>",
"post_tags": "</font>",
"fields": [
{
"name": {}
},
{
"description": {}
}
]
}
}
// 先过滤结果,name 和 description 被高亮标签包围
POST /book/_search
{
"query": {
"query_string": {
"query": "elasticsearch"
}
},
"highlight": {
"pre_tags": "<font color='pink'>",
"post_tags": "</font>",
"fields": [
{
"name": {}
},
{
"description": {}
}
]
}
}
pre_tags
:前置标签post_tags
:后置标签fields
:需要高亮的字段name
:这里声明 title 字段需要高亮,后面可以为这个字段设置特有配置,也可以空结果
文档批量操作(bulk 和 mget)
mget 批量查询
单条查询 GET /test_index/_doc/1
,如果查询多个 id 的文档一条一条查询,网络开销太大。
// 不同索引下批量查询
GET /_mget
{
"docs": [
{
"_index": "book",
"_id": 1
},
{
"_index": "book",
"_id": 2
}
]
}
// 同一索引下批量查询
GET /book/_mget
{
"docs": [
{
"_id": 2
},
{
"_id": 3
}
]
}
// _search 写法,同一索引下查询
POST /book/_search
{
"query": {
"ids": {
"values": [
"1",
"4"
]
}
}
}
bulk 批量增删改
Bulk 操作解释将文档的增删改查一些列操作,通过一次请求全都做完。减少网络传输次数。
POST /_bulk
{"action": {"metadata"}}
{"data"}
// 删除 1,创建 5,更新 2
POST /_bulk
{"delete":{"_index":"book","_id":"1"}}
{"create":{"_index":"book","_id":"5"}}
{"name":"test14","price":100.99}
{"update":{"_index":"book","_id":"2"}}
{"doc":{"name":"test"}}
参数说明:
delete
:删除一个文档,只要 1 个 JSON 串就可以了,删除的批量操作不需要请求体create
:相当于强制创建PUT /index/type/id/_create
index
:普通的 put 操作,可以是创建文档,也可以是全量替换文档update
:执行的是 局部更新 partial update 操作
注意:
- 格式:每个 JSON 不能换行。相邻 JSON 必须换行。
- 隔离:每个操作互不影响。操作失败的行会返回其失败信息。
- 每个 JSON 不能换行。相邻 JSON 必须换行。
bulk 会将要处理的数据载入内存中,所以数据量是有限的,最佳的数据量不是一个确定的数据,它取决
于你的硬件,你的文档大小以及复杂性,你的索引以及搜索的负载。
一般建议是 1000-5000 个文档,大小建议是 5-15MB ,默认不能超过 100M ,可以在 ES 的配置文件( config/elasticsearch.yml
)中配置:http.max_content_length: 10mb
Filter DSL
Elasticsearch 中的所有的查询都会触发相关度得分的计算。对于那些我们不需要相关度得分的场景下, Elasticsearch 以过滤器的形式提供了另一种查询功能,过滤器在概念上类似于查询,但是它们有非常快的执行速度,执行速度快主要有以下两个原因:
- 过滤器不会计算相关度的得分,所以它们在计算上更快一些。
- 过滤器可以被缓存到内存中,这使得在重复的搜索查询上,其要比相应的查询快出许多。
为了理解过滤器,可以将一个查询(像是 match_all
, match
, bool
等)和一个过滤器结合起来。我们以范围过滤器为例,它允许我们通过一个区间的值来过滤文档。这通常被用在数字和日期的过滤上。下面这个例子使用一个被过滤的查询,其返回 price 值是在 200 到 1000 之间(闭区间)的 book 。
# 已过期,不可用
POST /book/_search
{
"query": {
"filtered": {
"query": {
"match_all": {}
},
"filter": {
"range": {
"price": {
"gte": 200,
"lte": 1000
}
}
}
}
}
}
#5.0 之后的写法
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/_search?explain
{
"query": {
"match1": {
"name": "test"
}
}
}
// 使用 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
// 这里 size 控制是否返回 doc 内容
// 查询所有书中最贵的
POST /book/_search
{
"size": 0,
"aggs": {
"max_price": {
"max": {
"field": "price"
}
}
}
}
文档计数 count
// 统计price大于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": {
"_id_count": {
"cardinality": {
"field": "_id"
}
},
"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 多一些统计结果,例如: 平方和、方差、标准差、平均值加/减两个标准差的区间
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": [
55,
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
]
}
}
}
}
桶聚合
它执行的是对文档分组的操作(与 sql 中的 group by
类似),把满足相关特性的文档分到一个桶里,即桶分,输出结果往往是一个个包含多个文档的桶(一个桶就是一个 group )
bucket
:一个数据分组metric
:对一个数据分组执行的统计
// 统计在不同价格区间的 book 的平均价格
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"
}
}
}
}
}
}
值的个数统计
// 统计在不同价格区间的 book 的数量
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": {
"count_price": {
"value_count": {
"field": "price"
}
}
}
}
}
}
实现 having 效果
// 统计在不同价格区间的 book 的平均价格,book 的价格需要大于等于 200
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"
}
},
"having": {
"bucket_selector": {
"buckets_path": {
"avg_price": "average_price"
},
"script": {
"source": "params.avg_price >= 200 "
}
}
}
}
}
}
}
玩转 Elasticsearch 零停机索引重建
说明
Elasticsearch 是一个实时的分布式搜索引擎,为用户提供搜索服务,当我们决定存储某种数据时,在创建索引的时候需要数据结构完整确定下来,与此同时索引的设定和很多固定配置将不能改变。当需要改变数据结构时就需要重建索引,为此, Elasticsearch 团队提供了辅助工具帮助开发人员进行索引重建。零停机完成索引重建的三种方案。
方案一:外部数据导入方案
整体介绍
系统架构设计中,有关系型数据库用来存储数据, Elasticsearch 在系统架构里起到查询加速的作用,如果遇到索引重建的操作,待系统模块发布新版本后,可以从数据库将数据查询出来,重新灌到 Elasticsearch 即可。
方案二:基于 scroll + bulk + 索引别名 方案
整体介绍
利用 Elasticsearch 自带的一些工具完成索引的重建工作,当然在方案实际落地时,可能也会依赖客户端的一些功能,比如用 Java 客户端持续的做 scroll 查询、 bulk 命令的封装等。数据完全自给自足,不依赖其他数据源。
执行步骤
假设原索引名称是 book
,新的索引名称为 book_new
, Java 客户端使用别名 book_alias
连接 Elasticsearch ,该别名指向原索引 book
。
-
若 Java 客户端没有使用别名,需要给客户端分配一个:
PUT /book/_alias/book_alias
-
新建索引
book_new
,将mapping
信息,settings
信息等按新的要求全部定义好。 -
使用
scroll
API 将数据批量查询出来为了使用
scroll
,初始搜索请求应该在查询中指定scroll
参数,这可以告诉 Elasticsearch 需要保持搜索的上下文环境多久,1m
就是一分钟。GET /book/_search?scroll=1m { "query": { "match_all": {} }, "sort": [ "_doc" ], "size": 2 }
-
采用
bulk
API 将scoll
查出来的一批数据,批量写入新索引POST /_bulk { "index": { "_index": "book_new", "_id": "对应的id值" }} { 查询出来的数据值 }
-
反复执行修改后的步骤 3 和步骤 4 ,查询一批导入一批,以后可以借助 Java Client 或其他语言的 API 支持。
注意步骤 3 时需要指定上一次查询的 scroll_id
GET /_search/scroll { "scroll": "1m", "scroll_id": "FGluY2x1ZGVfY29udGV4dF91dWlkDXF1ZXJ5QW5kRmV0Y2gBFk9fVHF5X1N0UWdlbXl4UjJSQW83SHcAAAAAAADB1xZubmNTY0EzMlRqNlhfbmxmeVBlVThB" }
-
切换别名
book_alias
到新的索引book_new
上面,此时 Java 客户端仍然使用别名访问,也不需要修改任何代码,不需要停机。POST /_aliases { "actions": [ { "remove": { "index": "book", "alias": "book_alias" } }, { "add": { "index": "book_new", "alias": "book_alias" } } ] }
-
验证别名查询的是否为新索引的数据
方案特点
在数据传输上基本自给自足,不依赖于其他数据源, Java 客户端不需要停机等待数据迁移,网络传输占用带宽较小。只是 scroll
查询和 bulk
提交这部分,数据量大时需要依赖一些客户端工具。
补充一点
在 Java 客户端或其他客户端访问 Elasticsearch 集群时,使用别名是一个好习惯。
方案三:Reindex API方案
Elasticsearch v6.3.1 已经支持 Reindex API ,它对 scroll
、 bulk
做了一层封装,能够 对文档重建索引而不需要任何插件或外部工具。
最基础的命令:
POST _reindex
{
"source": {
"index": "book"
},
"dest": {
"index": "book_new"
}
}
GET /book/_search
{
"query": {
"match_all": {}
}
}
注意: 如果不手动创建新索引 book_new
的 mapping
信息,那么 Elasticsearch 将启动自动映射模板对数据进行类型映射,可能不是期望的类型。
version_type 属性
使用 reindex
API 也是创建快照后再执行迁移的,这样目标索引的数据可能会与原索引有差异, version_type
属性可以决定乐观锁并发处理的规则。
reindex
API 可以设置 version_type
属性,如下:
POST _reindex
{
"source": {
"index": "book"
},
"dest": {
"index": "book_new",
"version_type": "internal"
}
}
GET /book_new/_doc/2
version_type
属性含义如下:
internal
:直接拷贝文档到目标索引,对相同的 type 、文档 ID 直接进行覆盖,默认值external
: 迁移文档到目标索引时,保留 version 信息,对目标索引中不存在的文档进行创建,已存在的文档按 version 进行更新,遵循乐观锁机制。
op_type 属性和 conflicts 属性
如果 op_type
设置为 create
,那么迁移时只在目标索引中创建 ID 不存在的文档,已存在的文档,会提示错误,如下请求:
POST _reindex
{
"source": {
"index": "book"
},
"dest": {
"index": "book_new",
"op_type": "create"
}
}
如果加上 "conflicts": "proceed"
配置项,那么冲突信息将不展示,只展示冲突的文档数量。
query 支持
reindex
API 支持数据过滤、数据排序、 size
设置、 _source
选择等,也支持脚本执行,这里提供一个简单示例:
POST _reindex
{
"size": 100,
"source": {
"index": "book",
"query": {
"term": {
"language": "english"
}
},
"sort": {
"price": "desc"
}
},
"dest": {
"index": "book_new2"
}
}
小结
零停机索引重建操作的三个方案,从自研功能、 scroll
+ bulk
到 reindex
,我们作为 Elasticsearch 的使用者,三个方案的参与度是逐渐弱化的,但稳定性却是逐渐上升的,我们需要清楚地去了解各个方案的优劣,适宜的场景,然后根据实际的情况去权衡,哪个方案更适合我们的业务模型。
玩转 Elasticsearch Suggester 智能搜索建议
现代的搜索引擎,一般会具备 " Suggest As You Type " 功能,即在用户输入搜索的过程中,进行自动补全或者纠错。 通过协助用户输入更精准的关键词,提高后续全文搜索阶段文档匹配的程度。例如在京东上输入部分关键词,甚至输入拼写错误的关键词时,它依然能够提示出用户想要输入的内容。
如果自己亲手去试一下,可以看到京东在用户刚开始输入的时候是自动补全的,而当输入到一定长度,如果因为单词拼写错误无法补全,就开始尝试提示相似的词。
那么类似的功能在 Elasticsearch 里如何实现呢? 答案就在 Suggesters API 。 Suggesters 基本的运作原理是将输入的文本分解为 token ,然后在索引的字典里查找相似的 term 并返回。 根据使用场景的不同, Elasticsearch 里设计了 4 种类别的 Suggester ,分别是:
- Term Suggester
- Phrase Suggester
- Completion Suggester
- Context Suggester
在官方的参考文档里,对这 4 种 Suggester API 都有比较详细的介绍,下面的案例将在 Elasticsearch 7.x 上通过示例讲解 Suggester 的基础用法
Term Suggester
准备一个叫做 blogs
的索引,配置一个 text
字段,通过 bulk
API 写入几条文档:
PUT /blogs/
{
"mappings": {
"properties": {
"body": {
"type": "text"
}
}
}
}
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"}
GET /blogs/_search
{
"query": {
"match_all": {}
}
}
此时 blogs
索引里已经有一些文档了,可以进行下一步的探索。为帮助理解,我们先看看哪些 term
会存在于词典里。将输入的文本分析一下:
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
搜索看看效果:
POST /blogs/_search
{
"suggest": {
"my-suggestion": {
"text": "lucne rock",
"term": {
"suggest_mode": "missing",
"field": "body"
}
}
}
}
suggest
就是一种特殊类型的搜索, DSL 内部的 text
指的是 API 调用方提供的文本,也就是通常用户界面上用户输入的内容。这里的 lucne
是错误的拼写,模拟用户输入错误。 term
表示这是一个 term suggester 。 field
指定 suggester
针对的字段,另外有一个可选的 suggest_mode
。 范例里的 missing
实际上就是缺省值。
返回结果如下:
{
"took" : 97,
"timed_out" : false,
"_shards" : {
"total" : 1,
"successful" : 1,
"skipped" : 0,
"failed" : 0
},
"hits" : {
"total" : {
"value" : 0,
"relation" : "eq"
},
"max_score" : null,
"hits" : [ ]
},
"suggest" : {
"my-suggestion" : [
{
"text" : "lucne",
"offset" : 0,
"length" : 5,
"options" : [
{
"text" : "lucene",
"score" : 0.8,
"freq" : 2
}
]
},
{
"text" : "rock",
"offset" : 6,
"length" : 4,
"options" : [ ]
}
]
}
}
在返回结果里 "suggest" -> "my-suggestion"
部分包含了一个数组,每个数组项对应从输入文本分解出来的token
(存放在 text
这个 key 里)以及为该 token 提供的建议词项(存放在 options 数组里)。 示例里返回了 lucne
, rock
这 2 个词的建议项( options ),其中 rock
的 options
是空的,表示没有可以建议的选项,为什么? 上面提到了,我们为查询提供的 suggest mode 是 missing
,由于 rock
在索引的词典里已经存在了,够精准,就不建议啦。 只有词典里找不到词,才会为其提供相似的选项。
如果将 suggest_mode
换成 popular
会是什么效果?
尝试一下,重新执行查询,返回结果里 rock
这个词的 option
不再是空的,而是建议为 rocks
。
回想一下, rock
和 rocks
在索引词典里都是有的。 不难看出即使用户输入的 token
在索引的词典里已经有了,但是因为存在一个词频更高的相似项,这个相似项可能是更合适的,就被挑选到 options 里了。最后还有一个 always
mode ,其含义是不管 token 是否存在于索引词典里都要给出相似项。
有人可能会问,两个 term 的相似性是如何判断的? ES 使用了一种叫做 Levenstein edit distance 的算法,其核心思想就是一个词改动多少个字符就可以和另外一个词一致。 Term suggester 还有其他很多可选参数来控制这个相似性的模糊程度,这里就不一一赘述了。
Phrase suggester
Phrase suggester 在 Term suggester 的基础上,会考量多个 term
之间的关系,比如是否同时出现在索引的原文里,相邻程度,以及词频等等。看个范例就比较容易明白了:
POST /blogs/_search
{
"suggest": {
"my-suggestion": {
"text": "lucne and elasticsear rock",
"phrase": {
"field": "body",
"highlight": {
"pre_tag": "<em>",
"post_tag": "</em>"
}
}
}
}
}
// 返回结果
{
"took" : 204,
"timed_out" : false,
"_shards" : {
"total" : 1,
"successful" : 1,
"skipped" : 0,
"failed" : 0
},
"hits" : {
"total" : {
"value" : 0,
"relation" : "eq"
},
"max_score" : null,
"hits" : [ ]
},
"suggest" : {
"my-suggestion" : [
{
"text" : "lucne and elasticsear rock",
"offset" : 0,
"length" : 26,
"options" : [
{
"text" : "lucene and elasticsearch rock",
"highlighted" : "<em>lucene</em> and <em>elasticsearch</em> rock",
"score" : 0.004993905
},
{
"text" : "lucne and elasticsearch rock",
"highlighted" : "lucne and <em>elasticsearch</em> rock",
"score" : 0.0033391973
},
{
"text" : "lucene and elasticsear rock",
"highlighted" : "<em>lucene</em> and elasticsear rock",
"score" : 0.0029183894
}
]
}
]
}
}
options
直接返回一个 phrase
列表,由于加了 highlight
选项,被替换的 term
会被高亮。因为 lucene
和 elasticsearch
曾经在同一条原文里出现过,同时替换 2 个 term
的可信度更高,所以打分较高,排在第一位返回。 Phrase suggester 有相当多的参数用于控制匹配的模糊程度,需要根据实际应用情况去挑选和调试。
Completion Suggester
下面来谈一下 Completion Suggester,它主要针对的应用场景就是 "Auto Completion"。 此场景下用户每输入一个字符的时候,就需要即时发送一次查询请求到后端查找匹配项,在用户输入速度较高的情况下对后端响应速度要求比较苛刻。因此实现上它和前面两个 Suggester 采用了不同的数据结构,索引并非通过倒排来完成,而是将analyze 过的数据编码成 FST 和索引一起存放。对于一个 open 状态的索引,FST 会被 ES 整个装载到内存里的,进行前缀查找速度极快。但是 FST 只能用于前缀查找,这也是 Completion Suggester 的局限所在。
为了使用 Completion Suggester ,字段的类型需要专门定义如下:
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"
}
}
}
}
// 查询结果
{
"took" : 55,
"timed_out" : false,
"_shards" : {
"total" : 1,
"successful" : 1,
"skipped" : 0,
"failed" : 0
},
"hits" : {
"total" : {
"value" : 0,
"relation" : "eq"
},
"max_score" : null,
"hits" : [ ]
},
"suggest" : {
"blog-suggest" : [
{
"text" : "elastic i",
"offset" : 0,
"length" : 9,
"options" : [
{
"text" : "Elastic is the company behind ELK stack",
"_index" : "blogs_completion",
"_type" : "_doc",
"_id" : "ibPJhngBFx2uaqEXaUrL",
"_score" : 1.0,
"_source" : {
"body" : "Elastic is the company behind ELK stack"
}
}
]
}
]
}
}
值得注意的一点是 Completion Suggester 在索引原始数据的时候也要经过 analyze 阶段,取决于选用的 analyzer 不同,某些词可能会被转换,某些词可能被去除,这些会影响 FST 编码结果,也会影响查找匹配的效果。
比如我们删除上面的索引,重新设置索引的 mapping
,将 analyzer 更改为 english
后,再次执行相同的查询,居然没有匹配结果了,多么费解! 原来我们用的 english
analyzer 会剥离掉 stop word,而 is
就是其中一个,被剥离掉了!
POST _analyze
{
"text": "elasticsearch is rock solid",
"analyzer": "english"
}
// 返回结果
{
"tokens" : [
{
"token" : "elasticsearch",
"start_offset" : 0,
"end_offset" : 13,
"type" : "<ALPHANUM>",
"position" : 0
},
{
"token" : "rock",
"start_offset" : 17,
"end_offset" : 21,
"type" : "<ALPHANUM>",
"position" : 2
},
{
"token" : "solid",
"start_offset" : 22,
"end_offset" : 27,
"type" : "<ALPHANUM>",
"position" : 3
}
]
}
FST ( Finite StateTransducers )只编码了这 3 个 token
,并且默认的还会记录他们在文档中的位置和分隔符。 用户输入 elastic i
进行查找的时候,输入被分解成 elastic
和 i
, FST 没有编码这个 i
, 匹配失败。
试一下搜索 elastic is
,会发现又有结果,因为这次输入的 text
经过 english
analyzer 的时候 is
也被剥离了,只需在 FST 里查询 elastic
这个前缀,自然就可以匹配到了
其他能影响 completion suggester 结果的,还有如 preserve_separators
, preserve_position_increments
等等 mapping
参数来控制匹配的模糊程度。以及搜索时可以选用 Fuzzy Queries ,使得上面例子里的 elastic i
在使用 english
analyzer 的情况下依然可以匹配到结果。
"preserve_separators": false
,这个设置为false
,将忽略空格之类的分隔符"preserve_position_increments": true
,如果建议词第一个词是停用词,并且我们使用了过滤停用词的分析器,需要将此设置为false
因此用好 Completion Suggester 并不是一件容易的事,实际应用开发过程中,需要根据数据特性和业务需要,灵活搭配 analyzer
和 mapping
参数,反复调试才可能获得理想的补全效果。
回到篇首京东或者百度搜索框的补全 / 纠错功能,如果用 ES 怎么实现呢?我能想到的一个的实现方式:在用户刚开始输入的过程中,使用 Completion Suggester 进行关键词前缀匹配,刚开始匹配项会比较多,随着用户输入字符增多,匹配项越来越少。如果用户输入比较精准,可能 Completion Suggester 的结果已经够好,用户已经可以看到理想的备选项了。
如果 Completion Suggester 已经到了零匹配,那么可以猜测是否用户有输入错误,这时候可以尝试一下 Phrase Suggester 。如果 Phrase Suggester 没有找到任何 option
,开始尝试 term Suggester 。
精准程度上( Precision )看: Completion > Phrase > Term , 而召回率上( Recall )则反之。从性能上看, Completion Suggester 是最快的,如果能满足业务需求,只用 Completion Suggester 做前缀匹配是最理想的。 Phrase 和 Term 由于是做倒排索引的搜索,相比较而言性能应该要低不少,应尽量控制 suggester 用到的索引的数据量,最理想的状况是经过一定时间预热后,索引可以全量 map 到内存。
召回率(Recall) = 系统检索到的相关文件 / 系统所有相关的文件总数
准确率(Precision) = 系统检索到的相关文件 / 系统所有检索到的文件总数
从一个大规模数据集合中检索文档时,可把文档分成四组:
-
系统检索到的相关文档(A)
-
系统检索到的不相关文档(B)
-
相关但是系统没有检索到的文档(C)
-
不相关且没有被系统检索到的文档(D)
-
召回率 R :用实际检索到相关文档数作为分子,所有相关文档总数作为分母,即
R = A / ( A + C )
-
精度 P :用实际检索到相关文档数作为分子,所有检索到的文档总数作为分母,即
P = A / ( A + B )
举例:一个数据库有 1000 个文档,其中有 50 个文档符合相关定义的问题,系统检索到 75 个文档,但其中只有 45 个文档被检索出。
- 精度:P=45/75=60%
- 召回率:R=45/50=90%
Context Suggester
Completion Suggester 的扩展
可以在搜索中加入更多的上下文信息,然后根据不同的上下文信息,对相同的输入,比如 star
,提供不同的建议值,比如:
- 咖啡相关:starbucks
- 电影相关:star wars
玩转 Elasticsearch Java Client
说明
ES 提供多种不同的客户端:
TransportClient
:ES 提供的传统客户端,官方计划 8.0 版本删除此客户端。RestClient
:RestClient
是官方推荐使用的,它包括两种: Java Low Level REST Client 和 Java High Level REST Client 。 ES 在 6.0 之后提供 Java High Level REST Client , 两种客户端官方更推荐使用 Java High Level REST Client , 使用时加入对应版本的依赖即可。
SpringBoot 中使用 RestClient
-
pom.xml
<parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.0.1.RELEASE</version> </parent> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> </dependency> <dependency> <groupId>org.elasticsearch.client</groupId> <artifactId>elasticsearch-rest-high-level-client</artifactId> <version>7.3.0</version> <exclusions> <exclusion> <groupId>org.elasticsearch</groupId> <artifactId>elasticsearch</artifactId> </exclusion> </exclusions> </dependency> <dependency> <groupId>org.elasticsearch</groupId> <artifactId>elasticsearch</artifactId> <version>7.3.0</version> </dependency> </dependencies>
-
application.yml
lagouelasticsearch: elasticsearch: hostlist: 192.168.181.133:9200
-
配置类
@Configuration public class ElasticsearchConfig { @Value("${lagouelasticsearch.elasticsearch.hostlist}") private String hostlist; @Bean public RestHighLevelClient restHighLevelClient() { // 解析hostlist 信息 String[] split = hostlist.split(","); // 创建HttpHost数组 封装es的主机和端口 HttpHost[] httpHosts = new HttpHost[split.length]; for (int i = 0; i < split.length; i++) { String item = split[i]; httpHosts[i] = new HttpHost(item.split(":")[0], Integer.parseInt(item.split(":")[1]), "http"); } return new RestHighLevelClient(RestClient.builder(httpHosts)); } }
-
主启动类
@SpringBootApplication public class ESApplication { public static void main(String[] args) { SpringApplication.run(ESApplication.class, args); } }
-
测试类
创建索引
@Test
public void testCreateIndex() throws IOException {
// 创建一个索引创建请求对象
CreateIndexRequest createIndexRequest = new CreateIndexRequest("elasticsearch_test");
//设置映射
/* XContentBuilder builder = XContentFactory.jsonBuilder()
.startObject()
.field("properties")
.startObject()
.field("description").startObject().field("type","text").field("analyzer","ik_max_word").endObject()
.field("name").startObject().field("type","keyword").endObject()
.field("pic").startObject().field("type","text").field("index","false").endObject()
.field("studymodel").startObject().field("type","keyword").endObject()
.endObject()
.endObject();
createIndexRequest.mapping("doc",builder);
*/
createIndexRequest.mapping("doc", "{\n" + " \"properties\": {\n" + " \"description\": {\n" + " \"type\": \"text\",\n" + " \"analyzer\": \"ik_max_word\"\n" + " },\n" + " \"name\": {\n" + " \"type\": \"keyword\"\n" + " },\n" + " \"pic\": {\n" + " \"type\": \"text\",\n" + " \"index\": false\n" + " },\n" + " \"studymodel\": {\n" + " \"type\": \"keyword\"\n" + " }\n" + " }\n" + " }", XContentType.JSON);
// 操作索引的客户端
IndicesClient indicesClient = client.indices();
CreateIndexResponse createIndexResponse = indicesClient.create(createIndexRequest, RequestOptions.DEFAULT);
// 得到响应
boolean acknowledged = createIndexResponse.isAcknowledged();
System.out.println(acknowledged);
}
删除索引
@Test
public void testDeleteIndex() throws IOException {
// 构建 删除索引库的请求对象
DeleteIndexRequest deleteIndexRequest = new DeleteIndexRequest("elasticsearch_test");
IndicesClient indicesClient = client.indices();
AcknowledgedResponse deleteResponse = indicesClient.delete(deleteIndexRequest, RequestOptions.DEFAULT);
// 得到响应
boolean acknowledge = deleteResponse.isAcknowledged();
System.out.println(acknowledge);
}
添加文档
//添加文档
/*
POST /elasticsearch_test/_doc/1
{
"name": "spring cloud实战",
"description": "本课程主要从四个章节进行讲解: 1.微服务架构入门 2.spring cloud 基础入门 3.实战Spring Boot 4.注册中心eureka。",
"studymodel":"201001",
"timestamp": "2020-08-22 20:09:18",
"price": 5.6
}
*/
@Test
public void testAddDoc() throws IOException {
// 准备索取请求对象
//IndexRequest indexRequest = new IndexRequest("elasticsearch_test","doc");
IndexRequest indexRequest = new IndexRequest("elasticsearch_test");
//indexRequest.id("2");
// 文档内容 准备json数据
Map<String, Object> jsonMap = new HashMap<>();
jsonMap.put("name", "spring cloud实战3");
jsonMap.put("description", "本课程主要从四个章节进行讲解3: 1.微服务架构入门 2.spring cloud 基础入门 3.实战Spring Boot 4.注册中心eureka。");
jsonMap.put("studymodel", "3101001");
jsonMap.put("timestamp", "2020-07-22 20:09:18");
jsonMap.put("price", 35.6);
indexRequest.source(jsonMap);
// 执行请求
IndexResponse indexResponse = client.index(indexRequest, RequestOptions.DEFAULT);
DocWriteResponse.Result result = indexResponse.getResult();
System.out.println(result);
}
查询所有
//搜索全部记录
/*
GET /elasticsearch_test/_search
{
"query":{
"match_all":{}
}
}
*/
@Test
public void testSearchAll() throws IOException {
// 搜索请求对象
SearchRequest searchRequest = new SearchRequest("elasticsearch_test");
searchRequest.searchType(SearchType.QUERY_THEN_FETCH);
// 搜索源构建对象
SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder();
// 设置搜索方法
searchSourceBuilder.query(QueryBuilders.matchAllQuery());
searchSourceBuilder.fetchSource(new String[]{"name", "price", "timestamp"}, new String[]{});
// 请求对象设置 搜索源对象
searchRequest.source(searchSourceBuilder);
// 使用client 执行搜索
SearchResponse searchResponse = client.search(searchRequest, RequestOptions.DEFAULT);
// 搜索结果
SearchHits hits = searchResponse.getHits();
// 匹配到的总记录数
TotalHits totalHits = hits.getTotalHits();
System.out.println("查询到的总记录数:" + totalHits.value);
// 得到的匹配度高的文档
SearchHit[] searchHits = hits.getHits();
for (SearchHit hit : searchHits) {
String id = hit.getId();
// 源文档的内容
Map<String, Object> sourceMap = hit.getSourceAsMap();
String name = (String) sourceMap.get("name");
String timestamp = (String) sourceMap.get("timestamp");
String description = (String) sourceMap.get("description");
Double price = (Double) sourceMap.get("price");
System.out.println(id);
System.out.println(name);
System.out.println(timestamp);
System.out.println(description);
System.out.println(price);
}
}
根据 id 查询单个文档
// 查询文档
@Test
public void testGetDoc() throws IOException {
// 查询请求对象
GetRequest getRequest = new GetRequest("elasticsearch_test", "krMLh3gBFx2uaqEXEEqH");
GetResponse getResponse = client.get(getRequest, RequestOptions.DEFAULT);
// 得到文档内容
Map<String, Object> sourceMap = getResponse.getSourceAsMap();
System.out.println(sourceMap);
}
词条搜索
@Test
public void testTermQuery() throws IOException {
// 搜索请求对象
SearchRequest searchRequest = new SearchRequest("elasticsearch_test");
// 搜索源构建对象
SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder();
// 设置搜索方法
//searchSourceBuilder.query(QueryBuilders.termQuery("name","spring cloud实战"));
searchSourceBuilder.query(QueryBuilders.termQuery("description", "spring"));
searchSourceBuilder.fetchSource(new String[]{"name", "price", "timestamp"}, new String[]{});
// 请求对象设置 搜索源对象
searchRequest.source(searchSourceBuilder);
// 使用client 执行搜索
SearchResponse searchResponse = client.search(searchRequest, RequestOptions.DEFAULT);
// 搜索结果
SearchHits hits = searchResponse.getHits();
// 匹配到的总记录数
TotalHits totalHits = hits.getTotalHits();
System.out.println("查询到的总记录数:" + totalHits.value);
// 得到的匹配度高的文档
SearchHit[] searchHits = hits.getHits();
for (SearchHit hit : searchHits) {
String id = hit.getId();
// 源文档的内容
Map<String, Object> sourceMap = hit.getSourceAsMap();
String name = (String) sourceMap.get("name");
String timestamp = (String) sourceMap.get("timestamp");
String description = (String) sourceMap.get("description");
Double price = (Double) sourceMap.get("price");
System.out.println(name);
System.out.println(timestamp);
System.out.println(description);
System.out.println(price);
}
}
全文搜索,带分页
@Test
public void testSearchAllPage() throws IOException {
// 搜索请求对象
SearchRequest searchRequest = new SearchRequest("elasticsearch_test");
// 搜索源构建对象
SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder();
// 设置搜索方法
searchSourceBuilder.query(QueryBuilders.matchAllQuery());
searchSourceBuilder.fetchSource(new String[]{"name", "price", "timestamp"}, new String[]{});
// 设置分页参数
int page = 2;
int size = 2;
// 计算出 from
int form = (page - 1) * size;
searchSourceBuilder.from(form);
searchSourceBuilder.size(size);
// 设置price 降序
searchSourceBuilder.sort("price", SortOrder.DESC);
// 请求对象设置 搜索源对象
searchRequest.source(searchSourceBuilder);
// 使用client 执行搜索
SearchResponse searchResponse = client.search(searchRequest, RequestOptions.DEFAULT);
// 搜索结果
SearchHits hits = searchResponse.getHits();
// 匹配到的总记录数
TotalHits totalHits = hits.getTotalHits();
System.out.println("查询到的总记录数:" + totalHits.value);
// 得到的匹配度高的文档
SearchHit[] searchHits = hits.getHits();
for (SearchHit hit : searchHits) {
String id = hit.getId();
// 源文档的内容
Map<String, Object> sourceMap = hit.getSourceAsMap();
String name = (String) sourceMap.get("name");
String timestamp = (String) sourceMap.get("timestamp");
String description = (String) sourceMap.get("description");
Double price = (Double) sourceMap.get("price");
System.out.println(name);
System.out.println(timestamp);
System.out.println(description);
System.out.println(price);
}
}
词条搜索,带分页
@Test
public void testTermQueryPage() throws IOException {
// 搜索请求对象
SearchRequest searchRequest = new SearchRequest("elasticsearch_test");
// 搜索源构建对象
SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder();
// 设置搜索方法
searchSourceBuilder.query(QueryBuilders.termQuery("name", "spring cloud实战"));
searchSourceBuilder.fetchSource(new String[]{"name", "price", "timestamp"}, new String[]{});
// 设置分页参数
int page = 1;
int size = 2;
// 计算出 from
int form = (page - 1) * size;
searchSourceBuilder.from(form);
searchSourceBuilder.size(size);
// 设置price 降序
searchSourceBuilder.sort("price", SortOrder.DESC);
// 请求对象设置 搜索源对象
searchRequest.source(searchSourceBuilder);
// 使用client 执行搜索
SearchResponse searchResponse = client.search(searchRequest, RequestOptions.DEFAULT);
// 搜索结果
SearchHits hits = searchResponse.getHits();
// 匹配到的总记录数
TotalHits totalHits = hits.getTotalHits();
System.out.println("查询到的总记录数:" + totalHits.value);
// 得到的匹配度高的文档
SearchHit[] searchHits = hits.getHits();
for (SearchHit hit : searchHits) {
String id = hit.getId();
// 源文档的内容
Map<String, Object> sourceMap = hit.getSourceAsMap();
String name = (String) sourceMap.get("name");
String timestamp = (String) sourceMap.get("timestamp");
String description = (String) sourceMap.get("description");
Double price = (Double) sourceMap.get("price");
System.out.println(name);
System.out.println(timestamp);
System.out.println(description);
System.out.println(price);
}
}