ES

ES

参考文档:https://www.cnblogs.com/buchizicai/p/17093719.html

ES分布式搜索引擎

注意:在没有创建库的时候搜索,ES会创建一个库并自动创建该字段并且设置为String类型也就是text

什么是elasticsearch?

  • 一个开源的分布式搜索引擎,可以用来实现搜索,日志统计,分析,系统监控等功能

什么是elastic stack(ELK)

  • 是以Elasticsearch(存储,计算,搜索数据)为核心的技术栈,包括beats(数据抓取),Logstash(数据抓取),kibana(数据可视化);被广泛应用在日志数据分析实时监控等领域

什么是Lucence?

  • 是Apache的开源搜索引擎类库,提供了搜索引擎的核心API

正向索引和倒排索引

正向索引

设置了索引的话挺快的,但要是模糊查询则就很慢

如果是根据id查询,那么直接走索引,查询速度非常快。

如果是做模型查询,则需要逐行扫描数据,逐行扫描,也就是全表扫描,随着数据量增加,其查询效率也会越来越低。当数据量达到数百万时,就是一场灾难。

倒排索引

倒排索引的概念是基于MySQL这样的正向索引而言的。

elasticsearch采用倒排索引,有两个非常重要的概念:

  • 文档(document):用来搜索的数据,每条数据就是一个文档。例如,一个网页,一个商品信息
  • 字段(term):文档按照语义分成的词语

正向和倒排对比

概念区别:

  • 正向索引是最传统的,根据id索引的方式。但根据词条查询时,必须先逐条获取每个文档,然后判断文档中是否包含所需要的词条,是根据文档找词条的过程
  • 倒排索引则相反,是先找到用户要搜索的词条,根据词条得到保护词条的文档的id,然后根据id获取文档。是根据词条找文档的过程

优缺点:

正向索引

  • 优点:
    • 可以给多个字段创建索引
    • 根据索引字段搜索、排序速度非常快
  • 缺点:
    • 根据非索引字段,或者索引字段中的部分词条查找时,只能全表扫描。

倒排索引

  • 优点:
    • 根据词条搜索、模糊搜索时,速度非常快
  • 缺点:
    • 只能给词条创建索引,而不是字段
    • 无法根据字段做排序

基本概念

文档:像数据库中的一条数据

字段:像数据库中的列

ES是面向文档(Document)存储的,可以是数据库中的一条商品数据,一个订单信息。文档数据会被序列化为json格式后存储在es中。而json文档中往往包含很多字段(Field),类似于mysql中的列

索引(index):就像数据库里的表

例如:
	所有用户的文档,可以组织在一起,称为用户的索引
	所有商品的文档,可以组织在一起,称为商品的索引

映射:就像数据库中定义的表结构

数据库的表会有约束信息,用来定义表的结构、字段的名称、类型等信息。因此,索引库中就有映射(mapping),是索引中文档的字段约束信息,类似表的结构约束。

与mysql对比

  • mysql:擅长事务类型操作,可以确保数据的安全和一致性
  • es:擅长海量数据的搜索,分析,计算
MySQL Elasticsearch 说明
Table Index 索引(index),就是文档的集合,类似数据库的表(table)
Row Document 文档(Document),就是一条条的数据,类似数据库中的行(Row),文档都是JSON格式
Column Field 字段(Field),就是JSON文档中的字段,类似数据库中的列(Column)
Schema Mapping Mapping(映射)是索引中文档的约束,例如字段类型约束。类似数据库的表结构(Schema)
SQL DSL DSL是elasticsearch提供的JSON风格的请求语句,用来操作elasticsearch,实现CRUD

在企业中,往往是两者结合使用:

  • 对于安全性高的写操作,使用mysql实现
  • 对查询性能要求较高的搜索需求,使用ES实现
  • 两者再基于某种方式,实现数据的同步,保证一致性

安装es、kibana、分词器

分词器的作用:创建倒排序索引时对文档分词;用户搜索时,对输入的内容分词

IK分词器的分词模型

  • ik_smart :智能切分,粗粒度
  • ik_max_word:最细切分,细粒度

IK分词器如何拓展词条?如何停用词条?

  • 利用config目录的IkAnalyzer.cfg.xml文件添加拓展词典和停用词典
  • 在词典中添加拓展词条或者停用词条

部署单点ES

  1. 创建网络

    因为还需要部署kibana容器,因此需要让es和kibana容器互联,先创建网络:

    docker network create es-net

  2. 加载镜像

    docker pull elasticsearch=7.12.1

    docker pull kibana

    # 导入数据
    docker load -i es.tar
    
  3. 运行

    运行docker命令,部署单点es:

    docker run -d \
    	--name es \
        -e "ES_JAVA_OPTS=-Xms512m -Xmx512m" \
        -e "discovery.type=single-node" \
        -v es-data:/usr/share/elasticsearch/data \
        -v es-plugins:/usr/share/elasticsearch/plugins \
        --privileged \
        --network es-net \
        -p 9200:9200 \
        -p 9300:9300 \
    elasticsearch:7.12.1
    

    命令解释:

    • -e "cluster.name=es-docker-cluster":设置集群名称
    • -e "http.host=0.0.0.0":监听的地址,可以外网访问
    • -e "ES_JAVA_OPTS=-Xms512m -Xmx512m":内存大小
    • -e "discovery.type=single-node":非集群模式
    • -v es-data:/usr/share/elasticsearch/data:挂载逻辑卷,绑定es的数据目录
    • -v es-logs:/usr/share/elasticsearch/logs:挂载逻辑卷,绑定es的日志目录
    • -v es-plugins:/usr/share/elasticsearch/plugins:挂载逻辑卷,绑定es的插件目录
    • --privileged:授予逻辑卷访问权
    • --network es-net :加入一个名为es-net的网络中
    • -p 9200:9200:端口映射配置

    在浏览器中输入:http://xxx.xxx.xxx.xxx:9200,即可看到ES的响应结果

  4. 部署kibana

    1. 部署

      创建网络后,可以自己导包,或者pull

    2. 运行docker

      docker run -d \
      --name kibana \
      -e ELASTICSEARCH_HOSTS=http://es:9200 \
      --network=es-net \
      -p 5601:5601  \
      kibana:7.12.1
      
      • --network es-net :加入一个名为es-net的网络中,与elasticsearch在同一个网络中
      • -e ELASTICSEARCH_HOSTS=http://es:9200":设置elasticsearch的地址,因为kibana已经与elasticsearch在一个网络,因此可以用容器名直接访问elasticsearch
      • -p 5601:5601:端口映射配置

      kibana启动一般比较慢,需要多等待一会,可以通过命令:

      docker logs -f kibana

      查看运行日志,当查看到http server running at http:....,说明成功

      此时,在浏览器输入地址访问:http://00.00.00.00:5601,即可看到结果

      在kibana左侧中提供了一个DevTools界面,这个界面可以编写DSL来操作es,并且对dsl语句有自动补全功能

  5. 安装IK分词器

    1. 在线安装分词器(较慢)

      # 进入容器内部
      docker exec -it elasticsearch /bin/bash
      
      # 在线下载并安装
      ./bin/elasticsearch-plugin  install https://github.com/medcl/elasticsearch-analysis-ik/releases/download/v7.12.1/elasticsearch-analysis-ik-7.12.1.zip
      
      #退出
      exit
      #重启容器
      docker restart elasticsearch
      
    2. 离线安装ik插件(推荐)

