es学习笔记

为什么用elasticsearch

在引入elasticsearch前,我们的数据一般都存储在mysql上,所有的检索都是直接在数据库的查询,当数据库的数据量达到一定量时,数据库的检索效率就会很低,对此我们或许会有很多解决方案,比如对数据库采用分库分表(主从设置),数据库分库分表的确可以解决大部分数据量性能问题,但是同样还是会有两个问题无法避免:

单库单表数据过大,采用分表多表的关联查询难度很大,不易实现(目前市场上主流的集中分库分表插件都没有很好的解决)

没有建立索引的字段查询效率依旧不高 ; 前模糊、全模糊查询还会使索引失效;

精准度匹配,搜索结果低。

于是基于以上场景,出现了全文检索,我们去分析elasticsearch的几个特点:分布式(多shard的方式保证数据安全,也会提供自动resharding),一般不会因为数据量有性能问题,实时:elasticsearch的检索速度非常快,接近实时(注意刚刚存储的数据) 搜索引擎:相比Hbase,es的定位就是搜索索引,支持全文检索;

elasticsearch是什么

官方概念:elasticsearch是一个基于Lucene的搜索服务器。它提供了一个分布式多用户能力的全文搜索引擎,基于RESTful web接口。Elasticsearch是用Java开发的,并作为Apache许可条款下的开放源码发布,是当前流行的企业级搜索引擎。设计用于云计算中,能够达到实时搜索,稳定,可靠,快速,安装使用方便

比较笼统,可以更加直观地描述:

分布式的实时文件存储,每个字段都被索引并可被搜索

分布式的实时分析搜索引擎

可以扩展到上百台服务器,处理PB级结构化或非结构化数据

其实只要知道这几个关键字:分布式实时搜索引擎

Elasticsearch基本概念

Node 与 Cluster(节点与集群)

Elastic 本质上是一个分布式数据库,允许多台服务器协同工作,每台服务器可以运行多个 Elastic 实例。

单个 Elastic 实例称为一个节点(node)。一组节点构成一个集群(cluster)

节点(node): 一个节点是一个逻辑上独立的服务,可以存储数据,并参与集群的索引和搜索功能, 一个节点也有唯一的名字,群集通过节点名称进行管理和通信. 主节点:主节点的主要职责是和集群操作相关的内容,如创建或删除索引,跟踪哪些节点是群集的一部分,并决定哪些分片分配给相关的节点。稳定的主节点对集群的健康是非常重要的。虽然主节点也可以协调节点,路由搜索和从客户端新增数据到数据节点,但最好不要使用这些专用的主节点。一个重要的原则是,尽可能做尽量少的工作。 对于大型的生产集群来说,推荐使用一个专门的主节点来控制集群,该节点将不处理任何用户请求。 数据节点:持有数据和倒排索引。 客户端节点:它既不能保持数据也不能成为主节点,该节点可以响应用户的情况,把相关操作发送到其他节点;客户端节点会将客户端请求路由到集群中合适的分片上。对于读请求来说,协调节点每次会选择不同的分片处理请求,以实现负载均衡。 部落节点:部落节点可以跨越多个集群,它可以接收每个集群的状态,然后合并成一个全局集群的状态,它可以读写所有节点上的数据。

Index(索引)

索引是一类文档的集合 ES会索引所有字段,经过处理后写入一个反向索引(Inverted Index)。查找数据的时候,直接查找该索引。所以,Elastic 数据管理的顶层单位就叫做 Index(索引)。它是单个数据库的同义词。每个 Index 的名字必须是小写(可以把把索引当成数据库)。不能超过255个字节。

type(类型)

类型可以理解成一个索引的逻辑分区,用于标识不同的文档字段 信息的集合。但是由于ES还是以索引为粗粒度的单位,因此一个索引下的所有的类型,都存放在一个索引下。这也就导致不同类型相同字段名字的字段会存在类型定义冲突的问题。类比传统的关系型数据库领域来说,类型相当于“表”。

document(文档)

文档是存储数据信息的基本单元,使用json来表示。

Mapping

相当于数据库中的schema,用来约束字段的类型,不过 Elasticsearch 的 mapping 可以自动根据数据创建ES中,所有的文档在存储之前都要首先进行分析。用户可根据需要定义如何将文本分割成token、哪些token应该被过滤掉,以及哪些文本需要进行额外处理等等。

分片(shard)

ES的“分片(shard)”机制可将一个索引内部的数据分布地存储于多个节点,它通过将一个索引切分为多个底层物理的Lucene索引完成索引数据的分割存储功能,这每一个物理的Lucene索引称为一个分片(shard)。 每个分片其内部都是一个全功能且独立的索引,因此可由集群中的任何主机存储。创建索引时,用户可指定其分片的数量,默认数量为5个。

shard与replica (分片与备份)

在ES中,索引会备份成分片,每个分片是独立的lucene索引,可以完成搜索分析存储等工作。

分片的好处:

1 如果一个索引数据量很大,会造成硬件硬盘和搜索速度的瓶颈。如果分成多个分片,分片可以分摊压力。

2 分片允许用户进行水平的扩展和拆分

3 分片允许分布式的操作,可以提高搜索以及其他操作的效率

备份的好处

1 当一个分片失败或者下线时,备份的分片可以代替工作,提高了高可用性。

2 备份的分片也可以执行搜索操作,分摊了搜索的压力。

ES默认在创建索引时会创建5个分片,这个数量可以修改。

es特点

可扩展性

技术整合

部署简单

接口简单

功能强大

初始

关系型数据库中两个数据表示是独立的,即使他们里面有相同名称的列也不影响使用,但ES 中不是这样的。elasticsearch是基于Lucene开发的搜索引擎,而ES中不同type下名称相同 的filed最终在Lucene中的处理方式是一样的。

两个不同type下的两个user_name,在ES同一个索引下其实被认为是同一个filed,你必 须在两个不同的type中定义相同的filed映射。否则,不同type中的相同字段名称就会在 处理中出现冲突的情况,导致Lucene处理效率下降。

去掉type就是为了提高ES处理数据的效率。

Elasticsearch 7.x , URL中的type参数为可选。比如,索引一个文档不再要求提供文档类型。

Elasticsearch 8.x , 不再支持URL中的type参数。 解决:将索引从多类型迁移到单类型,每种类型文档一个独立索引

类比mysql:

在mysql中创建一个数据库,就相当于在es中创建一个索引

在mysql数据库中创建表,就相当于在es中创建一个类型

在mysql表中插入数据,就相当于是es创建文档,json格式

在mysql表中的列数据,就相当于es中的文档(json)的属性

Relational DB -> Databases -> Tables -> Rows -> Columns

Elasticsearch -> Indices -> Types -> Documents -> Fields

为什么删掉type

index、type的初衷

之前es将index、type类比于关系型数据库(例如mysql)中database、table,这么考虑的目的是“方便管理数据之间的关系”。

为什么现在要移除type?

在关系型数据库中table是独立的(独立存储),但es中同一个index中不同type是存储在同一个索引中的(lucene的索引文件),因此不同type中相同名字的字段的定义(mapping)必须一致。

不同类型的“记录”存储在同一个index中,会影响lucene的压缩性能。

【总结: es基于Lucene的索引文件扩展type结构,也受限制与同一个index里的不同的type存储再同样一个索引文件里这样不同type里的相同名字的field mapping必须一致,事实上数据库中的不同的table表是独立存储的,不同的table里的字段是独立设计的。于是type的用途非常有限,比如下面的替换方案】

我们一直认为ES中的“index”类似于关系型数据库的“database”,而“type”相当于一个数据表。ES的开发者们认为这是一个糟糕的认识。例如:关系型数据库中两个数据表示是独立的,即使他们里面有相同名称的列也不影响使用,但ES中不是这样的。

我们都知道elasticsearch是基于Lucene开发的搜索引擎,而ES中不同type下名称相同的filed最终在Lucene中的处理方式是一样的。举个例子,两个不同type下的两个user_name,在ES同一个索引下其实被认为是同一个filed,你必须在两个不同的type中定义相同的filed映射。否则,不同type中的相同字段名称就会在处理中出现冲突的情况,导致Lucene处理效率下降。

去掉type能够使数据存储在独立的index中,这样即使有相同的字段名称也不会出现冲突,去掉type就是为了提高ES处理数据的效率。

除此之外,在同一个索引的不同type下存储字段数不一样的实体会导致存储中出现稀疏数据,影响Lucene压缩文档的能力,导致ES查询效率的降低。

Elasticsearch 移除 type 之后的新姿势

