SpringCloud微服务 Day08 - 高效商品搜索与实时日志分析:利用倒排索引提升查询性能

黑马商城作为一个电商项目,商品的搜索肯定是访问频率最高的页面之一。目前搜索功能是基于数据库的模糊搜索来实现的,存在很多问题。

首先,查询效率较低。

由于数据库模糊查询不走索引,在数据量较大的时候,查询性能很差。黑马商城的商品表中仅仅有不到9万条数据,基于数据库查询时,搜索接口的表现如图:

 

改为基于搜索引擎后,查询表现如下:

 

需要注意的是,数据库模糊查询随着表数据量的增多,查询性能的下降会非常明显,而搜索引擎的性能则不会随着数据增多而下降太多。目前仅10万不到的数据量差距就如此明显,如果数据量达到百万、千万、甚至上亿级别,这个性能差距会非常夸张。

其次,功能单一

数据库的模糊搜索功能单一,匹配条件非常苛刻,必须恰好包含用户搜索的关键字。而在搜索引擎中,用户输入出现个别错字,或者用拼音搜索、同义词搜索都能正确匹配到数据。

综上,在面临海量数据的搜索,或者有一些复杂搜索需求的时候,推荐使用专门的搜索引擎来实现搜索功能。

目前全球的搜索引擎技术排名如下:

 

排名第一的就是我们今天要学习的elasticsearch.

elasticsearch是一款非常强大的开源搜索引擎,支持的功能非常多,例如:

 

 

代码搜索

商品搜索

解决方案搜索

地图搜索

通过今天的学习大家要达成下列学习目标:

  • 理解倒排索引原理

  • 会使用IK分词器

  • 理解索引库Mapping映射的属性含义

  • 能创建索引库及映射

  • 能实现文档的CRUD

1.初识elasticsearch

Elasticsearch的官方网站如下:

https://www.elastic.co/cn/elasticsearch/

本章我们一起来初步了解一下Elasticsearch的基本原理和一些基础概念。

1.1.认识和安装

Elasticsearch是由elastic公司开发的一套搜索引擎技术,它是elastic技术栈中的一部分。完整的技术栈包括:

  • Elasticsearch:用于数据存储、计算和搜索

  • Logstash/Beats:用于数据收集

  • Kibana:用于数据可视化

整套技术栈被称为ELK,经常用来做日志收集、系统监控和状态分析等等:

整套技术栈的核心就是用来存储搜索计算的Elasticsearch,因此我们接下来学习的核心也是Elasticsearch。

我们要安装的内容包含2部分:

  • elasticsearch:存储、搜索和运算

  • kibana:图形化展示

首先Elasticsearch不用多说,是提供核心的数据存储、搜索、分析功能的。

然后是Kibana,Elasticsearch对外提供的是Restful风格的API,任何操作都可以通过发送http请求来完成。不过http请求的方式、路径、还有请求参数的格式都有严格的规范。这些规范我们肯定记不住,因此我们要借助于Kibana这个服务。

Kibana是elastic公司提供的用于操作Elasticsearch的可视化控制台。它的功能非常强大,包括:

  • 对Elasticsearch数据的搜索、展示

  • 对Elasticsearch数据的统计、聚合,并形成图形化报表、图形

  • 对Elasticsearch的集群状态监控

  • 它还提供了一个开发控制台(DevTools),在其中对Elasticsearch的Restful的API接口提供了语法提示

1.1.1.安装elasticsearch

通过下面的Docker命令即可安装单机版本的elasticsearch:

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 hm-net \
  -p 9200:9200 \
  -p 9300:9300 \
  elasticsearch:7.12.1

注意,这里我们采用的是elasticsearch的7.12.1版本,由于8以上版本的JavaAPI变化很大,在企业中应用并不广泛,企业中应用较多的还是8以下的版本。

如果拉取镜像困难,可以直接导入课前资料提供的镜像tar包:

安装完成后,访问9200端口,即可看到响应的Elasticsearch服务的基本信息:

1.1.2.安装Kibana

通过下面的Docker命令,即可部署Kibana:

docker run -d \
--name kibana \
-e ELASTICSEARCH_HOSTS=http://es:9200 \
--network=hm-net \
-p 5601:5601  \
kibana:7.12.1

如果拉取镜像困难,可以直接导入课前资料提供的镜像tar包:

安装完成后,直接访问5601端口,即可看到控制台页面:

 

选择Explore on my own之后,进入主页面:

 

然后选中Dev tools,进入开发工具页面:

1.2.倒排索引

elasticsearch之所以有如此高性能的搜索表现,正是得益于底层的倒排索引技术。那么什么是倒排索引呢?

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

1.2.1.正向索引

我们先来回顾一下正向索引。

例如有一张名为tb_goods的表:

id

title

price

1

小米手机

3499

2

华为手机

4999

3

华为小米充电器

49

4

小米手环

49

...

...

...

其中的id字段已经创建了索引,由于索引底层采用了B+树结构,因此我们根据id搜索的速度会非常快。但是其他字段例如title,只在叶子节点上存在。

因此要根据title搜索的时候只能遍历树中的每一个叶子节点,判断title数据是否符合要求。

比如用户的SQL语句为:

select * from tb_goods where title like '%手机%';

那搜索的大概流程如图:

 

说明:

  • 1)检查到搜索条件为like '%手机%',需要找到title中包含手机的数据

  • 2)逐条遍历每行数据(每个叶子节点),比如第1次拿到id为1的数据

  • 3)判断数据中的title字段值是否符合条件

  • 4)如果符合则放入结果集,不符合则丢弃

  • 5)回到步骤1

综上,根据id精确匹配时,可以走索引,查询效率较高。而当搜索条件为模糊匹配时,由于索引无法生效,导致从索引查询退化为全表扫描,效率很差。

因此,正向索引适合于根据索引字段的精确搜索,不适合基于部分词条的模糊匹配。

而倒排索引恰好解决的就是根据部分词条模糊匹配的问题。

1.2.2.倒排索引

倒排索引中有两个非常重要的概念:

  • 文档(Document):用来搜索的数据,其中的每一条数据就是一个文档。例如一个网页、一个商品信息

  • 词条(Term):对文档数据或用户搜索数据,利用某种算法分词,得到的具备含义的词语就是词条。例如:我是中国人,就可以分为:我、是、中国人、中国、国人这样的几个词条

创建倒排索引是对正向索引的一种特殊处理和应用,流程如下:

  • 将每一个文档的数据利用分词算法根据语义拆分,得到一个个词条

  • 创建表,每行数据包括词条、词条所在文档id、位置等信息

  • 因为词条唯一性,可以给词条创建正向索引

此时形成的这张以词条为索引的表,就是倒排索引表,两者对比如下:

正向索引

id(索引)

title

price

1

小米手机

3499

2

华为手机

4999

3

华为小米充电器

49

4

小米手环

49

...

...

...

倒排索引

词条(索引)

文档id

小米

1,3,4

手机

1,2

华为

2,3

充电器

3

手环

4

倒排索引的搜索流程如下(以搜索"华为手机"为例),如图:

 

流程描述:

1)用户输入条件"华为手机"进行搜索。

2)对用户输入条件分词,得到词条:华为手机

3)拿着词条在倒排索引中查找(由于词条有索引,查询效率很高),即可得到包含词条的文档id:1、2、3

4)拿着文档id到正向索引中查找具体文档即可(由于id也有索引,查询效率也很高)。

虽然要先查询倒排索引,再查询倒排索引,但是无论是词条、还是文档id都建立了索引,查询速度非常快!无需全表扫描。

1.2.3.正向和倒排

那么为什么一个叫做正向索引,一个叫做倒排索引呢?

  • 正向索引是最传统的,根据id索引的方式。但根据词条查询时,必须先逐条获取每个文档,然后判断文档中是否包含所需要的词条,是根据文档找词条的过程

  • 倒排索引则相反,是先找到用户要搜索的词条,根据词条得到保护词条的文档的id,然后根据id获取文档。是根据词条找文档的过程

是不是恰好反过来了?

那么两者方式的优缺点是什么呢?

正向索引

  • 优点:

    • 可以给多个字段创建索引

    • 根据索引字段搜索、排序速度非常快

  • 缺点:

    • 根据非索引字段,或者索引字段中的部分词条查找时,只能全表扫描。

倒排索引

  • 优点:

    • 根据词条搜索、模糊搜索时,速度非常快

  • 缺点:

    • 只能给词条创建索引,而不是字段

    • 无法根据字段做排序

1.3.基础概念

elasticsearch中有很多独有的概念,与mysql中略有差别,但也有相似之处。

1.3.1.文档和字段

elasticsearch是面向文档(Document)存储的,可以是数据库中的一条商品数据,一个订单信息。文档数据会被序列化为json格式后存储在elasticsearch中:

