Elasticsearch

什么是Elasticsearch

Elasticsearch 是分布式、高性能、高可用、可伸缩的搜索和分析系统。

这里搜索的含义指的是就是在任何场景下,找寻你想要的信息,这个时候,会输入一段你要搜索的关键字,然后就期望找到这个关键字相关的全部信息。

为什么不用传统数据库做搜索

数据库也具备查询数据的能力,但是不太适合做全文检索。但是目前能想到的有2点不符合我的要求:

  1. 每条记录的指定字段的文本,可能会很长,比如说“商品描述”字段的长度,有长达数千个,甚至数万个字符,这个时候,每次都要对每条记录的所有文本进行扫描,懒判断说,你包不包含我指定的这个关键词(比如说"机构")
  2. 不能将搜索词拆分开来,尽可能去搜索更多的符合你的期望的结果,比如输入"生化飞机",就搜索不出来"生化危机"和"战斗飞机"。

什么是全文检索和lucene

  • 全文检索是基于倒排索引进行搜索,对全文进行分词得到一张表,表的key可以看做是分词得到的词语,value看做是每条记录的编号。
  • lucene,就是一个jar包,里面包含了封装好的各种建立倒排索引,以及进行搜索的代码,包括各种算法。我们就用java开发的时候,引入lucene jar,然后基于lucene的api进行去进行开发就可以了。用lucene,我们就可以去将已有的数据建立索引,lucene会在本地磁盘上面,给我们组织索引的数据结构。

什么是Elasticsearch

lucene可以做搜索,但是lucene是单机版的,如果想要做分布式来满足大数据的要求,那我们就需要在每台机器上去弄一个lucene,并且要去实现一台机器宕机后系统还能正常运转,前端过来的请求到底发送到哪个lucene机器建立的索引上也是我们要考虑的问题,还有很多分布式的问题需要我们考虑。Elasticsearch就是基于lucene做的封装,帮我们实现了我们想到的和没有想到的很多问题。

Elasticsearch功能

  1. 分布式的搜索引擎和数据分析引擎
    1. 搜索:网站的站内搜索,IT系统的检索
    2. 数据分析:电商网站,最近7天牙膏这种商品销量排名前10的商家有哪些;新闻网站,最近1个月访问量排名前3的新闻版块是哪些
  2. 全文检索,结构化检索,数据分析
    1. 全文检索:我想搜索商品名称包含牙膏的商品,select * from products where product_name like "%牙膏%"
    2. 结构化检索:我想搜索商品分类为日化用品的商品都有哪些,select * from products where category_id='日化用品'
    3. 数据分析:我们分析每一个商品分类下有多少个商品,select category_id,count(*) from products group by category_id
  3. 对海量数据进行近实时的处理
    1. 分布式:ES自动可以将海量数据分散到多台服务器上去存储和检索
    2. 近实时:在秒级别对数据进行搜索和分析

Elasticsearch核心概念

  1. Near Realtime(NRT):近实时。可以从2个层面来理解:从写入数据到数据可以被搜索到有一个小延迟(大概1秒);基于es执行搜索和分析可以达到秒级
  2. Cluster(集群):包含多个节点,每个节点属于哪个集群是通过一个配置(集群名称,默认是elasticsearch)来决定的,对于中小型应用来说,刚开始一个集群就一个节点很正常
  3. Node(节点):集群中的一个节点,节点也有一个名称(默认是随机分配的),节点名称很重要(在执行运维管理操作的时候),节点默认会去加入一个名称为“elasticsearch”的集群,如果直接启动一堆节点,那么它们会自动组成一个elasticsearch集群,当然一个节点也可以组成一个elasticsearch集群
  4. Document&field:文档是es中的最小数据单元,一个document可以是一条客户数据,一条商品分类数据,一条订单数据,通常用JSON数据结构表示。一个document里面有多个field,每个field就是一个数据字段。
  5. Index:索引,包含一堆有相似含义的文档数据,比如可以有一个客户索引,商品分类索引,订单索引,索引有一个名称。比如说建立一个product index,商品索引,里面可能就存放了所有的商品数据。
  6. type:类型,每个索引里都可以有一个或多个type,type是index中的一个逻辑数据分类,一个type下的document,都有相同的field,比如博客系统,有一个索引,可以定义用户数据type,博客数据type,评论数据type。商品index可以分为鞋类type, 服装类type等。
  7. shard:单台机器无法存储大量数据,es可以将一个索引中的数据切分为多个shard,分布在多台服务器上存储。有了shard就可以横向扩展,存储更多数据,让搜索和分析等操作分布到多台服务器上去执行,提升吞吐量和性能。每个shard都是一个lucene index。一台机器可以放多个shard.
  8. replica:任何一个服务器随时可能故障或宕机,此时shard可能就会丢失,因此可以为每个shard创建多个replica副本。replica可以在shard故障时提供备用服务,保证数据不丢失,多个replica还可以提升搜索操作的吞吐量和性能。primary shard(建立索引时一次设置,不能修改,默认5个),replica shard(随时修改数量,默认1个),默认每个索引10个shard,5个primary shard,5个replica shard,最小的高可用配置,是2台服务器。

Elasticsearch 的数据格式

elasticsearch是面向文档的,这里理解和关系型数据库的区别。对象数据存储到数据库中,只能拆解开来,变为扁平的通过外键关联多张表,每次查询的时候还得还原回对象格式。ES是面向文档的,文档中存储的数据结构,与面向对象的数据结构是一样的,基于这种文档数据结构,es可以提供复杂的索引,全文检索,分析聚合等功能,这需要我们转换一下思维。

集群管理

  1. 查看集群健康状态:GET /_cat/health?v
  • green:每个索引的primary shard和replica shard都是active状态
  • yellow:每个索引的primary shard都是active状态的,但是部分replica shard不是active状态,处于不可用的状态
  • red:不是所有索引的primary shard都是active状态的,部分索引有数据丢失了
  1. 快速查看集群已有索引:GET /_cat/indices?v
  2. 创建索引:PUT /test_index
  3. 删除索引:DELETE /test_index

Elasticsearch架构

Elasticsearch对复杂分布式机制的透明隐藏特性

Elasticsearch是一套分布式的系统,分布式是为了应对大数据量隐藏了复杂的分布式机制。

shard负载均衡:举例,假设现在有3个节点,总共有25个shard要分配到3个节点上去,es会自动进行均匀分配,以保持每个节点的均衡的读写负载请求

shard副本,请求路由,集群扩容,shard重分配。

垂直扩容与水平扩容

  • 垂直扩容:采购更强大的服务器,成本非常高昂,而且会有瓶颈,假设世界上最强大的服务器容量就是10T,但是当你的总数据量达到5000T的时候,你要采购多少台最强大的服务器啊

  • 水平扩容:业界经常采用的方案,采购越来越多的普通服务器,性能比较一般,但是很多普通服务器组织在一起,就能构成强大的计算和存储能力

水平扩容

(1)primary&replica自动负载均衡,6个shard,3 primary,3 replica
(2)每个node有更少的shard,IO/CPU/Memory资源给每个shard分配更多,每个shard性能更好
(3)扩容的极限,6个shard(3 primary,3 replica),最多扩容到6台机器,每个shard可以占用单台服务器的所有资源,性能最好
(4)超出扩容极限,动态修改replica数量,9个shard(3primary,6 replica),扩容到9台机器,比3台机器时,拥有3倍的读吞吐量
(5)3台机器下,9个shard(3 primary,6 replica),资源更少,但是容错性更好