随着 7.0 版本的即将发布,type 的移除也是越来越近了,在 6.0 的时候,已经默认只能支持一个索引一个 type 了,7.0 版本新增了一个参数 include_type_name ,即让所有的 API 是 type 相关的,这个参数在 7.0 默认是 true,不过在 8.0 的时候,会默认改成 false,也就是不包含 type 信息了,这个是 type 用于移除的一个开关。

让我们看看最新的使用姿势吧,当 include_type_name 参数设置成 false 后:

索引操作:PUT {index}/{type}/{id}需要修改成PUT {index}/_doc/{id}

Mapping 操作:PUT {index}/{type}/_mapping 则变成 PUT {index}/_mapping

所有增删改查搜索操作返回结果里面的关键字 _type 都将被移除

父子关系使用 join 字段来构建

替换策略

一个index只存储一种类型的“记录”。这种方案的优点:

a)lucene索引中数据比较整齐(相对于稀疏),利于lucene进行压缩。

b)文本相关性打分更加精确(tf、idf,考虑idf中命中文档总数)

用一个字段来存储type,如果有很多规模比较小的数据表需要建立索引,可以考虑放到同一个index中,每条记录添加一个type字段进行区分。这种方案的优点:

a)es集群对分片数量有限制,这种方案可以减少index的数量。

REST风格说明

一种软件架构风格,而不是标准,只是提供了一组设计原则和约束条件。它主要运用于客户端和服务器交互类的软件。基于这种风格设计的软件可以更简洁,更有层次,更易于实现缓存机制。

基本REST命令说明:

methodurl地址描述

PUTlocalhost:9200/index/type/id创建文档(指定id)

POSTlocalhost:9200/index/type创建文档(随机id)

POSTlocalhost:9200/index/type/id/_update修改文档

DELETElocalhost:9200/index/type/id删除文档

GETlocalhost:9200/index/type/id通过id获取文档

POSTlocalhost:9200/index/type/_search查询所有文档

初步检索

_cat

GET / _cat/nodes:查看所有节点

GET /_cat/health:查看es健康状况

GET /_cat/master:查看主节点

GET /_cat/indices:查看所有索引,类似show databases;

索引一个文档(保存)

保存一个数据,保存在那个索引的那个类型下,指定用那个唯一标识。

PUT customer/external/1 {“name”:“小张”} :zai1customer索引下的external类型下保存1号数据

这里使用post和put都是可以的,POST新增。如果不指定id,会自动生成id。指定id就会修改这个数据,并新增版本号。PUT可以新增可以修改。PUT必须指定id;由于PUT需要指定id,我们一般都用来做修改操作,不指定id会报错。

查询文档

get customer/external/1

更新文档

POST customer/external/1/_update

{

"doc":{

   "name":"小李"

  }

}

或者

POST customer/external/1/_update

{

   "name":"小李"

}

或者

PUT customer/external/1/_update

{

   "name":"小李"

}

不同:

POST操作会对比源文档数据,如果相同不会有什么操作,文档version不增加。

PUT操作总会将数据重新保存并增加version 版本;

带_update对比元数据如果一样就不进行任何操作。看场景;

    对于大并发更新,不带update;

    对于大并发查询偶尔更新,带update,对比更新,重新计算分配规则。更新同时增加属性

更新时增加属性

POST customer/external/1/_update

{

"doc":{

   "name":"小李",

   "age":20

  }

}

put和post不带_update也可以

删除文档&索引

DELETE /customer/external/1

DELETE customer

bulk批量API

POST /customer/external/_bulk

{"index":{  "_id":"1" }}

{"name":"jayChou"}

{"index":{  "_id":"2" }}

{"name":"jayChou"}

POST /_bulk

{"delete":{"_index":"website","_type":"blog","_id":"123"}}

{"create":{"_index":"website","_type":"blog","_id":"123"}}

{"title":"测试一下"}

{"index":{"_index":"website","_type":"blog","_id":"123"}}

{"title":"再测试一下"}

{"update":{"_index":"website","_type":"blog","_id":"123"}}

{"doc":{"title":"更新测试!"}}

Search API

es支持两种基本方式检索

一个是通过使用restfulURI发送搜索参数(uri+检索参数)

另一个是通过使用Rest request body来发送(uri+请求体)

GET /bank/_search

{

  "query": { "match_all": {} },

  "sort": [

   { "account_number": "asc" }

  ],

  # 查询10条数据,从10开始,即查询10-19的记录

  "from": 10,

  "size": 10

}

1)、基本语法格式 Elasticsearch提供了一个可以执行查询的 Json风格的DSL(domain-specific language领域特定语言)。这个被称为Query DSL。该查询语言非常全面,并且刚开始的时候感觉有点复杂,真正学好它的方法是从一些基础的示例开始的。

一个查询语句的典型结构

GET /bank/_search

{

    "query":{

        "match_all":{}

    },

    "from":5,

    "size":5,

    "sort":[

        {

            "balance":{

                "order":"desc"

            }

        }

    ],

    # 返回的属性(字段)

    "_source":["balance","firstname"]

}

query定义如何查询,

match_all查询类型【代表查询所有的所有】,es中可以在query中组合非常多的查询类型完成复杂查询

除了query参数之外,我们也可以传递其它的参数以改变查询结果。如sort,sizefromtsize限定,完成分页功能

sort排序,多字段排序,会在前序字段相等时后续字段内部排序,否则以前序为准

match

# 基本类型(非字符型)

GET bank/_search

{

  "query": {

   "match": {

     # 基本类型精准匹配

     "balance": "16418"

   }

  }

}

# 字符型,全文检索,进行相关性得分,全文检索按照评分进行排序,会对检索条件进行分词匹配

GET bank/_search

{

  "query": {

   "match": {

     "address": "Kings"

   }

  }

}

# 短语匹配,将需要匹配的值当成一个整体单词(不分词)进行检索

GET bank/_search

{

  "query": {

   "match_phrase": {

     "address": "mill road"

   }

  }

}

GET bank/_search

{

  "query": {

   "match": {

    # keyword也可以进行不分词检索,但是这个值要作为字段的全部值才可以,match_phrase是查询完整短语就可以了

     "address.keyword": "mill road"

   }

  }

}

# 多字段匹配,检索字段address或者字段city包含字符串mill或者movico(会进行分词)的记录

GET bank/_search

{

  "query": {

   "multi_match": {

     "query": "mill movico",

     "fields": ["address","city"]

   }

  }

}

# bool查询,must为必须满足的条件,must_not必须不满足条件,should表示满足最好,不满足也可以,满足是得分高

GET bank/_search

{

  "query": {

   "bool": {

     "must": [

       {"match": {

         "gender": "F"

       }},

       {

         "match": {

           "address": "mill"

         }

       }

     ],

     "must_not": [

       {"match": {

         "age": "38"

       }}

     ],

     "should": [

       {"match": {

         "lastname": "Long"

       }

       }

     ]

   }

  }

}

# term,和match一样,匹配某个属性的值,全文检索字段用match,其他非text字段匹配用term

GET /bank/_search

{

  "query": {

   "term": {

       "address": "mill"

     }

   }

}

分析

# 年龄范围,不使用filter,会有相关性得分

GET bank/_search

{

  "query": {

   "bool": {

     "must": [

       {

         "range": {

           "age": {

             "gte": 10,

             "lte": 30

           }

         }

       }

     ]

   }

  }

}

# 使用filter,不会相关性得分,max_socre和_score都为0,filter具有过滤功能

GET bank/_search

{

  "query": {

   "bool": {

     "filter": {

       "range": {

         "age": {

           "gte": 10,

           "lte": 30

         }

       }

     }

   }

  }

}

聚合 聚合提供了从数据中分组和提取数据的能力。最简单的聚合方法大致等于sQL GROUPBY和sQL聚合函数。在Elasticsearch中,您有执行搜索返回hits(命中结果)﹐并且同时返回聚合结果,把一个响应中的所有 hits(命中结果)分隔开的能力。这是非常强大且有效的,您可以执行查询和多个聚合,并且在一次使用中得到各自的(任何一个的)返回结果,使用一次简洁和简化的API来避免网络往返。

# 搜索address总包含mill的所有人的年龄分布以及平均年龄,但是不显示这些人的详情

GET bank/_search

{

  "query": {

   "match": {

     "address": "mill"

   }

  },

  "aggs": {

   "ageAgg": {

     "terms": {

       "field": "age",

       "size": 10   # 只取前10种可能(age分组可能有很多组)

     }

   },

   "ageAvg":{

     "avg": {

       "field": "age"

     }

   },

   "balanceAvg":{

     "avg": {

       "field": "balance"

     }

   }

  }

}

# 按照年龄聚合,并且请求这些年龄段的这些人的平均薪资

GET bank/_search