{
    "id": 1,
    "title": "小米手机",
    "price": 3499
}
{
    "id": 2,
    "title": "华为手机",
    "price": 4999
}
{
    "id": 3,
    "title": "华为小米充电器",
    "price": 49
}
{
    "id": 4,
    "title": "小米手环",
    "price": 299
}

因此,原本数据库中的一行数据就是ES中的一个JSON文档;而数据库中每行数据都包含很多列,这些列就转换为JSON文档中的字段(Field)

1.3.2.索引和映射

随着业务发展,需要在es中存储的文档也会越来越多,比如有商品的文档、用户的文档、订单文档等等:

 

所有文档都散乱存放显然非常混乱,也不方便管理。

因此,我们要将类型相同的文档集中在一起管理,称为索引(Index)。例如:

商品索引

{
    "id": 1,
    "title": "小米手机",
    "price": 3499
}

{
    "id": 2,
    "title": "华为手机",
    "price": 4999
}

{
    "id": 3,
    "title": "三星手机",
    "price": 3999
}

用户索引

{
    "id": 101,
    "name": "张三",
    "age": 21
}

{
    "id": 102,
    "name": "李四",
    "age": 24
}

{
    "id": 103,
    "name": "麻子",
    "age": 18
}

订单索引

{
    "id": 10,
    "userId": 101,
    "goodsId": 1,
    "totalFee": 294
}

{
    "id": 11,
    "userId": 102,
    "goodsId": 2,
    "totalFee": 328
}
  • 所有用户文档,就可以组织在一起,称为用户的索引;

  • 所有商品的文档,可以组织在一起,称为商品的索引;

  • 所有订单的文档,可以组织在一起,称为订单的索引;

因此,我们可以把索引当做是数据库中的表。

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

1.3.3.mysql与elasticsearch

我们统一的把mysql与elasticsearch的概念做一下对比:

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

如图:

那是不是说,我们学习了elasticsearch就不再需要mysql了呢?

并不是如此,两者各自有自己的擅长之处:

  • Mysql:擅长事务类型操作,可以确保数据的安全和一致性

  • Elasticsearch:擅长海量数据的搜索、分析、计算

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

  • 对安全性要求较高的写操作,使用mysql实现

  • 对查询性能要求较高的搜索需求,使用elasticsearch实现

  • 两者再基于某种方式,实现数据的同步,保证一致性

1.4.IK分词器

Elasticsearch的关键就是倒排索引,而倒排索引依赖于对文档内容的分词,而分词则需要高效、精准的分词算法,IK分词器就是这样一个中文分词算法。

1.4.1.安装IK分词器

方案一:在线安装

运行一个命令即可:

docker exec -it es ./bin/elasticsearch-plugin  install https://github.com/medcl/elasticsearch-analysis-ik/releases/download/v7.12.1/elasticsearch-analysis-ik-7.12.1.zip

然后重启es容器:

docker restart es

方案二:离线安装

如果网速较差,也可以选择离线安装。

首先,查看之前安装的Elasticsearch容器的plugins数据卷目录:

docker volume inspect es-plugins

结果如下:

[
    {
        "CreatedAt": "2024-11-06T10:06:34+08:00",
        "Driver": "local",
        "Labels": null,
        "Mountpoint": "/var/lib/docker/volumes/es-plugins/_data",
        "Name": "es-plugins",
        "Options": null,
        "Scope": "local"
    }
]

可以看到elasticsearch的插件挂载到了/var/lib/docker/volumes/es-plugins/_data这个目录。我们需要把IK分词器上传至这个目录。

找到课前资料提供的ik分词器插件,课前资料提供了7.12.1版本的ik分词器压缩文件,你需要对其解压:

 

然后上传至虚拟机的/var/lib/docker/volumes/es-plugins/_data这个目录:

 

最后,重启es容器:

docker restart es

1.4.2.使用IK分词器

IK分词器包含两种模式:

  • ik_smart:智能语义切分

  • ik_max_word:最细粒度切分

我们在Kibana的DevTools上来测试分词器,首先测试Elasticsearch官方提供的标准分词器:

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

结果如下:

{
  "tokens" : [
    {
      "token" : "黑",
      "start_offset" : 0,
      "end_offset" : 1,
      "type" : "<IDEOGRAPHIC>",
      "position" : 0
    },
    {
      "token" : "马",
      "start_offset" : 1,
      "end_offset" : 2,
      "type" : "<IDEOGRAPHIC>",
      "position" : 1
    },
    {
      "token" : "程",
      "start_offset" : 2,
      "end_offset" : 3,
      "type" : "<IDEOGRAPHIC>",
      "position" : 2
    },
    {
      "token" : "序",
      "start_offset" : 3,
      "end_offset" : 4,
      "type" : "<IDEOGRAPHIC>",
      "position" : 3
    },
    {
      "token" : "员",
      "start_offset" : 4,
      "end_offset" : 5,
      "type" : "<IDEOGRAPHIC>",
      "position" : 4
    },
    {
      "token" : "学",
      "start_offset" : 5,
      "end_offset" : 6,
      "type" : "<IDEOGRAPHIC>",
      "position" : 5
    },
    {
      "token" : "习",
      "start_offset" : 6,
      "end_offset" : 7,
      "type" : "<IDEOGRAPHIC>",
      "position" : 6
    },
    {
      "token" : "java",
      "start_offset" : 7,
      "end_offset" : 11,
      "type" : "<ALPHANUM>",
      "position" : 7
    },
    {
      "token" : "太",
      "start_offset" : 11,
      "end_offset" : 12,
      "type" : "<IDEOGRAPHIC>",
      "position" : 8
    },
    {
      "token" : "棒",
      "start_offset" : 12,
      "end_offset" : 13,
      "type" : "<IDEOGRAPHIC>",
      "position" : 9
    },
    {
      "token" : "了",
      "start_offset" : 13,
      "end_offset" : 14,
      "type" : "<IDEOGRAPHIC>",
      "position" : 10
    }
  ]
}

可以看到,标准分词器智能1字1词条,无法正确对中文做分词。

我们再测试IK分词器:

POST /_analyze
{
  "analyzer": "ik_smart",
  "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" : 5,
      "end_offset" : 7,
      "type" : "CN_WORD",
      "position" : 2
    },
    {
      "token" : "java",
      "start_offset" : 7,
      "end_offset" : 11,
      "type" : "ENGLISH",
      "position" : 3
    },
    {
      "token" : "太棒了",
      "start_offset" : 11,
      "end_offset" : 14,
      "type" : "CN_WORD",
      "position" : 4
    }
  ]
}

1.4.3.拓展词典

随着互联网的发展,“造词运动”也越发的频繁。出现了很多新的词语,在原有的词汇列表中并不存在。比如:“泰裤辣”,“传智播客” 等。

IK分词器无法对这些词汇分词,测试一下:

POST /_analyze
{
  "analyzer": "ik_max_word",
  "text": "传智播客开设大学,真的泰裤辣!"
}

结果:

{
  "tokens" : [
    {
      "token" : "传",
      "start_offset" : 0,
      "end_offset" : 1,
      "type" : "CN_CHAR",
      "position" : 0
    },
    {
      "token" : "智",
      "start_offset" : 1,
      "end_offset" : 2,
      "type" : "CN_CHAR",
      "position" : 1
    },
    {
      "token" : "播",
      "start_offset" : 2,
      "end_offset" : 3,
      "type" : "CN_CHAR",
      "position" : 2
    },
    {
      "token" : "客",
      "start_offset" : 3,
      "end_offset" : 4,
      "type" : "CN_CHAR",
      "position" : 3
    },
    {
      "token" : "开设",
      "start_offset" : 4,
      "end_offset" : 6,
      "type" : "CN_WORD",
      "position" : 4
    },
    {
      "token" : "大学",
      "start_offset" : 6,
      "end_offset" : 8,
      "type" : "CN_WORD",
      "position" : 5
    },
    {
      "token" : "真的",
      "start_offset" : 9,
      "end_offset" : 11,
      "type" : "CN_WORD",
      "position" : 6
    },
    {
      "token" : "泰",
      "start_offset" : 11,
      "end_offset" : 12,
      "type" : "CN_CHAR",
      "position" : 7
    },
    {
      "token" : "裤",
      "start_offset" : 12,
      "end_offset" : 13,
      "type" : "CN_CHAR",
      "position" : 8
    },
    {
      "token" : "辣",
      "start_offset" : 13,
      "end_offset" : 14,
      "type" : "CN_CHAR",
      "position" : 9
    }
  ]
}

可以看到,传智播客泰裤辣都无法正确分词。

所以要想正确分词,IK分词器的词库也需要不断的更新,IK分词器提供了扩展词汇的功能。

1)打开IK分词器config目录:

 

