【技术派后端篇】ElasticSearch 实战指南:环境搭建、API 操作与集成实践

1 ES介绍及基本概念

ElasticSearch是一个基于Lucene 的分布式、高扩展、高实时的基于RESTful 风格API的搜索与数据分析引擎。

RESTful 风格API的特点:

  1. 接受HTTP协议的请求,返回HTTP响应;
  2. 请求的参数是JSON,返回响应的内容也是JSON格式;
  3. 请求的类型(GET、POST、PUT、DELETE)就代表对资源的操作类型。

1.1 基本概念

在Elasticsearch(ES)的体系中,各核心概念清晰且独特,支撑着其强大的搜索与数据分析能力:

  • 字段(Field):字段代表一种属性,类似于关系型数据库表中的列。在数据库里,每一行数据由多个属性值构成,比如员工表中,"姓名""年龄""职位"这些列就是字段,对应具体员工的数据,如"张三""30""工程师"就是字段值。在ES中,一个字段同样承载特定的属性信息,像一篇博客文章,"标题""发布时间""内容"等都可作为字段。
  • 文档(Document):ES的数据存储方式与传统关系型数据库截然不同,所有数据均以JSON格式的document形式存在。document作为ES索引与搜索的最小数据单元,类比于数据库中的一行数据。以电商平台为例,一条商品信息记录就是一个document,它由"商品名称""价格""库存""描述"等多个字段值组成,可能像这样:
json 复制代码
{
    "商品名称": "智能手表",
    "价格": 1299,
    "库存": 50,
    "描述": "具备多种健康监测功能的智能手表"
}
  • 映射(Mapping):Mapping用于定义document中每个字段的类型、字段所采用的分词器等细节,其作用等同于关系型数据库中的表结构,为数据的存储与处理提供规范。例如,在一个新闻资讯索引中,"标题"字段可定义为字符串类型,并使用标准分词器,以便对标题进行合理切分用于搜索;"发布时间"字段定义为日期类型,方便按时间范围检索新闻。通过这样的映射定义,ES能更高效地管理和利用数据。
  • 索引(Index):Index是Elasticsearch存储数据的载体,可理解为关系型数据库中的数据库概念,用于存放一类相同或相似的document。假设运营一个在线图书馆系统,可能会有"小说类书籍索引""学术文献索引"等。"小说类书籍索引"中存放的就是所有小说书籍相关的document,每个document包含书籍的名称、作者、出版社、内容简介等信息。
  • 类型(Type):Type属于逻辑层面的数据分类方式,一种Type类似于一张表,像常见的用户表、订单表等。在Elasticsearch 6.X版本中,默认Type为_doc ;而到了ES 7.x版本,Type这一概念已被移除 。在早期的论坛系统ES应用中,可能会设置"用户类型"和"帖子类型","用户类型"的document存储用户的账号、密码、注册时间等信息;"帖子类型"的document存放帖子的标题、内容、发布者、发布时间等数据。但在7.x及之后版本,不再使用这种多类型区分方式,而是更强调数据的统一索引管理 。

1.2 ES原理与倒排索引

1.2.1 ES整体原理概述

ES 是一个分布式的搜索与数据分析引擎,基于 Lucene 构建。它将数据存储在分布式的节点上,以实现高可用性、高扩展性和高性能。其核心工作流程围绕索引和搜索两个关键环节。在索引阶段,数据被处理并构建索引结构;搜索阶段则依据构建好的索引快速检索出符合条件的数据。

1.2.2 倒排索引的概念

传统数据库使用的是正向索引,即从文档 ID 到文档内容及单词位置的映射。而倒排索引恰恰相反,是从单词到包含该单词的文档 ID 列表的映射。简单来说,倒排索引就是将文档中的每个单词提取出来,记录每个单词在哪些文档中出现过以及出现的位置等信息。

例如,有以下两篇技术博客文章:
文章 1(ID=1) :"Elasticsearch 是强大的搜索工具,适用于大数据场景。"
文章 2(ID=2) :"学习技术博客写作,掌握搜索技巧很重要,比如使用 Elasticsearch。"

倒排索引构建过程如下:

单词 包含该单词的文档ID列表
Elasticsearch 1, 2
1
强大的 1
搜索 1, 2
工具 1
适用于 1
大数据 1
场景 1
学习 2
技术博客 2
写作 2
掌握 2
技巧 2
很重要 2
比如 2
使用 2

这样在搜索时,根据输入的关键词能快速定位到包含该关键词的文档,大大提高搜索效率。

1.2.3 以技术博客网站文章检索为例

假设一个技术博客网站有大量文章,存储在ES中。当用户在搜索框输入关键词"Elasticsearch"进行检索时,ES的工作流程如下:

  1. 用户输入:用户在博客网站搜索框输入"Elasticsearch"。
  2. 请求发送:网站将搜索请求发送给ES服务器。
  3. 倒排索引查询:ES在倒排索引中查找"Elasticsearch"这个单词,找到对应的文档ID列表,即[1, 2]。
  4. 文档获取:根据文档ID,从存储文档的地方获取对应的两篇文章内容。
  5. 结果返回:将这两篇文章的相关信息(如标题、摘要等)返回给用户展示。

通过这种基于倒排索引的机制,ES能够快速准确地从海量的技术博客文章中找到用户需要的内容,为用户提供高效的搜索体验 。

2 ES环境搭建

2.1 Windows版安装步骤