{

  "query": {

   "match_all": {}

  },

  "aggs": {

   "ageAgg": {

     "terms": {

       "field": "age",

       "size": 10

     },

     "aggs":{

       "balanceAvg":{

         "avg": {

           "field": "balance"

         }

       }

     }

   }

  }

}

# 查出所有年龄分布,并且这些年龄段中M的平均薪资和F的平均薪资以及这个年龄段的总体平均薪资

GET bank/_search

{

  "query": {

   "match_all": {}

  },

  "aggs": {

   "ageAgg": {

     "terms": {

       "field": "age",

       "size": 100

     },

     "aggs": {

       "genderAgg": {

         "terms": {

     # text的文本字段,在进行聚合时,无法进行计算,应该使用keyword属性进行替代,进行精确匹配

           "field": "gender.keyword",

           "size": 2

         },

         "aggs": {

           "balanceAvg": {

             "avg": {

               "field": "balance"

             }

           }

         }

       },

       "ageBalance": {

         "avg": {

           "field": "balance"

         }

       }

     }

   }

  }

}

搜索工具

使用 Elasticsearch 作为一个简单的 NoSQL 风格的分布式文档存储系统。我们可以将一个 JSON 文档扔到 Elasticsearch 里,然后根据 ID 检索。但 Elasticsearch 真正强大之处在于可以从无规律的数据中找出有意义的信息——从“大数据”到“大信息”。

Elasticsearch 不只会存储(stores) 文档,为了能被搜索到也会为文档添加索引(indexes) ,这也是为什么我们使用结构化的 JSON 文档,而不是无结构的二进制数据。

文档中的每个字段都将被索引并且可以被查询 。不仅如此,在简单查询时,Elasticsearch 可以使用 所有(all) 这些索引字段,以惊人的速度返回结果。这是你永远不会考虑用传统数据库去做的一些事情。

搜索(search) 可以做到:

在类似于 gender 或者 age 这样的字段 上使用结构化查询,join_date 这样的字段上使用排序,就像SQL的结构化查询一样。

全文检索,找出所有匹配关键字的文档并按照相关性(relevance) 排序后返回结果。

以上二者兼而有之。

很多搜索都是开箱即用的,为了充分挖掘 Elasticsearch 的潜力,你需要理解以下三个概念:

映射(Mapping):描述数据在每个字段内如何存储

分析(Analysis):全文是如何处理使之可以被搜索的

领域特定查询语言(Query DSL):Elasticsearch 中强大灵活的查询语言

结果分析