注意,如果采用在线安装的通过,默认是没有config目录的,需要把课前资料提供的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)在IK分词器的config目录新建一个 ext.dic,可以参考config目录下复制一个配置文件进行修改

传智播客
泰裤辣

4)重启elasticsearch

docker restart es

# 查看 日志
docker logs -f elasticsearch

再次测试,可以发现传智播客泰裤辣都正确分词了:

{
  "tokens" : [
    {
      "token" : "传智播客",
      "start_offset" : 0,
      "end_offset" : 4,
      "type" : "CN_WORD",
      "position" : 0
    },
    {
      "token" : "开设",
      "start_offset" : 4,
      "end_offset" : 6,
      "type" : "CN_WORD",
      "position" : 1
    },
    {
      "token" : "大学",
      "start_offset" : 6,
      "end_offset" : 8,
      "type" : "CN_WORD",
      "position" : 2
    },
    {
      "token" : "真的",
      "start_offset" : 9,
      "end_offset" : 11,
      "type" : "CN_WORD",
      "position" : 3
    },
    {
      "token" : "泰裤辣",
      "start_offset" : 11,
      "end_offset" : 14,
      "type" : "CN_WORD",
      "position" : 4
    }
  ]
}

1.4.4.总结

分词器的作用是什么?

  • 创建倒排索引时,对文档分词

  • 用户搜索时,对输入的内容分词

IK分词器有几种模式?

  • ik_smart:智能切分,粗粒度

  • ik_max_word:最细切分,细粒度

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

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

  • 在词典中添加拓展词条或者停用词条

2.索引库操作

Index就类似数据库表,Mapping映射就类似表的结构。我们要向es中存储数据,必须先创建Index和Mapping

2.1.Mapping映射属性

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

  • type:字段数据类型,常见的简单类型有:

    • 字符串:text(可分词的文本)、keyword(精确值,例如:品牌、国家、ip地址)

    • 数值:longintegershortbytedoublefloat

    • 布尔:boolean

    • 日期:date

    • 对象:object

  • index:是否创建索引,默认为true

  • analyzer:使用哪种分词器

  • properties:该字段的子字段

例如下面的json文档:

{
    "age": 21,
    "weight": 52.1,
    "isMarried": false,
    "info": "黑马程序员Java讲师",
    "email": "zy@itcast.cn",
    "score": [99.1, 99.5, 98.9],
    "name": {
        "firstName": "云",
        "lastName": "赵"
    }
}

对应的每个字段映射(Mapping):

字段名

字段类型

类型说明

是否

参与搜索

是否

参与分词

分词器

age

integer

整数

——

weight

float

浮点数

——

isMarried

boolean

布尔

——

info

text

字符串,但需要分词

IK

email

keyword

字符串,但是不分词

——

score

float

只看数组中元素类型

——

name

firstName

keyword

字符串,但是不分词

——

lastName

keyword

字符串,但是不分词

——

2.2.索引库的CRUD

由于Elasticsearch采用的是Restful风格的API,因此其请求方式和路径相对都比较规范,而且请求参数也都采用JSON风格。

我们直接基于Kibana的DevTools来编写请求做测试,由于有语法提示,会非常方便。

2.2.1.创建索引库和映射

基本语法

  • 请求方式:PUT

  • 请求路径:/索引库名,可以自定义

  • 请求参数:mapping映射

格式

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

示例

# PUT /heima
{
  "mappings": {
    "properties": {
      "info":{
        "type": "text",
        "analyzer": "ik_smart"
      },
      "email":{
        "type": "keyword",
        "index": "false"
      },
      "name":{
        "properties": {
          "firstName": {
            "type": "keyword"
          }
        }
      }
    }
  }
}

2.2.2.查询索引库

基本语法

  • 请求方式:GET

  • 请求路径:/索引库名

  • 请求参数:无

格式

GET /索引库名

示例

GET /heima

2.2.3.修改索引库

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

虽然无法修改mapping中已有的字段,但是却允许添加新的字段到mapping中,因为不会对倒排索引产生影响。因此修改索引库能做的就是向索引库中添加新字段,或者更新索引库的基础属性。

语法说明

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

示例

PUT /heima/_mapping
{
  "properties": {
    "age":{
      "type": "integer"
    }
  }
}

2.2.4.删除索引库

语法:

  • 请求方式:DELETE

  • 请求路径:/索引库名

  • 请求参数:无

格式:

DELETE /索引库名

示例:

DELETE /heima

2.2.5.总结

索引库操作有哪些?

  • 创建索引库:PUT /索引库名

  • 查询索引库:GET /索引库名

  • 删除索引库:DELETE /索引库名

  • 修改索引库,添加字段:PUT /索引库名/_mapping

可以看到,对索引库的操作基本遵循的Restful的风格,因此API接口非常统一,方便记忆。

3.文档操作

有了索引库,接下来就可以向索引库中添加数据了。

Elasticsearch中的数据其实就是JSON风格的文档。操作文档自然保护等几种常见操作,我们分别来学习。

3.1.新增文档

语法:

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

示例:

POST /heima/_doc/1
{
    "info": "黑马程序员Java讲师",
    "email": "zy@itcast.cn",
    "name": {
        "firstName": "云",
        "lastName": "赵"
    }
}

响应:

3.2.查询文档

根据rest风格,新增是post,查询应该是get,不过查询一般都需要条件,这里我们把文档id带上。

语法:

GET /{索引库名称}/_doc/{id}

示例:

GET /heima/_doc/1

查看结果:

3.3.删除文档

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

语法:

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

示例:

DELETE /heima/_doc/1

结果:

3.4.修改文档

修改有两种方式:

  • 全量修改:直接覆盖原来的文档

  • 局部修改:修改文档中的部分字段

3.4.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": "赵"
    }
}

由于id1的文档已经被删除,所以第一次执行时,得到的反馈是created

 

所以如果执行第2次时,得到的反馈则是updated

3.4.2.局部修改

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

语法:

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

示例:

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

执行结果

3.5.批处理

批处理采用POST请求,基本语法如下:

POST _bulk
{ "index" : { "_index" : "test", "_id" : "1" } }
{ "field1" : "value1" }
{ "delete" : { "_index" : "test", "_id" : "2" } }
{ "create" : { "_index" : "test", "_id" : "3" } }
{ "field1" : "value3" }
{ "update" : {"_id" : "1", "_index" : "test"} }
{ "doc" : {"field2" : "value2"} }

其中:

  • index代表新增操作

    • _index:指定索引库名

    • _id指定要操作的文档id

    • { "field1" : "value1" }:则是要新增的文档内容

  • delete代表删除操作

    • _index:指定索引库名

    • _id指定要操作的文档id

  • update代表更新操作

    • _index:指定索引库名

    • _id指定要操作的文档id

    • { "doc" : {"field2" : "value2"} }:要更新的文档字段

示例,批量新增:

POST /_bulk
{"index": {"_index":"heima", "_id": "3"}}
{"info": "黑马程序员C++讲师", "email": "ww@itcast.cn", "name":{"firstName": "五", "lastName":"王"}}
{"index": {"_index":"heima", "_id": "4"}}
{"info": "黑马程序员前端讲师", "email": "zhangsan@itcast.cn", "name":{"firstName": "三", "lastName":"张"}}

批量删除:

POST /_bulk
{"delete":{"_index":"heima", "_id": "3"}}
{"delete":{"_index":"heima", "_id": "4"}}

3.6.总结

文档操作有哪些?

  • 创建文档:POST /{索引库名}/_doc/文档id { json文档 }

  • 查询文档:GET /{索引库名}/_doc/文档id

  • 删除文档:DELETE /{索引库名}/_doc/文档id

  • 修改文档:

    • 全量修改:PUT /{索引库名}/_doc/文档id { json文档 }

    • 局部修改:POST /{索引库名}/_update/文档id { "doc": {字段}}

4.RestAPI

ES官方提供了各种不同语言的客户端,用来操作ES。这些客户端的本质就是组装DSL语句,通过http请求发送给ES。

官方文档地址:

Elasticsearch clients | Elastic Docs

由于ES目前最新版本是8.8,提供了全新版本的客户端,老版本的客户端已经被标记为过时。而我们采用的是7.12版本,因此只能使用老版本客户端:

 

然后选择7.12版本,HighLevelRestClient版本:

4.1.初始化RestClient

在elasticsearch提供的API中,与elasticsearch一切交互都封装在一个名为RestHighLevelClient的类中,必须先完成这个对象的初始化,建立与elasticsearch的连接。

分为三步:

1)在item-service模块中引入esRestHighLevelClient依赖:

<dependency>
    <groupId>org.elasticsearch.client</groupId>
    <artifactId>elasticsearch-rest-high-level-client</artifactId>
</dependency>