master节点

  1. 创建或删除索引
  2. 增加或删除节点

节点平等的分布式架构

  1. 节点对等,每个节点都能接收所有的请求
  2. 自动请求路由
  3. 响应收集

master选举,replica容错,数据恢复

(1)9 shard,3 node
(2)master node宕机,自动master选举,red
(3)replica容错:新master将replica提升为primary shard,yellow
(4)重启宕机node,master copy replica到该node,使用原有的shard并同步宕机后的修改,green

shard&replica机制

(1)index包含多个shard
(2)每个shard都是一个最小工作单元,承载部分数据,lucene实例,完整的建立索引和处理请求的能力
(3)增减节点时,shard会自动在nodes中负载均衡
(4)primary shard和replica shard,每个document肯定只存在于某一个primary shard以及其对应的replica shard中,不可能存在于多个primary shard
(5)replica shard是primary shard的副本,负责容错,以及承担读请求负载
(6)primary shard的数量在创建索引的时候就固定了,replica shard的数量可以随时修改
(7)primary shard的默认数量是5,replica默认是1,默认有10个shard,5个primary shard,5个replica shard
(8)primary shard不能和自己的replica shard放在同一个节点上(否则节点宕机,primary shard和副本都丢失,起不到容错的作用),但是可以和其他primary shard的replica shard放在同一个节点上

Elasticsearch元数据

_index:

  • 类似的数据放在一个索引,非类似的数据放不同索引:product index(包含了所有的商品),sales index(包含了所有的商品销售数据),inventory index(包含了所有库存相关的数据)。如果你把比如product,sales,human resource(employee),全都放在一个大的index里面,比如说company index,不合适的。
  • index中包含了很多类似的document:类似是什么意思,其实指的就是说,这些document的fields很大一部分是相同的,你说你放了3个document,每个document的fields都完全不一样,这就不是类似了,就不太适合放到一个index里面去了。

为什么类似的数据放在一个_index

如果product和sales的数据放在一个index里面,那么比如有3个shard放了这些数据,如果前端对于product的请求很耗时,那么就会影响sales的请求。放在不同的index, 就会放在不同的shard, 这样不同种类的数据就不会互相影响。

_type

一个索引通常会划分为多个type,逻辑上对index中有些许不同的几类数据进行分类:因为一批相同的数据,可能有很多相同的fields,但是还是可能会有一些轻微的不同,可能会有少数fields是不一样的,举个例子,就比如说,商品,可能划分为电子商品,生鲜商品,日化商品,等等.

type,是一个index中用来区分类似的数据的,类似的数据,但是可能有不同的fields,而且有不同的属性来控制索引建立、分词器
field的value,在底层的lucene中建立索引的时候,全部是opaque bytes类型,不区分类型的
lucene是没有type的概念的,在document中,实际上将type作为一个document的field来存储,即_type,es通过_type来进行type的过滤和筛选
一个index中的多个type,实际上是放在一起存储的,因此一个index下,不能有多个type重名,而类型或者其他设置不同的,因为那样是无法处理的.

{
  "name": "geli kongtiao",
  "price": 1999.0,
  "service_period": "one year"
}

{
  "name": "aozhou dalongxia",
  "price": 199.0,
  "eat_period": "one week"
}

表明上看存储是这样的:

{
  "ecommerce": {
    "mappings": {
      "product": {
        "properties": {
          "desc": {
            "type": "text",
            "fields": {
              "keyword": {
                "type": "keyword",
                "ignore_above": 256
              }
            }
          },
          "name": {
            "type": "text",
            "fields": {
              "keyword": {
                "type": "keyword",
                "ignore_above": 256
              }
            },
            "fielddata": true
          },
          "price": {
            "type": "long"
          },
          "producer": {
            "type": "text",
            "fields": {
              "keyword": {
                "type": "keyword",
                "ignore_above": 256
              }
            }
          },
          "query": {
            "properties": {
              "match_all": {
                "type": "object"
              }
            }
          },
          "tags": {
            "type": "text",
            "fields": {
              "keyword": {
                "type": "keyword",
                "ignore_above": 256
              }
            },
            "fielddata": true
          }
        }
      },
      "xx": {
        "properties": {
          "name": {
            "type": "text",
            "fields": {
              "keyword": {
                "type": "keyword",
                "ignore_above": 256
              }
            },
            "fielddata": true
          }
        }
      }
    }
  }
}

在底层的存储是这样子的:


   "ecommerce": {
      "mappings": {
        "_type": {
          "type": "string",
          "index": "not_analyzed"
        },
        "name": {
          "type": "string"
        }
        "price": {
          "type": "double"
        }
        "service_period": {
          "type": "string"
        }
        "eat_period": {
          "type": "string"
        }
      }
   }
}

{
  "_type": "elactronic_goods",
  "name": "geli kongtiao",
  "price": 1999.0,
  "service_period": "one year",
  "eat_period": ""
}

{
  "_type": "fresh_goods",
  "name": "aozhou dalongxia",
  "price": 199.0,
  "service_period": "",
  "eat_period": "one week"
}

_id

代表document的唯一标识,与index和type一起,可以唯一标识和定位一个document

DSL

DSL:Domain Specified Language,特定领域的语言.

validate-query API

用于校验和提示查询语法。

GET /company/employee/_validate/query?explain
{
  "query": {
    "constant_score": {
      "filter": {
        "range": {
          "age": {
            "gte": 30
          }
        }
      }
    }
  }
}

explain用于解释不合法的原因。

简单查询

查询所有:

match_all 里不能加字段

GET /ecommerce/product/_search 
{
  "query":{
    "match_all":{}
  }
}

match、match_phase、term和query_string查询

  • term query会去倒排索引中寻找确切的term,它并不知道分词器的存在,就是说查询的时候,是查询字段分词结果中是否有term指定的字样。这种查询适合keyword 、numeric、date.

  • match query会把要查询的词语分词,然后把分词得到的结果去倒排索引里进行查询

  • match_phase: 会对输入做分词,但是需要结果中也包含所有的分词,而且顺序要求一样。以"hello world"为例,要求结果中必须包含hello和world,而且还要求他们是连着的,顺序也是固定的,hello that word不满足,world hello也不满足条件。

  • query_string:和match类似,但是match需要指定字段名,query_string是在所有字段中搜索,范围更广泛。

    GET /text/product2/_search 
    {
      "query": {
        "query_string": {
          "query": "I hello"
        }
      }
    }
    

查询名称包含yagao的商品,同时按照价格降序排序:

GET /ecommerce/product/_search
{
  "query": {
    "match": {
      "name": "yagao"
    }
  },
  "sort":[
    {"price": {"order": "desc"}}
    ],
  "_source": ["name", "price"]
}

分页查询商品,总共3条商品,假设每页就显示1条商品,现在显示第2页,所以就查出来第2个商品:

GET /ecommerce/product/_search
{
  "query": {
    "match": {
      "name": "yagao"
    }
  },
  "from": 1,
  "size": 1
}