2.1.1 ElasticSearch安装

  1. 安装Java环境:下载并安装JDK(Java Development Kit),并配置好Java环境变量。具体可参考相关Java安装教程。
  2. 下载安装包 :从ElasticSearch官网下载适合Windows系统的zip安装包。

注意:不要下载最新版本,最好根据IK分词器的版本来下载,不然可能会没有对应的分词器版本

  1. 解压文件 :将下载后的zip文件解压到指定目录,例如D:\elasticsearch

  2. 启动服务 :进入解压目录下的bin目录,双击elasticsearch.bat文件启动ElasticSearch服务。启动后,在浏览器地址栏输入http://localhost:9200/ ,若能看到ElasticSearch相关信息,说明启动成功。

  3. 安装为Windows服务(可选)

    • 设置环境变量 :右键点击"此电脑",选择"属性",点击"高级系统设置",在弹出窗口中点击"环境变量"。在系统变量中新建变量,变量名可设为ELASTICSEARCH_HOME ,变量值为ElasticSearch的安装目录(如D:\elasticsearch );然后在Path变量中添加%ELASTICSEARCH_HOME%\bin
    • 安装服务 :以管理员身份运行命令提示符,进入ElasticSearch的bin目录,执行elasticsearch-service.bat install命令安装服务。可使用elasticsearch-service.bat start启动服务、elasticsearch-service.bat stop停止服务、elasticsearch-service.bat remove移除服务。
  4. 修改 Elasticsearch 配置为 HTTP(不推荐用于生产环境) :如果发现启动ES后无法访问,打开 Elasticsearch 安装目录下的 config 文件夹,找到 elasticsearch.yml 文件,修改配置之后再重启ElasticSearch,即可正常访问。

2.1.2 Kibana安装

  1. 下载安装包 :从Kibana官网下载与ElasticSearch版本匹配的Windows版.zip安装包。
  2. 解压文件 :在D盘新建kibana目录,将下载的.zip文件解压到该目录下,解压后得到kibana-版本号-windows-x86_64文件夹(即$KIBANA_HOME )。
  3. 配置文件 :打开$KIBANA_HOME\config\kibana.yml配置文件,设置elasticsearch.url为指向ElasticSearch实例地址,默认http://localhost:9200一般无需修改。如果ElasticSearch服务地址有变化,需相应调整。
  4. 启动服务 :进入$KIBANA_HOME\bin目录,双击kibana.bat启动Kibana。启动后可在浏览器输入http://localhost:5601访问Kibana界面。

2.1.3 IK分词器安装

  1. 下载 :根据ElasticSearch版本,从https://github.com/medcl/elasticsearch-analysis-ik/releases下载对应的IK分词器压缩包。例如,若ElasticSearch是7.10.1版本,则下载ik-710.1.zip
  2. 解压与放置 :解压下载的文件,将解压后的文件夹复制到ElasticSearch安装目录下的plugins文件夹内,并将文件夹重命名为analysis-ik 。比如ElasticSearch安装在D:\elasticsearch ,则将文件夹复制到D:\elasticsearch\plugins\analysis-ik
  3. 重启ElasticSearch :以管理员身份运行bin目录下的elasticsearch.bat重启ElasticSearch,即可加载IK分词器。可通过发送请求测试分词效果,如使用Postman等工具。

2.2 Ubuntu版(Linux版)安装步骤