2)因为SpringBoot默认的ES版本是7.17.10,所以我们需要覆盖默认的ES版本:

  <properties>
      <maven.compiler.source>11</maven.compiler.source>
      <maven.compiler.target>11</maven.compiler.target>
      <elasticsearch.version>7.12.1</elasticsearch.version>
  </properties>

3)初始化RestHighLevelClient:

初始化的代码如下:

RestHighLevelClient client = new RestHighLevelClient(RestClient.builder(
        HttpHost.create("http://192.168.150.101:9200")
));

这里为了单元测试方便,我们创建一个测试类IndexTest,然后将初始化的代码编写在@BeforeEach方法中:

package com.hmall.item.es;

import org.apache.http.HttpHost;
import org.elasticsearch.client.RestClient;
import org.elasticsearch.client.RestHighLevelClient;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;

import java.io.IOException;

public class IndexTest {

    private RestHighLevelClient client;

    @BeforeEach
    void setUp() {
        this.client = new RestHighLevelClient(RestClient.builder(
                HttpHost.create("http://192.168.150.101:9200")
        ));
    }

    @Test
    void testConnect() {
        System.out.println(client);
    }

    @AfterEach
    void tearDown() throws IOException {
        this.client.close();
    }
}

4.1.创建索引库

由于要实现对商品搜索,所以我们需要将商品添加到Elasticsearch中,不过需要根据搜索业务的需求来设定索引库结构,而不是一股脑的把MySQL数据写入Elasticsearch.

4.1.1.Mapping映射

搜索页面的效果如图所示:

 

实现搜索功能需要的字段包括三大部分:

  • 搜索过滤字段

    • 分类

    • 品牌

    • 价格

  • 排序字段

    • 默认:按照更新时间降序排序

    • 销量

    • 价格

  • 展示字段

    • 商品id:用于点击后跳转

    • 图片地址

    • 是否是广告推广商品

    • 名称

    • 价格

    • 评价数量

    • 销量

对应的商品表结构如下,索引库无关字段已经划掉:

结合数据库表结构,以上字段对应的mapping映射属性如下:

字段名

字段类型

类型说明

是否

参与搜索

是否

参与分词

分词器

id

long

长整数

——

name

text

字符串,参与分词搜索

IK

price

integer

以分为单位,所以是整数

——

stock

integer

字符串,但需要分词

——

image

keyword

字符串,但是不分词

——

category

keyword

字符串,但是不分词

——

brand

keyword

字符串,但是不分词

——

sold

integer

销量,整数

——

commentCount

integer

评价,整数

——

isAD

boolean

布尔类型

——

updateTime

Date

更新时间

——

因此,最终我们的索引库文档结构应该是这样:

PUT /items
{
  "mappings": {
    "properties": {
      "id": {
        "type": "keyword"
      },
      "name":{
        "type": "text",
        "analyzer": "ik_max_word"
      },
      "price":{
        "type": "integer"
      },
      "stock":{
        "type": "integer"
      },
      "image":{
        "type": "keyword",
        "index": false
      },
      "category":{
        "type": "keyword"
      },
      "brand":{
        "type": "keyword"
      },
      "sold":{
        "type": "integer"
      },
      "commentCount":{
        "type": "integer",
        "index": false
      },
      "isAD":{
        "type": "boolean"
      },
      "updateTime":{
        "type": "date"
      }
    }
  }
}

4.1.2.创建索引

创建索引库的API如下:

 

代码分为三步:

  • 1)创建Request对象。

    • 因为是创建索引库的操作,因此Request是CreateIndexRequest

  • 2)添加请求参数

    • 其实就是Json格式的Mapping映射参数。因为json字符串很长,这里是定义了静态字符串常量MAPPING_TEMPLATE,让代码看起来更加优雅。

  • 3)发送请求

    • client.indices()方法的返回值是IndicesClient类型,封装了所有与索引库操作有关的方法。例如创建索引、删除索引、判断索引是否存在等

item-service中的IndexTest测试类中,具体代码如下:

@Test
void testCreateIndex() throws IOException {
    // 1.创建Request对象
    CreateIndexRequest request = new CreateIndexRequest("items");
    // 2.准备请求参数
    request.source(MAPPING_TEMPLATE, XContentType.JSON);
    // 3.发送请求
    client.indices().create(request, RequestOptions.DEFAULT);
}

static final String MAPPING_TEMPLATE = "{\n" +
            "  \"mappings\": {\n" +
            "    \"properties\": {\n" +
            "      \"id\": {\n" +
            "        \"type\": \"keyword\"\n" +
            "      },\n" +
            "      \"name\":{\n" +
            "        \"type\": \"text\",\n" +
            "        \"analyzer\": \"ik_max_word\"\n" +
            "      },\n" +
            "      \"price\":{\n" +
            "        \"type\": \"integer\"\n" +
            "      },\n" +
            "      \"stock\":{\n" +
            "        \"type\": \"integer\"\n" +
            "      },\n" +
            "      \"image\":{\n" +
            "        \"type\": \"keyword\",\n" +
            "        \"index\": false\n" +
            "      },\n" +
            "      \"category\":{\n" +
            "        \"type\": \"keyword\"\n" +
            "      },\n" +
            "      \"brand\":{\n" +
            "        \"type\": \"keyword\"\n" +
            "      },\n" +
            "      \"sold\":{\n" +
            "        \"type\": \"integer\"\n" +
            "      },\n" +
            "      \"commentCount\":{\n" +
            "        \"type\": \"integer\"\n" +
            "      },\n" +
            "      \"isAD\":{\n" +
            "        \"type\": \"boolean\"\n" +
            "      },\n" +
            "      \"updateTime\":{\n" +
            "        \"type\": \"date\"\n" +
            "      }\n" +
            "    }\n" +
            "  }\n" +
            "}";

4.2.删除索引库

删除索引库的请求非常简单:

DELETE /hotel

与创建索引库相比:

  • 请求方式从PUT变为DELTE

  • 请求路径不变

  • 无请求参数

所以代码的差异,注意体现在Request对象上。流程如下:

  • 1)创建Request对象。这次是DeleteIndexRequest对象

  • 2)准备参数。这里是无参,因此省略

  • 3)发送请求。改用delete方法

item-service中的IndexTest测试类中,编写单元测试,实现删除索引:

@Test
void testDeleteIndex() throws IOException {
    // 1.创建Request对象
    DeleteIndexRequest request = new DeleteIndexRequest("items");
    // 2.发送请求
    client.indices().delete(request, RequestOptions.DEFAULT);
}

4.3.判断索引库是否存在

判断索引库是否存在,本质就是查询,对应的请求语句是:

GET /hotel

因此与删除的Java代码流程是类似的,流程如下:

  • 1)创建Request对象。这次是GetIndexRequest对象

  • 2)准备参数。这里是无参,直接省略

  • 3)发送请求。改用exists方法

@Test
void testExistsIndex() throws IOException {
    // 1.创建Request对象
    GetIndexRequest request = new GetIndexRequest("items");
    // 2.发送请求
    boolean exists = client.indices().exists(request, RequestOptions.DEFAULT);
    // 3.输出
    System.err.println(exists ? "索引库已经存在!" : "索引库不存在!");
}

4.4.总结

JavaRestClient操作elasticsearch的流程基本类似。核心是client.indices()方法来获取索引库的操作对象。

索引库操作的基本步骤:

  • 初始化RestHighLevelClient

  • 创建XxxIndexRequest。XXX是CreateGetDelete

  • 准备请求参数( Create时需要,其它是无参,可以省略)

  • 发送请求。调用RestHighLevelClient#indices().xxx()方法,xxx是createexistsdelete

5.RestClient操作文档

索引库准备好以后,就可以操作文档了。为了与索引库操作分离,我们再次创建一个测试类,做两件事情:

  • 初始化RestHighLevelClient

  • 我们的商品数据在数据库,需要利用IHotelService去查询,所以注入这个接口

package com.hmall.item.es;

import com.hmall.item.service.IItemService;
import org.apache.http.HttpHost;
import org.elasticsearch.client.RestClient;
import org.elasticsearch.client.RestHighLevelClient;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;

import java.io.IOException;

@SpringBootTest(properties = "spring.profiles.active=local")
public class DocumentTest {

    private RestHighLevelClient client;
    @Autowired
    private IItemService itemService;

    @BeforeEach
    void setUp() {
        this.client = new RestHighLevelClient(RestClient.builder(
                HttpHost.create("http://192.168.150.101:9200")
        ));
    }
    
    @AfterEach
    void tearDown() throws IOException {
        this.client.close();
    }
}

5.1.新增文档

我们需要将数据库中的商品信息导入elasticsearch中,而不是造假数据了。

5.1.1.实体类

索引库结构与数据库结构还存在一些差异,因此我们要定义一个索引库结构对应的实体。

hm-service模块的com.hmall.item.domain.dto包中定义一个新的DTO:

package com.hmall.item.domain.po;