range过滤允许我们按照指定范围查找一批数据:

GET /ecommerce/product/_search
{
  "query": {
    "range": {
      "price": {
        "gte": 10,
        "lte": 20
      }
    }
  }
}

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

多条件查询

搜索商品名称包含yagao,而且售价大于25元的商品

GET /ecommerce/product/_search
{
  "query": {
    "bool": {
      "must": [
        {"match": {
          "name": "yagao"
        }},
        {"range": {
          "price": {
            "gte": 24
          }
        }}
      ]
    }
  }
}

scoll 批量查询

如果一次性要查出来比如10万条数据,那么性能会很差,此时一般会采取用scoll滚动查询,一批一批的查,直到所有数据都查询完处理完

使用scoll滚动搜索,可以先搜索一批数据,然后下次再搜索一批数据,以此类推,直到搜索出全部的数据来
scoll搜索会在第一次搜索的时候,保存一个当时的视图快照,之后只会基于该旧的视图快照提供数据搜索,如果这个期间数据变更,是不会让用户看到的
采用基于_doc进行排序的方式,性能较高
每次发送scroll请求,我们还需要指定一个scoll参数,指定一个时间窗口,每次搜索请求只要在这个时间窗口内能完成就可以了

GET /test_index/test_type/_search?scroll=1m
{
  "query": {
    "match_all": {}
  },
  "sort": [ "_doc" ],
  "size": 3
}
  "_scroll_id": "DnF1ZXJ5VGhlbkZldGNoBQAAAAAAACxeFjRvbnNUWVZaVGpHdklqOV9zcFd6MncAAAAAAAAsYBY0b25zVFlWWlRqR3ZJajlfc3BXejJ3AAAAAAAALF8WNG9uc1RZVlpUakd2SWo5X3NwV3oydwAAAAAAACxhFjRvbnNUWVZaVGpHdklqOV9zcFd6MncAAAAAAAAsYhY0b25zVFlWWlRqR3ZJajlfc3BXejJ3",
  "took": 5,
  "timed_out": false,
  "_shards": {
    "total": 5,
    "successful": 5,
    "failed": 0
  },
  "hits": {
    "total": 10,
    "max_score": null,
    "hits": [
      {
        "_index": "test_index",
        "_type": "test_type",
        "_id": "8",
        "_score": null,
        "_source": {
          "test_field": "test client 2"
        },
        "sort": [
          0
        ]
      },
      {
        "_index": "test_index",
        "_type": "test_type",
        "_id": "6",
        "_score": null,
        "_source": {
          "test_field": "tes test"
        },
        "sort": [
          0
        ]
      },
      {
        "_index": "test_index",
        "_type": "test_type",
        "_id": "AVp4RN0bhjxldOOnBxaE",
        "_score": null,
        "_source": {
          "test_content": "my test"
        },
        "sort": [
          0
        ]
      }
    ]
  }
}

获得的结果会有一个scoll_id,下一次再发送scoll请求的时候,必须带上这个scoll_id

GET /_search/scroll
{
    "scroll": "1m", 
    "scroll_id" : "DnF1ZXJ5VGhlbkZldGNoBQAAAAAAACxeFjRvbnNUWVZaVGpHdklqOV9zcFd6MncAAAAAAAAsYBY0b25zVFlWWlRqR3ZJajlfc3BXejJ3AAAAAAAALF8WNG9uc1RZVlpUakd2SWo5X3NwV3oydwAAAAAAACxhFjRvbnNUWVZaVGpHdklqOV9zcFd6MncAAAAAAAAsYhY0b25zVFlWWlRqR3ZJajlfc3BXejJ3"
}

scroll,看起来挺像分页的,但是其实使用场景不一样。分页主要是用来一页一页搜索,给用户看的;scoll主要是用来一批一批检索数据,让系统进行处理的.

filter

{
    "query" : {
        "filtered" : {
            "query" : {
                "term" : { "name" : "joe" }
            },
            "filter" : {
                "term" : { "year" : 1981 }
            }
        }
    }
}
GET /company/employee/_search
{
  "query": {
    "bool": {
      "must": [
        {
          "match": {
            "join_date": "2016-01-01"
          }
        }
      ],
      "filter": {
        "range": {
          "age": {
            "gte": 30
          }
        }
      }
    }
  }
}

一般来说,如果你是在进行搜索,需要将最匹配搜索条件的数据先返回,那么用query;如果你只是要根据一些条件筛选出一部分数据,不关注其排序,那么用filter. 除非是你的这些搜索条件,你希望越符合这些搜索条件的document越排在前面返回,那么这些搜索条件要放在query中;如果你不希望一些搜索条件来影响你的document排序,那么就放在filter中即可.

filter 不能单独放在query的下一层,可以放置在bool里,要是想单独使用可以在外层套用一个constant_score.

GET /company/employee/_search 
{
  "query": {
    "constant_score": {
      "filter": {
        "range": {
          "age": {
            "gte": 30
          }
        }
      }
    }
  }
}

多种query搜索条件

match、match_all、multi_match

GET /company/employee/_search
{
  "query": {
    "multi_match": {
      "query": "2016",
      "fields": ["_all"]
    }
  }
}

range query

GET /company/employee/_search 
{
  "query": {
    "range": {
      "age": {
        "gte": 30
      }
    }
  }
}

term query 和 terms query

GET /_search
{
    "query": { "terms": { "tag": [ "search", "full_text", "nosql" ] }}
}

排序

如果对一个string field进行排序,结果往往不准确,因为分词后是多个单词,elasticsearch拿分词后的哪个单词用于排序对用户而言是不清楚的。

GET /website/article/_search
{
  "query": {
    "match_all": {}
  },
  "sort": [
    {
      "title": {
        "order": "desc"
      }
    }
  ]
}

得到的结果:

{
  "took": 36,
  "timed_out": false,
  "_shards": {
    "total": 5,
    "successful": 5,
    "skipped": 0,
    "failed": 0
  },
  "hits": {
    "total": 3,
    "max_score": null,
    "hits": [
      {
        "_index": "website",
        "_type": "article",
        "_id": "3",
        "_score": null,
        "_source": {
          "post_date": "2017-01-03",
          "title": "my third article",
          "content": "this is my third article in this website",
          "author_id": 11400
        },
        "sort": [
          "third"
        ]
      },
      {
        "_index": "website",
        "_type": "article",
        "_id": "2",
        "_score": null,
        "_source": {
          "post_date": "2017-01-02",
          "title": "my second article",
          "content": "this is my second article in this website",
          "author_id": 11400
        },
        "sort": [
          "second"
        ]
      },
      {
        "_index": "website",
        "_type": "article",
        "_id": "1",
        "_score": null,
        "_source": {
          "title": "first article",
          "content": "this is my second article",
          "post_date": "2017-01-01",
          "author_id": 110
        },
        "sort": [
          "first"
        ]
      }
    ]
  }
}

通常解决方案是,将一个string field建立两次索引,一个分词,用来进行搜索;一个不分词,用来进行排序.

PUT /website 
{
  "mappings": {
    "article": {
      "properties": {
        "title": {
          "type": "text",
          "fields": {
            "raw": {
              "type": "string",
              "index": "not_analyzed"
            }
          },
          "fielddata": true
        },
        "content": {
          "type": "text"
        },
        "post_date": {
          "type": "date"
        },
        "author_id": {
          "type": "long"
        }
      }
    }
  }
}