"hits": [

   {

"_index":"us",//那个索引

"_type":"tweet",//那个类型

"_id":"7",//id

"_score":1,

"_index":" customer",//在哪个索引

"_type":"external",//在哪个类型

"_id":"1",//记录id

"_version":2,//版本号

"_seq_no":1,//并发控制字段,每次更新就会+1,用来做乐观锁

" _primary_term":1,//同上,主分片重新分配,如重启,就会变化

"found":true,

"_source": {//真正的内容

"name":"John Doe"

   }

},


并发控制:更新携带?if_seq_no=0&f_primary_term=1

响应还提供有关搜索请求的以下信息:

Taked–Elasticsearch运行查询所用的时间(毫秒)

timed_out–搜索请求是否超时

_shards–搜索了多少个shard,以及成功、失败或跳过的shard的详细信息。

max_score–找到的最相关文档的分数

hits.total.value-找到了多少匹配的文件

hits.sort-文档的排序位置(不按相关性分数排序时)

hits._score-文档的相关性分数(使用match\u all时不适用)

分页

和 SQL 使用 LIMIT 关键字返回单个 page 结果的方法相同,Elasticsearch 接受 from 和 size 参数:

size

显示应该返回的结果数量,默认是 10

from

显示应该跳过的初始结果数量,默认是 0

在分布式系统中深度分页

理解为什么深度分页是有问题的,我们可以假设在一个有 5 个主分片的索引中搜索。 当我们请求结果的第一页(结果从 1 到 10 ),每一个分片产生前 10 的结果,并且返回给 协调节点 ,协调节点对 50 个结果排序得到全部结果的前 10 个。

现在假设我们请求第 1000 页--结果从 10001 到 10010 。所有都以相同的方式工作除了每个分片不得不产生前10010个结果以外。 然后协调节点对全部 50050 个结果排序最后丢弃掉这些结果中的 50040 个结果。

可以看到,在分布式系统中,对结果排序的成本随分页的深度成指数上升。这就是 web 搜索引擎对任何查询都不要返回超过 1000 个结果的原因。

轻量搜索

有两种形式的搜索一种是 “轻量的” 查询字符串 版本,要求在查询字符串中传递所有的 参数,另一种是更完整的 请求体 版本,要求使用 JSON 格式和更丰富的查询表达式作为搜索语言。

查询字符串搜索非常适用于通过命令行做即席查询。例如,查询在 tweet 类型中 tweet 字段包含 elasticsearch 单词的所有文档:

GET/_all/tweet/_search?q=tweet:elasticsearch

下一个查询在 name 字段中包含 john 并且在 tweet 字段中包含 mary 的文档。实际的查询就是这样

+name:john +tweet:mary

_all 字段,这个简单搜索返回包含 mary 的所有文档:

GET/_search?q=mary

下面的查询针对tweents类型,并使用以下的条件:

name字段中包含mary或者john

date值大于2014-09-10

_all字段包含aggregations或者geo

+name:(maryjohn)+date:>2014-09-10+(aggregationsgeo)

倒排索引

索引特点:索引词典指向被索引的文档,

b+tree:索引词典指向被词条数据

传统的b+tree是通过,磁盘巡道来提高查询性能,减少磁盘巡道次数,减少磁盘指针落下的次数

es:直接通过内存找, 但是全放在内存,也有点不显示,所以出现Term index

使文本可被搜索:Elasticsearch 使用一种称为 倒排索引 的结构,它适用于快速的全文搜索。一个倒排索引由文档中所有不重复词的列表构成,对于其中每个词,有一个包含它的文档列表。

传统的数据库每个字段存储单个值,但这对全文检索并不够。文本字段中的每个单词需要被搜索,对数据库意味着需要单个字段有索引多值(这里指单词)的能力。

倒排索引:支持一个字段多个值匹配的数据结构,他包含一个有序列表,列表包含所有文档出现过的不重复个体称为 词项,对于每一个词项,包含了它所有曾出现过文档的列表。

这个倒排索引相比特定词项出现过的文档列表,会包含更多其它信息。它会保存每一个词项出现过的文档总数, 在对应的文档中一个具体词项出现的总次数,词项在文档中的顺序,每个文档的长度,所有文档的平均长度,等等。这些统计信息允许 Elasticsearch 决定哪些词比其它词更重要,哪些文档比其它文档更重要。

倒排索引表:维护了那个单词在那个记录中都有。

检索时,首先进行分词,然后去找倒排索引表进行查询记录,只要包含一个或多个词,就会查出记录,并做相关性评分。例如“红海特工行动”就会查出12345的记录,但是3号1号记录特别符合,5号记录较为符合,3号5号记录分别是3个词命中两个,和4个命中两个,所以3和5号记录相比,3好的评分就会高一些。这就是大概的一个原理。

不变性

倒排索引被写入磁盘后是 不可改变 的:它永远不会修改。 不变性有重要的价值:

不需要锁。如果你从来不更新索引,你就不需要担心多进程同时修改数据的问题

一旦索引被读入内核的文件系统缓存,便会留在哪里,由于其不变性。只要文件系统缓存中还有足够的空间,那么大部分读请求会直接请求内存,而不会命中磁盘。这提供了很大的性能提升

其它缓存(像filter缓存),在索引的生命周期内始终有效。它们不需要在每次数据改变时被重建,因为数据不会变化。

写入单个大的倒排索引允许数据被压缩,减少磁盘 I/O 和 需要被缓存到内存的索引的使用量。

当然,一个不变的索引也有不好的地方。主要事实是它是不可变的!你不能修改它。如果你需要让一个新的文档可被搜索,你需要重建整个索引。这要么对一个索引所能包含的数据量造成了很大的限制,要么对索引可被更新的频率造成了很大的限制。

怎在保留不变性的前提下实现倒排索引的更新

用更多的索引。通过增加新的补充索引来反映新近的修改,而不是直接重写整个倒排索引。每一个倒排索引都会被轮流查询到,从最早的开始,查询完后再对结果进行合并

Elasticsearch 基于 Lucene, 这个 java 库引入了按段搜索的概念。 每一  本身都是一个倒排索引, 但 索引 在 Lucene 中除表示所有  的集合外, 还增加了 提交点 的概念 ,就是 一个列出了所有已知段的文件 。

新的文档首先被添加到内存索引缓存中,然后写入到一个基于磁盘的段。

逐段搜索会以如下流程进行工作:

新文档被收集到内存索引缓存

不时地, 缓存被 提交 :

一个新的段--一个追加的倒排索引--被写入磁盘。

一个新的包含新段名字的 提交点 被写入磁盘。

磁盘进行 同步 — 所有在文件系统缓存中等待的写入都刷新到磁盘,以确保它们被写入物理文件。

新的段被开启,让它包含的文档可见以被搜索。

内存缓存被清空,等待接收新的文档。

== 一次完整的提交会将段刷到磁盘,并写入一个包含所有段列表的提交点。Elasticsearch 在启动或重新打开一个索引的过程中使用这个提交点来判断哪些段隶属于当前分片。==

当一个查询被触发,所有已知的段按顺序被查询。词项统计会对所有段的结果进行聚合,以保证每个词和每个文档的关联都被准确计算。 这种方式可以用相对较低的成本将新文档添加到索引。

文档的删除和更新

段是不可改变的,所以既不能从把文档从旧的段中移除,也不能修改旧的段来进行反映文档的更新。 取而代之的是,每个提交点会包含一个 .del 文件,文件中会列出这些被删除文档的段信息。

当一个文档被 “删除” 时,它实际上只是在 .del 文件中被 标记 删除。一个被标记删除的文档仍然可以被查询匹配到, 但它会在最终结果被返回前从结果集中移除。

文档更新也是类似的操作方式:当一个文档被更新时,旧版本文档被标记删除,文档的新版本被索引到一个新的段中。 可能两个版本的文档都会被一个查询匹配到,但被删除的那个旧版本文档在结果集返回前就已经被移除。

在段合并中, 我们展示了一个被删除的文档是怎样被文件系统移除的。

近实时搜索

在 Elasticsearch 中,写入和打开一个新段的轻量的过程叫做 refresh 。 默认情况下每个分片会每秒自动刷新一次。这就是为什么我们说 Elasticsearch 是  实时搜索:文档的变化并不是立即对搜索可见,但会在一秒之内变为可见。

这些行为可能会对新用户造成困惑: 他们索引了一个文档然后尝试搜索它,但却没有搜到。这个问题的解决办法是用 refresh API 执行一次手动刷新:

POST/_refresh刷新(Refresh)所有的索引。

POST/blogs/_refresh只刷新(Refresh)blogs索引。

持久化变更

如果没有fsync(同步内存中所有已修改的文件数据到储存设备),我们不能保证数据在断电或者是正常退出依然存在。为了保持可靠性,需要确保数据被持久化。

即使通过每秒刷新(refresh)实现了近实时搜索,我们仍然需要经常进行完整提交来确保能从失败中恢复。但在两次提交之间发生变化的文档:

Elasticsearch 增加了一个 translog ,或者叫事务日志,在每一次对 Elasticsearch 进行操作时均进行了日志记录。translog 提供所有还没有被刷到磁盘的操作的一个持久化纪录。当 Elasticsearch 启动的时候, 它会从磁盘中使用最后一个提交点去恢复已知的段,并且会重放 translog 中所有在最后一次提交后发生的变更操作。

translog 也被用来提供实时 CRUD 。当你试着通过ID查询、更新、删除一个文档,它会在尝试从相应的段中检索之前, 首先检查 translog 任何最近的变更。这意味着它总是能够实时地获取到文档的最新版本。

分析与分析器

分析 包含下面的过程:

首先,将一块文本分成适合于倒排索引的独立的 词条

之后,将这些词条统一化为标准格式以提高它们的“可搜索性”

分析器执行上面的工作。 分析器 实际上是将三个功能封装到了一个包里:

字符过滤器

首先,字符串按顺序通过每个 字符过滤器 。他们的任务是在分词前整理字符串。一个字符过滤器可以用来去掉HTML,或者将 & 转化成 and。

分词器

其次,字符串被 分词器 分为单个的词条。一个简单的分词器遇到空格和标点的时候,可能会将文本拆分成词条。

Token 过滤器

最后,词条按顺序通过每个 token 过滤器 。这个过程可能会改变词条(例如,小写化 Quick ),删除词条(例如, 像 a, and, the 等无用词),或者增加词条(例如,像 jump 和 leap 这种同义词)。

什么时候使用分析器

当我们 索引 一个文档,它的全文域被分析成词条以用来创建倒排索引。 但是,当我们在全文域 搜索 的时候,我们需要将查询字符串通过 相同的分析过程 ,以保证我们搜索的词条格式与索引中的词条格式一致。

全文查询,理解每个域是如何定义的,因此它们可以做 正确的事:

当你查询一个 全文 域时, 会对查询字符串应用相同的分析器,以产生正确的搜索词条列表。

当你查询一个 精确值 域时,不会分析查询字符串, 而是搜索你指定的精确值。

映射

Elasticsearch 支持 如下简单域类型:

字符串: string

整数 : byte, short, integer, long

浮点数: float, double

布尔型: boolean

日期: date

当未指定索引文档的属性类型时,会通过JSON中基本数据类型,尝试猜测域类型。

这意味着如果你通过引号( "123" )索引一个数字,它会被映射为 string 类型,而不是 long 。但是,如果这个域已经映射为 long ,那么 Elasticsearch 会尝试将这个字符串转化为 long ,如果无法转化,则抛出一个异常。

mapping是定义文档如何被处理的,属性字段是如何存储和检索的,

# 查看映射

GET bank/_mapping

# 创建索引指定映射

PUT /my_index

{

  "mappings": {

   "properties": {

     "age": {

       "type": "integer"

     },

     "email": {

       "type": "keyword"  #不会分词

     },

     "name":{

       "type": "text"

     }

   }

  }

}

# 添加一个映射,注意这里的index属性,表示是否参与检索,默认不写就是true

PUT /my_index/_mapping

{

  "properties":{

   "employee-id":{

     "type":"keyword",

     "index":false

   }

  }

}

# 更新映射,对于已经存在的映射字段我们不能更新,更新必须创建新的索引进行数据迁移

# 如果我们想要修改已存在的映射就会把之前已存在的很多数据的检索规则就会失效

# reindex-数据迁移

PUT /newbank

{

  "mappings": {

   "properties": {

     "account_number": {

       "type": "long"

     },

     "address": {

       "type": "text"

     },

     "age": {

       "type": "long"

     },

     "balance": {

       "type": "long"

     },

     "city": {

       "type": "keyword"

     },

     "email": {

       "type": "keyword"

     },

     "employer": {

       "type": "text"

     },

     "firstname": {

       "type": "text",

       "fields": {

         "keyword": {

           "type": "keyword",

           "ignore_above": 256

         }

       }

     },

     "gender": {

       "type": "text",

       "fields": {

         "keyword": {

           "type": "keyword",

           "ignore_above": 256

         }

       }

     },

     "lastname": {

       "type": "text",

       "fields": {

         "keyword": {

           "type": "keyword",

           "ignore_above": 256

         }

       }

     },

     "state": {

       "type": "text",

       "fields": {

         "keyword": {

           "type": "keyword",

           "ignore_above": 256

         }

       }

     }

   }

  }

}

POST _reindex

{

  "source": {

   "index": "bank",

   "type": "account"

  },

  "dest": {

   "index":"newbank"

  }

}

结构化搜索

结构化搜索(Structured search) 是指有关探询那些具有内在结构数据的过程。比如日期、时间和数字都是结构化的:它们有精确的格式,我们可以对这些格式进行逻辑操作。比较常见的操作包括比较数字或时间的范围,或判定两个值的大小。

精确值查找

当进行精确值查找时, 我们会使用过滤器(filters)。过滤器很重要,因为它们执行速度非常快,不会计算相关度(直接跳过了整个评分阶段)而且很容易被缓存。

term 查询

我们首先来看最为常用的 term 查询, 可以用它处理数字(numbers)、布尔值(Booleans)、日期(dates)以及文本(text)。

SELECT document FROM   products WHERE  price = 20

GET /my_store/products/_search

{

   "query" : {

       "constant_score" : {   # 不希望对查询进行评分计算

           "filter" : {

               "term" : {     # 支持文本、数字、日期

                   "price" : 20

               }

           }

       }

   }

}

组合过滤器

布尔过滤器

一个 bool 过滤器由三部分组成:

{

"bool": {

"must":     [],所有的语句都*必须(must)*匹配,与`AND`等价。

"should":   [],所有的语句都*不能(mustnot)*匹配,与`NOT`等价。

"must_not": [],至少有一个语句要匹配,与`OR`等价。

  }

}

嵌套布尔过滤器 bool 是一个复合的过滤器,可以接受多个子过滤器,需要注意的是 bool 过滤器本身仍然还只是一个过滤器。 这意味着我们可以将一个 bool 过滤器置于其他 bool 过滤器内部,这为我们提供了对任意复杂布尔逻辑进行处理的能力。

SELECTdocumentFROMproducts

WHEREproductID="KDKE-B-9947-#kL5"

OR(productID="JODL-X-1937-#pV7"

ANDprice=30)

GET/my_store/products/_search

{

"query": {

"filtered": {

"filter": {

"bool": {

"should": [# or

{"term": {"productID":"KDKE-B-9947-#kL5"}},# 1兄弟关系

{"bool": {# 1兄弟关系

"must": [

{"term": {"productID":"JODL-X-1937-#pV7"}},# 2兄弟关系

{"term": {"price":30}}# 2兄弟关系

                 ]

               }}

             ]

          }

        }

     }

  }

}

多个精确值

通常我们需要去搜索多个值时,使用term,我们只要将 term 字段的值改为数组即可

GET/my_store/products/_search

{

"query": {

"constant_score": {//不需要计算评分

"filter": {

"terms": {

"price": [20,30]

               }

           }

       }

   }

}

包含,而不是相等

term的工作原理:Elasticsearch 会在倒排索引中查找包括某 term 的所有文档,然后构造一个 bitset 。当 term 查询匹配标记目标词时,它直接在倒排索引中找到记录并获取相关的文档 ID,这些文档会同时作为结果返回。可以想象,这样不仅低效,而且代价高昂。正因如此, term 和 terms 是 必须包含(must contain) 操作,而不是 必须精确相等(must equal exactly) 。

范围

Elasticsearch 有 range 查询, 不出所料地,可以用它来查找处于某个范围内的文档:

"range": {

"price": {

"gte":20,

"lte":40

   }

}

"range": {

"timestamp": {

"gt":"2014-01-01 00:00:00",

"lt":"2014-01-07 00:00:00"

   }

}

range 查询可同时提供包含(inclusive)和不包含(exclusive)这两种范围表达式,可供组合的选项如下:

gt: > 大于(greater than)

lt: < 小于(less than)

gte: >= 大于或等于(greater than or equal to)

lte: <= 小于或等于(less than or equal to)

处理null值

exists 存在查询。 这个查询会返回那些在指定字段有任何值的文档

missing 查询本质上与 exists 恰好相反: 它返回某个特定  值字段的文档

GET/my_index/posts/_search

{

"query": {

"constant_score": {

"filter": {

"missing": {"field":"tags"}

"exists": {"field":"tags"}

           }

       }

   }

}

对象null处理

name对象

{

  "name" : {

     "first" : "John",

     "last" :  "Smith"

  }

}

对象属性的缺失如下

{

   "exists" : { "field" : "name" }

}

实际执行的是如下的

{

   "bool": {

       "should": [

           { "exists": { "field": "name.first" }},

           { "exists": { "field": "name.last" }}

       ]

   }

}

分词

分词器 接受一个字符串作为输入,将 这个字符串拆分成独立的词或 语汇单元(token) (可能会丢弃一些标点符号等字符),然后输出一个 语汇单元流(token stream) 。

例如,whitespace tokenizer遇到空白字符时分割文本。它会将文本."Quick brown fox!"分割为[Quick, brown, fox!]。

该tokenizer(分词器)还负责记录各个term(词条)的顺序或position位置(用于phrase短语和word proximity词近邻查询),以及term(词条)所代表的原始word(单词)的start(起始)和end(结束)的character offsets(字符偏移量)(用于高亮显示搜索的内容)。Elasticsearch提供了很多内置的分词器,可以用来构建custom analyzers(自定义分词器)。

关于索引的基本操作

1、创建一个索引!

PUT /索引名~/类型名~/文档id

{

请求体

}

2、自动创建索引!并且完成了数据的添加!其实可以简单的认为 索引 可以当作 数据库中的表

3、上面name字段并没有指定类型,应该是什么类型呢?

elaticsearch 的数据类型有哪些?

字符串类型

text、keyword

数值类型

long、integer、short、byte、double、float、half、scaled、float

日期类型

date

布尔类型

boolean

二进制类型

binary

等等。。。。

4、指定字段的类型

集群相关

一个运行中的 Elasticsearch 实例称为一个节点,而集群是由一个或者多个拥有相同 cluster.name 配置的节点组成, 它们共同承担数据和负载的压力。当有节点加入集群中或者从集群中移除节点时,集群将会重新平均分布所有的数据。

当一个节点被选举成为主节点时,它将负责管理集群范围内的所有变更,例如增加、删除索引,或者增加、删除节点等。 而主节点并不需要涉及到文档级别的变更和搜索等操作,所以当集群只拥有一个主节点的情况下,即使流量的增加它也不会成为瓶颈。 任何节点都可以成为主节点。我们的示例集群就只有一个节点,所以它同时也成为了主节点。

作为用户,我们可以将请求发送到 集群中的任何节点 ,包括主节点。 每个节点都知道任意文档所处的位置,并且能够将我们的请求直接转发到存储我们所需文档的节点。 无论我们将请求发送到哪个节点,它都能负责从各个包含我们所需文档的节点收集回数据,并将最终结果返回給客户端。

主节点(master)

配置:node.master:true

node.data:false(这里也可以配置成node.data:true)

机器配置:普通服务器即可(CPU、内存消耗yiban )

作用:索引的创建或删除;跟踪哪些节点是集群的一部分;决定哪些分片分配给相关的节点。

说明:稳定的主节点对于集群的健康是非常重要的。默认情况下任何集群中的一个节点都有可能被选为主节点。索引数据和搜索查询等操作会占用大量的cpu、内存、io资源,为了集群的稳定,分离主节点和数据节点是一个比较好的选择,让主节点尽可能做尽量少的工作。

数据节点(data)

配置:node.master:false(这里也可以配置成node.master:true)

node.data:true

机器配置:较高配置服务器,主要消耗磁盘、内存

作用:存储索引数据;对文档进行增删改查、聚合操作。

说明:数据节点对cpu、内存、io要求较高,在优化的时候需要监控数据节点的状态,当资源不够的时候,需要在集群中添加新的节点。

数据节点路径设置:每一个主节点和数据节点都需要知道分片、索引、元数据的物理存储位置。

客户节点(client)

配置:node.master:false

node.data:false

机器配置:普通服务器即可(如果要进行分组聚合操作的话,建议这个节点内存也分配多一点)

作用:处理路由请求;处理搜索;分发索引。

说明:从本质上来说,客户节点表现为负载平衡器。独立的客户端节点在一个比较大的集群中是非常有用的,他协调主节点和数据节点,客户端节点加入集群可以得到集群的状态,根据集群的状态可以直接路由请求。

注意:添加太多的客户端节点对集群是一种负担,因为主节点必须等待每一个节点集群状态的更新确认!客户节点的作用不应被夸大,数据节点也可以起到类似的作用。

部落节点

配置:elasticsearch.yml

tribe:

t1:

cluster.name: cluster_one

t2:

cluster.name: cluster_two

作用:部落节点可以跨越多个集群,它可以接收每个集群的状态,然后合并成一个全局集群的状态,它可以读写所有节点上的数据。

说明:T1和T2是任意的名字代表连接到每个集群。默认情况下部落节点通过广播可以做为客户端连接每一个集群。大多数情况下,部落节点可以像单节点一样对集群进行操作。

集群健康

Elasticsearch 的集群监控信息中包含了许多的统计数据,其中最为重要的一项就是 集群健康 , 它在 status 字段中展示为 green 、 yellow 或者 red 。

例如启动一个默认的es服务器之后查看:

$ curl http://127.0.0.1:9200/_cluster/health?pretty

  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current

                                Dload  Upload   Total   Spent    Left  Speed

100   467  100   467    0     0  29187      0 --:--:-- --:--:-- --:--:-- 29187{

  "cluster_name" : "my-application",

  "status" : "green",      #  status 字段指示着当前集群在总体上是否工作正常。green:所有的主分片和副本分片都正常运行。yellow:所有的主分片都正常运行,但不是所有的副本分片都正常运行。red:有主分片没能正常运行

  "timed_out" : false,

  "number_of_nodes" : 1,

  "number_of_data_nodes" : 1,

  "active_primary_shards" : 0,

  "active_shards" : 0,

  "relocating_shards" : 0,

  "initializing_shards" : 0,

  "unassigned_shards" : 0,

  "delayed_unassigned_shards" : 0,

  "number_of_pending_tasks" : 0,

  "number_of_in_flight_fetch" : 0,

  "task_max_waiting_in_queue_millis" : 0,

  "active_shards_percent_as_number" : 100.0

}

添加索引

索引实际上是指向一个或者多个物理 分片 的 逻辑命名空间 。

分片

一个 分片 是一个底层的工作单元 ,它仅保存了全部数据中的一部分。一个分片是一个 Lucene 的实例,它本身就是一个完整的搜索引擎。我们的文档被存储和索引到分片内,但是应用程序是直接与索引而不是与分片进行交互。

Elasticsearch 是利用分片将数据分发到集群内各处的。分片是数据的容器,文档保存在分片内,分片又被分配到集群内的各个节点里。 当你的集群规模扩大或者缩小时, Elasticsearch 会自动的在各节点中迁移分片,使得数据仍然均匀分布在集群里。

一个分片可以是 主 分片或者 副本 分片。 索引内任意一个文档都归属于一个主分片,所以主分片的数目决定着索引能够保存的最大数据量。一个索引必须创建主分片,副本分片可以没有。

理解:分片和主流关系型数据库的表分区的概念有点类似。分为主分片(primary shard)、副本分片(replica shard),并且他们不会存在于同一个节点中,可以有效的保证es的数据高可用性。

副本

一个副本分片只是一个主分片的拷贝。作用:1. 副本分片作为硬件故障时保护数据不丢失的冗余备份,2. 并为搜索和返回文档等读操作提供服务。

如果主分片有3个,那么一个副本replica就对应有1X3=3个replica shard副本分片。

副本分片数量 = 副本数量repilca num X 主分片数量primary shard num

注意:主分片数是在索引建立的时候就已经确定了,但是副本分片数可以随时修改。也就是说分片数量不允许修改,副本数量可以修改。

例子

(1). 在包含一个节点的集群内创建名为 indextest的索引。分配3个主分片和一份副本(每个主分片拥有一个副本分片)

# 分配3个主分片和一份副本(每个主分片拥有一个副本分片)

GET http://localhost:9200/indextest

{

    "settings":{

        "number_of_shards":3,

        "number_of_replicas":1

    }

}

# 此时健康状况

{

   "cluster_name": "my-application",

   "status": "yellow",   # 全部 主 分片都正常运行(集群可以正常服务所有请求),但是 副本 分片没有全部处在正常状态

   "timed_out": false,

   "number_of_nodes": 1,

   "number_of_data_nodes": 1,

   "active_primary_shards": 3,

   "active_shards": 3,

   "relocating_shards": 0,

   "initializing_shards": 0,

   "unassigned_shards": 3,

   "delayed_unassigned_shards": 0,

   "number_of_pending_tasks": 0,

   "number_of_in_flight_fetch": 0,

   "task_max_waiting_in_queue_millis": 0,

   "active_shards_percent_as_number": 50.0   # 可用性只有50%

}

实际上,所有3个副本分片都是 unassigned(未分配)—— 它们都没有被分配到任何节点。 因为:在同一个节点上既保存原始数据又保存副本是没有意义的,因为一旦失去了那个节点,我们也将丢失该节点上的所有副本数据。当前我们的集群是正常运行的,但是在硬件故障时有丢失数据的风险。也就是上面到的:主分片和副本分片不会在同一个节点中。

(2). 再次启动一个节点Node2加入到节点后再次查看健康信息:

当第二个节点加入到集群后,3个 副本分片 将会分配到这个节点上,且每个主分片对应一个副本分片。 这意味着当集群内任何一个节点出现问题时,我们的数据都完好无损。

所有新近被索引的文档都将会保存在主分片上,然后被并行的复制到对应的副本分片上。这就保证了我们既可以从主分片又可以从副本分片上获得文档。

# 此时的健康状况

{

   "cluster_name": "my-application",

   "status": "green",    # 表示所有6个分片(包括3个主分片和3个副本分片)都在正常运行

   "timed_out": false,

   "number_of_nodes": 2,

   "number_of_data_nodes": 2,

   "active_primary_shards": 3,

   "active_shards": 6,

   "relocating_shards": 0,

   "initializing_shards": 0,

   "unassigned_shards": 0,

   "delayed_unassigned_shards": 0,

   "number_of_pending_tasks": 0,

   "number_of_in_flight_fetch": 0,

   "task_max_waiting_in_queue_millis": 0,

   "active_shards_percent_as_number": 100.0

}

(3). 水平扩容: 我们再次启动第三个节点 Node3

Node 1 和 Node 2 上各有一个分片被迁移到了新的 Node 3 节点,现在每个节点上都拥有2个分片,而不是之前的3个。 这表示每个节点的硬件资源(CPU, RAM, I/O)将被更少的分片所共享,每个分片的性能将会得到提升。

分片是一个功能完整的搜索引擎,它拥有使用一个节点上的所有资源的能力。 我们这个拥有6个分片(3个主分片和3个副本分片)的索引可以最大扩容到6个节点,每个节点上存在一个分片,并且每个分片拥有所在节点的全部资源。

(4). 增加副本数量

PUT http://localhost:9200/indextest/_settings

{

    "number_of_replicas":2

}

# 此时查看健康状况

{

   "cluster_name": "my-application",

   "status": "green",    # 健康

   "timed_out": false,

   "number_of_nodes": 3,  # 3个节点

   "number_of_data_nodes": 3,   # 数据节点数3个

   "active_primary_shards": 3,  # 3个主分片

   "active_shards": 9,

   "relocating_shards": 0,

   "initializing_shards": 0,

   "unassigned_shards": 0,     # 未指定节点,配置了复本,仅使用一台机器部署会出现这种情况

   "delayed_unassigned_shards": 0,

   "number_of_pending_tasks": 0,

   "number_of_in_flight_fetch": 0,

   "task_max_waiting_in_queue_millis": 0,

   "active_shards_percent_as_number": 100.0   #集群分片的可用性100%

}

(5). 关闭主节点:

我们尝试将主节点关闭,集群必须拥有一个主节点来保证正常工作,所以发生的第一件事情就是选举一个新的主节点: Node 2。

在我们关闭 Node 1 的同时也失去了主分片 1 和 2 ,并且在缺失主分片的时候索引也不能正常工作。 如果此时来检查集群的状况,我们看到的状态将会为 red :不是所有主分片都在正常工作。

幸运的是,在其它节点上存在着这两个主分片的完整副本, 所以新的主节点立即将这些分片在 Node 2 和 Node 3 上对应的副本分片提升为主分片, 此时集群的状态将会为 yellow 。 这个提升主分片的过程是瞬间发生的,如同按下一个开关一般。

为什么我们集群状态是 yellow 而不是 green 呢? 虽然我们拥有所有的三个主分片,但是同时设置了每个主分片需要对应2份副本分片,而此时只存在一份副本分片。 所以集群不能为 green 的状态,不过我们不必过于担心:如果我们同样关闭了 Node 2 ,我们的程序 依然 可以保持在不丢任何数据的情况下运行,因为 Node 3 为每一个分片都保留着一份副本。

{

   "cluster_name": "my-application",

   "status": "yellow",

   "timed_out": false,

   "number_of_nodes": 2,

   "number_of_data_nodes": 2,

   "active_primary_shards": 3,

   "active_shards": 6,

   "relocating_shards": 0,

   "initializing_shards": 0,

   "unassigned_shards": 3,

   "delayed_unassigned_shards": 0,

   "number_of_pending_tasks": 0,

   "number_of_in_flight_fetch": 0,

   "task_max_waiting_in_queue_millis": 0,

   "active_shards_percent_as_number": 66.66666666666666

}

文档存储

Elasticsearch分片算法

shard = hash(routing) % number_of_primary_shards

routing值是一个任意字符串,它默认是_id但也可以自定义,这个routing字符串通过哈希函数生成一个数字,然后除以主切片的数量得到一个余数(remainder),余数的范围永远是0到number_of_primary_shards - 1,这个数字就是特定文档所在的分片。

这也解释了为什么主切片的数量只能在创建索引时定义且不能修改:如果主切片的数量在未来改变了,所有先前的路由值就失效了,文档也就永远找不到了。

所有的文档API(get、index、delete、bulk、update、mget)都接收一个routing参数,它用来自定义文档到分片的映射。自定义路由值可以确保所有相关文档。比如用户的文章,按照用户账号路由,就可以实现属于同一用户的文档被保存在同一分片上。

Elasticsearch分片与副本交互

假设有一个集群由三个节点组成。 它包含一个叫 blogs 的索引,有两个主分片,每个主分片有两个副本分片。相同分片的副本不会放在同一节点。

我们可以发送请求到集群中的任一节点。 每个节点都有能力处理任意请求。 每个节点都知道集群中任一文档位置,所以可以直接将请求转发到需要的节点上。 在下面的例子中,将所有的请求发送到 Node 1 ,我们将其称为 协调节点(coordinating node)* 。

新建、索引和删除文档

新建和删除请求都是写(write)操作,它们必须在主分片上成功完成才能复制到相关的复制分片上,下面我们罗列在主分片和复制分片上成功新建、索引或删除一个文档必要的顺序步骤:

客户端给Node 1发送新建、索引或删除请求。

节点使用文档的_id确定文档属于分片0。它转发请求到Node 3,说分片0位于这个节点上。

Node 3在主分片上执行请求,如果成功,它转发请求到相应的位于Node 1和Node 2的复制节点上。当所有的复制节点报告成功,Node 3报告成功到请求的节点,请求的节点再报告给客户端。

客户端接收到成功响应的时候,文档的修改已经被应用于主分片和所有的复制分片。你的修改生效了。

取回一个文档

可以从主分片或者从其它任意副本分片检索文档 ,以下是从主分片或者副本分片检索文档的步骤顺序:

客户端向 Node 1 发送获取请求。

节点使用文档的 _id 来确定文档属于分片 0 。分片 0 的副本分片存在于所有的三个节点上。 在这种情况下,它将请求转发到 Node 2 。

Node 2 将文档返回给 Node 1 ,然后将文档返回给客户端。

在处理读取请求时,协调结点在每次请求的时候都会通过轮询所有的副本分片来达到负载均衡。

在文档被检索时,已经被索引的文档可能已经存在于主分片上但是还没有复制到副本分片。 在这种情况下,副本分片可能会报告文档不存在,但是主分片可能成功返回文档。 一旦索引请求成功返回给用户,文档在主分片和副本分片都是可用的。

局部更新文档

以下是部分更新一个文档的步骤:

客户端向 Node 1 发送更新请求。

它将请求转发到主分片所在的 Node 3 。

Node 3 从主分片检索文档,修改 _source 字段中的 JSON ,并且尝试重新索引主分片的文档。 如果文档已经被另一个进程修改,它会重试步骤 3 ,超过 retry_on_conflict 次后放弃。

如果 Node 3 成功地更新文档,它将新版本的文档并行转发到 Node 1 和 Node 2 上的副本分片,重新建立索引。 一旦所有副本分片都返回成功, Node 3 向协调节点也返回成功,协调节点向客户端返回成功。

多文档模式

以下是使用单个 mget 请求取回多个文档所需的步骤顺序:

客户端向 Node 1 发送 mget 请求。

Node 1 为每个分片构建多文档获取请求,然后并行转发这些请求到托管在每个所需的主分片或者副本分片的节点上。一旦收到所有答复, Node 1 构建响应并将其返回给客户端。

以下是使用单个 mget 请求取回多个文档所需的步骤顺序:

客户端向 Node 1 发送 mget 请求。

Node 1 为每个分片构建多文档获取请求,然后并行转发这些请求到托管在每个所需的主分片或者副本分片的节点上。一旦收到所有答复, Node 1 构建响应并将其返回给客户端。

相关性得分

每个文档都有相关性评分,用一个正浮点数字段 _score 来表示 。 _score 的评分越高,相关性越高。

查询语句会为每个文档生成一个 _score 字段。Lucene和es的打分机制是一个公式。将查询作为输入,使用不同的手段来确定每一篇文档的得分,将每一个因素最后通过公式综合起来,返回该文档的最终得分。这个综合考量的过程,就是我们希望相关的文档被优先返回的考量过程。在Lucene和es中这种相关性称为得分。 在开始计算得分之前,es使用了被搜索词条的频率和它有多常见来影响得分,从两个方面理解:

一个词条在某篇文档中出现的次数越多,该文档就越相关。

一个词条如果在不同的文档中出现的次数越多,它就越不相关!

我们称之为TF-IDF,TF是词频(term frequency),而IDF是逆文档频率(inverse document frequency)。

词频:TF

考虑一篇文档得分的首要方式,是查看一个词条在文档中出现的次数,比如某篇文章围绕es的打分展开的,那么文章中肯定会多次出现相关字眼,当查询时,我们认为该篇文档更符合,所以,这篇文档的得分会更高。

逆文档频率:IDF

相对于词频,逆文档频率稍显复杂,如果一个词条在索引中的不同文档中出现的次数越多,那么它就越不重要。逆文档词频只检查词条是否出现在某篇文档中,而不检查它在这篇文档中出现了多少次,那是词频该干的事儿。

逆文档词频是一个重要的因素,用来平衡词条的词频。比如the 的 得等词几乎出现在所有的文档中(中文中比如的),如果这个鬼东西要不被均衡一下,那么他们的频率将完全淹没我们的目标词,有效的均衡了这个常见词的相关性影响,以达到实际的相关性得分将会对查询的词条有一个更准确地描述。

当词频和逆文档词频计算完成。就可以使用TF-IDF公式来计算文档的得分了。

Lucene评分公式

之前的讨论Lucene默认评分公式被称为TF-IDF,一个基于词频和逆文档词频的公式。Lucene实用评分公式如下:

其他的打分方法

除了TF-IDF结合向量空间模型的实用评分模式,是es和Lucene最为主流的评分机制,但这并不是唯一的,除了TF-IDF这种实用模型之外,其他的模型包括:

Okapi BM25。

随机性分歧(Divergence from randomness),即DFR相似度。

LM Dirichlet相似度。

LM Jelinek Mercer相似度。

这里简要的介绍BM25几种主要设置,即k1、b和discount_overlaps:

k1和b是数值的设置,用于调整得分是如何计算的。

k1控制对于得分而言词频(TF)的重要性。

b是介于0 ~ 1之间的数值,它控制了文档篇幅对于得分的影响程度。

默认情况下,k1设置为1.2,而b则被设置为0.75

discount_overlaps的设置用于告诉es,在某个字段中,多少个分词出现在同一位置,是否应该影响长度的标准化,默认值是true。

案例

使用jsoup工具爬取数据。

/**

* Jsoup获取一个数据

*/

publicstaticList<Content>parseJD(Stringkeyword)throwsException{

// 获取请求 https://search.jd.com/Search?keyword=java

Stringurl="https://search.jd.com/Search?keyword="+keyword;

// 解析网页 Jsoup 返回Document 就是浏览器的Document对象

Documentdocument=Jsoup.parse(newURL(url),30000);

Elementelement=document.getElementById("J_goodsList");

Elementselements=document.getElementsByTag("li");

ArrayList<Content>goodsList=newArrayList<>();

for(Elementel:elements) {

Stringimg=el.getElementsByTag("img").eq(0).attr("data-lazy-img");

Stringprice=el.getElementsByClass("p-price").eq(0).text();

Stringtitle=el.getElementsByClass("p-name").eq(0).text();

if(price.length()>0) {

goodsList.add(newContent(title,img,price));

       }

   }

returngoodsList;

}

放入es中

/**

* 解析数据放入 es 索引中

* @param keyword  关键词、目标词

* @return

* @throws Exception

*/

publicBooleanparseContent(Stringkeyword)throwsException{

//获取数据

List<Content>contents=HtmlParseUtil.parseJD(keyword);

// 批量放入es中

BulkRequestbulkRequest=newBulkRequest();

bulkRequest.timeout("60s");

for(inti=0;i<contents.size();i++) {

bulkRequest.add(newIndexRequest("jd_goods")

.source(JSON.toJSONString(contents.get(i)),XContentType.JSON));

   }

BulkResponsebulk=restHighLevelClient.bulk(bulkRequest,RequestOptions.DEFAULT);

return!bulk.hasFailures();

}

获取数据

/**

* 获取这些数据

* @param keyword

* @param pageNo

* @param pageSize

* @return

* @throws IOException

*/

publicList<Map<String,Object>>searchHighLightPage(Stringkeyword,intpageNo,intpageSize)throwsIOException{

if(pageNo<=1)pageNo=1;

// 条件查询

SearchRequestrequest=newSearchRequest("jd_goods");

SearchSourceBuildersourceBuilder=newSearchSourceBuilder();

// 分页

sourceBuilder.from(pageNo);

sourceBuilder.size(pageSize);

// 精准匹配

TermQueryBuildertermQueryBuilder=QueryBuilders.termQuery("title",keyword);

sourceBuilder.query(termQueryBuilder);

sourceBuilder.timeout(newTimeValue(60,TimeUnit.SECONDS));

// 高亮展示

HighlightBuilderhighlightBuilder=newHighlightBuilder();

highlightBuilder.field("title");

highlightBuilder.requireFieldMatch(false);

highlightBuilder.preTags("<span style='color:red'>");

highlightBuilder.postTags("</span>");

sourceBuilder.highlighter(highlightBuilder);

// 执行搜索

request.source(sourceBuilder);

SearchResponsesearch=restHighLevelClient.search(request,RequestOptions.DEFAULT);

// 解析结果

ArrayList<Map<String,Object>>list=newArrayList<>();

for(SearchHithit:search.getHits().getHits()) {

// 解析高亮字段

Map<String,HighlightField>highlightFields=hit.getHighlightFields();

HighlightFieldtitle=highlightFields.get("title");// 高亮的字段

Map<String,Object>sourceAsMap=hit.getSourceAsMap();// 原来的结果

if(title!=null) {

Text[]fragments=title.fragments();

Stringn_title="";

for(Textfragment:fragments) {

n_title+=fragment;

           }

sourceAsMap.put("title",n_title);

       }

list.add(sourceAsMap);

   }

returnlist;

}

问题

整个数据文档被分配到分片上,索引创建后又不可实时调整分片数量,考虑到数据集的增长,那是不是分片越多越好呢

每个分片本质上就是一个Lucene索引, 因此会消耗相应的文件句柄, 内存和CPU资源

每个搜索请求会调度到索引的每个分片。 如果分片分散在不同的节点倒是问题不太,但当分片开始竞争相同的硬件资源时, 性能便会逐步下降

ES使用词频统计来计算相关性。这些统计也会分配到各个分片上。如果在大量分片上只维护了很少的数据, 则将导致最终的文档相关性较差。

那集群节点数、索引分片数、分片副本数配置多少合适呢?

ElasticSearch推荐的最大JVM堆空间是30~32G, 所以建议将分片最大容量限制为30GB, 然后再对分片数量做合理估算。 例如, 你认为你的数据能达到200GB, 故推荐分配7到8个分片。

根据节点数量的1.5~3倍的原则来创建分片。例如你有3个节点, 则推荐你创建的分片数最多不超过9(3x3)个。

如果给每个分片分配1个副本,则你所需的节点数将加倍。如果需要为每个分片分配2个副本, 则需要3倍的节点数。

Text、keyword同是字符串类型,二者有何区别?在什么场景,该用什么类型呢?

Text:如果一个字段是要被全文检索的,比如说产品标题、新闻内容、博客内容,那么可以使用text。用了text后,字段内容会被分词,在生成倒排索引之前,字符串会被分词器分成一个个词项。Text类型的字段不用于排序、很少用于聚合,这种字段也被称为analyzed字段。

Keyword:这种类型适用于结构化的字段,例如标签、email地址、手机号码等等。可用于过滤、排序、聚合等场景。这种字符串也称为not-analyzed字段。

倒排索引能够实现快速搜索的需求,

Elasticsearch cluster 的内存多半都被消耗在了正排索引上, 那我们为什么还需要正排索引?

正牌索引的场景:

按照字段排序(sort)

按照字段进行聚合(Aggregations)

过滤器,例如业态、城市、状态过滤

script排序中使用到某些字段

脑裂,集群节点角色

“脑裂”问题可能的成因

网络问题:集群间的网络延迟导致一些节点访问不到master,认为master挂掉了从而选举出新的master,并对master上的分片和副本标红,分配新的主分片

节点负载:主节点的角色既为master又为data,访问量较大时可能会导致ES停止响应造成大面积延迟,此时其他节点得不到主节点的响应认为主节点挂掉了,会重新选取主节点。

内存回收:data节点上的ES进程占用的内存较大,引发JVM的大规模内存回收,造成ES进程失去响应。

解决方案:

减少误判:discovery.zen.ping_timeout节点状态的响应时间,默认为3s,可以适当调大,如果master在该响应时间的范围内没有做出响应应答,判断该节点已经挂掉了。调大参数(如6s,discovery.zen.ping_timeout:6),可适当减少误判。

选举触发 discovery.zen.minimum_master_nodes:1。该参数是用于控制选举行为发生的最小集群主节点数量。

当备选主节点的个数大于等于该参数的值,且备选主节点中有该参数个节点认为主节点挂了,进行选举。官方建议为(n/2)+1,n为主节点个数(即有资格成为主节点的节点个数)增大该参数,当该值为2时,我们可以设置master的数量为3,这样,挂掉一台,其他两台都认为主节点挂掉了,才进行主节点选举。

角色分离:即master节点与data节点分离,限制角色

mget和search的区别

GET/MGET必须指定三元组:index、type、id(http://127.0.0.1:9200/index/type/id),也就是说,根据文档id从正排索引中获取内容。GET操作只能对单个文档进行处理,MGET是对GET的进一步封装(封装了多个GET请求),由index,type,_id三元组来确定唯一文档。

(1)客户端向NODE1发送读请求(此时NODE1作为协调节点)

(2)NODE1是同文档ID来确定文档属于分片0,通过集群状态中的内容路由表信息获取分片0有三个副本数据,位于三个节点中,此时它可以通过将请求发送到任意节点,图上所示是将请求发送到NODE2。

(3)NODE2将文档返回给NODE1,NODE1将文档返回给客户端(因为只是获取单个数据信息,不会涉及协调节点的聚合等操作)

Search操作。ES中的数据可以分为两类:1. 精确值:比如日期和用户id,ip等信息。2. 全文:文章内容,比如日志,或者邮件的内容。

这两种类型的数据在查询的时候也是不相同的:对精确值的比较是二进制,查询要么匹配,要么不匹配;全文内容的查询无法给出“有”还是“没有”的结果,它只能找到结果是“看起来像”你要查询的东西,因此把查询结果按相似度排序,评分越高,相似度越大。大致流程:

(1)客户端发送请求协调节点

(2)因为查询的时候不知道文档位于哪个分片,因此索引的所有分片(某个副本)都要参与搜索,调节点计算出索引的分片位置进行搜索(也就是查询阶段)。

(3)协调节点进行结果合并,根据获取到返回的文档ID,再次访问并获取文档内容。比如:有5个分片,查询前10个匹配度最高的文档,那么每个分片都能查询出分片的TOP10,协调节点将5*10=50的结果再次排序,返回最红TOP10的结果给客户。

es中内存和硬盘的关系

数据主要存在磁盘,但是为了快速访问磁盘上的数据,也会有一部分数据是常驻内存里的。

ctrl+f与拼音输入法

问题补充

分页10000条,之后出错怎么处理?

默认只能查询到10000: The maximum value of from + size for searches to this index.Defaults to 10000. Search requests take heap memory and time proportional to from + size and this limits that memory.See Scroll or Search After for a more efficient alternative to raising this.

修改一下设置,把这个设置到自己想要的极限即可,我这里设置1000000:

PUT policy_document/_settings

{

  "index":{

   "max_result_window":1000000

  }

}

ES为了避免深分页,不允许使用分页(from&size)查询10000条以后的数据,因此如果要查询第10000条以后的数据,要使用ES提供的 scroll(游标) 来查询假设取的页数较大时(深分页),如请求第20页,Elasticsearch不得不取出所有分片上的第1页到第20页的所有文档,并做排序,最终再取出from后的size条结果作爲最终的返回值。假设你有16个分片,则需要在coordinate node彙总到 shards* (from+size)条记录,即需要16*(20+10)记录后做一次全局排序。所以,当索引非常非常大(千万或亿),是无法使用from + size 做深分页的,分页越深则越容易OOM,即便不OOM,也很消耗CPU和内存资源。因此ES使用index.max_result_window:10000作爲保护措施 ,即默认 from + size 不能超过10000,虽然这个参数可以动态修改,也可以在配置文件配置,但是最好不要这麽做,应该改用ES游标来取得数据.

scroll游标原理

scroll时每次获取一页的内容,然后返回一个scroll_id,根据这个scroll_id不断的获取下一贝的内谷,自到结果集心配完毕。

可以把 scroll 理解爲关系型数据库里的 cursor(光标),因此,scroll 并不适合用来做实时搜索,而更适用于后台批处理任务,比如群发,scroll 具体分爲初始化和遍历两步,初始化时将所有符合搜索条件的搜索结果缓存起来,可以想象成快照保存一个快照,之后对index所作的修改,都无法获得,无法实现实时查询,在遍历时,从这个快照里取数据,也就是说,在初始化后对索引插入、删除、更新数据都不会影响遍历结果,游标可以增加性能的原因,是因为如果做深分页,每次搜索都必须重新排序,非常浪费,使用scroll就是一次把要用的数据都排完了,分批取出,因此比使用from+size还好。

使用初始化返回的scroll_id来进行请求,每一次请求都会继续返回初始化中未读完数据,并且会返回一个scroll_id,这个scroll_id可能会改变,因此每一次请求应该带上上一次请求返回的scroll_id。要注意返回的是_scroll_id,但是放在请求裡的是scroll_id,两者拼写上有不同,且每次发送scroll请求时,都要再重新刷新这个scroll的开启时间,以防不小心超时导致数据取得不完整

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,547评论 6 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,399评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,428评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,599评论 1 274
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,612评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,577评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,941评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,603评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,852评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,605评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,693评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,375评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,955评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,936评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,172评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,970评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,414评论 2 342

推荐阅读更多精彩内容