import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;

import java.time.LocalDateTime;

@Data
@ApiModel(description = "索引库实体")
public class ItemDoc{

    @ApiModelProperty("商品id")
    private String id;

    @ApiModelProperty("商品名称")
    private String name;

    @ApiModelProperty("价格(分)")
    private Integer price;

    @ApiModelProperty("商品图片")
    private String image;

    @ApiModelProperty("类目名称")
    private String category;

    @ApiModelProperty("品牌名称")
    private String brand;

    @ApiModelProperty("销量")
    private Integer sold;

    @ApiModelProperty("评论数")
    private Integer commentCount;

    @ApiModelProperty("是否是推广广告,true/false")
    private Boolean isAD;

    @ApiModelProperty("更新时间")
    private LocalDateTime updateTime;
}

5.1.2.API语法

新增文档的请求语法如下:

POST /{索引库名}/_doc/1
{
    "name": "Jack",
    "age": 21
}

对应的JavaAPI如下:

 

可以看到与索引库操作的API非常类似,同样是三步走:

  • 1)创建Request对象,这里是IndexRequest,因为添加文档就是创建倒排索引的过程

  • 2)准备请求参数,本例中就是Json文档

  • 3)发送请求

变化的地方在于,这里直接使用client.xxx()的API,不再需要client.indices()了。

5.1.3.完整代码

我们导入商品数据,除了参考API模板“三步走”以外,还需要做几点准备工作:

  • 商品数据来自于数据库,我们需要先查询出来,得到Item对象

  • Item对象需要转为ItemDoc对象

  • ItemDTO需要序列化为json格式

因此,代码整体步骤如下:

  • 1)根据id查询商品数据Item

  • 2)将Item封装为ItemDoc

  • 3)将ItemDoc序列化为JSON

  • 4)创建IndexRequest,指定索引库名和id

  • 5)准备请求参数,也就是JSON文档

  • 6)发送请求

item-serviceDocumentTest测试类中,编写单元测试:

@Test
void testAddDocument() throws IOException {
    // 1.根据id查询商品数据
    Item item = itemService.getById(100002644680L);
    // 2.转换为文档类型
    ItemDoc itemDoc = BeanUtil.copyProperties(item, ItemDoc.class);
    // 3.将ItemDTO转json
    String doc = JSONUtil.toJsonStr(itemDoc);

    // 1.准备Request对象
    IndexRequest request = new IndexRequest("items").id(itemDoc.getId());
    // 2.准备Json文档
    request.source(doc, XContentType.JSON);
    // 3.发送请求
    client.index(request, RequestOptions.DEFAULT);
}

5.2.查询文档

我们以根据id查询文档为例

5.2.1.语法说明

查询的请求语句如下:

GET /{索引库名}/_doc/{id}

与之前的流程类似,代码大概分2步:

  • 创建Request对象

  • 准备请求参数,这里是无参,直接省略

  • 发送请求

不过查询的目的是得到结果,解析为ItemDTO,还要再加一步对结果的解析。示例代码如下:

 

可以看到,响应结果是一个JSON,其中文档放在一个_source属性中,因此解析就是拿到_source,反序列化为Java对象即可。

其它代码与之前类似,流程如下:

  • 1)准备Request对象。这次是查询,所以是GetRequest

  • 2)发送请求,得到结果。因为是查询,这里调用client.get()方法

  • 3)解析结果,就是对JSON做反序列化

5.2.2.完整代码

item-serviceDocumentTest测试类中,编写单元测试:

@Test
void testGetDocumentById() throws IOException {
    // 1.准备Request对象
    GetRequest request = new GetRequest("items").id("100002644680");
    // 2.发送请求
    GetResponse response = client.get(request, RequestOptions.DEFAULT);
    // 3.获取响应结果中的source
    String json = response.getSourceAsString();
    
    ItemDoc itemDoc = JSONUtil.toBean(json, ItemDoc.class);
    System.out.println("itemDoc= " + ItemDoc);
}

5.3.删除文档

删除的请求语句如下:

DELETE /hotel/_doc/{id}

与查询相比,仅仅是请求方式从DELETE变成GET,可以想象Java代码应该依然是2步走:

  • 1)准备Request对象,因为是删除,这次是DeleteRequest对象。要指定索引库名和id

  • 2)准备参数,无参,直接省略

  • 3)发送请求。因为是删除,所以是client.delete()方法

item-serviceDocumentTest测试类中,编写单元测试:

@Test
void testDeleteDocument() throws IOException {
    // 1.准备Request,两个参数,第一个是索引库名,第二个是文档id
    DeleteRequest request = new DeleteRequest("item", "100002644680");
    // 2.发送请求
    client.delete(request, RequestOptions.DEFAULT);
}

5.4.修改文档

修改我们讲过两种方式:

  • 全量修改:本质是先根据id删除,再新增

  • 局部修改:修改文档中的指定字段值

在RestClient的API中,全量修改与新增的API完全一致,判断依据是ID:

  • 如果新增时,ID已经存在,则修改

  • 如果新增时,ID不存在,则新增

这里不再赘述,我们主要关注局部修改的API即可。

5.4.1.语法说明

局部修改的请求语法如下:

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

代码示例如图:

 

与之前类似,也是三步走:

  • 1)准备Request对象。这次是修改,所以是UpdateRequest

  • 2)准备参数。也就是JSON文档,里面包含要修改的字段

  • 3)更新文档。这里调用client.update()方法

5.4.2.完整代码

item-serviceDocumentTest测试类中,编写单元测试:

@Test
void testUpdateDocument() throws IOException {
    // 1.准备Request
    UpdateRequest request = new UpdateRequest("items", "100002644680");
    // 2.准备请求参数
    request.doc(
            "price", 58800,
            "commentCount", 1
    );
    // 3.发送请求
    client.update(request, RequestOptions.DEFAULT);
}

5.5.批量导入文档

在之前的案例中,我们都是操作单个文档。而数据库中的商品数据实际会达到数十万条,某些项目中可能达到数百万条。

我们如果要将这些数据导入索引库,肯定不能逐条导入,而是采用批处理方案。常见的方案有:

  • 利用Logstash批量导入

    • 需要安装Logstash

    • 对数据的再加工能力较弱

    • 无需编码,但要学习编写Logstash导入配置

  • 利用JavaAPI批量导入

    • 需要编码,但基于JavaAPI,学习成本低

    • 更加灵活,可以任意对数据做再加工处理后写入索引库

接下来,我们就学习下如何利用JavaAPI实现批量文档导入。

5.5.1.语法说明

批处理与前面讲的文档的CRUD步骤基本一致:

  • 创建Request,但这次用的是BulkRequest

  • 准备请求参数

  • 发送请求,这次要用到client.bulk()方法

BulkRequest本身其实并没有请求参数,其本质就是将多个普通的CRUD请求组合在一起发送。例如:

  • 批量新增文档,就是给每个文档创建一个IndexRequest请求,然后封装到BulkRequest中,一起发出。

  • 批量删除,就是创建N个DeleteRequest请求,然后封装到BulkRequest,一起发出

因此BulkRequest中提供了add方法,用以添加其它CRUD的请求:

 

可以看到,能添加的请求有:

  • IndexRequest,也就是新增

  • UpdateRequest,也就是修改

  • DeleteRequest,也就是删除

因此Bulk中添加了多个IndexRequest,就是批量新增功能了。示例:

@Test
void testBulk() throws IOException {
    // 1.创建Request
    BulkRequest request = new BulkRequest();
    // 2.准备请求参数
    request.add(new IndexRequest("items").id("1").source("json doc1", XContentType.JSON));
    request.add(new IndexRequest("items").id("2").source("json doc2", XContentType.JSON));
    // 3.发送请求
    client.bulk(request, RequestOptions.DEFAULT);
}

5.5.2.完整代码

当我们要导入商品数据时,由于商品数量达到数十万,因此不可能一次性全部导入。建议采用循环遍历方式,每次导入1000条左右的数据。

item-serviceDocumentTest测试类中,编写单元测试:

@Test
void testLoadItemDocs() throws IOException {
    // 分页查询商品数据
    int pageNo = 1;
    int size = 1000;
    while (true) {
        Page<Item> page = itemService.lambdaQuery().eq(Item::getStatus, 1).page(new Page<Item>(pageNo, size));
        // 非空校验
        List<Item> items = page.getRecords();
        if (CollUtils.isEmpty(items)) {
            return;
        }
        log.info("加载第{}页数据,共{}条", pageNo, items.size());
        // 1.创建Request
        BulkRequest request = new BulkRequest("items");
        // 2.准备参数,添加多个新增的Request
        for (Item item : items) {
            // 2.1.转换为文档类型ItemDTO
            ItemDoc itemDoc = BeanUtil.copyProperties(item, ItemDoc.class);
            // 2.2.创建新增文档的Request对象
            request.add(new IndexRequest()
                            .id(itemDoc.getId())
                            .source(JSONUtil.toJsonStr(itemDoc), XContentType.JSON));
        }
        // 3.发送请求
        client.bulk(request, RequestOptions.DEFAULT);

        // 翻页
        pageNo++;
    }
}