排序:

GET /website/article/_search
{
  "query": {
    "match_all": {}
  },
  "sort": [
    {
      "title.raw": {
        "order": "desc"
      }
    }
  ]
}

排序的bouncing result问题

bouncing results问题:两个document field值相同,想要进行排序,在不同的shard上这2个document可能排序不同;每次请求轮询打到不同的replica shard上;每次页面上看到的搜索结果的排序都不一样。这就是bouncing result,也就是跳跃的结果。

搜索的时候,是轮询将搜索请求发送到每一个replica shard(primary shard),但是在不同的shard上,可能document的排序不同

解决方案就是将preference设置为一个字符串,比如说user_id,让每个user每次搜索的时候,都使用同一个replica shard去执行,就不会看到bouncing results了

preference: 该参数能够让你控制哪些分片或者节点会用来处理搜索请求。

聚合分析和下钻分析

{
  "took": 4,
  "timed_out": false,
  "_shards": {
    "total": 5,
    "successful": 5,
    "skipped": 0,
    "failed": 0
  },
  "hits": {
    "total": 3,
    "max_score": 1,
    "hits": [
      {
        "_index": "ecommerce",
        "_type": "product",
        "_id": "2",
        "_score": 1,
        "_source": {
          "name": "jiajieshi yagao",
          "desc": "youxiao fangzhu",
          "price": 25,
          "producer": "jiajieshi producer",
          "tags": [
            "fangzhu"
          ]
        }
      },
      {
        "_index": "ecommerce",
        "_type": "product",
        "_id": "1",
        "_score": 1,
        "_source": {
          "name": "gaolujie yagao",
          "desc": "gaoxiao meibai",
          "price": 30,
          "producer": "gaolujie producer",
          "tags": [
            "meibai",
            "fangzhu"
          ]
        }
      },
      {
        "_index": "ecommerce",
        "_type": "product",
        "_id": "3",
        "_score": 1,
        "_source": {
          "name": "zhonghua yagao",
          "desc": "caoben zhiwu",
          "price": 70,
          "producer": "zhonghua producer",
          "tags": [
            "qingxin"
          ]
        }
      }
    ]
  }
}

聚合的执行速度很快,并且就像搜索一样几乎是实时的。

聚合的两个主要的概念,分别是 桶 和 指标。

桶(Buckets)

一个桶就是满足特定条件的文档的集合。

  • 当聚合开始被执行,每个文档会决定符合哪个桶的条件,如果匹配到,文档将放入相应的桶并接着进行聚合操作,一个文档可以根据条件放入不同的桶里。像是一个员工属于男性桶或者女性桶,日期2014-10-28属于十月桶,也属于2014年桶

  • 桶可以被嵌套在其他桶里面:像是北京能放在中国桶裡,而中国桶能放在亚洲桶裡

指标(Metrics)

指标就是对桶内的文档进行统计计算

  • 桶能让我们划分文档到多个有意义的集合, 但是最终我们需要的是对这些桶内的文档进行一些指标的计算

  • 指标通常是简单的数学运算(像是min、max、avg、sum),而这些是通过当前桶中的文档的值来计算的,利用指标能让你计算像平均薪资、最高出售价格、95%的查询延迟这样的数据

aggs 聚合的模板

  • 当query和aggs一起存在时,会先执行query的主查询,主查询query执行完后会搜出一批结果,而这些结果才会被拿去aggs拿去做聚合。

    • 另外要注意aggs后面会先接一层自定义的这个聚合的名字,然后才是接上要使用的聚合桶。
    • 如果有些情况不在意查询结果是什麽,而只在意aggs的结果,可以把size设为0,如此可以让返回的hits结果集是0,加快返回的速度。
  • 一个aggs裡可以有很多个聚合,每个聚合彼此间都是独立的,因此可以一个聚合拿来统计数量、一个聚合拿来分析数据、一个聚合拿来计算标准差...,让一次搜索就可以把想要做的事情一次做完

  • aggs可以嵌套在其他的aggs裡面,而嵌套的桶能作用的文档集范围,是外层的桶所输出的结果集。

GET 127.0.0.1/mytest/doc/_search
{
    "query": { ... },
    "size": 0,
    "aggs": {
        "custom_name1": {  //aggs后面接著的是一个自定义的name
            "桶": { ... }  //再来才是接桶
        },
        "custom_name2": {  //一个aggs裡可以有很多聚合
            "桶": { ... }
        },
        "custom_name3": {
            "桶": {
               .....
            },
            "aggs": {  //aggs可以嵌套在别的aggs裡面
                "in_name": { //记得使用aggs需要先自定义一个name
                    "桶": { ... } //in_name的桶作用的文档是custom_name3的桶的结果
                }
            }
        }
    }
}

返回结果:

{
   "hits": {
       "total": 8,
       "max_score": 0,
       "hits": [] //因为size设为0,所以没有查询结果返回
   },
   "aggregations": {
       "custom_name1": {
           ...
       },
       "custom_name2": {
           ...
       },
       "custom_name3": {
           ... ,
           "in_name": {
              ....
           }
       }
   }
  }

聚合中常用的桶 terms、filter、top_hits

terms

terms桶 : 针对某个field的值进行分组,field有几种值就分成几组

  • terms桶在进行分组时,会爲此field中的每种值创建一个新的桶
  • 要注意此 "terms桶" 和平常用在主查询query中的 "查找terms" 是不同的东西

计算每个tag下的商品数量:

GET /ecommerce/product/_search
{
  "size": 0, 
  "aggs": {
    "group_by_tag": {
      "terms": {
        "field": "tags"
      }
    }
  }
}

返回结果:

{
  "took": 8,
  "timed_out": false,
  "_shards": {
    "total": 5,
    "successful": 5,
    "skipped": 0,
    "failed": 0
  },
  "hits": {
    "total": 3,
    "max_score": 0,
    "hits": []
  },
  "aggregations": {
    "group_by_tag": {
      "doc_count_error_upper_bound": 0,
      "sum_other_doc_count": 0,
      "buckets": [
        {
          "key": "fangzhu",
          "doc_count": 2
        },
        {
          "key": "meibai",
          "doc_count": 1
        },
        {
          "key": "qingxin",
          "doc_count": 1
        }
      ]
    }
  }
}

因为tags总共有3种值,fangzhu、meibai、qingxin,所以terms桶为他们产生了3个bucket,并计算了每个bucket中符合的文档有哪些

bucket和bucket间是独立的,也就是说一个文档可以同时符合好几个bucket,像是{"tags": ["fangzhu", "meibai"]}就同时符合了fangzhu和meibai。

对名称中包含yagao的商品,计算每个tag下的商品数量:

GET /ecommerce/product/_search
{
  "query": {
    "match": {
      "name": "yagao"
    }
  }, 
  "size": 0, 
  "aggs": {
    "group_by_tag": {
      "terms": {
        "field": "tags"
      }
    }
  }
}

先分组,再算每组的平均值,计算每个tag下的商品的平均价格