2.2.1 ElasticSearch安装

  1. 下载和解压
    • 从ElasticSearch官网(https://www.elastic.co/cn/downloads/elasticsearch )选择合适版本下载,然后上传到Ubuntu服务器。也可在命令行执行wget https://artifacts.elastic.co/downloads/elasticsearch/elasticsearch-版本号-linux-x86_64.tar.gz下载(下载速度可能较慢)。
    • 执行解压缩命令tar -zxvf elasticsearch-版本号-linux-x86_64.tar.gz -C /usr/local
  2. 配置JDK(若有冲突) :新版本ElasticSearch压缩包自带JDK,若Ubuntu已安装JDK且版本不符,启动时会报错。可切换到解压目录的bin目录,执行export JAVA_HOME=/usr/local/elasticsearch-版本号/jdk指定使用自带JDK 。
  3. 创建专用用户root用户不能直接启动ElasticSearch,需创建专用用户,如user-es 。使用命令useradd user-es创建用户,passwd user-es设置密码 。
  4. 修改配置文件 :进入config文件夹,编辑elasticsearch.yml文件。可修改http.port等参数,如http.port: 19200 。还可根据需求配置集群相关参数等。
  5. 解决内存权限问题 :若启动报错max virtual memory areas vm.max_map_count (65530) is too low, increase to at least (262144) ,切换到root用户(sudo su ),编辑/etc/sysctl.conf文件,在文件末尾添加vm.max_map_count=262144 ,保存退出后执行sysctl -p刷新配置 。
  6. 启动服务 :切换到专用用户(su user-es ),进入ElasticSearch的bin目录,执行./elasticsearch启动服务。启动成功后可通过http://服务器IP:端口号(如http://127.0.0.1:19200 )访问,出现相关信息则安装成功。

2.2.2 Kibana安装

  1. 下载和解压 :从Kibana官网(https://www.elastic.co/cn/downloads/kibana )下载与ElasticSearch版本匹配的安装包,例如kibana-版本号-linux-x86_64.tar.gz 。使用命令wget https://artifacts.elastic.co/downloads/kibana/kibana-版本号-linux-x86_64.tar.gz下载后,执行tar -zxvf kibana-版本号-linux-x86_64.tar.gz解压 。
  2. 修改配置文件 :进入解压后的目录,编辑config/kibana.yml文件,配置内容如下:
yaml 复制代码
server.port: 5601
server.host: "0.0.0.0"
elasticsearch.url: "http://服务器IP:9200"
kibana.index: ".kibana"

服务器IP替换为实际ElasticSearch服务器的IP地址。

  1. 启动服务 :切换到解压目录的bin目录,执行./kibana &&表示在后台运行)启动Kibana。启动过程中若ElasticSearch未启动会有警告,连接成功后可通过浏览器访问http://服务器IP:5601

2.2.3 IK分词器安装

  1. 下载 :根据ElasticSearch版本,从https://github.com/medcl/elasticsearch-analysis-ik/releases下载对应的IK分词器压缩包。
  2. 解压与放置 :解压下载的文件,将解压后的文件夹(需命名为analysis-ik )复制到ElasticSearch安装目录下的plugins文件夹内。比如ElasticSearch安装在/usr/local/elasticsearch-版本号 ,则将文件夹复制到/usr/local/elasticsearch-版本号/plugins/analysis-ik
  3. 重启ElasticSearch :切换到启动ElasticSearch的专用用户,进入ElasticSearch的bin目录,执行./elasticsearch重启服务,即可加载IK分词器。

3 RESTful API操作ES

3.1 操作索引

  • 添加索引
json 复制代码
PUT user

向Elasticsearch集群添加名为user的索引。若索引已存在,会返回相应错误提示。

  • 查询单个索引
json 复制代码
GET /teacher

获取名为teacher的索引的相关信息,如索引的基本设置、映射等。若索引不存在,会返回404错误。

  • 查询多个索引
json 复制代码
GET /user,student

同时获取userstudent两个索引的相关信息。可一次性查看多个索引的状态,方便对比和管理。

  • 查询所有索引
json 复制代码
GET /_all

返回Elasticsearch集群中的所有索引的相关信息。在管理多个索引,需要快速了解整体索引情况时使用。

  • 删除索引
json 复制代码
DELETE /student

从Elasticsearch集群中删除名为student的索引及其包含的所有文档数据。删除操作不可逆,谨慎使用。

  • 打开索引
json 复制代码
POST /student/_open

将处于关闭状态的student索引打开,使其可进行数据的读写和查询操作。索引关闭时,对其进行的任何数据操作都会失败。

  • 关闭索引
json 复制代码
POST /student/_close

关闭student索引,关闭后索引不可读写,但索引数据仍存储在磁盘上。可用于在维护索引或减少资源占用时临时关闭索引。

3.2 数据类型

3.2.1 简单数据类型

我们先来看一个简单的映射定义:

json 复制代码
PUT teacher/_mapping
{
    "properties": {
      "id": {
        "type": "integer"  
      },
      "name": {
        "type": "text"
      },
      "isMale": {
        "type": "boolean"
      }
    }
}

在定义映射的时候,类比于定义数据库中的表结构,我们需要指明每一个字段的名称,数据类型等等信息,所以我们先得了解映射中包含的数据类型。

  • 字符串

    • text:会分词,不支持聚合
    • keyword:不会分词,将全部内容作为一个词条,支持聚合
  • 数值:long, integer, short, byte, double, float, half_float, scaled_float

  • 布尔:boolean

  • 二进制:binary

  • 范围类型:integer_range,float_range, long_range, double_range, date_range

  • 日期:date

3.2.2 复杂数据类型

  • 数组:[ ] 没有专门的数组类型,ES会自动处理数组类型数据

比如说 给某一个字段 定义为 integer类型,那么这个字段是可以存储 integer数组的,ES会自动处理

  • 对象:{ } Object: object(for single JSON objects 单个JSON对象)

3.3 操作映射

  • 给已存在索引添加映射
json 复制代码
PUT /user/_mapping
{
    "properties":{
        "id": {"type":"integer"},
        "name":{"type":"keyword"},
        "age":{"type":"long"}
    }
}

为已存在的user索引添加字段映射,定义id为整数类型、name为关键词类型、age为长整型。添加映射时,字段类型一旦确定,后续修改会受限制。

  • 创建索引并指定映射
json 复制代码
PUT teacher
{
    "mappings": {
        "properties": {
            "tid":{"type": "integer"},
            "tname":{"type": "text"}
        }
    }
}

创建名为teacher的索引,并同时指定其映射,定义tid为整数类型、tname为文本类型。在创建索引时规划好映射,能确保数据存储和查询的准确性。

  • 查询映射
json 复制代码
GET /teacher/_mapping

获取teacher索引的映射信息,包括字段类型、分词器设置等。方便查看索引的结构定义,排查查询问题。

  • 修改映射(只能添加字段)
json 复制代码
PUT /teacher/_mapping
{
    "properties":{
        "height":{"type":"float"},
        "weight":{"type":"double"}
    }
}

teacher索引的映射中添加heightweight字段,分别为浮点型和双精度型。Elasticsearch不支持直接修改已存在字段的类型,只能添加新字段。

3.4 操作文档

  • 添加文档 - 指定文档id
json 复制代码
POST /user/_doc/1
{
    "id": 1001,
    "name":"贾宝玉",
    "age":15
}

user索引中添加一个文档,并指定文档的id为1。若指定id的文档已存在,会覆盖原有文档。

  • 添加文档 - 不指定文档id
json 复制代码
POST /user/_doc
{
    "id":1002,
    "name":"贾迎春",
    "age":13
}

user索引添加文档,Elasticsearch会自动生成一个唯一的文档id。适用于对文档id无特定要求的场景。

  • 查询文档 - 根据文档id查询
json 复制代码
GET /user/_doc/1
GET /user/_doc/NsORPo0BJehhapNsslSw

根据文档id获取user索引中的文档。若文档不存在,会返回404错误。

  • 查询文档 - 查询所有文档
json 复制代码
GET /user/_search

获取user索引中的所有文档。在数据量较大时,可能需要结合分页参数使用,避免一次性返回过多数据。

  • 修改文档 - 直接覆盖
json 复制代码
POST /user/_doc/1
{
    "id": 2001,
    "name":"刘姥姥",
    "age":80
}

用新的文档数据覆盖user索引中id为1的文档。会完全替换原有文档内容,包括所有字段。

  • 修改指定字段
json 复制代码
POST /user/_update/1
{
    "doc": {
        "name":"贾琏",
        "age": 30
    }
}

仅修改user索引中id为1的文档的nameage字段,其他字段保持不变。比直接覆盖更灵活,可减少数据传输量。

  • 删除文档(指定文档id)
json 复制代码
DELETE /user/_doc/1

删除user索引中id为1的文档。删除操作不可逆,操作前需确认数据是否不再需要。

3.5 IK分词器测试

  • ik_max_word分词模式
json 复制代码
GET /_analyze
{
    "text": "湖南是全国唯一一个叫湖南的省份",
    "analyzer": "ik_max_word"
}

使用ik_max_word分词器对文本进行分词,它会尽可能细粒度地切分文本,如上述文本会被切分成"湖南""是""全国""唯一""一个""叫""湖南""的""省份"等多个词条。

  • ik_smart分词模式
json 复制代码
GET /_analyze
{
    "text": "湖南是全国唯一一个叫湖南的省份",
    "analyzer": "ik_smart"
}

ik_smart分词器的粒度更粗,会对文本进行更合理的合并切分,上述文本可能被切分成"湖南""是""全国""唯一一个""叫""湖南的省份"等词条。

  • 使用ES自带的分词器
json 复制代码
GET /_analyze
{
    "text": "stone is best man, ciggar is handsome man",
    "analyzer": "standard"
}

使用ES自带的standard分词器对英文文本进行分词,它会按空格和标点符号进行切分,并将单词转换为小写形式,如上述文本会被切分成"stone""is""best""man""ciggar""is""handsome""man"等词条。

3.6 ES批量操作

  • 创建索引并设置映射
json 复制代码
DELETE /teacher
PUT /teacher
{
    "mappings": {
        "properties": {
            "id":{"type": "integer"},
            "name":{
                "type": "text",
                "analyzer": "ik_max_word"
            },
            "age":{
                "type": "integer"
            }
        }
    }
}

先删除可能存在的teacher索引,再创建新的teacher索引,并设置其映射。其中name字段使用ik_max_word分词器进行分词。

  • 查询索引中的所有文档
json 复制代码
GET /teacher/_search

获取teacher索引中的所有文档,用于查看当前索引中的数据内容。

  • 批量操作
json 复制代码
POST /_bulk
{"create":{"_index":"teacher","_id":"1"}}
{"id":1,"name":"金角大王","age":30}
{"create":{"_index":"teacher","_id":"2"}}
{"id":2,"name":"银角大王","age":20}
{"create":{"_index":"teacher","_id":"3"}}
{"id":3,"name":"小钻风","age":10}
{"delete":{"_index":"teacher","_id":"1"}}
{"update":{"_index":"teacher","_id":"2"}}
{"doc":{"name":"狮驼岭","age":1000}}

在一次请求中对teacher索引执行多个操作,包括创建文档(create)、删除文档(delete)和更新文档(update)。批量操作可减少网络请求次数,提高数据处理效率。

  • 创建另一个索引并设置映射
json 复制代码
PUT /member
{
    "mappings": {
        "properties": {
            "id":{"type": "integer"},
            "name":{"type": "keyword"},
            "addr":{
                "type": "text",
                "analyzer": "ik_max_word"
            },
            "gender":{"type": "keyword"},
            "money":{"type": "integer"}
        }
    }
}

创建名为member的索引,并设置其映射。addr字段使用ik_max_word分词器,方便对地址进行分词查询。

  • 查询索引信息
json 复制代码
GET /member/_search
GET /member/_mapping

分别查询member索引中的所有文档和获取member索引的映射信息,用于了解索引的数据和结构。

3.7 ES高级查询

  • match_all查询
json 复制代码
GET /member/_search
{
    "query": {
        "match_all": {}
    },
    "from": 0,
    "size": 3
}

查询member索引中的所有文档,并指定从第0条开始,返回3条数据。常用于获取索引的部分数据样本,或在不关心具体查询条件时获取全部数据。

  • term查询
json 复制代码
GET /member/_search
{
    "query": {
        "term": {
            "addr": {
                "value": "山区"
            }
        }
    }
}

member索引中查询addr字段值为"山区"的文档。term查询不会对搜索关键字进行分词,直接与目标字段进行精确匹配。

  • 全文查询 - match
json 复制代码
GET /member/_search
{
    "query": {
        "match": {
            "addr": "湖北武汉"
        }
    }
}

member索引的addr字段进行全文查询,会对搜索关键字"湖北武汉"进行分词,然后与addr字段的分词结果进行匹配。默认取并集,即只要匹配到其中一个分词结果的文档就会被返回。

  • 模糊查询 - 通配符查询
json 复制代码
GET /member/_search
{
    "query": {
        "wildcard": {
            "addr": {
                "value": "武?"
            }
        }
    }
}

member索引中查询addr字段值以"武"开头且后面跟任意一个字符的文档。?表示占位,*表示通配任意多个字符。

  • 正则查询
json 复制代码
GET /member/_search
{
    "query": {
        "regexp": {
            "addr": "[A-Z a-z 0-9_]+(.)*"
        }
    }
}

使用正则表达式对member索引的addr字段进行查询,匹配由字母、数字、下划线组成且后面可跟任意字符的地址。

  • 前缀查询
json 复制代码
GET /member/_search
{
    "query": {
        "prefix": {
            "addr": {
                "value": "青"
            }
        }
    }
}

member索引中查询addr字段值以"青"开头的文档。前缀查询不太适用于text类型字段,因为text类型字段会被分词,可能导致查询结果不准确。

  • queryString多条件查询
json 复制代码
GET /member/_search
{
    "query": {
        "query_string": {
            "fields": ["addr","gender"], 
            "query": "武汉 AND 男"
        }
    }
}

member索引中查询addr字段包含"武汉"且gender字段为"男"的文档。注意ANDOR要大写,且不要使用simple_query_string,它不会识别ANDOR关键字。

  • 排序查询
json 复制代码
GET /member/_search
{
    "query": {
        "match_all": {}
    },
    "sort": [
        {
            "money": {
                "order": "asc"
            }
        }
    ]
}

查询member索引中的所有文档,并按money字段的值进行升序排序。可根据需求指定其他字段进行排序,以及选择升序(asc)或降序(desc)。

  • 范围查询
json 复制代码
GET /member/_search
{
    "query": {
        "range": {
            "money": {
                "gte": 300,
                "lte": 600
            }
        }
    },
    "sort": [
        {
            "money": {
                "order": "desc"
            }
        }
    ],
    "from": 0,
    "size": 3
}

member索引中查询money字段值在300到600之间的文档,并按money字段的降序排序,返回前3条数据。

3.8 bool查询

  • must条件
json 复制代码
GET /member/_search
{
    "query": {
        "bool": {
            "must": [
                {
                    "term": {
                        "addr": {
                            "value": "武汉"
                        }
                    }
                },
                {
                    "term": {
                        "gender": {
                            "value": "女"
                        }
                    }
                }
            ]
        }
    }
}

查询member索引中addr字段为"武汉"且gender字段为"女"的文档,会计算文档的近似度得分。must条件下的所有子条件都必须满足才能匹配到文档。

  • filter条件
json 复制代码
GET /member/_search
{
    "query": {
        "bool": {
            "filter": [
                {
                    "match": {
                        "addr": "湖北武汉"
                    }
                },
                {
                    "range": {
                        "money": {
                            "gte": 200,
                            "lte": 500
                        }
                    }
                }
            ]
        }
    }
}

查询member索引中addr字段包含"湖北武汉"且money字段值在200到500之间的文档,filter不会计算近似度得分,查询效率更高,适用于不需要计算得分的精确过滤场景。

  • must_not条件
json 复制代码
GET /member/_search
{
    "query": {
        "bool": {
            "must_not": [
                {
                    "match": {
                        "addr": "武汉"
                    }
                },
                {
                    "term": {
                        "gender": {
                            "value": "女"
                        }
                    }
                }
            ]
        }
    }
}

查询member索引中addr字段不包含"武汉"且gender字段不为"女"的文档,同样不会计算近似度得分。

  • should条件
json 复制代码
GET /member/_search
{
    "query": {
        "bool": {
            "should": [
                {
                    "term": {
                        "addr": {
                            "value": "武汉"
                        }
                    }
                },
                {
                    "term": {
                        "gender": {
                            "value": "女"
                        }
                    }
                }
            ]
        }
    }
}

查询member索引中addr字段为"武汉"或者gender字段为"女"的文档,会计算近似度得分。只要满足should中的一个子条件,文档就可能被返回。

  • 综合练习
json 复制代码
GET /member/_search
{
    "query": {
        "bool": {
            "filter": [
                {
                    "range": {
                        "money": {
                            "gte": 200,
                            "lte": 600
                        }
                    }
                },
                {
                    "term": {
                        "gender": "男"
                    }
                }
            ],
            "must": [
                {
                    "match": {
                        "addr": "湖北武汉"
                    }
                }
            ]
        }
    }
}

查询member索引中money字段值在200到600之间、gender为"男"且addr包含"湖北武汉"的文档。

4 技术派整合ES

4.1 构造 RestHighLevelClient 客户端

  • 引入依赖 :在 paicoding-service 模块的 pom 文件中,我们需要引入 es 和 es-client 相关的依赖。具体的依赖配置如下:
xml 复制代码
<!--引入es-high-level-client相关依赖  start-->
<dependency>
    <groupId>org.elasticsearch</groupId>
    <artifactId>elasticsearch</artifactId>
    <version>6.8.2</version>
</dependency>

<dependency>
    <groupId>org.elasticsearch.client</groupId>
    <artifactId>elasticsearch-rest-client</artifactId>
    <version>6.8.2</version>
</dependency>
<dependency>
    <groupId>org.elasticsearch.client</groupId>
    <artifactId>elasticsearch-rest-high-level-client</artifactId>
    <version>6.8.2</version>
</dependency>
  • 配置 yml 文件 :在 paicoding-web 模块的 src->main->resource-env->dev->application-dal.yml 中,配置 ES 的相关信息。具体配置如下:
yaml 复制代码
# elasticsearch配置
elasticsearch:
  # 是否开启ES?本地启动如果没有安装ES,可以设置为false关闭ES
  open: false
  # es集群名称
  clusterName: elasticsearch
  hosts: 127.0.0.1:9200
  userName: elastic
  password: elastic
  # es 请求方式
  scheme: http
  # es 连接超时时间
  connectTimeOut: 1000
  # es socket 连接超时时间
  socketTimeOut: 30000
  # es 请求超时时间
  connectionRequestTimeOut: 500
  # es 最大连接数
  maxConnectNum: 100
  # es 每个路由的最大连接数
  maxConnectNumPerRoute: 100

在配置 yml 文件时,一定要注意格式的正确性,避免因格式错误导致配置失效。

  • 编写 config 配置类 RestHighLevelClient:配置类的作用是将 RestHighLevelClient 客户端的创建交由 Spring 管理。具体的配置类代码如下:
java 复制代码
/**
 * es配置类
 *
 * @author ygl
 * @since 2023-05-25
 **/
@Slf4j
@Data
@Configuration
// 下面这个表示只有 elasticsearch.open = true 时,采进行es的配置初始化;当不使用es时,则不会实例 RestHighLevelClient
@ConditionalOnProperty(prefix = "elasticsearch", name = "open")
@ConfigurationProperties(prefix = "elasticsearch")
public class ElasticsearchConfig {

    // 是否开启ES
    private Boolean open;

    // es host ip 地址(集群)
    private String hosts;

    // es用户名
    private String userName;

    // es密码
    private String password;

    // es 请求方式
    private String scheme;

    // es集群名称
    private String clusterName;

    // es 连接超时时间
    private int connectTimeOut;

    // es socket 连接超时时间
    private int socketTimeOut;

    // es 请求超时时间
    private int connectionRequestTimeOut;

    // es 最大连接数
    private int maxConnectNum;

    // es 每个路由的最大连接数
    private int maxConnectNumPerRoute;


    /**
     * 如果@Bean没有指定bean的名称,那么这个bean的名称就是方法名
     */
    @Bean(name = "restHighLevelClient")
    public RestHighLevelClient restHighLevelClient() {

        // 此处为单节点es
        String host = hosts.split(":")[0];
        String port = hosts.split(":")[1];
        HttpHost httpHost = new HttpHost(host, Integer.parseInt(port));

        // 构建连接对象
        RestClientBuilder builder = RestClient.builder(httpHost);

        // 设置用户名、密码
        CredentialsProvider credentialsProvider = new BasicCredentialsProvider();
        credentialsProvider.setCredentials(AuthScope.ANY, new UsernamePasswordCredentials(userName, password));

        // 连接延时配置
        builder.setRequestConfigCallback(requestConfigBuilder -> {
            requestConfigBuilder.setConnectTimeout(connectTimeOut);
            requestConfigBuilder.setSocketTimeout(socketTimeOut);
            requestConfigBuilder.setConnectionRequestTimeout(connectionRequestTimeOut);
            return requestConfigBuilder;
        });
        // 连接数配置
        builder.setHttpClientConfigCallback(httpClientBuilder -> {
            httpClientBuilder.setMaxConnTotal(maxConnectNum);
            httpClientBuilder.setMaxConnPerRoute(maxConnectNumPerRoute);
            httpClientBuilder.setDefaultCredentialsProvider(credentialsProvider);
            return httpClientBuilder;
        });

        return new RestHighLevelClient(builder);
    }
}

需要注意的是,在 yml 中的 elasticsearch.hosts 配置中,不要添加 https 前缀,只需填写 127.0.0.1:9200 即可,否则在配置类中创建 HttpHost 时会出错。

4.2 技术派首页全局查询走 ES 数据库

在进行 ES 查询整合之前,我们先来了解一下大致的逻辑:

  1. 根据查询条件在 ES 中查询数据。
  2. 从查询结果中提取出 id,并将这些 id 添加到 ids 集合中。
  3. 使用 ids 集合去 MySQL 数据库中查询相应的数据。

通过这种方式,可以大大提高查询的效率,相比直接在 MySQL 中进行 like% 值 % 的查询,性能有显著提升。

在 paicoding-service 模块的 ArticleReadServiceImpl 类中,我们需要注入 RestHighLevelClient 客户端,然后编写实现代码。具体代码如下:

java 复制代码
public List<SimpleArticleDTO> querySimpleArticleBySearchKey(String key) {
    // TODO 当KEY为空时,返回热门推荐
    if (StringUtils.isBlank(key)) {
        return Collections.emptyList();
    }
    key = key.trim();
    SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder();
    MultiMatchQueryBuilder multiMatchQueryBuilder = QueryBuilders.multiMatchQuery(key,
            ESFieldConstant.ES_FIELD_TITLE,
            ESFieldConstant.ES_FIELD_SHORT_TITLE);
    searchSourceBuilder.query(multiMatchQueryBuilder);
    SearchRequest searchRequest = new SearchRequest(new String[]{ESIndexConstant.ES_INDEX_ARTICLE}, searchSourceBuilder);

    SearchResponse searchResponse = null;
    try {
        searchResponse = restHighLevelClient.search(searchRequest, RequestOptions.DEFAULT);
    } catch (IOException e) {
        e.printStackTrace();
    }

    SearchHits hits = searchResponse.getHits();
    SearchHit[] hitList = hits.getHits();
    List<Integer> ids = new ArrayList<>();
    for (SearchHit documentFields : hitList) {
        ids.add(Integer.parseInt(documentFields.getId()));
    }

    if (ObjectUtils.isEmpty(ids)) {
        return null;
    }
    List<ArticleDO> records = articleDAO.selectByIds(ids);
    return records.stream().map(s -> new SimpleArticleDTO()
           .setId(s.getId())
           .setTitle(s.getTitle())).collect(Collectors.toList());
}

4.3 兼容未安装 ES 继续走 MySQL 查询

考虑到有些小伙伴可能没有安装 ES 和 Canal,为了保证代码在这种情况下也能正常运行,我们进行了兼容性处理,使得在未安装 ES 时,首页查询依然可以走 MySQL 数据库。

首先,在 yml 中增加了是否开启 ES 的配置项:

yaml 复制代码
# elasticsearch配置
elasticsearch:
  # 是否开启ES?本地启动如果没有安装ES,可以设置为false关闭ES
  open: true
  # es集群名称
  clusterName: elasticsearch
  hosts: 127.0.0.1:9200
  userName: elastic
  password: elastic
  # es 请求方式
  scheme: http
  # es 连接超时时间
  connectTimeOut: 1000
  # es socket 连接超时时间
  socketTimeOut: 30000
  # es 请求超时时间
  connectionRequestTimeOut: 500
  # es 最大连接数
  maxConnectNum: 100
  # es 每个路由的最大连接数
  maxConnectNumPerRoute: 100

然后,在 Service 层将 open 值注入:

java 复制代码
@Value("${elasticsearch.open}")
private Boolean openEs;

最后,在业务层的代码中实现兼容逻辑:

java 复制代码
@Override
public List<SimpleArticleDTO> querySimpleArticleBySearchKey(String key) {
    // TODO 当KEY为空时,返回热门推荐
    if (StringUtils.isBlank(key)) {
        return Collections.emptyList();
    }
    // 如果没有开启ES,那么继续从MYSQL中获取数据
    if (!openEs) {
        List<ArticleDO> records = articleDAO.listSimpleArticlesBySearchKey(key);
        return records.stream().map(s -> new SimpleArticleDTO()
               .setId(s.getId())
               .setTitle(s.getTitle())).collect(Collectors.toList());
    }

    // ES整合逻辑
    key = key.trim();
    SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder();
    MultiMatchQueryBuilder multiMatchQueryBuilder = QueryBuilders.multiMatchQuery(key,
            ESFieldConstant.ES_FIELD_TITLE,
            ESFieldConstant.ES_FIELD_SHORT_TITLE);
    searchSourceBuilder.query(multiMatchQueryBuilder);
    SearchRequest searchRequest = new SearchRequest(new String[]{ESIndexConstant.ES_INDEX_ARTICLE}, searchSourceBuilder);

    SearchResponse searchResponse = null;
    try {
        searchResponse = restHighLevelClient.search(searchRequest, RequestOptions.DEFAULT);
    } catch (IOException e) {
        e.printStackTrace();
    }

    SearchHits hits = searchResponse.getHits();
    SearchHit[] hitList = hits.getHits();
    List<Integer> ids = new ArrayList<>();
    for (SearchHit documentFields : hitList) {
        ids.add(Integer.parseInt(documentFields.getId()));
    }

    if (ObjectUtils.isEmpty(ids)) {
        return null;
    }
    List<ArticleDO> records = articleDAO.selectByIds(ids);
    return records.stream().map(s -> new SimpleArticleDTO()
           .setId(s.getId())
           .setTitle(s.getTitle())).collect(Collectors.toList());
}

5 ES集群相关知识

5.1 ES集群基础概念

  1. 定义与组成:Elasticsearch(ES)集群是由多个ES节点组成的集合 ,节点间协同工作,实现数据存储、搜索及分析功能。
  2. 优势
    • 效率提升:多节点并行处理搜索请求,相比单节点搜索效率更高。
    • 存储扩容:突破单节点磁盘空间限制,利用多个节点存储资源,扩大数据存储容量。
    • 高可用性 :部分节点故障时,集群仍能对外提供服务,保障整体可用性。

5.2 ES集群节点

  1. 节点类型
    • 主节点(Master Node):负责管理集群元数据,如索引创建、删除,分片分配,监控节点状态等。为防止脑裂,一般设置奇数个主节点。脑裂是指在集群中,多个节点都认为自己是主节点,导致集群出现多个"大脑",从而使集群状态混乱、数据不一致等问题。设置奇数个主节点,是因为在选举主节点时,奇数个节点能避免出现平局情况,降低脑裂风险 。
    • 数据节点(Data Node):用于存储数据,执行数据的增、删、改、查及聚合操作,需根据数据量和查询需求合理配置资源。
    • 协调节点(Coordinate Node):不承担主节点和数据节点功能,专门负责接收请求、转发请求至其他节点、汇总返回数据。大规模集群并发查询量大时,可添加独立协调节点。
    • Ingest节点:对文档数据进行预处理,如通过管道实现数据转换与丰富,可按需水平扩展。一个节点可兼具多种角色,小规模集群可不严格区分。
  2. 节点协作:数据节点向主节点发送心跳数据包汇报状态,主节点依此监控集群状态。

5.3 ES集群分片

  1. 主分片(Primary Shard)
    • 定义:将索引文档数据分散存储的单元,所有主分片数据合起来构成完整索引数据,主分片数量至少为1 。
    • 作用:承载索引的原始数据存储,是数据写入和读取的基础单元。
  2. 副本分片(Replica Shard)
    • 定义:主分片的备份,数量大于等于0 。
    • 作用:提高数据可用性和查询性能,主分片故障时可顶替提供服务,还可分担查询负载。
  3. 分片规则
    • 主分片和副本分片不能存储在同一节点上。
    • 单节点ES可有多主分片,但无副本分片。
    • 创建索引时可指定主、副本分片数量,索引创建后,主分片数量不可更改,仅副本分片数量可调整。

5.4 文档在ES集群中的操作

  1. 存储流程

    • 用户将存储请求发送到集群中任意一个节点。
    • 接收请求的节点通过文档路由,把数据发送给对应的主分片。
    • 主分片将数据同步给它的所有副本分片。
  2. 搜索流程

    • 用户向集群中任意一个节点发起搜索请求。
    • 接收请求的节点(协调节点)把每个主分片和它的副本分片看作"同一组"分片,从中选择一组有完整数据的分片(默认轮训选择),然后分发请求。
    • 被选中的分片进行查询,之后返回结果。
    • 协调节点把收到的结果聚合,返回给用户。

5.5 ES 集群相关问题及解决

  • 脑裂问题 :因网络问题,集群被分成两个,出现两个 "大脑"(主节点) ,导致数据不一致 。解决方法包括设置
    • 最小主节点数量 ,计算公式为discovery.zen.minimum_master_nodes: (有master资格节点数/2) + 1
    • 设置专门奇数个主节点
    • 采用单播发现机制 (关闭多播发现机制discovery.zen.ping.multicast.enabled: false ,配置单播发现主节点 ip 地址discovery.zen.ping.unicast.hosts : ("master1", "master2", "master3") )
    • 配置选举发现数,延长 ping master 等待时长 (如discovery.zen.ping_timeout: 30 ,discovery.zen.minimum_master_nodes: 2 ) 。
  • 分片数量配置 :索引分片数指定后一般不可更改,除非重做索引 。ElasticSearch 推荐最大 JVM 堆空间 30 - 32G ,可据此估算分片数量,如预计数据量 200GB ,最多分配 7 - 8 个分片 。初始阶段,可按节点数量的 1.5 - 3 倍创建分片,如 3 个节点,分片数最多不超 9 个 。对于基于日期、搜索少、数据量小(如每个索引 1GB 甚至更小 )的索引需求,如日志管理,建议分配 1 个分片 。

6 总结

本文全面讲解 ElasticSearch(ES)相关知识。ES 是基于 Lucene 的分布式搜索与分析引擎,有分布式、高扩展、高实时特性,采用 RESTful 风格 API 。介绍了字段、文档等基本概念,基于倒排索引的原理。详述 Windows 和 Ubuntu 版搭建步骤,RESTful API 操作及 ES 集成。还提及 ES 集群中的脑裂问题,主节点在管理集群元数据中起关键作用,为防止脑裂,通常设置奇数个主节点,避免选举时出现平局,降低集群状态混乱、数据不一致等风险。

7 参考链接

  1. 技术派实现ES查询
  2. 项目仓库(GitHub)
  3. 项目仓库(码云)
相关推荐
jianghx10243 小时前
Docker部署ES,开启安全认证并且设置账号密码(已运行中)
安全·elasticsearch·docker·es账号密码设置
IT小哥哥呀3 小时前
电池制造行业数字化实施
大数据·制造·智能制造·数字化·mom·电池·信息化
Xi xi xi3 小时前
苏州唯理科技近期也正式发布了国内首款神经腕带产品
大数据·人工智能·经验分享·科技
yumgpkpm4 小时前
华为鲲鹏 Aarch64 环境下多 Oracle 、mysql数据库汇聚到Cloudera CDP7.3操作指南
大数据·数据库·mysql·华为·oracle·kafka·cloudera
UMI赋能企业5 小时前
制造业流程自动化提升生产力的全面分析
大数据·人工智能
TDengine (老段)5 小时前
TDengine 数学函数 FLOOR 用户手册
大数据·数据库·物联网·时序数据库·iot·tdengine·涛思数据
派可数据BI可视化7 小时前
商业智能BI 浅谈数据孤岛和数据分析的发展
大数据·数据库·数据仓库·信息可视化·数据挖掘·数据分析
jiedaodezhuti8 小时前
Flink性能调优基石:资源配置与内存优化实践
大数据·flink
阿里云大数据AI技术8 小时前
云栖实录 | AI 搜索智能探索:揭秘如何让搜索“有大脑”
人工智能·搜索引擎
Lx3529 小时前
Flink窗口机制详解:如何处理无界数据流
大数据