5.6.小结

文档操作的基本步骤:

  • 初始化RestHighLevelClient

  • 创建XxxRequest。

    • XXX是IndexGetUpdateDeleteBulk

  • 准备参数(IndexUpdateBulk时需要)

  • 发送请求。

    • 调用RestHighLevelClient#.xxx()方法,xxx是indexgetupdatedeletebulk

  • 解析结果(Get时需要)

6.作业

6.1.服务拆分

搜索业务并发压力可能会比较高,目前与商品服务在一起,不方便后期优化。

需求:创建一个新的微服务,命名为search-service,将搜索相关功能抽取到这个微服务中

6.2.商品查询接口

item-service服务中提供一个根据id查询商品的功能,并编写对应的FeignClient

6.3.数据同步

每当商品服务对商品实现增删改时,索引库的数据也需要同步更新。

提示:可以考虑采用MQ异步通知实现。

理解:Elasticsearch

理论理解

Elasticsearch 是一款基于 Lucene 构建的开源搜索引擎,广泛应用于各种场景,如电商搜索、日志分析、数据监控等。其核心优势在于能够处理大规模数据的快速查询,尤其是在面对模糊查询、全文搜索等复杂需求时,能够提供显著的性能提升。

Elasticsearch 的核心技术之一是倒排索引,这种索引方式与传统的数据库正向索引不同。正向索引按照数据的主键(如文档 ID)进行索引,而倒排索引则是按照关键词(如商品名称中的词条)进行索引。在倒排索引中,系统记录每个词条及其所在文档的位置,查询时可以通过词条快速定位到相关文档,而无需全表扫描,极大地提升了查询效率。

主要特点
  • 分布式架构:Elasticsearch 天生支持分布式,能够将数据分片存储在多个节点上,确保系统的高可用性与扩展性。即使数据量增长,系统也能灵活扩展,保证查询的高效性。

  • 实时搜索:Elasticsearch 支持近实时搜索。虽然数据存储在分布式集群中,但它能够在数据写入后几乎立即进行查询,适用于需要实时数据分析和反馈的场景。

  • 支持多种查询方式:除了常规的精确匹配,Elasticsearch 还支持复杂的查询,如模糊查询、范围查询、短语查询等,且能对中文等复杂语言进行有效处理。

  • 强大的聚合功能:Elasticsearch 不仅提供单纯的文本检索,还支持强大的聚合查询,可以用于统计分析、数据挖掘等场景,如按品牌分类的销售总额统计等。

IK 分词器

IK 分词器是 Elasticsearch 中用于处理中文文本的工具,针对中文的特殊性,提供了智能分词和最大粒度分词两种模式。它能够自动识别中文文本中的词条,并将其拆分成具备实际意义的单元。与标准的分词器相比,IK 分词器能更好地处理中文文本的切分问题,支持拼音搜索和同义词搜索等功能。IK 分词器的关键优势在于:

  • 智能分词:通过 ik_smart 模式,能够对文本进行合理的粗粒度分词,适合处理一些简单的查询需求。

  • 最大粒度分词:ik_max_word 模式能够进行更精细的分词,适合进行更精准的搜索,尤其是对于长文本或需要高精度搜索的场景。

  • 扩展词典:IK 分词器提供了扩展词典功能,允许用户根据需求添加自定义词汇,如“泰裤辣”等新兴词汇。这样可以确保分词器能及时识别新出现的词语,提高分词的准确性。

倒排索引原理

倒排索引是 Elasticsearch 的核心技术,它的基本思路是:将每个文档的关键词分词后,以词条为单位进行索引。与传统数据库的正向索引不同,倒排索引将查询操作的顺序反转,先索引词条,再根据词条查找相关文档。

在倒排索引中,文档被分词为多个词条,然后每个词条会被映射到包含该词条的文档 ID。查询时,Elasticsearch 会通过倒排索引找到匹配的词条,再根据文档 ID 快速提取相关文档,从而大大减少了查询时间和计算量。

例如,对于一篇包含“手机”一词的商品描述,倒排索引将该词与文档 ID 关联起来。若用户搜索“手机”,Elasticsearch 就能迅速查找到所有包含该词的文档,无需遍历所有文档,极大提高了搜索效率。

索引与映射

在 Elasticsearch 中,索引是用来存储数据的容器,相当于数据库中的表。而映射则是对索引中存储文档的字段类型和属性进行定义的操作,类似于数据库中的表结构定义。在映射中,可以指定字段的数据类型、是否分词、使用哪种分词器等。

例如,在电商平台中,商品数据可以按类别、品牌、价格等字段进行映射。通过设置合适的映射,可以确保每个字段的数据按照预期的方式进行存储和查询。

  • 字段类型:如 text 类型用于存储可分词的文本数据,keyword 类型用于存储不进行分词的精确匹配数据。

  • 分词器:指定使用的分词器(如 IK 分词器),决定了文本如何被分词。

  • 索引与查询:设置 index: false 可以禁用字段的索引,以优化存储和查询性能。

Elasticsearch 与传统数据库的对比

Elasticsearch 与传统数据库(如 MySQL)的最大区别在于,它并不是设计来替代关系型数据库,而是专注于高效的全文搜索和数据分析。它的优势在于:

  • 高效查询:通过倒排索引,Elasticsearch 在处理大数据量时依旧能保持高效的查询速度,尤其是在面对模糊查询、全文检索时,比传统数据库有更好的表现。

  • 分布式架构:Elasticsearch 是分布式的,可以横向扩展,支持高并发和大规模数据的处理,而传统数据库则多为纵向扩展,存在性能瓶颈。

  • 实时处理:Elasticsearch 能提供接近实时的搜索和分析,适合实时数据监控和日志分析等场景。

总结

Elasticsearch 是一款强大的搜索引擎,通过倒排索引和分布式架构,能够高效地处理大规模数据的搜索和分析。IK 分词器使其能够处理中文等复杂语言的分词问题,而映射和索引提供了灵活的数据管理方式。无论是在电商、社交平台还是日志分析等场景,Elasticsearch 都能提供显著的性能提升。

 

大厂实战理解:Elasticsearch 的应用

电商平台的商品搜索

在大厂中,Elasticsearch 主要应用于电商平台的商品搜索模块。例如,在平台如京东、淘宝、美团等的商品搜索功能中,Elasticsearch 被广泛应用来处理海量的商品数据并提供快速响应。在这些平台上,商品的数量非常庞大,通常达到数百万甚至数亿条,使用传统的数据库进行查询时,尤其是在模糊查询和排序方面,查询效率会迅速下降。因此,电商平台往往依赖于 Elasticsearch 来确保搜索速度和用户体验。

在电商系统中,商品的搜索不仅仅是通过产品名称进行简单的匹配,还涉及到了多种复杂的功能,如多条件筛选(品牌、价格、评价等)、模糊查询(同义词、拼音搜索)、排序(销量、价格、更新时间等)。Elasticsearch 的倒排索引和高效的查询能力能够在海量商品数据中快速定位用户需求的商品,提升用户体验。

例如,用户搜索“iPhone 12”时,Elasticsearch 会根据倒排索引结构快速查找包含“iPhone”或“12”的商品,支持拼音模糊查询,还能根据其他搜索条件(如品牌、价格范围)进行过滤和排序。

社交平台与内容管理系统

在社交平台(如微博、知乎、微信等)和内容管理系统中,Elasticsearch 主要用于处理海量的用户内容搜索和推荐。在这些平台中,用户生成的内容(如帖子、评论、图片等)和实时数据(如点赞数、评论数)需要被快速检索并展示给用户。

这些平台的用户内容通常是非结构化的文本数据,因此倒排索引技术能够高效地将文本内容转化为词条进行索引,从而支持快速的全文搜索。例如,用户搜索某个话题时,Elasticsearch 可以快速提供与话题相关的帖子、评论等内容,且支持高效的模糊匹配。

另外,Elasticsearch 的聚合查询功能也被广泛应用于这些平台的数据统计和分析,例如对内容的点赞数、评论数等进行聚合统计,以支持热点话题的分析与推荐。

日志管理与实时监控

Elasticsearch 在大厂的日志管理和实时监控系统中也得到了广泛应用。企业通常会面临大规模分布式系统生成的海量日志数据,这些日志数据需要实时分析并触发报警。Elasticsearch 的分布式架构和高效的检索能力使其成为处理和分析日志数据的理想选择。