解压分词器安装包

  1. 解压ik分词器压缩包,重命名为ik

  2. 上传到es容器的插件数据卷中

  3. 重启容器

    docker restart es

  4. 查看es日志

    docker logs -f es

  5. 测试

    IK分词器包含两种模式:

    • ik_smart : 最少切分
    • ik_max_word: 最细切分

    在kibana的Dev tools中输入以下代码:

    ”analyzer“ 就是选择分词器模式

    GET /_analyze
    {
      "analyzer": "ik_max_word",
      "text": "黑马程序员学习java太棒了"
    }
    

    结果:

    {
      "tokens": [
        {
          "token": "黑马",
          "start_offset": 0,
          "end_offset": 2,
          "type": "CN_WORD",
          "position": 0
        },
        {
          "token": "程序员",
          "start_offset": 2,
          "end_offset": 5,
          "type": "CN_WORD",
          "position": 1
        },
        {
          "token": "程序",
          "start_offset": 2,
          "end_offset": 4,
          "type": "CN_WORD",
          "position": 2
        },
        {
          "token": "员",
          "start_offset": 4,
          "end_offset": 5,
          "type": "CN_CHAR",
          "position": 3
        },
        {
          "token": "学习",
          "start_offset": 5,
          "end_offset": 7,
          "type": "CN_WORD",
          "position": 4
        },
        {
          "token": "java",
          "start_offset": 7,
          "end_offset": 11,
          "type": "ENGLISH",
          "position": 5
        },
        {
          "token": "太棒了",
          "start_offset": 11,
          "end_offset": 14,
          "type": "CN_WORD",
          "position": 6
        },
        {
          "token": "太棒",
          "start_offset": 11,
          "end_offset": 13,
          "type": "CN_WORD",
          "position": 7
        },
        {
          "token": "了",
          "start_offset": 13,
          "end_offset": 14,
          "type": "CN_CHAR",
          "position": 8
        }
      ]
    }
    

扩展分词器

随着互联网的发展,“造词运动”也越发的频繁,出现了很多的新词,在原因词汇列表中并不存在,所以需要不断更新

  1. 打开IK分词器config目录

  2. 在IKAnalyzer.cfg.xml配置文件内容添加:

    <?xml version="1.0" encoding="UTF-8"?>
    <!DOCTYPE properties SYSTEM "http://java.sun.com/dtd/properties.dtd">
    <properties>
            <comment>IK Analyzer 扩展配置</comment>
            <!--用户可以在这里配置自己的扩展字典 *** 添加扩展词典-->
            <entry key="ext_dict">ext.dic</entry>
    </properties>
    
  3. 新建一个 ext.dic,可以参考config目录下复制一个配置文件进行修改

    白嫖
    奥利给
    

    注意当前文件的编码必须是 UTF-8 格式,严禁使用Windows记事本编辑

  4. 重启es

    docker restart es
    
    # 查看日志
    docker logs -f  elasticsearch
    

    日志中已经成功加载ext.dic配置文件

索引库操作

索引库就是类似数据库表,mapping映射就类似表的结构

要向es中存储数据,必须先创建“库” 和 “表”

mapping映射属性

mapping是对索引库中文档的约束,常见的mapping属性包括:

  • type: 字段数据类型,常见的数据类型有
    • 字符串:text(可分词的文本),keyword(精确值,例如:品牌,国家,IP地址;不支持分词)
    • 数值:long、integer、short、byte、double、float
    • 布尔:boolean
    • 日期:date
    • 对象:object
  • index:是否创建索引,默认为true
  • analyzer:使用那种分词器
  • properties:该字段的子字段

例如下面的json文档

{	
    # 类型为integer;参与搜索,因此需要index为true;无需分词器
    "age": 21,
    # 类型为float,参与搜索,因此需要index为true,无需分词器
    "weight": 52.1类型为boolean;参与搜索,因此需要index为true;无需分词器,
    # 类型为boolean;参与搜索,因此需要index为true;无需分词器
    "isMarried": false,
    # 类型为字符串,需要分词,因此是text;参与搜索,因此需要index为true;分词器可以用ik_smart
    "info": "真相只有一个!",
    # 类型为字符串,但是不需要分词,因此是keyword;不参与搜索,因此需要index为false;无需分词器
    "email": "zy@itcast.cn",
    # 虽然是数组,但是我们只看元素的类型,类型为float;参与搜索,因此需要index为true;无需分词器
    "score": [99.1, 99.5, 98.9],
	# 类型为object,需要定义多个子属性
    "name": {
        # 类型为字符串,但是不需要分词,因此是keyword;参与搜索,因此需要index为true;无需分词器
        "firstName": "柯",
        "lastName": "南"
    }
}

索引库的CRUD