GET /ecommerce/product/_search
{
  
  "size": 0, 
  "aggs": {
    "group_by_tag": {
      "terms": {
        "field": "tags"
      },
      "aggs": {
        "avg_price": { // avg_price计算每个bucket的平均price
          "avg": {
            "field": "price"
          }
        }
      }
    }
  }
}

计算每个tag下的商品的平均价格,并且按照平均价格降序排序。

可以在每个bucket里指定order进行排序

GET /ecommerce/product/_search
{
  
  "size": 0, 
  "aggs": {
    "group_by_tag": {
      "terms": {
        "field": "tags", 
        "order": {
          "avg_price": "desc"
        }
      },
      
      "aggs": {
        "avg_price": { 
          "avg": {
            "field": "price"
          }
        }
      }
    }
  }
}

按照指定的价格范围区间进行分组,然后在每组内再按照tag进行分组,最后再计算每组的平均价格

GET /ecommerce/product/_search
{
  
  "size": 0, 
  "aggs": {
    "group_by_price":{
      "range": {
        "field": "price",
        "ranges": [
          {
            "from": 10,
            "to": 20
          },
          {
            "from": 20,
            "to": 40
          },
          {
            "from": 40,
            "to": 100
          }
        ]
      },
      "aggs": {
        "tag": {
          "terms": {
            "field": "tags"
          },
          "aggs": {
            "avg_price": {
              "avg": {"field": "price"}
            }
          }
        }
      }
    }
    
  }
}

返回结果

{
  "took": 10,
  "timed_out": false,
  "_shards": {
    "total": 5,
    "successful": 5,
    "skipped": 0,
    "failed": 0
  },
  "hits": {
    "total": 3,
    "max_score": 0,
    "hits": []
  },
  "aggregations": {
    "group_by_price": {
      "buckets": [
        {
          "key": "10.0-20.0",
          "from": 10,
          "to": 20,
          "doc_count": 0,
          "tag": {
            "doc_count_error_upper_bound": 0,
            "sum_other_doc_count": 0,
            "buckets": []
          }
        },
        {
          "key": "20.0-40.0",
          "from": 20,
          "to": 40,
          "doc_count": 2,
          "tag": {
            "doc_count_error_upper_bound": 0,
            "sum_other_doc_count": 0,
            "buckets": [
              {
                "key": "fangzhu",
                "doc_count": 2,
                "avg_price": {
                  "value": 27.5
                }
              },
              {
                "key": "meibai",
                "doc_count": 1,
                "avg_price": {
                  "value": 30
                }
              }
            ]
          }
        },
        {
          "key": "40.0-100.0",
          "from": 40,
          "to": 100,
          "doc_count": 1,
          "tag": {
            "doc_count_error_upper_bound": 0,
            "sum_other_doc_count": 0,
            "buckets": [
              {
                "key": "qingxin",
                "doc_count": 1,
                "avg_price": {
                  "value": 70
                }
              }
            ]
          }
        }
      ]
    }
  }
}

filter桶

一个用来过滤的桶, 过滤得到的结果只有一个bucket.

要注意此处的 "filter桶" 和用在主查询query的 "过滤filter" 的用法是一模一样的,都是过滤. 不过差别是 "filter桶" 会自己给创建一个新的桶,而不会像 "过滤filter" 一样依附在query下. 因为filter桶毕竟还是一个聚合桶,因此他可以和别的桶进行嵌套,但他不是依附在别的桶上.

GET /ecommerce/product/_search
{
  "size": 0, 
  "aggs": {
    "filter_name": {
      "filter": {
        "bool": {
          "must":[
            {"match": {
              "name": "yagao"
            }}
            ]
        }
      }
    }
  }
}

filter桶和terms桶嵌套使用,先过滤出name为yagao的文档,再对这些文档进行tags分组.

GET /ecommerce/product/_search
{
  "size": 0, 
  "aggs": {
    "filter_name": {
      "filter": {
        "bool": {
          "must":[
            {"match": {
              "name": "yagao"
            }}
            ]
        }
      },
      "aggs": {
        "group_by_tags": {
          "terms": {
            "field": "tags"
          }
        }
      }
    }
  }
}

top_hits桶

在某个桶底下找出这个桶的前几笔hits,返回的hits格式和主查询query返回的hits格式一模一样.

top_hits桶支持的参数:

  • from、size

  • sort : 设置返回的hits的排序

    ​ 要注意,假设在主查询query裡已经对数据设置了排序sort,此sort并不会对aggs裡面的数据造成影响,也就是说主查询query查找出来的数据会先丢进aggs而非先经过sort,因此就算主查询设置了sort,也不会影响aggs数据裡的排序.因此如果在top_hits桶裡的返回的hits数据想要排序,需要自己在top_hits桶裡设置sort.如果没有设置sort,默认使用主查询query所查出来的_score排序

  • _source : 设置返回的字段

使用terms桶分组,再使用top_hits桶找出每个group裡面的price最小的前5笔hits.

GET /ecommerce/product/_search
{
  "size": 0, 
  "aggs": {
    "ggp": {
      "terms": {
        "field": "tags"
      },
      "aggs": {
        "hits": {
          "top_hits": {
            "size": 5,
            "sort":[
              {"price": "desc"}
              ]
          }
        }
      }
    }
  }
}

Elasticsearch的并发冲突

当并发操作ES的线程越多,或者并发请求越多,或者读取一份数据供用户查阅和操作的时间越长,就越可能出现错误。因为这段时间里ES内的数据很可能就已经被修改了,那么我们拿到的就是旧数据,基于旧数据去操作,后面的结果就错了。

悲观锁和乐观锁的并发控制

悲观锁并发控制方案,常见于关系型数据库。就是在各种情况下,都上锁。上锁之后,就只有 -一个线程可以操作这一条数据了,当然,不同的场景下,上的锁不同,行级锁,表级锁,读锁,写锁。

乐观锁并发方案,就是对取到的数据记录一个版本,然后更新的时候比较这个版本和数据库版本是否一致,不一致说明数据已经被改动了,这时就重复页面逻辑,再次比较,直到已有数据版本和数据库一致才进行更新操作。

  1. 悲观锁的优点是:方便,直接加锁,对应用程序来说,透明,不需要做额外的操作;缺点,并发能力很低,同一-时间只能有一条线程操作数据

  2. 乐观锁的优点是:并发能力很高,不给数据加锁,大星线程并发操作;缺点,麻烦,每次跟新的时候,都要先比对版本号,然后可能需要重新加载数据,再次修改,再写;这个过程,可能要重复好几次。

Elasticsearch乐观锁并发控制

PUT /test_index/test_type/7?version=2 在请求中带上version参数,只有ES中文档的_version 和 version 相同才可以进行更新。

external version

es提供了一个feature,就是说,你可以不用它提供的内部_version版本号来进行并发控制,可以基于你自己维护的一个版本号来进行并发控制。举个列子,加入你的数据在mysql里也有一份,然后你的应用系统本身就维护了一个版本号,无论是什么自己生成的,程序控制的。这个时候,你进行乐观锁并发控制的时候,可能并不是想要用es内部的_version来进行控制,而是用你自己维护的那个version来进行控制。

es,_version=1,?version=1,才能更新成功
es,_version=1,?version>1&version_type=external,才能成功.且更新后的document的version就变成了设置的version,不像之前的自动加1的操作。

Elasticsearch partial update