以阿里巴巴的日志收集和分析系统为例,Elasticsearch 负责存储和查询各类日志数据,帮助运维人员实时监控系统运行状态,检测系统异常。在这种场景下,Elasticsearch 结合 Logstash 和 Kibana(即 ELK Stack)形成一个完整的数据收集、存储、查询和可视化解决方案。通过 Kibana,开发和运维人员可以对日志进行实时查询、聚合分析,生成趋势图和警报,从而及时发现并解决潜在问题。

推荐系统与智能推荐

在大型内容平台或电商平台中,推荐系统是另一项重要的应用。推荐系统通常基于用户行为数据(如点击、购买历史、评分等)来预测和推荐用户可能感兴趣的商品或内容。Elasticsearch 在这里的应用不仅限于提供搜索功能,还可以作为推荐系统的一部分,帮助平台对用户进行个性化推荐。

例如,在一个电商平台中,用户在搜索商品时,Elasticsearch 可以结合用户的历史搜索行为,提供更加个性化的商品推荐。基于用户的历史数据,平台可能会提供与其过去购买或浏览过的商品相关的推荐商品。

实时分析与大数据处理

Elasticsearch 的聚合查询功能非常适合进行大数据的实时分析。在一些大厂的业务中,Elasticsearch 被用来实时处理和分析流式数据,帮助企业做出快速决策。例如,在广告投放系统中,Elasticsearch 可以帮助企业实时监控广告的展示、点击和转化情况。通过聚合查询,企业能够快速分析广告投放的效果,并进行实时优化。

Elasticsearch 也可以与 Hadoop 等大数据框架结合,作为数据分析的一个环节。利用 Elasticsearch 强大的查询与聚合能力,可以对大规模的数据进行实时搜索和分析,帮助企业在海量数据中提取有价值的洞察。

总结

在大厂中,Elasticsearch 被广泛应用于电商、社交平台、内容管理系统、日志分析、推荐系统等多个领域。其倒排索引、分布式架构、强大的聚合查询能力以及对中文的支持使其成为处理大规模数据搜索和分析的理想工具。通过高效的查询能力和实时分析功能,Elasticsearch 极大地提升了用户体验和数据处理效率,成为大数据时代不可或缺的核心技术之一。

大厂面试题:Elasticsearch 相关

1. Elasticsearch 是什么?它的主要功能有哪些?

参考答案
Elasticsearch 是一个开源的分布式搜索引擎,基于 Apache Lucene 构建,支持全文搜索、结构化搜索和实时数据分析。它的主要功能包括:

  • 全文搜索:支持快速的文本匹配,适合大规模文档的搜索。

  • 分布式架构:能够横向扩展,处理海量数据。

  • 实时搜索:能够在数据写入后几乎立即进行查询,适合实时监控和日志分析。

  • 聚合查询:支持复杂的数据统计和分析,如最大值、最小值、平均值等。

  • 分词功能:支持自定义分词器(如 IK 分词器),适应中文、英文等语言的处理。

  • 支持复杂查询:包括模糊查询、范围查询、地理位置查询等。

2. Elasticsearch 中的倒排索引是什么?它如何提高搜索效率?

参考答案
倒排索引是 Elasticsearch 的核心技术之一,它与传统的正向索引相反。正向索引是按照文档 ID 对字段进行索引,而倒排索引是先对字段内容(如单词、词条等)进行索引,再找到这些词条所在的文档 ID。这使得在进行搜索时,可以通过词条快速定位到文档,而不需要遍历整个数据集,从而大大提高了查询效率。倒排索引特别适合处理全文搜索和模糊查询等操作,能显著提升查询速度。

3. 在 Elasticsearch 中,什么是“Mapping”?它与 MySQL 中的“Schema”有什么相似之处?

参考答案
Mapping 是 Elasticsearch 中对索引中文档字段的定义和配置,类似于 MySQL 中的表结构(Schema)。通过 Mapping,您可以定义字段的数据类型(如 textkeywordinteger 等),以及字段是否需要分词、使用哪种分词器等属性。Mapping 是对 Elasticsearch 索引结构的描述,决定了数据如何存储和索引。与 MySQL 中的表结构类似,Mapping 定义了索引中文档的“格式”或“结构”。

4. Elasticsearch 中如何处理中文分词?可以使用哪些分词器?

参考答案
Elasticsearch 提供了多种分词器,其中 IK 分词器 是专门用于中文的分词器。IK 分词器提供了两种分词模式:

  • ik_smart:智能分词模式,进行粗粒度的分词,适合一般的查询需求。

  • ik_max_word:最细粒度分词模式,适合需要高精度的查询场景。

IK 分词器能够根据中文的语言特点,对文本进行分词,分词结果会影响 Elasticsearch 中的倒排索引,从而提高中文搜索的精确度和效率。使用 IK 分词器时,可以根据需要扩展词典,支持一些自定义词汇的识别。

5. 如何在 Elasticsearch 中创建索引(Index)及其映射(Mapping)?

参考答案
在 Elasticsearch 中,创建索引及其映射通常使用 RESTful API 来操作。创建索引时,必须先定义索引的映射,映射包含字段的类型、是否分词、分词器的配置等。

示例:
创建索引时,使用 PUT 请求,指定索引名称,并定义映射内容。

PUT /my_index
{
  "mappings": {
    "properties": {
      "name": {
        "type": "text",
        "analyzer": "ik_max_word"
      },
      "age": {
        "type": "integer"
      },
      "is_active": {
        "type": "boolean"
      }
    }
  }
}
  • 在上面的例子中,my_index 是要创建的索引名称,name 字段使用 ik_max_word 分词器,age 字段类型为整数,is_active 字段类型为布尔值。

6. 什么是 Elasticsearch 中的“Shards” 和 “Replicas”?它们如何影响性能和数据高可用性?

参考答案

  • Shards:Elasticsearch 使用分片(Shards)将数据分布到不同的节点上。每个索引可以有多个分片,分片是数据存储和查询的基本单元。分片可以在多个节点上分布,这样可以横向扩展 Elasticsearch 集群,提升查询性能和存储容量。

  • Replicas:副本(Replicas)是分片的副本,通常用于提高数据的高可用性和查询性能。每个分片可以有多个副本,副本不仅用于备份数据,还可以提升读取性能。副本越多,系统的容错性和查询性能越好。

影响

  • 性能:合理的分片数量能够提高数据的分布和并发查询的性能,副本能够通过负载均衡分摊查询请求,提高读操作的性能。

  • 高可用性:副本为数据提供了冗余存储,防止单点故障,确保在某个节点发生故障时数据不会丢失。

7. Elasticsearch 中如何处理数据的实时性?如何保证查询和索引的一致性?

参考答案
Elasticsearch 支持接近实时(Near Real-Time, NRT)搜索。在 Elasticsearch 中,数据写入后会经过索引流程并存储在主分片中,通常会有一段延迟,称为 refresh interval,即数据写入后多少时间后可以被查询到。默认情况下,这个时间间隔为 1 秒,意味着写入的数据大约 1 秒后就能在搜索中被查询到。

一致性
Elasticsearch 提供了分布式一致性,在使用多个副本的情况下,当主分片或副本接收到数据写入请求时,会通过 write consistency 来确保数据在多个副本之间的一致性。用户可以配置一致性级别,以确保写入操作的数据一致性。

8. Elasticsearch 的聚合查询是什么?举个实际应用的例子。

参考答案
聚合查询是 Elasticsearch 中用于分析数据的功能,能够在搜索结果的基础上执行统计、分组、过滤等操作。常见的聚合类型包括:

  • terms 聚合:按字段的值进行分组统计。

  • range 聚合:对数值进行范围统计。

  • date_histogram 聚合:按时间字段进行分组统计。

示例:
假设我们有一个电商平台的商品索引,每个商品有一个价格字段,我们希望统计价格范围内的商品数量,可以使用 range 聚合。

GET /products/_search
{
  "aggs": {
    "price_range": {
      "range": {
        "field": "price",
        "ranges": [
          { "to": 100 },
          { "from": 100, "to": 500 },
          { "from": 500 }
        ]
      }
    }
  }
}

这个查询会返回三个价格范围的商品数量:小于 100 的商品、在 100 到 500 之间的商品、以及大于 500 的商品。

9. Elasticsearch 中如何执行批量操作(Bulk Operations)?

参考答案
Elasticsearch 提供了批量操作接口,可以一次性执行多个增、删、改、查操作,极大地提高了批量数据处理的效率。批量操作使用 _bulk API,它允许在一个请求中提交多个操作。

示例:

POST /_bulk
{ "index": { "_index": "my_index", "_id": "1" } }
{ "name": "John", "age": 30 }
{ "index": { "_index": "my_index", "_id": "2" } }
{ "name": "Jane", "age": 25 }

这个示例将会一次性插入两条数据到索引 my_index 中。每条数据的操作类型(index)和文档内容都在同一个批量请求中。