CRUD简单描述:

  • 创建索引库:PUT /索引库名
  • 查询索引库:GET /索引库名
  • 删除索引库:DELETE /索引库名
  • 修改索引库(添加字段):PUT /索引库名/_mapping

创建索引库和映射

基本语法

  • 请求方式:put
  • 请求路径:/索引库名,可以自定义
  • 请求参数:mapping映射

格式

PUT /索引库名称
{
  "mappings": {
    "properties": {
      "字段名":{
        "type": "text",
        "analyzer": "ik_smart"
      },
      "字段名2":{
        "type": "keyword",
        "index": "false"
      },
      "字段名3":{
        "properties": {
          "子字段": {
            "type": "keyword"
          }
        }
      },
      // ...略
    }
  }
}

示例

PUT /conan
{
  "mappings": {
    "properties": {
      "column1":{
        "type": "text",
        "analyzer": "ik_smart"
      },
      "column2":{
        "type": "keyword",
        "index": "false"
      },
      "column3":{
        "properties": {
          "子字段1": {
            "type": "keyword"
          },
          "子字段2": {
            "type": "keyword"
          }
        }
      },
      // ...略
    }
  }
}

查询索引库

基本语法

  • 请求方式:GET
  • 请求路径:/索引库名
  • 请求参数:无

格式

GET /索引库名

GET /api-gateway-prd

修改索引库

这里的修改是只能增加新的字段到mapping中

虽然倒排索引结构并不复杂,但是一旦数据结果改变(比如改变了分词器),就需要重新创建倒排索引,这简直是灾难,索引库一旦创建,无法修改mapping

虽然无法修改mapping中已有的字段,但是却允许添加新的字段到mapping中,因为不会对倒排索引产生影响。

PUT /索引库名/_mapping
{
  "properties": {
    "新字段名":{
      "type": "integer"
    }
  }
}

删除索引库

语法:

  • 请求方式 DELETE
  • 请求路径 /索引库名
  • 请求参数 无
DELETE /索引库名

文档操作

文档操作有哪些?

  • 创建文档:POST /{索引库名}/_doc/文档id
  • 查询文档:GET /{索引库名}/_doc/文档id
  • 删除文档:DELETE /{索引库名}/_doc/文档id
  • 修改文档:
    • 全量修改:PUT /{索引库名}/_doc/文档id
    • 增量修改:POST /{索引库名}/_update/文档id { "doc": {字段}}

新增文档

语法

POST /索引库名/_doc/文档id
{
    "字段1": "值1",
    "字段2": "值2",
    "字段3": {
        "子属性1": "值3",
        "子属性2": "值4"
    },
    // ...
}

示例
POST /heima/_doc/1
{
    "info": "真相只有一个!",
    "email": "zy@itcast.cn",
    "name": {
        "firstName": "柯",
        "lastName": "南"
    }
}

查询文档

语法:

GET /{索引库名称}/_doc/{id}
//批量查询:查询该索引库下的全部文档
GET /{索引库名称}/_search

通过kibana查看数据:

GET /heima/_doc/1

删除文档

删除使用DELETE请求,同样,需要根据id进行删除

语法:

DELETE /{索引库名}/_doc/id值

# 根据id删除数据
DELETE /heima/_doc/1

修改文档

修改文档有两种方式:

  • 全量修改:直接覆盖原来的文档
  • 增量修改:修改文档中的部分字段
全量修改

覆盖原来的文档,其本质是:

  • 根据指定的id删除文档
  • 新增一个相同id的文档

注意:如果根据id删除时,id不存在,第二步的更新也会执行,也就从修改变成了新增操作了

语法:

PUT /{索引库名}/_doc/文档id
{
    "字段1": "值1",
    "字段2": "值2",
    // ... 略
}


示例
PUT /heima/_doc/1
{
    "info": "黑马程序员高级Java讲师",
    "email": "zy@itcast.cn",
    "name": {
        "firstName": "云",
        "lastName": "赵"
    }
}
增量修改

增量修改是只修改指定id匹配的文档中的部分字段

语法:

POST /{索引库名}/_update/文档id
{
    "doc": {
         "字段名": "新的值",
    }
}

示例
POST /heima/_update/1
{
  "doc": {
    "email": "ZhaoYun@itcast.cn"
  }
}

ES搜索引擎

es的查询依然是基于json风格的DSL来实现的

DSL设置查询条件

DSL查询分类

ES提供了基于json的DSL(Domain Specific Language)来定义查询,常见的查询类型包括:

  • 查询所有:查询出所有数据,一般测试用。例如:match_all

  • 全文检索(full text)查询:利用分词器对用户输入内容分词,然后去倒排索引库中匹配。例如:

    • match_query
    • multi_match_query
  • 精确查询:根据精确词条值查找数据,一般是找keyword,数值,日期,Boolean等类型字段,

  • 地理(geo)查询:根据经纬度查询

  • 复合(compound)查询:复合查询可以将上述查询条件组合起来,合并查询条件

    GET /indexName/_search
    {
      "query": {
        "查询类型": {
          "查询条件": "条件值"
        }
      }
    }
    

    我们以查询所有为例,其中:

    • 查询类型为match_all
    • 没有查询条件
    // 查询所有
    GET /indexName/_search
    {
      "query": {
        "match_all": {
        }
      }
    }
    

    其它查询无非就是查询类型查询条件的变化。

match和multi_match的区别是什么?

  • match:根据一个字段查询 (单字段查询)【推荐:使用copy_to构造all字段】
  • mulit_match:根据多个字段查询,参与查询字段越多,查询性能越差 (多字段查询,任意一个字段符合条件就算符号查询条件)