全量更新:查询,放界面,用户光修改,可能就 10 分钟,甚至半小时。然后修改完以后,再写回去,可能 es 中的数据早就被人修改了。所以并发冲突的情况就会发生的较多。

其实 es 内部对 partial update 的实际执行,跟传统的全量替换方式,是几乎一样的

  1. 内部先获取 document
  2. 将传过来的 field 更新到 document 的 json 中
  3. 将老的 document 标记为 deleted
  4. 将修改后的新的 document 创建出来

Partial update 相比于全量替换的优点

  1. 所有的查询、修改和写回操作,都发生在 es 中的一个 shard 内部,避免了所有的网络数据传输的开销(减少 2 次网络请求),大大提升了性能
  2. 减少了查询和修改中的时间间隔,可以有效减少并发冲突的情况(但不能完全杜绝,内部也使用乐观锁的并发控制策略)。全量更新的情况下获取到的其他字段的信息可能已经被修改了,partial update的情况下更新的时候会快速获取那些其他字段。
  3. 内部可能有多个partial update在操作,所以内部更新的时候也是基于内部拿到的version的版本来做并发控制,可以指定失败重试次数retry_on_conflict
POST /test_index/test_type/10/_update?retry_on_conflict=5
{
  "doc": {
    "test_field2": "updated test2"
  }
}

![image-20191202114957873](../../../Library/Application Support/typora-user-images/image-20191202114957873.png)

mget

_mget 语法有3种用法

GET /_mget
{
   "docs" : [
      {
         "_index" : "test_index",
         "_type" :  "test_type",
         "_id" :    1
      },
      {
         "_index" : "test_index",
         "_type" :  "test_type",
         "_id" :    2
      }
   ]
}
GET /test_index/_mget
{
   "docs" : [
      {
         "_type" :  "test_type",
         "_id" :    1
      },
      {
         "_type" :  "test_type",
         "_id" :    2
      }
   ]
}
GET /test_index/test_type/_mget
{
   "ids": [1, 2]
}

一般来说,在进行查询的时候,如果一次性要查询多条数据的话,那么一定要用batch批量操作的api尽可能减少网络开销次数,可能可以将性能提升数倍,甚至数十倍.

bulk update

格式要求严谨,每个json串不能换行,只能放一行,同时一个json串和一个json串之间,必须有一个换行

{"action": {"metadata"}}
{"data"}
POST /_bulk
{ "delete": { "_index": "test_index", "_type": "test_type", "_id": "3" }} 
{ "create": { "_index": "test_index", "_type": "test_type", "_id": "12" }}
{ "test_field":    "test12" }
{ "index":  { "_index": "test_index", "_type": "test_type", "_id": "2" }}
{ "test_field":    "replaced test2" }
{ "update": { "_index": "test_index", "_type": "test_type", "_id": "1", "_retry_on_conflict" : 3} }
{ "doc" : {"test_field2" : "bulk test1"} }

_bulk的操作和mget一样,可以把index和type提到url中。

bulk操作中,任意一个操作失败,是不会影响其他的操作的,但是在返回结果里,会告诉你异常日志。

bulk request会加载到内存里,如果太大的话,性能反而会下降,因此需要反复尝试一个最佳的bulk size。一般从10005000条数据开始,尝试逐渐增加。另外,如果看大小的话,最好是在515MB之间。

document的路由

每次增删改查一个document的时候是怎么把这个document路由到对应的shard的?

我们知道,一个 index 的数据会被分为多片,每片都在一个 shard 中。所以说,一个 document,只能存在于一个 shard 中。

当客户端创建 document 的时候。es 此时就需要决定说,这个 document 是放在这个 index 的哪个 shard 上。这个过程,就称之为 document routing,数据路由。

primary shard一旦指定个数,不允许修改的。但是 replica shard 可以随时修改,这也是由路由算法决定的。比如存储的时候基于3个shard的路由算法存储了documnet,一旦shard数量更改之后,在做查询的时候基于新的shard的数量计算的路由算法就会匹配到别的shard上。

路由算法:shard = hash(routing) % number_of_primary_shards

其中routing没有指定的时候默认就是_id, 也可以自定义。

put /index/type/id?routing=user_id

Elasticsearch增删改查内部原理

增删改内部原理

  1. 客户端选择任意一个node发送请求过去,这个node就是coordinating node(协调节点, 每个node都能根据路由算法找到document在哪个shard上)。
  2. coordinating node,对document进行路由,将请求转发给对应的node(有primary shard)
  3. 实际的node上的primary shard处理请求,然后将数据同步到replica node
  4. coordinating node,如果发现primary node和所有replica node都搞定之后,就返回响应结果给客户端

对于增删改操作,只能由primary shard进行操作,并同步到replica shard。

对于搜索操作,会打到所有的shard上,对于指定id的搜索会依据路由算法路由到某个shard上。

查询内部原理

  1. 客户端发送请求到任意一个node,成为coordinate node
  2. coordinate node对document进行路由,路由之后就知道请求应该发给哪个shard了,此时会使用round-robin随机轮询算法,在primary shard以及其所有replica中随机选择一个,让读请求负载均衡
  3. 接收请求的node返回document给coordinate node
  4. coordinate node返回document给客户端
  5. 特殊情况:document如果还在建立索引过程中,可能只有primary shard有,任何一个replica shard都没有,此时可能会导致无法读取到document,但是document完成索引建立之后,primary shard和replica shard就都有了

round-robin 随机轮询算法: 比如说 coordinate node,接收到了对一个 document 的 4 次查询,就会使用算法,将 2 次查询请求转发给 P1, 将 2 次查询请求转发给 R1, 尽量让 primary shard 和所有的 replica shard 均匀的服务读请求,得到负载均衡的效果

写一致性

我们在发送任何一个增删改操作的时候,比如说put /index/type/id,都可以带上一个consistency参数,指明我们想要的写一致性是什么?
put /index/type/id?consistency=quorum

one:要求我们这个写操作,只要有一个primary shard是active活跃可用的,就可以执行
all:要求我们这个写操作,必须所有的primary shard和replica shard都是活跃的,才可以执行这个写操作
quorum:默认的值,要求所有的shard中,必须是大部分的shard都是活跃的,可用的,才可以执行这个写操作。

quorum机制:写之前必须确保大多数shard都可用,int( (primary + number_of_replicas) / 2 ) + 1,当number_of_replicas>1时才生效(为了兼容单节点单shard的情况)。

quorum不齐全时可以进行wait操作。

等待期间,期望活跃的shard数量可以增加,最后实在不行,就会timeout
我们其实可以在写操作的时候,加一个timeout参数,比如说put /index/type/id?timeout=30,这个就是说自己去设定quorum不齐全的时候,es的timeout时长,可以缩短,也可以增长

search的timeout机制

默认情况下,没有所谓的 timeout,比如说,如果你的捏素特别疼别慢,每个 shard 都要花好几分钟才能 1 询出来所有的数据,那么你的搜索请求也会等待好几分钟之后才会返回.

我们有些搜索应用,对时间是很敏感的,比如说我么的电商网站,你不能说让用户等各 10 分钟,才能等到一次搜索请求的结果,如果那样的话,人家早就走了,不来买东西了。

