es功能
- 分布式的实时文档存储,每个字段都可以被索引、搜索
- 分布式实时分析搜索引擎
- 胜任上百个服务节点的扩展,支持PB级的结构化、非结构化数据管理(存储、索引、分析、搜索)
es安装
- ./bin/elastaicsearch 启动es
- -d 后台启动
- ctrl+c 终止服务
- curl ‘http://localhost:9200/?pretty‘,测试启动是否成功(windows需要安装一个curl客户端)
- elasticsearch.yml 重要配置
es插件(head,sense,marvel)
head插件可以用来快速查看elasticsearch中的数据概况以及非全量的数据,也支持控件化查询和rest请求,但是体验都不是很好。一般就用它来看各个索引的数据量以及分片的状态。
直接访问下面的地址即可:
http://localhost:9200/_plugin/head/sense插件可以方便的执行rest请求,但是中文输入的体验不是很好。
安装sense只需要在Kibana端安装插件即可,插件会自动安装到kibana的应用菜单中
http://localhost:5601/app/sensemarvel
marvel工具可以帮助使用者监控elasticsearch的运行状态,不过这个插件需要license。安装完license后可以安装marvel的agent,agent会收集elasticsearch的运行状态。
然后在Kibana段安装marvel插件,这个插件与sense类似,都集成在kibana的导航列表面
kibana sense安装
- kibana是es的可视化及分析平台
- sense是kibana提供的交互式控制台
- ./bin/kibana plugin –install elastic/sense 安装sense
- ./bin/kibana 启动kibana
- http://localhost:5601/app/sense 浏览器中打开sense
与es交互
java API(9300)
es提供了两种形式的java客户端(要将es提供的客户端支持的代码引入自己的java工程或者项目中去),这两种客户端与es的交互均是9300端口。集群中的节点本身也是通过9300端口彼此通信,使用es原生的传输协议。
- 节点客户端(Node client):节点客户端作为非数据节点加入到本地集群中,节点客户端本身不存储任何数据,但是它知道数据在集群中的哪个节点,并可以把请求转发到正确的节点
- 传输客户端(Transport client):传输客户端将请求发送到远程集群,它本身不加入集群,但是可以将请求转发到集群中的一个节点上
RESTful API with JSON over HTTP
所有其他语言可以使用RESTful API 通过端口9200与es进行通信。如 JavaScript、Python、PHP、Ruby、Perl
es请求格式:
crul -XVERB "protocal://host:port/path?queryStr' -d 'body'
示例:
curl -XGET 'http://localhost:9200/_count?pretty' -d '
{
"query" : { "match_all" : { } }
}'
缩略格式(sense控制台也使用此种缩略格式):
GET /_count
{"query" : { "match_all" : { } }}
面向文档
应用程序中的对象一般有复杂的数据结构,当存储到关系型数据库中时,往往需要对其进行扁平化处理,而在查询数据时,又要重新构造对象,这其中需要大量的工作进行设计。
而es是直接存储对象或者文档,并对整个文档进行索引,使之可以被检索。在es中是对文档进行索引、检索、排序和过滤,而不是对行列数据。
序列化格式:JSON
索引/类型/文档/属性 (_index/_type)
一个es集群可以包含多个索引(库),每个索引又可以包含多种类型(类/表),每个类型可以包含多个文档(对象/行-一条数据),每个文档可以包含多个属性(字段/列)
索引(名称)/ 索引(动词)/ 倒排索引
- 索引(名称):一个索引类似于关系型数据库中的一个库,是存储文档的地方。
- 索引(动词):存储一个文档到一个索引库中,即为索引一个文档。类似于SQL中的insert动作。
- 倒排索引:
关系型数据库中通过指定一个列,对此列的所有取值建立某种方式的(如B-Tree)索引,以便检索
es则是通过对每个单词或字建立倒排索引,来达到相同的目的。默认情况下,es对每个属性都会建立倒排索引,如果某个属性没有建立倒排索引,那么检索时是无法检索到的。
简单示例
索引一个文档到库
无需事先进行创建索引库、创建类型、指定类型的属性等操作,只需直接执行如下的索引一个文档的操作,es会在后台使用默认设置完成其它的一切管理任务。
PUT /magecorp/employee/1
{
"first_name" : "John",
"last_name" : "Smith",
"age" : 25,
"about" : "I love to go rock climbing",
"interests": [ "sports", "music" ]
}
PUT /megacorp/employee/2
{
"first_name" : "Jane",
"last_name" : "Smith",
"age" : 32,
"about" : "I like to collect rock albums",
"interests": [ "music" ]
}
PUT /megacorp/employee/3
{
"first_name" : "Douglas",
"last_name" : "Fir",
"age" : 35,
"about": "I like to build cabinets",
"interests": [ "forestry" ]
}
路径 /megacorp/employee/1 包含了三部分的信息:
megacorp 索引名称
employee 类型名称
1 特定雇员的ID
请求体 —— JSON 文档 —— 包含了这位员工的所有详细信息,他的名字叫 John Smith ,今年 25 岁,喜欢攀岩。
检索文档 - GET - 指定索引库、类型、ID
GET /megacorp/employee/1
{
"_index" : "megacorp",
"_type" : "employee",
"_id" : "1",
"_version" : 1,
"found" : true,
"_source" : {
"first_name" : "John",
"last_name" : "Smith",
"age" : 25,
"about" : "I love to go rock climbing",
"interests": [ "sports", "music" ]
}
}
轻量搜索 - /{index}/{type}/_search?q=field:value
适用于通过命令行或者交互式客户端做即席查询或一次性查询,比较简洁,在开发阶段非常方便,但也晦涩、易出错。
相反的在生产环境中更多地使用功能全面的请求体 request body方式的 查询API,除了能完成以上所有功能,还有一些附加功能。
例:
+name:(jane kitty) join_date:>2014-05-01 -age:<20 +(aggregations geo)
- +前缀表示必须与查询条件匹配,-前缀表示必须不匹配,没有+、-前缀的其它条件都是可选的–匹配的越多,文档就越相关
- +(elastic kibana) 表示_all字段中必须包含elastic 或者 kibana
es会把某文档的所有域的值拼接成一个字符串,作为此文档的一个域,此域的名称为_all,当q中不指定字段,则默认是对_all字段进行检索,如/_search?q=mary
,则是查询文档中包含“mary”这个词的所有文档。
GET /megacorp/employee/_search?q=last_name:Smith #检索某类型下的文档,指定查询条件
GET /megacorp/employee/_search #检索某类型下的所有文档,无查询条件
{
"took": 6, #耗时?
"timed_out": false,
"_shards": { ... },
"hits": { #检索出的结果的,汇总信息?
"total": 3,
"max_score": 1, #检索出的结果中相关度最高分
"hits": [
{
"_index": "megacorp", #索引库
"_type": "employee", #类型
"_id": "3",
"_score": 1, #相关度得分
"_source": {
"first_name": "Douglas",
"last_name": "Fir",
"age": 35,
"about": "I like to build cabinets",
"interests": [ "forestry" ]
}
},
{
"_index": "megacorp",
"_type": "employee",
"_id": "1",
"_score": 1,
"_source": {
"first_name": "John"
...
}
}...
]
}
}
轻量搜索:
GET /matecorp/employee/_search?q=last_name:Smith
用DSL查询表达式如下:
GET /matecorp/employee/_search
{
"query":{
"match":{
"last_name" : "Smith"
}
}
}
过滤器 filter
#查询last_name=Smith,年龄大于30的
GET /magecorp/employee/_search
{
"query":{
"bool":{
"must":{ #必须匹配
"match" : {
"last_name" : "Smith"
}
},
"filter":{ #过滤器
"range" : { #取值范围,相对于between
"age" : {
"gt" : 30
}
}
}
}
}
}
全文搜索
#检索所有喜欢攀岩(rock climbing)的员工
GET /magecorp/employee/_search
{
"query":{
"match":{ #若要完全匹配整个短语,用match_phrase
"about": "rock climbing"
}
}
}
#结果如下:名为John的雇员的相关性得分_score要高于名为Jane的,因为Jane的about为"I like to collect rock albums",只匹配到了单词rock
{
...
"hits": {
"total": 2,
"max_score": 0.16273327,
"hits": [
{
...
"_score": 0.16273327,
"_source": {
"first_name": "John",
"last_name": "Smith",
"age": 25,
"about": "I love to go rock climbing",
"interests": [ "sports", "music" ]
}
},
{
...
"_score": 0.016878016,
"_source": {
"first_name": "Jane",
"last_name": "Smith",
"age": 32,
"about": "I like to collect rock albums",
"interests": [ "music" ]
}
}
]
}
}
高亮搜索 highlight
GET /magecorp/employee/_search
{
"query" : {
"match_phrase" : { #匹配整个短语
"about" : "rock climbing"
}
},
"highlight": {
"fields" : {
"about" : {}
}
}
}
聚合 - aggs(aggregations)
#挖掘所有兴趣爱好的受欢迎度
GET /megacorp/employee/_search
{
"aggs":{ #指定查询为聚合查询
"popularity_interests":{ #给查询结果取个名
"terms":{ #汇总方式:terms:列出所有结果项
"field" : "interests" #依据的是一个属性,属性名是interests
}
}
}
}
{
...
"hits": { ... },
"aggregations": {
"popularity_interests": { #名为popularity_interests的查询的结果
"buckets": [
{
"key": "music",
"doc_count": 2
},
{
"key": "forestry",
"doc_count": 1
},
{
"key": "sports",
"doc_count": 1
}
]
}
}
}
#指定查询条件的聚合:叫 Smith 的兴趣的受欢迎程度,直接添加适当的查询来组合查询:
GET /magecorp/employee/_search
{
"query" : { "match" : { "first_name" : "Smith" } },
"aggs" : { "popularity_interests" : { "terms" : { "field" : "interests" } } }
}
#分级汇总(嵌套聚合):某兴趣爱好的人气的平均年龄段
GET /magecorp/employee/_search
{
"aggs" : {
"popularity_interests" : {
"terms" : { "field" : "interests" } , #汇总方式:terms:列出所有结果项
"aggs" : { #嵌套聚合汇总
"avg_age" : { #查询结果名称
"avg" : { #汇总方式:avg:求平均
"field" : "age"
}
}
}
}
}
}
集群特性
- 天生分布式,所有节点平等,自动选举master节点。若只有一个节点,那么它就会成为主节点。
- 单点故障:只需两个节点,即可应对单点故障(在elasticsearch.yml中配置)
cluster.name: escluster
node.name: node-0
network.host: 192.168.174.20
http.port: 9200
discovery.zen.ping.multicast.enabled: false
discovery.zen.ping.unicast.hosts: ["192.168.174.20","192.168.174.21","192.168.174.22"]
- 节点故障:
- 若故障的节点是主节点,则集群会立刻重新选举出一个新的主节点。
- 若故障节点上有主分片,则集群会立即从其它节点上找到这些主分片的副本分片,并将其提升为主分片。(在这些动作做完之前,status为red,当提升副本分片为主分片后,status为yellow)
- 集群中有节点加入或者退出时,集群会自动重新平均分布集群中的所有数据
节点、主分片、副本分片(node 、priamary_shard、replacation)
node : number_of_nodes , number_of_data_nodes
shard :
- active_primary_shards(正常运行的主分片数。如果有主分片没能正常运行,则status为red)。
- active_shards(所有正常运行的分片数。所有的主分片都正常运行,但不是所有的副本分片都正常运行。则status为yellow)
- unassigned_shards(没有分配到任何节点的副本分片数)
当status为yellow时,集群是正常运行的,能正常对外提供服务,但是有丢失数据的故障风险(因为有副本是故障的,那么一旦主分片数据丢失,又没有可用的副本来恢复,整个数据就有丢失的风险了)
索引与分片
- 索引是逻辑命名空间,一个索引库会指向一个或者多个物理分片
- 一个分片是一个底层的工作单元,是一个Lucene的实例,是一个完整的搜索引擎。一个文档最终会被物理地存储和索引到一个分片内,但应用程序是直接与索引而不是分片进行交互。
- 一个索引的主分片数在索引创建时确定,且不可更改。一个索引默认有5个主分片,可以通过如下定义语句,设定索引库的主分片数
PUT /myIndex
{
"settings" : {
"number_of_shards" : "3", #3个主分片
"number_of_replicas" : "2", #每个分片有两个副本,共6个副本分片
}
}
- 索引的副本数可以动态的进行水平扩充,水平扩展能提高负载能力,提高检索和访问性能,
PUT /my_index/settings
{
"number_of_replicas":5
}
当然,如果只是在相同节点数目的集群上增加更多的副本分片并不能提高性能,因为每个分片从节点上获得的资源会变少。 你需要增加更多的硬件资源来提升吞吐量。
但是更多的副本分片数提高了数据冗余量:按照上面的节点配置,我们可以在失去2个节点的情况下不丢失任何数据
文档和对象
- 通常情况下,es中的术语 对象 和 文档 是可以互换的。
- 但对象可以包含(嵌套)了另外的对象,或者包含在(嵌套在)一个对象中
- 而文档则只能是一个类型中的顶层或者根对象,这个根对象被序列化为JSON文档,存储到索引库中,有唯一的ID。
文档元数据(_index 、 _type、_id)
- _index:
索引库中存储的应该是因共同的特性被分组到一起的文档集合。
如:所有的产品在索引 products 中,而所有的交易信息存储到索引 sales 中。
索引名称只能以小写字母开头。 - _type:
Elasticsearch 公开了一个称为 types (类型)的特性,它允许您在索引中对数据进行逻辑分区。不同 types 的文档可能有不同的字段,但最好能够非常相似。
如,所有的产品都放在一个索引中,但是你有许多不同的产品类别,比如 “electronics” 、 “kitchen” 和 “lawn-care”。
type名称可以以大小写字母开头 - _id
id可以是自己指定的(如果文档本身有一个可以作唯一标识的字段,如学号等),那么应该在将文档索引到es中时直接提供:
PUT /{index}/{type}/{id}
{ "field1":"value"}
#如果不自己指定id,那么可以用autogenerating IDS,注意语法改成用POST(存储文档在这个url命名空间下),而不是PUT(使用这个URL存储文档):
POST /{index}/{type}
{ "field1":"value"}
#自动生成的 ID 是 URL-safe、 基于 Base64 编码且长度为20个字符的 GUID 字符串。 这些 GUID 字符串由可修改的 FlakeID 模式生成,这种模式允许多个节点并行生成唯一 ID ,且互相之间的冲突概率几乎为零
取回一个文档
#返回头部head信息(-i)
GET /magecorp/employee/1
{
...
"_version": 1,
"found": true,
"_source": {
"first_name": "John",
"last_name": "Smith",
...
}
}
curl -i -XGET http://hadoop01:9200/megacorp/employee/10
HTTP/1.1 404 Not Found
Content-Type: application/json; charset=UTF-8
Content-Length: 65
{"_index":"megacorp","_type":"employee","_id":"10",
"found":false}
#返回文档的一个或者几个字段(?_source=字段名1,字段名2)
GET /megacorp/employee/1?_source=first_name,age
{
"_index": "megacorp",
...
"found": true,
"_source": {
"first_name": "John",
"age": "25"
}
}
#只想获得_source字段,不想获得元数据,则设置端点为_source
GET /megacorp/employee/1/_source
{
"first_name": "John",
"last_name": "Smith",
...
}
检查文档是否存在( -i -XHEAD )
#status为200时为存在,status为404时,为不存在
curl -i -XHEAD http://hadoop01:9200/megacorp/employee/10
HTTP/1.1 404 Not Found
Content-Type: text/plain; charset=UTF-8
Content-Length: 0
更新与创建文档
#如果直接用 PUT /{index}/{type}/{id} {body}
#那么可能是更新,也可能是创建新文档(当id已经存在时,更新文档;否则创建新文档)
#如果需要确保创建一个新的文档,有以下三种请求格式
POST /{index}/{type}/ {body}
PUT /{index}/{type}/{id}/_create
PUT /{index}/{type}/{id}?op_type=create
当指定的id已经存在时,会返回status=409,document_already_exists
删除文档(DELETE)
DELETE /megacorp/employee/1
#如果要删除的id存在,则status为200,且返回的_version为加1后的
#如果id不存在,则status为404
并发控制(乐观锁,?version=1&version_type=external)
Elasticsearch 是分布式的。当文档创建、更新或删除时, 新版本的文档必须复制到集群中的其他节点。
Elasticsearch 也是异步和并发的,这意味着这些复制请求被并行发送,并且到达目的地时也许 顺序是乱的 。 Elasticsearch 需要一种方法确保文档的旧版本不会覆盖新的版本。
es通过检查版本号来检测是否发生了并发冲突(conflict),如果冲突,则返回status:409。
{
"error": {
"root_cause": [
...
],
"type": "version_conflict_engine_exception",
"reason": "[blog][1]: version conflict, current [2], provided [1]",
"index": "website",
"shard": "3"
},
"status": 409
}
- 自动生成的版本号
对于自动生成的版本号,可以通过设置源版本号?version=源版本号
来检测是否发生了并发的冲突
所谓源版本号是指从库中获取到文档时,此文档的版本号,如果更新后需要重新索引到库中时,库中的相同ID的文档的版本号已经不是我之前取出的版本号了,那么说文档在我的取出和重新更新这个时间段内,被别的线程做了一次更新。此时即为发生了冲突。
PUT /megacorp/employee/2?version=1
- 外部系统数据自身提供版本号
很常见的是,es中的数据是先存储到结构化数据库中,然后再被索引到es中。数据在主库中的所有更新都要同步到es中,这更加提高了并发冲突的可能性。
如果主数据库中已经有了版本号,或者有可以作为版本号的字段,比如timestamp,那么就可以通过?version=新版本号&version_type=external
来检测是否发生了冲突。
外部版本号的检测机制是检测当前索引库中的相同ID文档的版本号是否小于请求中给定的新版本号,如果不是的话那么就发生了冲突。
外部版本号不仅可以在更新、删除时指定,也可以在创建时指定
#创建一个ID=4的雇员,指定版本号=5
PUT /megacorp/employee/4?version=5&version_type=external
{ "first_name":"hellen",...}
#更新ID=4的雇员信息,指定版本号=10
PUT /megacorp/employee/4?version=10&version_type=external
{ "first_name":"Helen",...}
文档的部分更新
POST /megacorp/employee/1/_update
{
"doc" : { "interests" : ["swiming" , "pingpang" ] }
}
#或者用groovy脚本更新,注意要配置一下允许脚本
POST /megacorp/employee/1/_update
{
"script" : "ctx._source.interests+=newInterest",
"params" :{
"newInterest" : "swiming"
}
}
取回多个文档(/_mget:multi-get)
GET /_mget
{
"docs":[
{
"_index" : "megacorp",
"_type" : "employee",
"_id" : "1"
},
{
"_index" : "megacorp",
"_type" : "employee",
"_id" : "2",
"_source": "interests" #指定取回的字段
}
]
}
GET /megacorp/employee/_mget
{
"ids" : ["1","2"]
}
批量操作(/_bulk:bulk API)
bulk API提供在一个步骤中进行多个create\index\update\delete的不同action的操作。
批量请求避免了单独处理每个请求的网络延时和开销,因此可以提高性能。但同时整个批量请求都要由接收到请求的节点加载到内存中,则其它后来的请求所能获得的内存就会相应缩减。因此批量操作并不是越多越好,超过某个量,性能将不再提升,反而可能降低,一般占用内存5M~15M比较合适。最好的办法是逐渐增加批的大小,进行尝试。一般以1000-5000条为一个批。
格式如下:
POST /_bulk 或者 POST /{index}/{type}/_bulk
{action1 : {metadata1}}
{request body1}
{action2 : {metadata2}}
{request body2}
其中action必须是以下选项之一:
- create :如果文档不存在,则创建文档(PUT /{index}/{type}/{id}/_create)
- index:创建一个新文档或者对现有文档进行全部更新(PUT /{index}/{type}/{id})
- update:对一个文档进行部分更新(POST /{index}/{type}/{id}/_update)
- delete:删除一个文档(DELETE /{index}/{type}/{id})
POST /_bulk
{"create":{"_index": "megacorp","_type":"employee","_id": "4"}} #create,不存在则创建,已存在则报错
{"first_name": "helilio",...} #注意body的两个花括号{}必须是在同一行
{"index":{"_index": "megacorp","_type":"employee","_id": "1"}} #不存在在创建,一存在则替换整个文档
{"first_name": "John",...}
{"update":{"_index": "megacorp", "_type": "employee", "_id": "2"}} #部分更新
{"doc":{"age": "11"}
{"delete":{"_index": "megacorp", "_type": "employee", "_id": "1"}}
分布式文档存储
路由一个文档到某个分片
shard = hash(_id) % number_of_primary_shards
因此索引库一旦创建后,则不能再修改主分片数,否则就无法正确的找到文档在索引库的哪个分片上
请求的接收方式
- 客户端将请求发送到专门的协调节点,协调节点会接收所有请求,并计算请求的数据路由到的分片所在的节点,将请求转发到分片所在节点,分片所在节点处理完后,向协调节点报告处理成,协调节点响应给客户端
- 轮询所有节点:没有专门的协调节点,而是轮询地发送请求到集群的所有节点,这样负载能力更强
集群对于更新操作的处理步骤
- 协调节点接收请求,路由算出数据应在哪个主分片上,并找到主分片所在的节点。然后转发请求到主分片所在节点
- 主分片所在节点处理完成后,将请求并行发送到副本分片所在节点上,副本分片处理成功后向主分片节点报告成功,
- 主分片节点向协调节点报告成功,协调节点向客户端报告成功
搜索-最基本的工具
- 空搜索:
GET /_search
查询所有索引库下的所有文档,默认返回前10个文档 - 多索引、多类型
/idx1,idx2/_search #索引库idx1,idx2下的所有文档
/_all/user,tweet/_search #所有索引库下类型为user,tweet的所有文档
/idx1,idx2/user,tweet/_search
/idx*,db*/_search #所有名称以idx或者db为前缀的索引库下的所有文档
- 分页:
/_search?size=5&from=10
一个请求可能跨越多个分片,那么分页是如何实现的呢?
假如我们所请求的索引共有5各分片,现在请求第一页,前10个文档,那么会先从每个分片上各取相关度前10的文档,然后汇总这5个分片的所有前10共50个文档,再对这50个文档的相关度进行排序,拿排名前10的10个文档作为请求的结果。 - 深度分页的问题:据上所述(分页的实现),如果我们是要拿第1000页的10个文档,即相关度排名为从10000到10010的10个文档。那么要各从5个分片上拿相关度排名前1000*10+10=10010的10010个文档,然后汇总这5个分片的共10010*5=50050个文档,再从这50050个文档中取出排名从10000到10010的10个文档。
因此对结果排序的成本,随着分页的深度成指数级上升,这就是任何web搜索引擎对任何查询都不要返回超过1000个结果的原因
映射和分析
精确值 和 全文文本
- es中的数据可以概括的分为两类:精确值、全文文本
- 查询精确值的结果:文档要么匹配、要么不匹配
- 查询全文文本的结果:文档的匹配度(相关度)有多大
- 很少对全文类型的域做精确匹配,而是希望在文本类型的域中搜索。
分析(分词 + 标准化)
对文本分析的过程
1. 将文本分成一个个的词条
2. 将每个词条统一化为标准格式
分析器就是执行上面的工作,分三个步骤:
1. 字符过滤器:在分词前先整理下字符串,如将’&’转成 and
2. 分词器:字符串被分成一个个的词条。例如空格分词器、
3. Token过滤器:每个词条按顺序通过Token过滤器,如将词条转为小写,提取词根,删除无意义词条如the 、a、的、了等,增加同义词如jump则加leap
es自带的分词器:
- 标准分析器(默认,最常用)
根据 Unicode 联盟 定义的 单词边界 划分文本;删除绝大部分标点;将词条小写。
- 简单分析器
在任何不是字母的地方分隔文本;将词条小写。
- 空格分析器
在空格的地方划分文本。(不进行小写处理)
- 语言分析器
特定语言分析器有许多可选语言,会考虑指定语言的特定。例如 ‘英语’:删除英语无意义词(and the a);提取词根;将词条小写。
测试分析器
GET /_analyze
{
"analyzer":"standard", #指定要测试的分析器
"text":" to test The Standard analyzer" #要分析的文本
}
域类型与域映射
简单核心域类型:
- 字符串:string
- 日期:date
- 整形:byte, short, integer, long
- 浮点型:float, double
- 布尔型:boolean
动态映射:
当索引一个文档,此文档中有一个新的域(属性),即在文档所属类型的映射中未出现过的域,es会对其进行动态映射(根据一些默认的规则,自动设置新域的映射)
新域的值类型 | 新域映射的类型 |
---|---|
整数:123 | long |
浮点数:123 | double |
字符串形式的有效日期:2017-01-01 | date |
普通字符串:foo bar | string |
布尔型:true 或 false | boolean |
查看映射:GET /{index}/_mapping/{type}
自定义域映射:
- 设置索引类型
"index" : "analyzed" #全文索引
"index" : "not_analyzed" #精确值索引
"index" : "no" #不索引:永远检索不到
- 设置分析器,默认为 standard 分析器, 但你可以指定一个内置的分析器替代它,例如 whitespace 、 simple 和 english:
"analyzer" : "english"
复杂核心域类型:
- 多值域:数组
- 空域:将不会被索引,有如下三种形式:
"null_value": null,
"empty_array": [],
"array_with_null_value": [ null ]
- 内部域(内部对象):
#如:
{
"age": 23
"name":{
"first" : "li",
"last" : "ling",
"full" : "li ling",
}
}
将被索引为:
{
"age":[23],
"name.first":[li],
"name.last":[ling],
"name.full":[li,ling]
}
}
使用DSL查询表达式检索
- 两种查询情况:过滤情况(filtering context)与 查询情况(query context)
过滤情况下,查询语句被设置成不评分的查询,只需判断是否匹配,因此性能较好,并且会被缓存。
查询情况下,查询语句被设置成评分的查询,即首先判断文档是否匹配,还要判断文档匹配的有多好 叶子语句(leaf clauses) 与 复合语句(bool)
重要叶子语句:- match_all ,相当于空查询
- match,即可作用于全文文本进行全文查询,又可作用于精确值域做精确匹配
multi_match:可以作用在多个字段上执行相同的match查询
{ "muti_match":{ "query" : "elastic", #共同的要查询的信息 "fields":[ "title" , "body"] } }
range:范围查询(gt, gte , lt , lte)
{ "range":{ "age" : { "lt" : "30", "gt": 20 } } }
term:精确值查询
{ "term": { "age" : 25 } } { "term": { "join_date" : "2017-01-01"} }
terms:精确值查询,允许匹配多个值
{ "terms": { "age" : [25,27,29] } }
exists 和 missing:查询某个字段是否有值
{ "exists": { "field " : "title" } }
复合语句bool的可接收的参数:
- must:文档必须匹配才能被包含进来
- must_not:文档必须不匹配才能被包含进来
- should:如果满足这些查询中的任意语句,将增加_score,主要用于相关性得分
- filter:必须匹配,但不以评分查询进行,只用于过滤
- constant_score:用来代替bool
{
"bool":{
"must":{"match":{"skills":["java","python"]}}, #最好会java或者python,会影响相关度得分_score
"must_not":{"range":{"exception_salary":{"gt":25000}}}, #期望薪资最好不超过25K,会影响相关度得分_score
"should" :{"match":{"skills":["hadoop","elastic"]}}, #会hadoop和elastic的优先,会影响相关度得分_score
"filter" :{"range":{"age":["lt","30"]}} # 年龄必须不能超过30岁,过滤掉30岁以上的,不参与相关度评分
}
}
- constant_score:用来代替bool,在bool里只有filter的情况下。
{
"constant_score":{
"filter" :{"range":{"age":["lt","30"]}} # 年龄必须不能超过30岁,过滤掉30岁以上的,不参与相关度评分
}
}
验证查询(/_validate/query?explain)
排序
自定义排序字段,及多级排序
默认是按照相关性排序,即按_score字段降序,但是也可以自己指定某个字段进行排序,也可以指定多个字段进行多级排序
{
"query":{...},
"sort" : {
"join_date" : { "order" : "desc"}, #先按照入党日期降序排序
"_score" : { "order" : "desc"} #再按照相关性得分降序排序
}
}
设置的排序字段是多值字段
,可以设置"mode" : "min" | "max" | "avg" | "sum"
{
"query":{...},
"sort" : {
"dates" : {
"mode" : "min",
"order" : "desc"
}, #先按照入党日期降序排序
}
}
字符串排序
字符串被分析器分析后也是一个多值字段,但是因为多个值的顺序总是不固定的,而且用设置 “` “mode”为 “min” 或者 “max” 也并不能达到我们想要的效果。
例如,我们原本有两个文档的content分别为“i love marry” 和 “i love lily”,我们想要的效果是按照字母顺序,含 lily的文档 应该 排在 含marry的文档前。
这时,我们可以对content的域映射做如下设置:
"content": {
"type": "string",
"analyzer": "english",
"fields": {
"raw": {
"type": "string",
"index": "not_analyzed" #设置此字段不被分析,不被分词,
}
}
}
{"sort":{"content.raw":{"order":"desc"}}}
相关性得分的计算方式
es的相似度算法 :检索词频率/反向文档频率= TF/IDF ,包括以下内容:
- 检索词频率(TF)
检索词在该字段出现的频率?出现频率越高,相关性也越高。 字段中出现过 5 次要比只出现过 1 次的相关性高。 - 反向文档频率(IDF)
每个检索词在索引库中出现的频率?频率越高,相关性越低。检索词出现在多数文档中会比出现在少数文档中的权重更低。 - 字段长度准则
字段的长度是多少?长度越长,相关性越低。 检索词出现在一个短的 title 要比同样的词出现在一个长的 content 字段权重更大。
单个查询可以联合使用 TF/IDF 和其他方式,比如短语查询中检索词的距离或模糊查询里的检索词相似度。
相关性并不只是全文本检索的专利。也适用于 yes|no 的子句,匹配的子句越多,相关性评分越高。
如果多条查询子句被合并为一条复合查询语句 ,比如 bool 查询,则每个查询子句计算得出的评分会被合并到总的相关性评分中。
explain 查看相关性得分_score的分值来源
GET /megacorp/employee/_search?explain&format=yaml
{
"query": {
"match": {
"about": "rock"
}
}
}
"_explanation": {
"description": "weight(tweet:honeymoon in 0)
[PerFieldSimilarity], result of:",
"value": 0.076713204,
"details": [
{
"description": "fieldWeight in 0, product of:",
"value": 0.076713204,
"details": [
{
"description": "tf(freq=1.0), with freq of:",
"value": 1,
"details": [
{
"description": "termFreq=1.0", #检索词 `honeymoon` 在这个文档的 `tweet` 字段中的出现次数。
"value": 1
}
]
},
{
"description": "idf(docFreq=1, maxDocs=1)", #检索词 `honeymoon` 在索引上所有文档的 `tweet` 字段中出现的次数。
"value": 0.30685282
},
{
"description": "fieldNorm(doc=0)",
"value": 0.25, #在这个文档中, `tweet` 字段内容的长度 -- 内容越长,值越小。
}
]
}
]
}
分布式检索:Query and Fetch(查询 然后 取回)
分页查询:
- 客户端发送检索请求到某节点,此节点成为本次请求的协调节点,协调节点向所有相关分片所在节点发送检索请求,
- 这些节点经过检索后,返回给协调节点一个_id 和_score的排序列表
取回:
- 协调节点从所有其它节点返回的数据中挑选出需要被取回的_id,再向相关节点发出_mget请求,
- 相关节点根据协调节点的_mget请求,获取丰富文档,返回给协调节点
- 协调节点返回文档给客户端
注意:深度分页本身并不符合人的行为,一般很少人翻到第50页,除非是爬虫spider或者机器人。因此最好直接做一个全局设置,比如_score小于某个值或者是数据量少于1000等等。
搜索参数
- preference(偏好):指定特定的节点处理检索请求,即设置检索偏好用哪些节点。
为了防止出现bouncing results(每次查询结果的顺序都不一样),因为分布式、异步的原因,主分片和每个副本分片上数据的timestamp都是不尽相同的,那如果恰好检索是按照timestamp排序的话,而两条数据的时间戳又是相同的,那如果两次查询落在了不同的分片上,就有可能出现查询的结果的顺序不一致的情况。
因此设置preference就很有必要,可以设置 _primary, _primary_first, _local, _only_node:xyz, _prefer_node:xyz, 和 _shards:2,3 。
建议用随机字符串,并且随机字符串与用户会话ID即session有关,这样可以使得用户的本次会话内相同检索请求只会被同一节点处理,又不会永远都要求这一节点处理,负载在长期时间上是均衡的。 - timeout(超时时间):检索的耗时符合水桶理论,检索时,需要从多个分片上先查询再取回,最终协调节点合并所有结果。如果有一个分片在查询或者取回过程耗时过长,那本次检索就因此而超时。因此设置timeout就很有必要。
超时检查是基于每个文档做的,即timeout在每个文档上都有值。
有时尽管一次检索总共的耗时时间早已超过设置的超时时间,但是超时的判断结果缺失timeout:false,这有可能有以下两点原因:
a. 有些查询有大量工作是在文档评估之前完成的,这些工作属于”setup”阶段,并不考虑超时设置。
b.因为时间检查是基于每个文档做的,因此一个查询已经在某个文档上执行并在下个文档被评估前永远不会超时,这意味着不良脚本比如无限循环的脚本会永远被执行下去 - routing(路由):有时存储文档时,客户端自行设定了路由方式。那么当检索时,客户端也能确定要检索的数据在哪些分片上,这时在检索时直接设定/_search?routing=shard1,shard2,这中设定在大规模搜索系统时就能派上用途
- search_type(搜索类型),默认搜索类型是query_then_fetch,如果想改善相关性精确度,可以设置为dfs_query_then_fetch
游标查询(规避深度分页)
游标查询用_doc字段来排序,避免深度分页需要将多个分片上的结果集合并的资源消耗。
游标查询分两个阶段:
- 查询初始化
- 批量拉取结果
开启游标需设置scroll(过期时间),游标查询首先要初始化,会保存当前时间点的数据快照,并在到了过期时间后,丢弃或刷新快照,因此批量的拉取结果需要在过期时间内完成。过期时间设置的足够批量拉取结果就可以了,因为保存的快照也很消耗资源,所以应该及时释放游标窗口。
#游标查询初始化
/_search?scroll=1m #保持游标查询窗口1分钟,过期时间为1分钟
{
"query":{..}
"sort": "_doc", #用_doc字段来排序,而不是_score
"size" :1000
}
#初始化会返回一个_scroll_id
{
"_scroll_id": "cXVlcnlUaGVuRm...",
"took": 7,
"timed_out": false,
...
}
#将初始化返回的_scroll_id作为参数,直接获取下一批结果
/_search/scroll
{
"scroll" : "1m",
"scroll_id" : "cXVlcnlUaGVuRmV0Y2g7NT..."
}
索引管理:优化索引和搜索
手动创建索引,自定义配置适量的分片数、分析器、映射
- 禁止自动创建索引: es.yml: action.auto_create_index: false
- 手动创建、配置索引:
PUT /index_name
{
"settings" : { ... }
"mappings" : {
"type1" : {},
"type2" : {}
}
}
删除索引( DELETE )
禁止通配符和_all方式,以避免误删索引:
es.yml : action.destructive_requires_name: true
DELETE /index_name
DELETE /index_a,index_b
DELETE /index*
DELETE /_all
DELETE /*
analysis - 配置及自定义分析器
分析器是用于将全文字符串转换成适合搜索的倒排索引。
在索引的设置中,用名称为analysis 的配置项来处理分析器相关的设置。
#创建一个分析器
PUT /my_index
{
"analysis": {
analyzer: {
"es_std" : { #创建一个名称为es_std的分析器
"type" : "standard",
"stop" : "_spanish_" # 用西班牙语的停用词列表
}
}
}
}
#以上只是创建了一个分析器,但是没有将它设置为任何字段的分析器
自定义分析器的几个项
- char_filter(字符过滤器):html_strip(html 清除),
- analyzer(分词器):stadard分词器,whitespace分词器
- filter(token filter 词条过滤器 ):lowercase,stop_words(停用词),stemmer(词干提取),ngram(部分匹配),edge_ngram(自动补全)
#自定义字符过滤器、词条过滤器
PUT /my_index
{
"settings":{
"analysis" : {
"char_filter":{ #创建自定义的字符过滤器:将&转成and
"&_to_and" : {
"type" : { "mapping" },
"mappings" : ["&=> and "]
}
},
"filter": { #创建自定义词条过滤器
"my_stop_words" : {
"type" : "stop",
"stop_words" : [ "the" , "a"]
}
},
"analyzer": { #创建自定义分析器
"my_analyzer" : {
"type" : "custom",
"char_filter" : [ "the" , "a"],
"tokennizer" : "standard",
"filter" : ["lowercase","my_stop_words"]
}
}
}
}
}
类型与映射
在同一个索引下,即便是不同类型下的属性(字段),如果名称相同,那么其映射属性也必须完全相同。不能出现字段名称相同,但是字段type不同或者idnex方式不同。
为什么?
因为在一个lucene索引中,字段是单一扁平的模式,lucene中并不存在类型的概念。在es中的不同类型下的同名属性,在lucene中其实会是同一个字段。
在es中,所有的类型最终其实共享相同的映射。
所以es中的类型,和关系型数据库中的表,的地位和性质完全不同。在关系型数据库中的表是一个完整的逻辑概念,但是在es中的类型,其实只是对多个字段的映射信息的一个命名而已。
比如有个文档: {"name":"liyl","age":33,"address":"shanghai"}
可以在es中可以映射出多种类型:
{"type1":{"properties":{"name":{"type":"string"},"age":{"type":"string"}}}}
{"type2":{"properties":{"age":{"type":"string"},"address":{"type":"string"}}}}
重要的一点是: 类型可以很好的区分同一个集合(索引的文档)中的不同细分。在不同的细分中数据的整体模式是相同的(或相似的)
技术上讲,同一索引下可以有多个类型,且他们的字段都不尽相同。
但是属性完全不同的类型不适合存储在同一索引中,因为这将意味着索引中将有一半的数据是空的(字段将是稀疏的),最终将导致性能问题,因此这种情况下最好放在两个单独的索引里。
根对象
许多人容易把字段 _source:{name:"lily",age:25}
的值对象理解为根对象,实际上在es中,或者说在Lucene中,_source:{}只是根对象的一个字段,是根对象的元数据。而_type和_id也是根对象的一个字段,是其元数据。
根对象的组成:
- 一个properties节点:列出了文档中可能包含的每个字段的映射信息,例:
"address" : {
"type" : "string | long | double | date | bool " ,
"index" : "analyzed | not_analyzed | no" , # 字段是否可以被当成全文来搜索,还是作为一个精准的值,还是完全不能被检索到
"analyzer" : "standard | simple | whitespace | english | spanish " #如果index设置为analyzed,即字段被当做全文来搜索,那么在对字段建索引和被搜索时,使用什么分析器
}
各种元数据:都是以下划线_开头的,如:_index、_type、_id、_all、_source
_source:是文档体,会被存储,当然也会占用存储空间。
可以设置为不被存储,但这并不会影响其被索引和检索。方式如下:PUT /my_index { "mappings" : { "my_type" : { "_source" :{ "enabled" : false } } } }
但是选择存储_source字段,则有更多好处:
可以直观的每个文档体的内容,更方便于调试。例如:检索时为什么没有检索到,相关性得分的分值等。
当映射改变,需要重新索引数据时,不需要从其它数据仓库中取数据
当需要看文档体的内容时,不需要再从其它数据仓库(如结构化数据库)中取。- _all:_all字段不被存储。
把文档体中所有属性的值拼接为一个字符串,然后将其作为一个全文字段被索引到es中,其名称为_all。
如果不再需要对_all字段进行全文检索,可以禁用它:
PUT /my_index/_mapping/my_type { "_all":{ "enabled" : "false" } }
include_in_all可以控制每个文档体中的属性,是否要被索引到_all字段中:
#首先在映射信息中,设置include_in_all:false #然后在某些属性的映射信息中,设置include_in_all:true PUT /my_index/my_type/_mapping { "my_type" : { "include_in_all" : false, "properties" : { "age" : { "type" : "byte" "include_in_all" : true } } } }
_all字段被索引时,仅经过分词器,默认用standard分词器,也可以设置为其它分词器:
PUT /my_index/_mapping { "my_type" : { "_all" : { "analyzer" : "simple" } } }
_uid :type#id拼接成的字符串,会被存储和索引。
- _id,_index:既不被存储,也不被索引,用_uid字段来派生出_id
- _index 被存储,但不被索引,因为有_id可以
- 设置项:控制如何动态处理新的字段,如analyzer 、 dynamic_date_formats 和 dynamic_templates
其它设置,可以同时应用在根对象和其它object类型的字段上,如:enabled、dynamic、include_in_all
dynamic 动态映射:true(自动映射新字段)、false(忽略新字段、不对其做映射)、strict(遇到新字段报错。提示管理员事先手动映射)
es对于文档体中出现的新属性会做动态映射,但这可能会出现非预期结果,比如文档体中出现字段”logging” : “2014-01-01”时,es会将logging字段映射为date类型,但实际上我们想将其作为一个字符串类型。
可以对某个类型的属性设置禁用动态映射,但有选择性的对其子文档的属性设置启用动态映射,:PUT /my_index { "mappings" : { "my_type" : { "dynamic" : "strict ", "properties": { "age" : { "type" : "byte "}, "address" : { "dynamic" : "true", "properties": { "state" : { "type" : "string"}, "province" : { "type" : "string"}, ... } } } } } }
date_detection : true 若字段未映射过,且为字符串,则自动检测其是否为符合日期类型规则,若是则将新字段动态映射为date类型。
可以设置不自动检测是否符合日期类型规则:PUT /my_index { "mappings" : { "my_type" : { "date_detection" : false, "properties" : {...} } } }
dynamic_date_formats settings 自定义日期类型字符串检测规则:
# The default value for dynamic_date_formats is: # [ "strict_date_optional_time","yyyy/MM/dd HH:mm:ss Z||yyyy/MM/dd Z"] PUT /my_index { "mappings" : { "my_type" : { "dynamic_date_formats" : ["yyyy/MM/dd","yyyy-MM-dd"] } } }
dynamic_templates,自定义动态映射规则
如,所有以_es结尾的字段都用spanish解析器,所有以_en结尾的字段都用english解析器:PUT /my_index { "mappings" : { "my_type" : { "dynamic_templates" : [ "es" : { "match" : "*_es", #如果字段以_es结尾 "match_mapping_type" : "string", #并且字段是string了下 "mapping" : { #那么将字段其做如下动态映射 "type" : "string", "analyzer" : "spanish" } }, "en" : { "match" : "*_en", #如果字段以_en结尾 "match_mapping_type" : "string", #并且字段是string了下 "mapping" : { #那么将字段其做如下动态映射 "type" : "string", "analyzer" : "english" } }, ] } } }
_default_ 所有类型的缺省映射
设置了_default_后,不必再为每个新类型重复设置个性化的默认映射配置PUT /my_index { "mappings" : { "_default_" : { "_all" : { "enabled" : false}, #默认每个类型都不索引_all字段, "date_detection" : false, #默认不检测日期字段 "dynamic_date_formats" : ["MM/dd/yyyy"], "dynamic" : "strict" #如果有未映射的新字段出现,报错 } } }
重新索引数据
es支持添加新的字段映射,但不支持改动某字段的映射信息,若有如此需求,则需重新索引。重新索引数据,需要以下步骤
- 创建新索引,及新的映射信息(类型)
- 用游标scoll=1m大量读取数据,然后用_bulk批量将数据索引到新索引库中。
并行执行重建索引的任务更有效率,但是注意并行执行时的任务分割方式,一般可以用时间来分割,如下:
GET /old_index/_search?scorll=1m
{
"query" : {
"range" : {
"date" : {
"gt" : "2014-01-01",
"lt" : "2015-01-01"
}
}
},
"sort" : ["_doc"],
"size" : 1000
}
深入搜索
结构化搜索
constant_score、filters、term :精确值查找 :缓存
- 请尽可能多的使用filters过滤式查询,因为过滤式查询速度非常快,不会计算相关度(直接跳过了整个评分阶段),而且容易被缓存。
- term
term查询不会对查询词做分析,直接以查询词原样做精确查询。
适用于number\date\bool类型字段的查询,
适用于string类型的not_analyzed字段的查询。
但不适用于string类型的analyzed字段,比如对于文档:
{"product_name":"圆珠笔","product_no":"XHDK-A-1293-#fJ3"}
做查询:
{"term" : { "product_no" : "XHDK-A-1293-#fJ3" } }
查询结果的total : 0。
为什么?因为es会对文档中的XHDK-A-1293-#fJ3经过分析器分析后,可能被索引成了xhdk 1293 fj3等几个词条,而term查询则不对查询词XHDK-A-1293-#fJ3做任何分析。 - 过滤器执行查询时的操作:
- 查找匹配文档
- 为每个查询创建一个bitset(一个只包含0和1 的数组),将匹配的文档的bitset位置为1。
- 启发式的迭代所有查询的bitset,一般来说先迭代稀疏的bitset,因为这样可以过滤掉大量文档。同理,在执行组合查询时,也会先执行filters查询,因为非评分查询速度快,又能过滤掉大量文档。
- 有选择性地缓存一些使用频率高的查询。
bool :复合过滤器(compound filter)
- must:AND(所有的语句都必须匹配)
- must_not :NOT(所有的语句都必须不匹配)
- should :OR(至少要匹配一个语句)
部分匹配
edge-Ngrams 实现 search as you type(即时搜索)
- 自定义auto_complete过滤器
filter { auto_complete { type : edge_ngram , min_gram : 1 , max_gram : 20}} - 自定义auto_complete分析器
analyzer { auto_complete { type : custom , filter : [ lowercase , auto_complete ] } - 设置字段映射信息的解析器为auto_complete,
注意同时需要设置 search_analyzer :standard,
PUT /my_index/_mapping/typex
{…fieldx { analyzer : auto_complete , search_analyzer : standard }}
即时搜索的另一种实现方式
是将所有可能词加载到内存。然后直接给出用户提示,而非实时查询索引。