10. 如何优化 Elasticsearch 查询性能?

参考答案

  1. 合理设计索引映射:确保字段类型正确设置,避免不必要的字段分词。对于不需要进行全文搜索的字段(如 ID、邮件地址等),应使用 keyword 类型而不是 text 类型。

  2. 使用过滤器(Filter)代替查询(Query):过滤器不计算相关性评分,因此比查询要更高效。过滤器适用于范围查询、精确匹配等操作。

  3. 避免深度分页:深度分页(如使用 fromsize 进行深分页)会导致性能下降,使用 search_afterscroll 更为高效。

  4. 合理设置分片和副本:分片过多会导致性能下降,分片过少则可能无法充分利用集群的资源。合理设置副本数量能够提高查询性能。

  5. 启用查询缓存:Elasticsearch 会缓存频繁查询的结果,启用查询缓存可以加速相同查询的响应速度。

这些优化措施能够确保 Elasticsearch 在面对大规模数据和高并发查询时,仍能保持高效的性能。

 

大厂场景题:Elasticsearch 相关

场景 1:电商平台商品搜索优化

问题描述:
你在一家电商公司工作,负责优化商品搜索模块。目前,电商平台商品数量巨大,每天都在不断增加,用户通过搜索框输入商品名称、类别、价格范围等进行搜索。搜索接口的响应时间越来越长,尤其是在高并发的情况下。现有的商品搜索使用的是传统的数据库模糊查询(LIKE)来实现,随着数据量的增加,性能逐渐变差。你需要使用 Elasticsearch 来优化商品搜索功能,提升查询速度和用户体验。

问题要求:

  1. 请你说明如何使用 Elasticsearch 来优化商品搜索,涉及的关键概念和配置。

  2. 如何处理中文搜索,确保用户可以使用拼音和同义词进行搜索。

  3. 如何保证 Elasticsearch 在高并发情况下的高可用性?

参考答案:

  1. 使用 Elasticsearch 优化搜索

    • 倒排索引:通过 Elasticsearch 的倒排索引功能,能够将商品名称和描述等字段转换成词条索引,避免了传统数据库中模糊查询的全表扫描。使用倒排索引,能够在极大数据量下仍保持较低的查询延迟。

    • 分布式架构:通过 Elasticsearch 的分布式架构,将商品数据分布到多个节点上,能够在高并发情况下实现负载均衡,提高查询性能。

  2. 中文搜索与拼音、同义词处理

    • 使用 IK 分词器:IK 分词器能够处理中文文本的分词,包括支持中文分词和拼音搜索。在商品名称、描述等字段中,使用 IK 分词器能确保较高的查询精度。

    • 使用 同义词处理:可以通过扩展 IK 分词器的词典,添加常见的同义词,例如将“手机”和“智能机”视为同义词,确保用户输入的不同查询词能够正确匹配到商品。

  3. 高并发情况下的高可用性

    • 副本设置:为每个分片配置多个副本,确保在节点故障的情况下,副本能够提供备份数据,保证系统的高可用性。

    • 自动扩容:随着商品数据量的增加,可以动态扩展 Elasticsearch 集群的节点,自动将索引分片分布到更多节点上,提高处理能力。

    • 集群监控与报警:配置集群监控,实时监控节点健康状态,及时发现性能瓶颈和硬件故障,并进行报警处理。

场景 2:日志监控与分析

问题描述:
你负责一个大规模的分布式系统的运维工作,系统中有多个微服务,每个服务都会生成大量的日志数据。日志数据需要被实时采集、存储,并能在出现问题时迅速定位。为了实现实时监控和故障定位,你决定使用 Elasticsearch、Logstash 和 Kibana(ELK Stack)来处理这些日志数据。请你设计一个方案,确保日志数据能够高效、实时地存储、查询和分析。

问题要求:

  1. 如何使用 ELK Stack 处理日志数据,简要描述每个组件的功能。

  2. 你如何设计 Elasticsearch 的索引和映射来优化日志查询?

  3. 如果有大量日志数据需要存储,并且查询非常频繁,如何优化 Elasticsearch 的性能和存储效率?

参考答案:

  1. 使用 ELK Stack 处理日志数据:

    • Logstash:用于数据采集和预处理,将来自不同微服务的日志数据收集并转发到 Elasticsearch。Logstash 可以对日志进行过滤、转换、格式化等操作,以确保数据的格式一致性。

    • Elasticsearch:用于存储和索引日志数据,提供高效的全文搜索和聚合查询能力。Elasticsearch 将日志数据转化为倒排索引,使得查询变得快速且高效。

    • Kibana:用于数据的可视化,帮助运维人员实时监控日志,并通过图形化的方式展示各种系统性能指标、错误日志、异常行为等。

  2. 设计 Elasticsearch 索引和映射优化日志查询:

    • 时间字段:日志数据中通常有时间戳字段,可以根据时间字段创建 date_histogram 聚合查询,帮助快速按时间进行查询和分析。

    • 字段类型选择:对于 message 等文本字段,选择 text 类型并指定合适的分词器,如 IK 分词器;对于 status_codelog_level 等字段,使用 keyword 类型以支持精确查询。

    • 分片设计:根据日志量和查询频率合理设置分片数,避免过多分片造成性能问题。对于大规模日志数据,可以设置时间字段为分片键,按天、周等单位进行分片。

    • 映射优化:日志通常包含大量的动态字段,使用 动态映射(dynamic mapping) 时,需要合理配置字段类型,以避免 Elasticsearch 自动推断错误的字段类型。

  3. 优化 Elasticsearch 性能和存储效率:

    • 存储优化:使用 index.refresh_interval 设置较长的刷新间隔,减少不必要的 I/O 操作。根据业务需求调整索引的生命周期管理(ILM),定期删除过期的日志数据,减少存储压力。

    • 查询优化:利用 filter 来避免计算相关性评分,使用缓存来加速频繁查询。将频繁查询的字段创建为 keyword 类型,以避免全文搜索的性能问题。

    • 分片与副本策略:合理规划索引的分片数目和副本数目,避免过多的分片影响性能。为查询密集型的索引配置适当的副本数,以增加查询吞吐量。

场景 3:推荐系统的数据存储与查询

问题描述:
你在一家大数据公司工作,负责设计一个基于用户历史行为的商品推荐系统。该系统需要存储大量的用户行为数据(如浏览记录、购买记录、评价等),并通过分析这些数据向用户推荐商品。由于用户量庞大,数据量巨大,如何利用 Elasticsearch 设计高效的数据存储和查询结构,以支持实时推荐功能?

问题要求:

  1. 如何在 Elasticsearch 中设计用户行为数据的索引和映射?

  2. 如何使用 Elasticsearch 聚合功能为推荐系统提供支持?

  3. 如何处理推荐系统中的实时数据更新和查询高并发问题?

参考答案:

  1. 设计用户行为数据的索引和映射:

    • 用户行为索引:为每个用户的行为数据(浏览、购买、评分等)创建独立的索引,每个索引可以包含时间戳、用户 ID、商品 ID、行为类型等字段。为 user_idproduct_id 字段使用 keyword 类型,避免分词影响查询效率。

    • 映射设计:对于行为日志中的文本字段(如商品描述、评论内容),使用 text 类型并指定合适的分词器;对于时间字段使用 date 类型,确保行为记录按时间排序。

    • 动态数据:由于用户行为数据经常变化,可以利用 dynamic mapping 来自动处理新增字段,保证数据的灵活性。

  2. 使用 Elasticsearch 聚合功能支持推荐:

    • 聚合查询:通过聚合查询可以实现基于用户历史行为的分析。例如,使用 terms aggregation 聚合用户最常浏览的商品或最受欢迎的商品,根据这些聚合结果进行推荐。

    • 商品评分聚合:使用 avgsum 聚合,计算某商品的平均评分或总销量,从而为用户推荐评分高、销量大的商品。

    • 地理位置聚合:如果推荐系统涉及到地理位置,可以使用 Elasticsearch 的地理位置查询与聚合,按距离推荐用户附近的商品或服务。

  3. 实时数据更新与高并发查询:

    • 实时数据更新:为保证实时推荐,用户行为数据可以在每次行为发生时立即更新 Elasticsearch 中的索引。使用 refresh_interval 配置合适的刷新间隔,确保数据尽快可用。对于大流量的数据更新,可以通过异步写入和批量操作提高写入效率。

    • 高并发查询优化:针对高并发的查询需求,可以通过增加副本数量来分担查询负载,提升系统的查询吞吐量。同时,使用 search_after 替代传统的深分页查询,避免深分页带来的性能问题。

这些场景题和参考答案可以帮助你理解 Elasticsearch 在不同业务场景中的应用,以及如何优化性能和设计合理的索引结构。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

夏驰和徐策

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值