timeout 机制,指定每个 shard,就只能在 timeout 时间范围内,将搜索到的部分数据(也可能全都搜索到了),直接理解返回给 client 程序,而不是等到所有的数据全都搜索出来以后再返回。

确保说,一次搜索请求可以在用户指定的 timeout 时长内完成。为-些时间敏感的搜索应用提供良好的支持。

GET /_search?timeout=10m

multi-index和multi-type搜索模式

/_search:所有索引,所有type下的所有数据都搜索出来
/index1/_search:指定一个index,搜索其下所有type的数据
/index1,index2/_search:同时搜索两个index下的数据
/*1,*2/_search:按照通配符去匹配多个索引
/index1/type1/_search:搜索一个index下指定的type的数据
/index1/type1,type2/_search:可以搜索一个index下多个type的数据
/index1,index2/type1,type2/_search:搜索多个index下的多个type的数据
/_all/type1,type2/_search:_all,可以代表搜索所有index下的指定type的数据

deep paging

什么叫 deep paging?简单来说,就是搜索的特别深,比如总共有 60000 条数据,每个 shard上分了 20000 条数据。每页是 10 条数据,这个时候,你要搜索到第 1000 页,实际上要拿到的是 10001~10010 大家自己思考一下,是第几条到第几条数啊?

每个 shard,其实都要返回的是最后 10 条数据,这种理解是错误的。

你的请求首先可能是打到一个不包含这个 index 的 shard 的 node 上去,这个 node 就是一个 coordinate node,那么这个 coordinate node 就会将搜索请求转发到 index 的三个 shard 所在的 node 上去。

比如说我们刚才说的情况下,要搜索 60000 条数据中的第 1000 页,实际上每个 shard 都要将内部的 20000 条数据中的第 10001~10010 条数据,拿出来,不是才 10 条,是 10010 条数据。3 个 shard 每个 shard 都返回 10010 条数据给 coordinate node, coordinate node 会收到总共 30030 条数据,然后在这些数据中进行排序,score,相关度分数,然后取到排位最高的前 10 条数据,其实就是我们要的最后的第 1000 页的 10 条数据。

搜索的过深的时候,就需要在 coordinate node。上保存大量的数据,还要进行大量数据的排序,排序之后,再取出对应的那一页。所以这个过程,即耗费网络带宽,耗费内存,还耗费 cpu。所以 deep paging 的性能问题。我们应该尽量避免出现这种 deep paging 操作。

_all metadata的原理和作用

GET /test_index/test_type/_search?q=test

直接可以搜索所有的field,任意一个field包含指定的关键字就可以搜索出来。我们在进行中搜索的时候,难道是对document中的每一个field都进行一次搜索吗?不是的

es中的_all元数据,在建立索引的时候,我们插入一条document,它里面包含了多个field,此时,es会自动将多个field的值,全部用字符串的方式串联起来,变成一个长的字符串,作为_all field的值,同时建立索引

后面如果在搜索的时候,没有对某个field指定搜索,就默认搜索_all field,其中是包含了所有field的值的.

{
  "name": "jack",
  "age": 26,
  "email": "jack@sina.com",
  "address": "guamgzhou"
}

"jack 26 jack@sina.com guangzhou",作为这一条document的_all field的值,同时进行分词后建立对应的倒排索引.

精确匹配与全文搜索的对比

1、exact value

2017-01-01,exact value,搜索的时候,必须输入2017-01-01,才能搜索出来
如果你输入一个01,是搜索不出来的

2、full text
(1)缩写 vs. 全程:cn vs. china
(2)格式转化:like liked likes
(3)大小写:Tom vs tom
(4)同义词:like vs love

2017-01-01,2017 01 01,搜索2017,或者01,都可以搜索出来
china,搜索cn,也可以将china搜索出来
likes,搜索like,也可以将likes搜索出来
Tom,搜索tom,也可以将Tom搜索出来
like,搜索love,同义词,也可以将like搜索出来

不是说单纯的只是匹配完整的一个值,而是可以对值进行拆分词语后(分词)进行匹配,也可以通过缩写、时态、大小写、同义词等进行匹配.

分词器

内置分词器

分词器分出的结果再进行倒排索引。

考虑如下字符串: Set the shape to semi-transparent by calling set_trans(5)

standard analyzer:set, the, shape, to, semi, transparent, by, calling, set_trans, 5(默认的是standard)
simple analyzer:set, the, shape, to, semi, transparent, by, calling, set, trans
whitespace analyzer:Set, the, shape, to, semi-transparent, by, calling, set_trans(5)
language analyzer(特定的语言的分词器,比如说,english,英语分词器):set, shape, semi, transpar, call, set_tran, 5。

默认情况下,对于一个query string, es会使用它对应的field建立倒排索引时相同的分词器去进行分词,分词和normalization,只有这样,才能实现正确的搜索.

测试分词效果:

GET /_analyze
{
  "analyzer": "standard",
  "text": ["Text to analyze", "hello is world"]
}
{
  "tokens": [
    {
      "token": "text",
      "start_offset": 0,
      "end_offset": 4,
      "type": "<ALPHANUM>",
      "position": 0
    },
    {
      "token": "to",
      "start_offset": 5,
      "end_offset": 7,
      "type": "<ALPHANUM>",
      "position": 1
    },
    {
      "token": "analyze",
      "start_offset": 8,
      "end_offset": 15,
      "type": "<ALPHANUM>",
      "position": 2
    },
    {
      "token": "hello",
      "start_offset": 16,
      "end_offset": 21,
      "type": "<ALPHANUM>",
      "position": 3
    },
    {
      "token": "is",
      "start_offset": 22,
      "end_offset": 24,
      "type": "<ALPHANUM>",
      "position": 4
    },
    {
      "token": "world",
      "start_offset": 25,
      "end_offset": 30,
      "type": "<ALPHANUM>",
      "position": 5
    }
  ]
}

(1)往es里面直接插入数据,es会自动建立索引,同时建立type以及对应的mapping
(2)mapping中就自动定义了每个field的数据类型
(3)不同的数据类型(比如说text和date),可能有的是exact value,有的是full text
(4)exact value,在建立倒排索引的时候,分词的时候,是将整个值一起作为一个关键词建立到倒排索引中的;full text,会经历各种各样的处理,分词,normaliztion(时态转换,同义词转换,大小写转换),才会建立到倒排索引中
(5)同时呢,exact value和full text类型的field就决定了,在一个搜索过来的时候,对exact value field或者是full text field进行搜索的行为也是不一样的,会跟建立倒排索引的行为保持一致;比如说exact value搜索的时候,就是直接按照整个值进行匹配,full text query string,也会进行分词和normalization再去倒排索引中去搜索
(6)可以用es的dynamic mapping,让其自动建立mapping,包括自动设置数据类型;也可以提前手动创建index和type的mapping,自己对各个field进行设置,包括数据类型,包括索引行为,包括分词器,等等.

mapping

只能创建index时手动建立mapping,或者新增field mapping,但是不能update field mapping.

索引的3种方式:

  • analyzed(分词)
  • not_analyzed(不分词)
  • no(不支持搜索)

手动指定mapping:

创建mapping:

PUT /website
{
  "mappings": {
    "article": {
      "properties": {
        "author_id": {
          "type": "long"
        },
        "title": {
          "type": "text",
          "analyzer": "english"
        },
        "content": {
          "type": "text"
        },
        "post_date": {
          "type": "date"
        },
        "publisher_id": {
          "type": "text",
          "index": "not_analyzed"
        }
      }
    }
  }
}

修改mapping

PUT /website/_mapping/article
{
  "properties" : {
    "new_field" : {
      "type" :    "string",
      "index":    "not_analyzed"
    }
  }
}

测试mapping

GET /website/_analyze
{
  "field": "content",
  "text": "my-dogs" 
}

_mapping复杂数据类型以及object类型数据底层结构

  1. multivalue field:{ "tags": [ "tag1", "tag2" ]},数组里的数据类型需要一致,建立索引的方式和string一样。

  2. object field

    {
      "address": {
        "country": "china",
        "province": "guangdong",
        "city": "guangzhou"
      },
      "name": "jack",
      "age": 27,
      "join_date": "2017-01-01"
    }
    

    ​ 存储格式:

    {
        "name":            [jack],
        "age":          [27],
        "join_date":      [2017-01-01],
        "address.country":         [china],
        "address.province":   [guangdong],
        "address.city":  [guangzhou]
    }
    
    {
        "authors": [
            { "age": 26, "name": "Jack White"},
            { "age": 55, "name": "Tom Jones"},
            { "age": 39, "name": "Kitty Smith"}
        ]
    }
    

    ​ 存储格式:

    {
        "authors.age":    [26, 55, 39],
        "authors.name":   [jack, white, tom, jones, kitty, smith]
    }
    

score 计算

relevance score算法,简单来说,就是计算出,一个索引中的文本,与搜索文本,他们之间的关联匹配程度

Elasticsearch使用的是 term frequency/inverse document frequency算法,简称为TF/IDF算法

Term frequency:搜索文本中的各个词条在field文本中出现了多少次,出现次数越多,就越相关

搜索请求:hello world

doc1:hello you, and world is very good
doc2:hello, how are you

Inverse document frequency:搜索文本中的各个词条在整个索引的所有文档中出现了多少次,出现的次数越多,就越不相关

搜索请求:hello world

doc1:hello, today is very good
doc2:hi world, how are you

比如说,在index中有1万条document,hello这个单词在所有的document中,一共出现了1000次;world这个单词在所有的document中,一共出现了100次,那么doc2更相关

Field-length norm:field长度,field越长,相关度越弱

搜索请求:hello world

doc1:{ "title": "hello article", "content": "babaaba 1万个单词" }
doc2:{ "title": "my article", "content": "blablabala 1万个单词,hi world" }

hello world在整个index中出现的次数是一样多的

doc1更相关,title field更短

doc values

搜索的时候,要依靠倒排索引;排序的时候,需要依靠正排索引,看到每个document的每个field,然后进行排序,所谓的正排索引,其实就是doc values

在建立索引的时候,一方面会建立倒排索引,以供搜索用;一方面会建立正排索引,也就是doc values,以供排序,聚合,过滤等操作使用

doc values是被保存在磁盘上的,此时如果内存足够,os会自动将其缓存在内存中,性能还是会很高;如果内存不足够,os会将其写入磁盘上

doc1: { "name": "jack", "age": 27 }
doc2: { "name": "tom", "age": 30 }

document name age

doc1 jack 27
doc2 tom 30

query 和 fetch 流程

1、query phase

(1)搜索请求发送到某一个coordinate node,构构建一个priority queue,长度以paging操作from和size为准,默认为10
(2)coordinate node将请求转发到所有shard,每个shard本地搜索,并构建一个本地的priority queue
(3)各个shard将自己的priority queue返回给coordinate node,并构建一个全局的priority queue

2、fetch phbase工作流程

(1)coordinate node构建完priority queue之后,就发送mget请求根据id去所有shard上获取对应的document(query phase拿到的是一对document id)
(2)各个shard将document返回给coordinate node
(3)coordinate node将合并后的document结果返回给client客户端

创建、修改和删除索引

创建索引:

PUT /my_index
{
  "settings": {
    "number_of_shards": 1,
    "number_of_replicas": 0
  },
  "mappings": {
    "my_type": {
      "properties": {
        "my_field": {
          "type": "text"
        }
      }
    }
  }
}

修改索引

PUT /my_index/_settings
{
    "number_of_replicas": 1
}

删除索引

DELETE /my_index
DELETE /index_one,index_two
DELETE /index_*
DELETE /_all

_source和_all

_source

好处

(1)查询的时候,直接可以拿到完整的document,不需要先拿document id,再发送一次请求拿document
(2)partial update基于_source实现
(3)reindex时,直接基于_source实现,不需要从数据库(或者其他外部存储)查询数据再修改
(4)可以基于_source定制返回field
(5)debug query更容易,因为可以直接看到_source

如果不需要上述好处,可以禁用_source

PUT /my_index/_mapping/my_type2
{
  "_source": {"enabled": false}
}

_all

将所有field打包在一起,作为一个_all field,建立索引。没指定任何field进行搜索时,就是使用_all field在搜索。

PUT /my_index/_mapping/my_type3
{
  "_all": {"enabled": false}
}

也可以在field级别设置include_in_all field,设置是否要将field的值包含在_all field中

PUT /my_index/_mapping/my_type4
{
  "properties": {
    "my_field": {
      "type": "text",
      "include_in_all": false
    }
  }
}

dynamic策略

true:遇到陌生字段,就进行dynamic mapping
false:遇到陌生字段,就忽略, 用这个字段搜索不出内容
strict:遇到陌生字段,就报错。

PUT /my_index
{
  "mappings": {
    "my_type": {
      "dynamic": "strict",
      "properties": {
        "title": {
          "type": "text"
        },
        "address": {
          "type": "object",
          "dynamic": "true"
        }
      }
    }
  }
}

mapping如果已经设置成strict,那么传到Elasticsearch的数据要按照规定的格式ceiling可以。

定制dynamic mapping策略

(1)date_detection

默认会按照一定格式识别date,比如yyyy-MM-dd。但是如果某个field先过来一个2017-01-01的值,就会被自动dynamic mapping成date,后面如果再来一个"hello world"之类的值,就会报错。可以手动关闭某个type的date_detection,如果有需要,自己手动指定某个field为date类型。

PUT /my_index/_mapping/my_type
{
    "date_detection": false
}

2)定制自己的dynamic mapping template(type level)

PUT /my_index
{
    "mappings": {
        "my_type": {
            "dynamic_templates": [
                { "en": {
                      "match":              "*_en", 
                      "match_mapping_type": "string",
                      "mapping": {
                          "type":           "string",
                          "analyzer":       "english"
                      }
                }}
            ]
}}}
PUT /my_index/my_type/1
{
  "title": "this is my first article"
}
PUT /my_index/my_type/2
{
  "title_en": "this is my first article"
}

title没有匹配到任何的dynamic模板,默认就是standard分词器,不会过滤停用词,is会进入倒排索引,用is来搜索是可以搜索到的.
title_en匹配到了dynamic模板,就是english分词器,会过滤停用词,is这种停用词就会被过滤掉,用is来搜索就搜索不到了.

posted @ 2020-03-10 21:03  龙云飞谷  阅读(448)  评论(0编辑  收藏  举报