注:搜索字段越多,对查询性能影响越大,因此建议采用copy_to,然后单字段查询的方式。

  1. 使用场景

    全文检索查询的基本流程如下:

    • 对用户搜索的内容做分词,得到词条
    • 根据词条去倒排索引库中匹配,得到文档id
    • 根据文档id找到文档,返回给用户

    比较常用的场景包括:

    • 商城的输入框搜索
    • 百度输入框搜索
  2. match查询

    # 语法如下
    GET /indexName/_search
    
    {
        "query": {
    		"match": {
               "FIELD":"TEXT"
            }
        }
    }
    
  3. mulit_match查询

    # 语法如下
    
    GET /indexName/_search
    {
      "query": {
        "multi_match": {
          "query": "TEXT",
          "fields": ["FIELD1", " FIELD12"]
        }
      }
    }
    
  4. 精准查询

    精准查询类型:

    • term查询:根据词条精确匹配,一般搜索keyword类型、数值类型、布尔类型、日期类型字段
    • range查询:根据数值范围查询,可以是数值、日期的范围

    精确查找一般是查找keyword,数值,日期,Boolean等类型字段,所以不会对搜索条件分词

    常见的有:

    • term:根据词条精确值查询
    • range:根据值的范围查询
    1. term查询

      因为精确查询的字段是搜索时不分词的字段,因此查询的条件也必须是不分词的词条,查询时,用户输入的内容跟自动值完全匹配时才认为符合条件,如果用户输入的内容过多,反而搜不到数据

      // term查询
      GET /indexName/_search
      {
        "query": {
          "term": {
            "FIELD": {
              "value": "VALUE"
            }
          }
        }
      }
      
    2. range查询

      范围查询,一般应用在对数值类型做范围过滤的时候,比如做价格范围过滤

      // range查询
      GET /indexName/_search
      {
        "query": {
          "range": {
            "FIELD": {
              "gte": 10, // 这里的gte代表大于等于,gt则代表大于
              "lte": 20 // lte代表小于等于,lt则代表小于
            }
          }
        }
      }
      
  5. 地理坐标查询

    所谓的地理坐标查询,其实就是根据经纬度查询,官方文档:https://www.elastic.co/guide/en/elasticsearch/reference/current/geo-queries.html

    常见的使用场景包括:

    • 携程:搜索我附近的酒店
    • 滴滴:搜索我附近的出租车
    • 微信:搜索我附近的人
    1. 矩形范围查询 (一般很少用)

      矩形范围查询,也就是geo_bounding_box查询,查询坐标落在某个矩形范围的所有文档:

      查询时,需要指定矩形的左上,右下,两个点的坐标,然后画出一个矩形,落在该矩形内的都是符合条件的点

      // geo_bounding_box查询
      GET /indexName/_search
      {
        "query": {
          "geo_bounding_box": {
            "FIELD": {
              "top_left": { // 左上点
                "lat": 31.1,
                "lon": 121.5
              },
              "bottom_right": { // 右下点
                "lat": 30.9,
                "lon": 121.7
              }
            }
          }
        }
      }
      
    2. 附近(圆形)查询

      附近查询,也叫做距离查询(geo_distance):查询到指定中心点小于某个距离值的所有文档。

      换句话来说,在地图上找一个点作为圆心,以指定距离为半径,画一个圆,落在圆内的坐标都算符合条件:

      // geo_distance 查询
      GET /indexName/_search
      {
        "query": {
          "geo_distance": {
            "distance": "15km", // 半径
            "FIELD": "31.21,121.5" // 圆心
          }
        }
      }
      
      1. 复合查询

        复合(compound)查询:复合查询可以将其它简单查询组合起来,实现更复杂的搜索逻辑。常见的有两种:

        • function_score:算分函数查询,可以控制文档相关性算分,控制文档排名
        • bool query:布尔查询,利用逻辑关系组合多个其它的查询,实现复杂搜索
        GET /hotel/_search
        {
          "query": {
            "function_score": {           
              "query": { // 原始查询,可以是任意条件
                  "bool": {
                      "must": [
                          {"term": {"city": "上海" }}
                      ],
                      "should": [
                          {"term": {"brand": "皇冠假日" }},
                          {"term": {"brand": "华美达" }}
                      ],
                      "must_not": [
                          { "range": { "price": { "lte": 500 } }}
                      ],
                      "filter": [
                          { "range": {"score": { "gte": 45 } }}
                      ]
                  }
              },
              "functions": [ // 算分函数
                {
                  "filter": { // 满足的条件,品牌必须是如家【品牌是如家的才加分,这里是加分条件】
                    "term": {
                      "brand": "如家"
                    }
                  },
                  "weight": 2 // 算分权重为2
                }
              ],
              "boost_mode": "sum" // 加权模式,求和
            }
          }  
        }
        
        • 相关性算分

          elasticsearch会根据词条和文档的相关度做打分,算法由两种:

          • TF-IDF算法
          • BM25算法,elasticsearch5.1版本后采用的算法

          当我们利用match查询时,文档结果会根据与搜索词条的关联度打分(_score),返回结果时按照分值降序排列。

          例如,我们搜索 "虹桥如家",结果如下:

          copy[
            {
              "_score" : 17.850193,
              "_source" : {
                "name" : "虹桥如家酒店真不错",
              }
            },
            {
              "_score" : 12.259849,
              "_source" : {
                "name" : "外滩如家酒店真不错",
              }
            },
            {
              "_score" : 11.91091,
              "_source" : {
                "name" : "迪士尼如家酒店真不错",
              }
            }
          ]
          
      2. 算分函数查询

        在搜索出来的结果的分数基础上,再手动与指定的数字进行一定运算来改变算分,从而改变结果的排序。

        function score query定义的三要素是什么?

        • 过滤条件:哪些文档要加分
        • 算分函数:如何计算function score
        • 加权方式:function score 与 query score如何运算

        要想认为控制相关性算分,就需要利用elasticsearch中的function score 查询了。

        1. function score 查询

          语法说明

          image

        function score 查询中包含四部分内容:

        • 原始查询条件:query部分,基于这个条件搜索文档,并且基于BM25算法给文档打分,原始算分(query score)

        • 过滤条件:filter部分,符合该条件的文档才会重新算分

        • 算分函数

          :符合filter条件的文档要根据这个函数做运算,得到的

          函数算分

          (function score),有四种函数

          • weight:函数结果是常量
          • field_value_factor:以文档中的某个字段值作为函数结果
          • random_score:以随机数作为函数结果
          • script_score:自定义算分函数算法
        • 运算模式

          :算分函数的结果、原始查询的相关性算分,两者之间的运算方式,包括:

          • multiply:相乘
          • replace:用function score替换query score
          • 其它,例如:sum、avg、max、min

          function score的运行流程如下:

          • 1)根据原始条件查询搜索文档,并且计算相关性算分,称为原始算分(query score)
          • 2)根据过滤条件,过滤文档
          • 3)符合过滤条件的文档,基于算分函数运算,得到函数算分(function score)
          • 4)将原始算分(query score)和函数算分(function score)基于运算模式做运算,得到最终结果,作为相关性算分。

          举例

          需求:给“如家”这个品牌的酒店排名靠前一些

          翻译一下这个需求,转换为之前说的四个要点:

          • 原始条件:不确定,可以任意变化
          • 过滤条件:brand = "如家"
          • 算分函数:可以简单粗暴,直接给固定的算分结果,weight
          • 运算模式:比如求和

          因此最终的DSL语句如下:

          GET /hotel/_search
          {
            "query": {
              "function_score": {
                "query": {  
                	"match": {
                      "all": "外滩" // 原始查询,可以是任意条件
                  }
                },
                "functions": [ // 算分函数
                  {
                    "filter": { // 满足的条件,品牌必须是如家【品牌是如家的才加分,这里是加分条件】
                      "term": {
                        "brand": "如家"
                      }
                    },
                    "weight": 2 // 算分权重为2 算分函数是固定值
                  }
                ],
                "boost_mode": "sum" // 加权模式,求和  即:原始算分 + weight
              }
            }
          }
          
      • 布尔查询

        布尔查询时一个或多个查询子句的组合,每一个子句就是一个子查询,子查询的组合方式有:

        • must:必须匹配每个子查询,类似“与”
        • should:选择性匹配子查询,类似“或”
        • must_not:必须不匹配,不参与算分,类似“非”
        • filter:必须匹配,不参与算分

        注意:尽量在筛选的时候最多使用不参与算分的must_not 和 filter 以保证性能良好

        比如在搜索酒店时,除了关键字搜索外,我们还可能根据品牌、价格、城市等字段做过滤:

        image

        每一个不同的字段,其查询的条件、方式都不一样,必须是多个不同的查询,而要组合这些查询,就必须用bool查询了。

        需要注意的是,搜索时,参与打分的字段越多,查询的性能也越差。因此这种多条件查询时,建议这样做:

            - 搜索框的关键字搜索,是全文检索查询,使用must查询,参与算分
            - 其它过滤条件,采用filter查询。不参与算分
        
            ###### bool查询
        
            1)语法
        
            ```json
            GET /hotel/_search
            {
              "query": {
                "bool": {
                  "must": [
                    {"term": {"city": "上海" }}
                  ],
                  "should": [
                    {"term": {"brand": "皇冠假日" }},
                    {"term": {"brand": "华美达" }}
                  ],
                  "must_not": [
                    { "range": { "price": { "lte": 500 } }}
                  ],
                  "filter": [
                    { "range": {"score": { "gte": 45 } }}
                  ]
                }
              }
            }
            ```
        
            2)示例
        
            需求:搜索名字包含“如家”,价格不高于400,在坐标31.21,121.5周围10km范围内的酒店。
        
            分析:
        
            - 名称搜索,属于全文检索查询,应该参与算分。放到must中
            - 价格不高于400,用range查询,属于过滤条件,不参与算分。放到must_not中
            - 周围10km范围内,用geo_distance查询,属于过滤条件,不参与算分。放到filter中
        
            ![image](https://img2023.cnblogs.com/blog/2729274/202302/2729274-20230205173526171-218786081.png)
        
        1. 设置搜索结果

          搜索的结果可以按照用户指定的方式去处理或展示。

          2.0 搜索结果种类

          查询的DSL是一个大的JSON对象,包含下列属性:

          • query:查询条件
          • from和size:分页条件
          • sort:排序条件
          • highlight:高亮条件
          • aggs:定义聚合

          示例:

          image

          image

          2.1 排序

          在使用排序后就不会进行算分了,根据排序设置的规则排列

          普通字段是根据字典序排序

          地理坐标是根据举例远近排序

          2.1.1普通字段排序

          keyword、数值、日期类型排序的排序语法基本一致。

          语法

          排序条件是一个数组,也就是可以写多个排序条件。按照声明的顺序,当第一个条件相等时,再按照第二个条件排序,以此类推
          (可以参考下面的图片案例)

      GET /indexName/_search
      {
        "query": {
          "match_all": {}
        },
        "sort": [
          {
            "FIELD": "desc"  // 排序字段、排序方式ASC、DESC
          }
        ]
      }
      

      示例

      需求描述:酒店数据按照用户评价(score)降序排序,评价相同的按照价格(price)升序排序

      image

      2.1.2 地理坐标排序

      地理坐标排序略有不同。

      语法说明

      GET /indexName/_search
      {
        "query": {
          "match_all": {}
        },
        "sort": [
          {
            "_geo_distance" : {
                "FIELD" : "纬度,经度", // 文档中geo_point类型的字段名、目标坐标点
                "order" : "asc", // 排序方式
                "unit" : "km" // 排序的距离单位
            }
          }
        ]
      }
      

      这个查询的含义是:

      • 指定一个坐标,作为目标点
      • 计算每一个文档中,指定字段(必须是geo_point类型)的坐标 到目标点的距离是多少
      • 根据距离排序

      示例:

      需求描述:实现对酒店数据按照到你的位置坐标的距离升序排序

      提示:获取你的位置的经纬度的方式:https://lbs.amap.com/demo/jsapi-v2/example/map/click-to-get-lnglat/

      假设我的位置是:31.034661,121.612282,寻找我周围距离最近的酒店。

      image

      2.2 分页

      elasticsearch会禁止from+ size 超过10000的请求

      elasticsearch 默认情况下只返回top10的数据。而如果要查询更多数据就需要修改分页参数了。elasticsearch中通过修改from、size参数来控制要返回的分页结果:

      • from:从第几个文档开始
      • size:总共查询几个文档

      类似于mysql中的limit ?, ?

      2.2.1 基本分页

      分页的基本语法如下:

      GET /hotel/_search
      {
        "query": {
          "match_all": {}
        },
        "from": 0, // 分页开始的位置,默认为0
        "size": 10, // 期望获取的文档总数
        "sort": [
          {"price": "asc"}
        ]
      }
      
      2.2.2 深度分页

      原理:elasticsearch内部分页时,必须先查询 0~1000条,然后截取其中的990 ~ 1000的这10条

      现在,我要查询990~1000的数据,查询逻辑要这么写:

      GET /hotel/_search
      {
        "query": {
          "match_all": {}
        },
        "from": 990, // 分页开始的位置,默认为0
        "size": 10, // 期望获取的文档总数
        "sort": [
          {"price": "asc"}
        ]
      }
      

      这里是查询990开始的数据,也就是 第990~第1000条 数据。

      集群情况的深度分页

      针对深度分页,ES提供了两种解决方案,官方文档

      • search after:分页时需要排序,原理是从上一次的排序值开始,查询下一页数据。【官方推荐】
      • scroll:原理将排序后的文档id形成快照,保存在内存。

      不过,elasticsearch内部分页时,必须先查询 0~1000条,然后截取其中的990 ~ 1000的这10条:

      image

      查询TOP1000,如果es是单点模式,这并无太大影响。

      但是elasticsearch将来一定是集群,例如我集群有5个节点,我要查询TOP1000的数据,并不是每个节点查询200条就可以了。

      因为节点A的TOP200,在另一个节点可能排到10000名以外了。

      因此要想获取整个集群的TOP1000,必须先查询出每个节点的TOP1000,汇总结果后,重新排名,重新截取TOP1000。

      image

      那如果我要查询9900~10000的数据呢?是不是要先查询TOP10000呢?那每个节点都要查询10000条?汇总到内存中?

      当查询分页深度较大时,汇总数据过多,对内存和CPU会产生非常大的压力,因此elasticsearch会禁止from+ size 超过10000的请求

      2.3 高亮

      注意:

      • 高亮是对关键字高亮,因此搜索条件必须带有关键字,而不能是范围这样的查询。
      • 默认情况下,高亮的字段,必须与搜索指定的字段一致,否则无法高亮
      • 如果要对非搜索字段高亮,则需要添加一个属性:required_field_match=false

      使用场景:在百度等搜索后,会对结果中出现搜索字段的部分进行高亮处理。

      高亮原理

      高亮显示的实现分为两步:

      • 1)给文档中的所有关键字都添加一个标签,例如<em>标签
      • 2)页面给<em>标签编写CSS样式
      实现高亮

      1)语法

      GET /hotel/_search
      {
        "query": {
          "match": {
            "FIELD": "TEXT" // 查询条件,高亮一定要使用全文检索查询
          }
        },
        "highlight": {
          "fields": { // 指定要高亮的字段
            "FIELD": { //【要和上面的查询字段FIELD一致】
              "pre_tags": "<em>",  // 用来标记高亮字段的前置标签
              "post_tags": "</em>" // 用来标记高亮字段的后置标签
            }
          }
        }
      }
      

      2)示例:组合字段all的案例

      image

      2.4 数据聚合

      2.4 数据聚合

      类似于mysql中的【度量(Metric)聚合】聚合语句实现AVG,MAX,MIN;以及【桶(Bucket)聚合】GroupBy实现分组

      聚合(aggregations可以让我们极其方便的实现对数据的统计、分析、运算。例如:

      • 什么品牌的手机最受欢迎?
      • 这些手机的平均价格、最高价格、最低价格?
      • 这些手机每月的销售情况如何?

      实现这些统计功能的比数据库的sql要方便的多,而且查询速度非常快,可以实现近实时搜索效果。

      aggs代表聚合,与query同级,此时query的作用是?

      • 限定聚合的的文档范围

      聚合必须的三要素:

      • 聚合名称
      • 聚合类型
      • 聚合字段

      聚合可配置属性有:

      • size:指定聚合结果数量
      • order:指定聚合结果排序方式
      • field:指定聚合字段
      2.4.1 聚合种类

      注意:参加聚合的字段必须是keyword、日期、数值、布尔类型

      聚合常见的有三类:

      • 桶(Bucket)聚合:用来对文档做分组

        • TermAggregation:按照文档字段值分组,例如按照品牌值分组、按照国家分组
        • Date Histogram:按照日期阶梯分组,例如一周为一组,或者一月为一组
      • 度量(Metric)聚合:用以计算一些值,比如:最大值、最小值、平均值等

        • Avg:求平均值
        • Max:求最大值
        • Min:求最小值
        • Stats:同时求max、min、avg、sum等
      • 管道(pipeline)聚合:其它聚合的结果为基础做聚合

        如:用桶聚合实现种类排序,然后使用度量聚合实现各个桶的最大值、最小值、平均值等

      2.4.2 桶(Bucket)聚合

      以统计酒店品牌种类,并对其进行数据分组

      GET /hotel/_search
      {
        "query": { //限定要聚合的文档范围,只要添加query条件【一般在没搜索关键字时不写query】
          "range": {
            "price": {
              "lte": 200 // 只对200元以下的文档聚合
            }
          }
        }, 
        "size": 0,  // 设置size为0,结果中不包含查询结果文档,只包含聚合结果
        "aggs": { // 定义聚合
          "brandAgg": { //给聚合起个名字
            "terms": { // 聚合的类型,按照品牌值聚合,所以选择term
              "field": "brand", // 参与聚合的字段
              "order": {
                "doc_count": "asc" // 对聚合结果按照doc_count升序排列
              },
              "size": 20 // 希望获取的聚合结果数量【设置多少就最多只显示多少】
            }
          }
        }
      }
      

      image

      2.4.3 度量(Metric) and 管道(pipeline)聚合

      度量聚合很少单独使用,一般是和桶聚合一并结合使用

      我们对酒店按照品牌分组,形成了一个个桶。现在我们需要对桶内的酒店做运算,获取每个品牌的用户评分的min、max、avg等值。

      这就要用到Metric聚合了,例如stat聚合:就可以获取min、max、avg等结果。

      语法如下:

      这次的score_stats聚合是在brandAgg的聚合内部嵌套的子聚合。因为我们需要在每个桶分别计算。

      GET /hotel/_search
      {
        "size": 0, 
        "aggs": {
          "brandAgg": { 
            "terms": { 
              "field": "brand", 
              "order": {
                "scoreAgg.avg": "desc" // 对聚合结果按照指定字段降序排列
              },
              "size": 20
            },
            "aggs": { // 是brands聚合的子聚合,也就是分组后对每组分别计算
              "score_stats": { // 聚合名称
                "stats": { // 聚合类型,这里stats可以计算min、max、avg等
                  "field": "score" // 聚合字段,这里是score
                }
              }
            }
          }
        }
      }
      

      另外,我们还可以给聚合结果做个排序,例如按照每个桶的酒店平均分做排序:

      image

      ES与mysql数据同步

  6. 常用的三种数据同步方案

    • 同步调用

      实现简单,粗暴

      业务耦合度高

    • 异步通知

      低耦合,实现难度一般

      依赖mq的可靠性

    • 监听binlog

      完全解除服务间耦合

      开启binlog增加数据库负担,实现复杂度高

同步调用

image-20250220095956267

基本步骤如下:

  • hotel-demo对外提供接口,用来修改Elasticsearch中的数据
  • 酒店管理服务在完成数据库操作后,直接调用hotel-demo提供的接口

异步通知

image-20250220100819151

流程如下:

  • hotel-admin(消息发送者)对MySql数据完成增删改操作后,发送MQ消息
  • hotel-demo(消息接收者)监听MQ,接收到消息后完成es数据修改

监听binlog

image-20250220101303479

流程如下:

  • 给mysql开启binlog功能
  • mysql完成增,删,改,查操作都会记录在binlog中
  • hotel-demo基于canal监听binlog变化,实时更新elasticsearch中的内容

实现数据同步

当数据发生增删改操作时,需要对es中的数据也要完成相同操作

步骤:

  • 单机部署并启动MQ(单机部署在MQ部分有讲)
  • 接收者中声明exchange、queue、RoutingKey
  • 在hotel-admin发送者中的增、删、改业务中完成消息发送
  • 在hotel-demo接收者中完成消息监听,并更新elasticsearch中数据
  • 启动并测试数据同步功能

ES集群

搭建ES集群

  1. 创建ES集群

    部署es集群可以直接用docker-compose来完成,要求虚拟机内存至少有4G

    首先编写一个docker-compose文件,内容如下:

    version: '2.2'
    services:
      es01:
        image: docker.elastic.co/elasticsearch/elasticsearch:7.12.1
        container_name: es01
        environment:
          - node.name=es01
          - cluster.name=es-docker-cluster
          - discovery.seed_hosts=es02,es03
          - cluster.initial_master_nodes=es01,es02,es03
          - bootstrap.memory_lock=true
          - "ES_JAVA_OPTS=-Xms512m -Xmx512m"
        ulimits:
          memlock:
            soft: -1
            hard: -1
        volumes:
          - data01:/usr/share/elasticsearch/data
        ports:
          - 9200:9200
        networks:
          - elastic
      es02:
        image: docker.elastic.co/elasticsearch/elasticsearch:7.12.1
        container_name: es02
        environment:
          - node.name=es02
          - cluster.name=es-docker-cluster
          - discovery.seed_hosts=es01,es03
          - cluster.initial_master_nodes=es01,es02,es03
          - bootstrap.memory_lock=true
          - "ES_JAVA_OPTS=-Xms512m -Xmx512m"
        ulimits:
          memlock:
            soft: -1
            hard: -1
        volumes:
          - data02:/usr/share/elasticsearch/data
        networks:
          - elastic
      es03:
        image: docker.elastic.co/elasticsearch/elasticsearch:7.12.1
        container_name: es03
        environment:
          - node.name=es03
          - cluster.name=es-docker-cluster
          - discovery.seed_hosts=es01,es02
          - cluster.initial_master_nodes=es01,es02,es03
          - bootstrap.memory_lock=true
          - "ES_JAVA_OPTS=-Xms512m -Xmx512m"
        ulimits:
          memlock:
            soft: -1
            hard: -1
        volumes:
          - data03:/usr/share/elasticsearch/data
        networks:
          - elastic
    
    volumes:
      data01:
        driver: local
      data02:
        driver: local
      data03:
        driver: local
    
    networks:
      elastic:
        driver: bridge
    

    es运行需要修改一些linux系统权限,修改/etc/sysctl.conf文件

    vi /etc/sysctl.conf
    

    添加下面的内容:

    vm.max_map_count=262144
    

    然后执行命令,让配置生效:

    copysysctl -p
    

    通过docker-compose启动集群:

    docker-compose up -d
    

    集群状态监控

    kibana可以监控es集群,不过新版本需要依赖es的x-pack 功能,配置比较复杂。

    这里推荐使用cerebro来监控es集群状态,官方网址:https://github.com/lmenezes/cerebro

    课前资料已经提供了安装包:

    image

    解压即可使用,非常方便。

    解压好的目录如下:

    image

    进入对应的bin目录:

    image

    双击其中的cerebro.bat文件即可启动服务。

    image

    访问http://localhost:9000 即可进入管理界面:

    image

    输入你的elasticsearch的任意节点的地址和端口,点击connect即可:

    image

    绿色的条,代表集群处于绿色(健康状态)。

    1.3创建索引库

    创建索引库的时候需要设置分片数量(其他还有多少个ES服务在该集群)以及副本数量(本服务的数据拷贝几份)

    方法一:利用kibana的DevTools创建索引库

    如果没有启动ES的可视化界面Kibana,那就用方法二

    在DevTools中输入指令:

    PUT /itcast
    {
      "settings": {
        "number_of_shards": 3, // 分片数量
        "number_of_replicas": 1 // 副本数量
      },
      "mappings": {
        "properties": {
          // mapping映射定义 ...
        }
      }
    }
    
    方法二:利用cerebro创建索引库

    利用cerebro还可以创建索引库:

    image

    填写索引库信息:

    image

    点击右下角的create按钮:

    image

    查看分片效果

    回到首页,即可查看索引库分片效果:

    image

    2.集群脑裂问题

    master eligible节点的作用是什么?

    • 参与集群选主
    • 主节点可以管理集群状态、管理分片信息、处理创建和删除索引库的请求

    data节点的作用是什么?

    • 数据的CRUD

    coordinator节点的作用是什么?

    • 路由请求到其它节点
    • 合并查询到的结果,返回给用户

    2.1.集群职责划分

    通过改变配置文件中的 true——> false 来改变职责。如data数据职责节点就只保留data为true其他为false

    注意:每个节点都是路由,这样可以保证不管哪个节点接收到请求可以分给其他人已经从其他人那接收信息。

    elasticsearch中集群节点有不同的职责划分:

    image

    默认情况下,集群中的任何一个节点都同时具备上述四种角色。

    但是真实的集群一定要将集群职责分离:(因为不同职责对CPU要求不同)

    • master节点:对CPU要求高,但是内存要求低
    • data节点:对CPU和内存要求都高
    • coordinating节点:对网络带宽、CPU要求高

    职责分离可以让我们根据不同节点的需求分配不同的硬件去部署。而且避免业务之间的互相干扰。

    一个典型的es集群职责划分如图:

    image

    2.2.脑裂问题

    ES 7.0后默认配置了( eligible节点数量 + 1 )/ 2来解决脑裂问题

    脑裂是因为集群中的节点失联导致的。

    例如一个集群中,主节点与其它节点失联:

    image

    此时,node2和node3认为node1宕机,就会重新选主:

    image

    当node3当选后,集群继续对外提供服务,node2和node3自成集群,node1自成集群,两个集群数据不同步,出现数据差异。

    当网络恢复后,因为集群中有两个master节点,集群状态的不一致,出现脑裂的情况:

    image

    解决脑裂的方案是,要求选票超过 ( eligible节点数量 + 1 )/ 2 才能当选为主,因此eligible节点数量最好是奇数。对应配置项是discovery.zen.minimum_master_nodes,在es7.0以后,已经成为默认配置,因此一般不会发生脑裂问题

    例如:3个节点形成的集群,选票必须超过 (3 + 1) / 2 ,也就是2票。node3得到node2和node3的选票,当选为主。node1只有自己1票,没有当选。集群中依然只有1个主节点,没有出现脑裂。

    3.集群分布式存储

    当新增文档时,应该保存到不同分片,保证数据均衡,那么coordinating node如何确定数据该存储到哪个分片呢?

    3.1.分片存储测试

    插入三条数据:

    image

    image

    image

    测试可以看到,三条数据分别在不同分片:

    image

    结果:

    image

    3.2.分片存储原理

    elasticsearch会通过hash算法来计算文档应该存储到哪个分片:

    image

    说明:

    • _routing默认是文档的id
    • 算法与分片数量有关,因此索引库一旦创建,分片数量不能修改!

    新增文档的流程如下:

    image

    解读:

    • 1)新增一个id=1的文档
    • 2)对id做hash运算,假如得到的是2,则应该存储到shard-2
    • 3)shard-2的主分片在node3节点,将数据路由到node3
    • 4)保存文档
    • 5)同步给shard-2的副本replica-2,在node2节点
    • 6)返回结果给coordinating-node节点

    4. 集群分布式查询

    原理:

    elasticsearch的查询分成两个阶段:

    • scatter phase:分散阶段,coordinating node会把请求分发到每一个分片
    • gather phase:聚集阶段,coordinating node汇总data node的搜索结果,并处理为最终结果集返回给用户

    image

    5.集群故障转移

    ES本身已经配置好了有集群故障转移,不需要我们再去配置

    集群的master节点会监控集群中的节点状态,如果发现有节点宕机,会立即将宕机节点的分片数据迁移到其它节点,确保数据安全,这个叫做故障转移。

    1)例如一个集群结构如图:

    image

    现在,node1是主节点,其它两个节点是从节点。

    2)突然,node1发生了故障:

    image

    宕机后的第一件事,需要重新选主,例如选中了node2:

    image

    node2成为主节点后,会检测集群监控状态,发现:shard-1、shard-0没有副本节点。因此需要将node1上的数据迁移到node2、node3:

    image

    3.2.分片存储原理

    elasticsearch会通过hash算法来计算文档应该存储到哪个分片:

    image

    说明:

    • _routing默认是文档的id
    • 算法与分片数量有关,因此索引库一旦创建,分片数量不能修改!

    新增文档的流程如下:

    image

    解读:

    • 1)新增一个id=1的文档
    • 2)对id做hash运算,假如得到的是2,则应该存储到shard-2
    • 3)shard-2的主分片在node3节点,将数据路由到node3
    • 4)保存文档
    • 5)同步给shard-2的副本replica-2,在node2节点
    • 6)返回结果给coordinating-node节点

    4. 集群分布式查询

    原理:

    elasticsearch的查询分成两个阶段:

    • scatter phase:分散阶段,coordinating node会把请求分发到每一个分片
    • gather phase:聚集阶段,coordinating node汇总data node的搜索结果,并处理为最终结果集返回给用户

    image

    5.集群故障转移

    ES本身已经配置好了有集群故障转移,不需要我们再去配置

    集群的master节点会监控集群中的节点状态,如果发现有节点宕机,会立即将宕机节点的分片数据迁移到其它节点,确保数据安全,这个叫做故障转移。

    1)例如一个集群结构如图:

    image

    现在,node1是主节点,其它两个节点是从节点。

    2)突然,node1发生了故障:

    image

    宕机后的第一件事,需要重新选主,例如选中了node2:

    image

    node2成为主节点后,会检测集群监控状态,发现:shard-1、shard-0没有副本节点。因此需要将node1上的数据迁移到node2、node3:

    image

posted @ 2025-02-20 22:51  小郑[努力版]  阅读(35)  评论(0)    收藏  举报