赞
踩
elasticsearch是一款非常强大的开源搜索引擎,具备非常多强大功能,可以帮助我们从海量数据中快速找到需要的内容:
在GitHub搜索代码:展示相关信息,并高亮显示相同部分
在电商网站搜索商品:展示相关产品
在百度搜索答案:展示相关信息,并高亮显示相同部分
在打车软件搜索附近的车:显示最近车辆位置
elasticsearch 结合 kibana、Logstash、Beats,也就是elastic stack(ELK)。被广泛应用在日志数据分析、实时监控等领域:
elasticsearch 是 elastic stack 的核心(不可替代),负责存储、搜索、分析数据。
根据id查询,正向索引查询速度非常快。
但如果是基于title做模糊查询,只能是逐行扫描数据,流程如下:
用户搜索数据,条件是title符合`"%手机%"
逐行获取数据,比如id为1的数据
判断数据中的title是否符合用户搜索条件
如果符合则放入结果集,不符合则丢弃。回到步骤1
逐行扫描,也就是全表扫描,随着数据量增加,其查询效率也会越来越低。当数据量达到数百万时,就是一场灾难。
elasticsearch采用倒排索引:
文档(document):每条数据就是一个文档
词条(term):文档按照语义分成的词语
倒排索引的搜索流程如下(以搜索"华为手机"为例):
用户输入条件"华为手机"
进行搜索。
对用户输入内容 分词,得到词条:华为
、手机
拿着词条在倒排索引中查找,可以得到包含词条的文档id:1、2、3
拿着文档id到正向索引中查找具体文档。
正向索引:
优点:
可以给多个字段创建索引
根据索引字段搜索、排序速度非常快
缺点:
根据非索引字段,或者索引字段中的部分词条查找时,只能全表扫描。
倒排索引:
优点:
根据词条搜索、模糊搜索时,速度非常快
缺点:
只能给词条创建索引,而不是字段
无法根据字段做排序
ES 是面向文档(Document)存储,可以是数据库中的一条商品数据,一个订单信息
文档数据会被序列化为 json格式 后存储
Json文档中往往包含很多的字段(Field),类似于数据库中的列。
索引(Index),就是相同类型的文档的集合
我们可以把索引当做是数据库中的表
数据库的表有 约束信息,用来定义表的结构、字段的名称、类型等信息
索引库中就有 映射(mapping),是索引中文档的字段约束信息,类似表的结构约束
MySQL | ES | 说明 |
---|---|---|
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 两者各自有自己的擅长支出:
Mysql:擅长事务类型操作,可以确保数据的安全和一致性
Elasticsearch:擅长海量数据的搜索、分析、计算
因此在企业中,往往是两者结合使用:
对安全性要求较高的写操作,使用mysql实现
对查询性能要求较高的搜索需求,使用elasticsearch实现
两者再基于某种方式,实现数据的同步,保证一致性
因为我们还需要部署kibana容器,因此需要让es和kibana容器互联。这里先创建一个网络:
docker network create es-net
docker pull elasticsearch:7.14.2
- docker run -d \
- --name es \
- -e "ES_JAVA_OPTS=-Xms1024m -Xmx1024m" \
- -e "discovery.type=single-node" \
- -v es-data:/usr/share/elasticsearch/data \
- -v es-plugins:/usr/share/elasticsearch/plugins \
- --privileged=true \
- --network es-net \
- -p 9200:9200 \
- -p 9300:9300 \
- elasticsearch:7.14.2
命令解释:
-d
:后台运作
-e "cluster.name=es-docker-cluster"
:设置集群名称
-e "http.host=0.0.0.0"
:监听的地址,可以外网访问
-e "ES_JAVA_OPTS=-Xms1024m -Xmx1024m"
:内存大小
-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://192.168.116.129:9200 即可看到elasticsearch的响应结果
kibana可以给我们提供一个elasticsearch的可视化界面,便于我们学习。
docker pull kibana:7.14.2
- docker run -d \
- --name kibana \
- -e ELASTICSEARCH_HOSTS=http://es:9200 \
- --network=es-net \
- -p 5601:5601 \
- kibana:7.14.2
--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://192.168.116.129:5601
kibana 中提供了一个DevTools界面:可以编写 DSL 来操作 elasticsearch ,并且对 DSL 语句有自动补全功能。
- # 进入容器内部
- docker exec -it es /bin/bash
-
- # 在线下载并安装
- ./bin/elasticsearch-plugin install https://github.com/medcl/elasticsearch-analysis-ik/releases/download/v7.14.2/elasticsearch-analysis-ik-7.14.2.zip
-
- # 退出
- exit
-
- # 重启容器
- docker restart es
IK分词器包含两种模式:
ik_smart
:最少切分
ik_max_word
:最细切分
- 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
- }
- ]
- }
随着互联网的发展,“造词运动”也越发的频繁。出现了很多新的词语,在原有的词汇列表中并不存在。比如:“奥力给”,“传智播客” 等。所以我们的词汇也需要不断的更新,IK分词器提供了扩展词汇的功能。
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)重启elasticsearch
- docker restart es
- # 查看日志:日志中已经成功加载 ext.dic 配置文件
- docker logs -f es
在互联网项目中,在网络间传输的速度很快,所以很多语言是不允许在网络上传递的,如:关于宗教、政治等敏感词语,那么我们在搜索时也应该忽略当前词汇。
IK分词器也提供了强大的停用词功能,让我们在索引时就直接忽略当前的停用词汇表中的内容。
1)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>
- <!--用户可以在这里配置自己的扩展停止词字典 *** 添加停用词词典-->
- <entry key="ext_stopwords">stopword.dic</entry>
- </properties>
3)在 stopword.dic 添加停用词
尔尔
4)重启elasticsearch
- # 重启服务
- docker restart es
- docker restart kibana
-
- # 查看 日志
- docker logs -f es
日志中已经成功加载stopword.dic配置文件
5)测试效果:
- GET /_analyze
- {
- "analyzer": "ik_max_word",
- "text": "传智播客Java就业率超过95%,尔尔都点赞,奥力给!"
- }
注意当前文件的编码必须是 UTF-8 格式,严禁使用Windows记事本编辑
部署 ES 集群可以直接使用docker-compose来完成,不过要求你的Linux虚拟机至少有4G的内存空间
首先编写一个docker-compose文件,内容如下:
- version: '2.2'
- services:
- es01:
- image: docker.elastic.co/elasticsearch/elasticsearch:7.14.2
- 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.14.2
- 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.14.2
- 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
Run docker-compose
to bring up the cluster:
docker-compose up
索引库就类似数据库表,mapping映射就类似表的结构,我们要向es中存储数据,必须先创建“库”和“表”。
mapping是对索引库中文档的约束,常见的mapping属性包括:
type:字段数据类型,常见的简单类型有:
字符串:text(可分词的文本)、keyword(精确值,例如:品牌、国家、ip地址)
数值:long、integer、short、byte、double、float
布尔:boolean
日期:date
对象:object
index:是否创建索引,默认为true
analyzer:使用哪种分词器
properties:该字段的子字段
- {
- "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;参与搜索,因此需要index为true;无需分词器
weight:类型为float;参与搜索,因此需要index为true;无需分词器
isMarried:类型为boolean;参与搜索,因此需要index为true;无需分词器
info:类型为字符串,需要分词,因此是text;参与搜索,因此需要index为true;分词器可以用ik_smart
email:类型为字符串,但是不需要分词,因此是keyword;不参与搜索,因此需要index为false;无需分词器
score:虽然是数组,但是我们只看元素的类型,类型为float;参与搜索,因此需要index为true;无需分词器
name:类型为object,需要定义多个子属性
name.firstName;类型为字符串,但是不需要分词,因此是keyword;参与搜索,因此需要index为true;无需分词器
name.lastName;类型为字符串,但是不需要分词,因此是keyword;参与搜索,因此需要index为true;无需分词器
语法
请求方式:PUT
请求路径:/索引库名,可以自定义
请求参数:mapping映射
- PUT /索引库名称
- {
- "mappings": {
- "properties": {
- "字段名":{
- "type": "text",
- "analyzer": "ik_smart"
- },
- "字段名2":{
- "type": "keyword",
- "index": "false"
- },
- "字段名3":{
- "properties": {
- "子字段": {
- "type": "keyword"
- }
- }
- },
- // ...略
- }
- }
- }
示例
- # 创建索引库-名称 heima
- PUT /heima
- {
- # mapping映射
- "mappings": {
- # 具体字段
- "properties": {
- # info 字段
- "info": {
- # 可分词的文本
- "type": "text",
- # 分词器
- "analyzer": "ik_smart"
- },
- # email 字段
- "email": {
- # 精确值
- "type": "keyword",
- # 无需创建索引
- "index": false
- },
- # name 字段
- "name": {
- "type": "object",
- # 子属性
- "properties": {
- "firstName": {
- "type": "keyword"
- },
- "lastName": {
- "type": "keyword"
- }
- }
- }
- }
- }
- }
语法
请求方式:GET
请求路径:/索引库名
请求参数:无
GET /索引库名
示例
倒排索引结构虽然不复杂,但是一旦数据结构改变(比如改变了分词器),就需要重新创建倒排索引,这简直是灾难。因此索引库一旦创建,无法修改mapping。
虽然无法修改mapping中已有的字段,但是却允许添加新的字段到mapping中,因为不会对倒排索引产生影响。
语法
- PUT /索引库名/_mapping
- {
- "properties": {
- "新字段名":{
- "type": "integer"
- }
- }
- }
示例
添加字段后:
语法
请求方式:DELETE
请求路径:/索引库名
请求参数:无
DELETE /索引库名
示例
语法
- 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": "赵"
- }
- }
根据rest风格,新增是post,查询应该是get,不过查询一般都需要条件,这里我们把文档id带上。
语法
GET /{索引库名称}/_doc/{id}
示例
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": "zhaoyun@itcast.cn",
- "name": {
- "firstName": "云",
- "lastName": "赵"
- }
- }
增量修改是只修改指定id匹配的文档中的部分字段。
语法
- POST /{索引库名}/_update/文档id
- {
- "doc": {
- "字段名": "新的值",
- }
- }
示例
- POST /heima/_update/1
- {
- "doc": {
- "email": "ZhaoYun@itcast.cn"
- }
- }
Elasticsearch提供了基于JSON的DSL(Domain Specific Language)来定义查询。常见的查询类型包括:
查询所有:查询出所有数据,一般测试用。
match_all
全文检索(full text)查询:利用分词器对用户输入内容分词,然后去倒排索引库中匹配。
match_query
multi_match_query
精确查询:根据精确词条值查找数据,一般是查找keyword、数值、日期、boolean等类型字段。
ids
range
term
地理(geo)查询:根据经纬度查询。
geo_distance
geo_bounding_box
复合(compound)查询:复合查询可以将上述各种查询条件组合起来,合并查询条件。
bool
function_score
- GET /indexName/_search
- {
- "query": {
- "match_all": {
- }
- }
- }
搜索字段越多,对查询性能影响越大,因此建议采用copy_to,然后单字段查询的方式
- // 单字段检索
- GET /indexName/_search
- {
- "query": {
- "match": {
- // 检索字段:检索内容
- "FIELD": "TEXT"
- }
- }
- }
- // 多字段查询
- GET /indexName/_search
- {
- "query": {
- "multi_match": {
- // 检索内容
- "query": "TEXT",
- // 检索字段
- "fields": ["FIELD1", " FIELD12"]
- }
- }
- }
- // term查询:根据词条精确查询
- GET /indexName/_search
- {
- "query": {
- "term": {
- // 查询字段
- "FIELD": {
- "value": "VALUE"
- }
- }
- }
- }
- // range查询:根据值的范围查询
- GET /indexName/_search
- {
- "query": {
- "range": {
- // 查询字段
- "FIELD": {
- "gte": 10, // 这里的gte代表大于等于,gt则代表大于
- "lte": 20 // lte代表小于等于,lt则代表小于
- }
- }
- }
- }
- // 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
- }
- }
- }
- }
- }
- // geo_distance 查询
- GET /indexName/_search
- {
- "query": {
- "geo_distance": {
- "distance": "15km", // 半径
- "FIELD": "31.21,121.5" // 圆心
- }
- }
- }
复合(compound)查询:复合查询可以将其它简单查询组合起来,实现更复杂的搜索逻辑。常见的有两种:
fuction score:算分函数查询,可以控制文档相关性算分,控制文档排名
bool query:布尔查询,利用逻辑关系组合多个其它的查询,实现复杂搜索
在elasticsearch中,早期使用的打分算法是TF-IDF算法,在后来的5.1版本升级中,改进为BM25算法:
TF-IDF算法有一各缺陷,就是词条频率越高,文档得分也会越高,单个词条对文档影响较大。而BM25则会让单个词条的算分有一个上限,曲线更加平滑:
示例 :给“如家”这个品牌的酒店排名靠前一些:
原始条件:不确定,可以任意变化
过滤条件:brand = "如家"
算分函数:可以简单粗暴,直接给固定的算分结果,weight
运算模式:比如求和
- GET /hotel/_search
- {
- "query": {
- "function_score": {
- // 原始查询,可以是任意条件
- "query": {....},
- // 算分函数
- "functions": [
- {
- // 过滤条件
- "filter": {
- "term": {
- // 满足的条件,品牌必须是如家
- "brand": "如家"
- }
- },
- "weight": 2 // 算分权重为2
- }
- ],
- "boost_mode": "sum" // 加权模式,求和
- }
- }
- }
原始函数:
算分函数查询:
布尔查询是一个或多个查询子句的组合,每一个子句就是一个子查询。子查询的组合方式有:
must:必须匹配每个子查询,类似“与”
should:选择性匹配子查询,类似“或”
must_not:必须不匹配,不参与算分,类似“非”
filter:必须匹配,不参与算分
elasticsearch默认是根据相关度算分(_score)来排序,但是也支持自定义方式对搜索 结果排序 。可以排序字段类型有:keyword类型、数值类型、地理坐标类型、日期类型等。
- GET /indexName/_search
- {
- "query": {
- "match_all": {}
- },
- "sort": [
- {
- // 排序字段、排序方式ASC、DESC
- "FIELD": "desc"
- }
- ]
- }
- GET /indexName/_search
- {
- "query": {
- "match_all": {}
- },
- "sort": [
- {
- "_geo_distance" : {
- // 文档中geo_point类型的字段名、目标坐标点
- "FIELD" : "纬度,经度",
- // 排序方式
- "order" : "asc",
- // 排序的距离单位
- "unit" : "km"
- }
- }
- ]
- }
- GET /hotel/_search
- {
- "query": {
- "match_all": {}
- },
- "from": 0, // 分页开始的位置,默认为0
- "size": 10, // 期望获取的文档总数
- "sort": [
- {"price": "asc"}
- ]
- }
针对深度分页,ES提供了两种解决方案,官方文档:
search after:分页时需要排序,原理是从上一次的排序值开始,查询下一页数据。官方推荐使用的方式。
scroll:原理将排序后的文档id形成快照,保存在内存。官方已经不推荐使用。
分页查询的常见实现方案以及优缺点:
from + size
:
优点:支持随机翻页
缺点:深度分页问题,默认查询上限(from + size)是10000
场景:百度、京东、谷歌、淘宝这样的随机翻页搜索
after search
:
优点:没有查询上限(单次查询的size不超过10000)
缺点:只能向后逐页查询,不支持随机翻页
场景:没有随机翻页需求的搜索,例如手机向下滚动翻页
scroll
:
优点:没有查询上限(单次查询的size不超过10000)
缺点:会有额外内存消耗,并且搜索结果是非实时的
场景:海量数据的获取和迁移。从ES7.1开始不推荐,建议用 after search方案。
- GET /hotel/_search
- {
- "query": {
- "match": {
- // 查询条件,高亮一定要使用全文检索查询
- "FIELD": "TEXT"
- }
- },
- "highlight": {
- "fields": {
- // 指定要高亮的字段
- "FIELD": {
- "pre_tags": "<em>", // 用来标记高亮字段的前置标签
- "post_tags": "</em>" // 用来标记高亮字段的后置标签
- "require_field_match": "false" // 查询字段 与 指定字段不匹配
- }
- }
- }
- }
- GET /hotel/_search
- {
- "query": {
- "match": { "name": "如家" }
- },
- "from": 0, // 分页开始的位置
- "size": 20, // 期望获取的文档总数
- "sort": [
- { "price": "asc" }, // 普通排序
- {
- "_geo_distance" : { // 距离排序
- "location" : "31.040699,121.618075",
- "order" : "asc",
- "unit" : "km"
- }
- }
- ],
- "highlight": {
- "fields": { // 高亮字段
- "name": {
- "pre_tags": "<em>", // 用来标记高亮字段的前置标签
- "post_tags": "</em>" // 用来标记高亮字段的后置标签
- }
- }
- }
- }
- CREATE TABLE `tb_hotel` (
- `id` bigint(20) NOT NULL COMMENT '酒店id',
- `name` varchar(255) NOT NULL COMMENT '酒店名称;例:7天酒店',
- `address` varchar(255) NOT NULL COMMENT '酒店地址;例:航头路',
- `price` int(10) NOT NULL COMMENT '酒店价格;例:329',
- `score` int(2) NOT NULL COMMENT '酒店评分;例:45,就是4.5分',
- `brand` varchar(32) NOT NULL COMMENT '酒店品牌;例:如家',
- `city` varchar(32) NOT NULL COMMENT '所在城市;例:上海',
- `star_name` varchar(16) DEFAULT NULL COMMENT '酒店星级,从低到高分别是:1星到5星,1钻到5钻',
- `business` varchar(255) DEFAULT NULL COMMENT '商圈;例:虹桥',
- `latitude` varchar(32) NOT NULL COMMENT '纬度;例:31.2497',
- `longitude` varchar(32) NOT NULL COMMENT '经度;例:120.3925',
- `pic` varchar(255) DEFAULT NULL COMMENT '酒店图片;例:/img/1.jpg',
- PRIMARY KEY (`id`)
- ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
字段名、字段数据类型,可以参考数据表结构的名称和类型
是否参与搜索要分析业务来判断,例如图片地址,就无需参与搜索
是否分词呢要看内容,内容如果是一个整体就无需分词,反之则要分词
分词器,我们可以统一使用ik_max_word
ES中支持两种地理坐标数据类型:
geo_point:由纬度(latitude)和经度(longitude)确定的一个点。例如:"32.8752345, 120.2981576"
geo_shape:有多个geo_point组成的复杂几何图形。例如一条直线,"LINESTRING (-77.03653 38.897676, -77.009051 38.889939)"
字段拷贝可以使用copy_to属性将当前字段拷贝到指定字段。示例:
- "all": {
- "type": "text",
- "analyzer": "ik_max_word"
- },
- "brand": {
- "type": "keyword",
- "copy_to": "all"
- }
- PUT /hotel
- {
- "mappings": {
- "properties": {
- "id": {
- "type": "keyword"
- },
- "name":{
- "type": "text",
- "analyzer": "ik_max_word",
- "copy_to": "all"
- },
- "address":{
- "type": "keyword",
- "index": false
- },
- "price":{
- "type": "integer"
- },
- "score":{
- "type": "integer"
- },
- "brand":{
- "type": "keyword",
- "copy_to": "all"
- },
- "city":{
- "type": "keyword",
- "copy_to": "all"
- },
- "starName":{
- "type": "keyword"
- },
- "business":{
- "type": "keyword"
- },
- "location":{
- "type": "geo_point"
- },
- "pic":{
- "type": "keyword",
- "index": false
- },
- "all":{
- "type": "text",
- "analyzer": "ik_max_word"
- }
- }
- }
- }
- <dependency>
- <groupId>org.elasticsearch.client</groupId>
- <artifactId>elasticsearch-rest-client</artifactId>
- <version>7.14.2</version>
- </dependency>
- <dependency>
- <groupId>org.elasticsearch</groupId>
- <artifactId>elasticsearch</artifactId>
- <version>7.14.2</version>
- </dependency>
- <dependency>
- <groupId>org.elasticsearch.client</groupId>
- <artifactId>elasticsearch-rest-high-level-client</artifactId>
- <version>7.14.2</version>
- </dependency>
- 这里为了单元测试方便,我们创建一个测试类HotelIndexTest,然后将初始化的代码编写在@BeforeEach方法中:
- @SpringBootTest
- public class _01初始化RestHighLevelClient {
- private RestHighLevelClient client;
-
- @BeforeEach
- void setUp() {
- this.client = new RestHighLevelClient(RestClient.builder(
- // https 会报错
- HttpHost.create("http://192.168.116.129:9200")
- ));
- }
-
- @AfterEach
- void tearDown() throws IOException {
- this.client.close();
- }
- }
- @SpringBootTest
- public class _02创建索引库 {
- private RestHighLevelClient client;
- @Test
- void createHotelIndex() throws IOException {
- // 1.创建Request对象
- CreateIndexRequest request = new CreateIndexRequest("hotel");
- // 2.准备请求的参数:DSL语句
- request.source(MAPPING_TEMPLATE, XContentType.JSON);
- // 3.发送请求
- client.indices().create(request, RequestOptions.DEFAULT);
- }
- }
-
- public class HotelConstants {
- public 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" +
- " \"copy_to\": \"all\"\n" +
- " },\n" +
- " \"address\":{\n" +
- " \"type\": \"keyword\",\n" +
- " \"index\": false\n" +
- " },\n" +
- " \"price\":{\n" +
- " \"type\": \"integer\"\n" +
- " },\n" +
- " \"score\":{\n" +
- " \"type\": \"integer\"\n" +
- " },\n" +
- " \"brand\":{\n" +
- " \"type\": \"keyword\",\n" +
- " \"copy_to\": \"all\"\n" +
- " },\n" +
- " \"city\":{\n" +
- " \"type\": \"keyword\",\n" +
- " \"copy_to\": \"all\"\n" +
- " },\n" +
- " \"starName\":{\n" +
- " \"type\": \"keyword\"\n" +
- " },\n" +
- " \"business\":{\n" +
- " \"type\": \"keyword\"\n" +
- " },\n" +
- " \"location\":{\n" +
- " \"type\": \"geo_point\"\n" +
- " },\n" +
- " \"pic\":{\n" +
- " \"type\": \"keyword\",\n" +
- " \"index\": false\n" +
- " },\n" +
- " \"all\":{\n" +
- " \"type\": \"text\",\n" +
- " \"analyzer\": \"ik_max_word\"\n" +
- " }\n" +
- " }\n" +
- " }\n" +
- "}";
- }
- @SpringBootTest
- public class _03删除索引库 {
- private RestHighLevelClient client;
-
- @Test
- void deleteHotelIndex() throws IOException {
- // 1.创建Request对象
- DeleteIndexRequest request = new DeleteIndexRequest("hotel");
- // 2.发送请求
- client.indices().delete(request, RequestOptions.DEFAULT);
- }
- }
- @SpringBootTest
- public class _04判断索引库是否存在 {
- private RestHighLevelClient client;
-
- @Test
- void isExistHotelIndex() throws IOException {
- // 1.创建Request对象
- GetIndexRequest request = new GetIndexRequest("hotel");
- // 2.发送请求
- boolean exists = client.indices().exists(request, RequestOptions.DEFAULT);
- // 3.输出
- System.err.println(exists ? "索引库已经存在!" : "索引库不存在!");
- }
- }
- @SpringBootTest
- public class _05新增文档 {
- @Autowired
- private IHotelService hotelService;
-
- private RestHighLevelClient client;
-
- @Test
- void createDocument() throws IOException {
- // 1.根据id查询酒店数据
- Hotel hotel = hotelService.getById(61083L);
- // 2.转换为文档类型
- HotelDoc hotelDoc = new HotelDoc(hotel);
- // 3.将HotelDoc转json
- String json = JSON.toJSONString(hotelDoc);
-
- // 4.准备Request对象
- IndexRequest request = new IndexRequest("hotel").id(hotelDoc.getId().toString());
- // 5.准备Json文档
- request.source(json, XContentType.JSON);
- // 6.发送请求
- client.index(request, RequestOptions.DEFAULT);
- }
- }
- @SpringBootTest
- public class _06根据id查询文档 {
- @Autowired
- private IHotelService hotelService;
-
- private RestHighLevelClient client;
-
- @Test
- void getDocumentById() throws IOException {
- // 1.准备Request
- GetRequest request = new GetRequest("hotel", "61083");
- // 2.发送请求,得到响应
- GetResponse response = client.get(request, RequestOptions.DEFAULT);
- // 3.解析响应结果
- String json = response.getSourceAsString();
-
- HotelDoc hotelDoc = JSON.parseObject(json, HotelDoc.class);
- System.out.println(hotelDoc);
- }
- }
在RestClient的API中,全量修改与新增的API完全一致,判断依据是ID:
如果新增时,ID已经存在,则修改
如果新增时,ID不存在,则新增
- @SpringBootTest
- public class _07根据id修改文档 {
- private RestHighLevelClient client;
-
- @Test
- void updateDocumentById() throws IOException {
- // 1.准备Request
- UpdateRequest request = new UpdateRequest("hotel", "61083");
- // 2.准备请求参数
- request.doc(
- "price", "952",
- "starName", "四钻"
- );
- // 3.发送请求
- client.update(request, RequestOptions.DEFAULT);
- }
- }
- @SpringBootTest
- public class _08根据id删除文档 {
- private RestHighLevelClient client;
-
- @Test
- void deleteDocumentById() throws IOException {
- // 1.准备Request
- DeleteRequest request = new DeleteRequest("hotel", "61083");
- // 2.发送请求
- client.delete(request, RequestOptions.DEFAULT);
- }
- }
- @SpringBootTest
- public class _09批量导入文档 {
- @Autowired
- private IHotelService hotelService;
- private RestHighLevelClient client;
-
- @Test
- void BatchImportById() throws IOException {
- // 1.批量查询酒店数据
- List<Hotel> hotels = hotelService.list();
- // 2.创建Request
- BulkRequest request = new BulkRequest();
- // 3.准备参数,添加多个新增的Request
- for (Hotel hotel : hotels) {
- // 3.1 转换为文档类型HotelDoc
- HotelDoc hotelDoc = new HotelDoc(hotel);
- // 3.2 创建新增文档的Request对象
- IndexRequest tempHotel = new IndexRequest("hotel")
- .id(hotelDoc.getId().toString());
- // 3.3 获取json文档
- tempHotel.source(JSON.toJSONString(hotelDoc), XContentType.JSON);
- request.add(tempHotel);
- }
- // 4.发送请求
- client.bulk(request, RequestOptions.DEFAULT);
- }
- }
- @SpringBootTest
- public class _10查询所有 {
- private RestHighLevelClient client;
-
- @Test
- void matchAll() throws IOException {
- // 1.准备Request
- SearchRequest request = new SearchRequest("hotel");
- // 2.组织DSL参数
- request.source()
- .query(QueryBuilders.matchAllQuery());
- // 3.发送请求,得到响应结果
- SearchResponse response = client.search(request, RequestOptions.DEFAULT);
- handleResponse(response);
- }
- private void handleResponse(SearchResponse response) {
- // 4.解析结果
- SearchHits searchHits = response.getHits();
- // 4.1.查询的总条数
- long total = searchHits.getTotalHits().value;
- System.out.println("共搜索" + total + "条数据。");
- // 4.2.查询的结果数组
- SearchHit[] hits = searchHits.getHits();
- for (SearchHit hit : hits) {
- // 4.3.获取文档 source
- String json = hit.getSourceAsString();
- // 4.4 反序列化
- HotelDoc hotelDoc = JSON.parseObject(json, HotelDoc.class);
- // 4.5.打印
- System.out.println(hotelDoc);
- }
- }
- }
- @SpringBootTest
- public class _11全文检索查询 {
- private RestHighLevelClient client;
-
- @Test
- void match() throws IOException {
- // 1.准备Request
- SearchRequest request = new SearchRequest("hotel");
- // 2.组织DSL参数
- request.source()
- .query(QueryBuilders.matchQuery("all", "如家"));
- // 3.发送请求,得到响应结果
- SearchResponse response = client.search(request, RequestOptions.DEFAULT);
- handleResponse(response);
- }
- }
- @SpringBootTest
- public class _12精确查询 {
-
- private RestHighLevelClient client;
-
- @Test
- void term() throws IOException {
- // 1.准备Request
- SearchRequest request = new SearchRequest("hotel");
- // 2.组织DSL参数
- request.source()
- .query(QueryBuilders.termQuery("city", "上海"));
- // 3.发送请求,得到响应结果
- SearchResponse response = client.search(request, RequestOptions.DEFAULT);
- // 4.解析响应
- handleResponse(response);
- }
- @Test
- void range() throws IOException {
- // 1.准备Request
- SearchRequest request = new SearchRequest("hotel");
- // 2.组织DSL参数
- request.source()
- .query(QueryBuilders.rangeQuery("price").gte(100).lte(500));
- // 3.发送请求,得到响应结果
- SearchResponse response = client.search(request, RequestOptions.DEFAULT);
- // 4.解析响应
- handleResponse(response);
- }
- }
- @SpringBootTest
- public class _13复合查询 {
-
- private RestHighLevelClient client;
-
- @Test
- void booleanQuery() throws IOException {
- // 1.准备Request
- SearchRequest request = new SearchRequest("hotel");
- // 2.组织DSL参数
- request.source()
- .query(QueryBuilders
- // 2.1 准备BooleanQuery
- .boolQuery()
- // 2.2 添加term
- .must(QueryBuilders.termQuery("city", "上海"))
- // 2.3 添加range
- .filter(QueryBuilders.rangeQuery("price").lte(250))
- );
- // 3.发送请求
- SearchResponse response = client.search(request, RequestOptions.DEFAULT);
- // 4.解析响应
- handleResponse(response);
- }
- }
- @SpringBootTest
- public class _14排序和分页 {
- private RestHighLevelClient client;
-
- @Test
- void pageAndSort() throws IOException {
- // 页码,每页大小
- int page = 1, size = 5;
- // 1.准备Request
- SearchRequest request = new SearchRequest("hotel");
- // 2.准备DSL
- request.source()
- // 2.1 查询
- .query(QueryBuilders.matchAllQuery())
- // 2.2 排序 sort
- .sort("price", SortOrder.ASC)
- // 2.3 分页 from、size
- .from((page - 1) * size).size(5);
- // 3.发送请求
- SearchResponse response = client.search(request, RequestOptions.DEFAULT);
- // 4.解析响应
- handleResponse(response);
- }
- }
- @SpringBootTest
- public class _15高亮 {
-
- private RestHighLevelClient client;
-
- @Test
- void highlighter() throws IOException {
- // 1.准备Request
- SearchRequest request = new SearchRequest("hotel");
- // 2.准备DSL
- request.source()
- // 2.1 查询
- .query(QueryBuilders.matchQuery("all", "如家"))
- // 2.2 高亮
- .highlighter(new HighlightBuilder()
- // 高亮内容
- .field("name")
- // 不匹配查询字段
- .requireFieldMatch(false));
- // 3.发送请求
- SearchResponse response = client.search(request, RequestOptions.DEFAULT);
- // 4.解析响应
- handleResponse(response);
- }
-
- private void handleResponse(SearchResponse response) {
- // 4.解析响应
- SearchHits searchHits = response.getHits();
- // 4.1.获取总条数
- long total = searchHits.getTotalHits().value;
- System.out.println("共搜索到" + total + "条数据");
- // 4.2.文档数组
- SearchHit[] hits = searchHits.getHits();
- // 4.3.遍历
- for (SearchHit hit : hits) {
- // 获取文档source
- String json = hit.getSourceAsString();
- // 反序列化
- HotelDoc hotelDoc = JSON.parseObject(json, HotelDoc.class);
- // 获取高亮结果
- Map<String, HighlightField> highlightFields = hit.getHighlightFields();
- if (!CollectionUtils.isEmpty(highlightFields)) {
- // 根据字段名获取高亮结果
- HighlightField highlightField = highlightFields.get("name");
- if (highlightField != null) {
- // 获取高亮值
- String name = highlightField.getFragments()[0].string();
- // 覆盖非高亮结果
- hotelDoc.setName(name);
- }
- }
- System.out.println("hotelDoc = " + hotelDoc);
- }
- }
- }
聚合常见的有三类:
桶(Bucket)聚合:用来对文档做分组
TermAggregation:按照文档字段值分组,例如按照品牌值分组、按照国家分组
Date Histogram:按照日期阶梯分组,例如一周为一组,或者一月为一组
度量(Metric)聚合:用以计算一些值,比如:最大值、最小值、平均值等
Avg:求平均值
Max:求最大值
Min:求最小值
Stats:同时求max、min、avg、sum等
管道(pipeline)聚合:其它聚合的结果为基础做聚合
注意:参加聚合的字段必须是 keyword、日期、数值、布尔类型;一定不能是 text(可分词的文本)
aggs代表聚合,与query同级,此时query的作用:
限定聚合的的文档范围
聚合必须的三要素:
聚合名称
聚合类型
聚合字段
聚合可配置属性有:
size:指定聚合结果数量
order:指定聚合结果排序方式
field:指定聚合字段
Bucket聚合:统计所有数据中的酒店品牌有几种,其实就是按照品牌对数据分组,根据酒店品牌的名称做聚合。
- GET /hotel/_search
- {
- "size": 0, // 设置size为0,结果中不包含文档,只包含聚合结果
- "aggs": { // 定义聚合
- "brandAgg": { //给聚合起个名字
- "terms": { // 聚合的类型,按照品牌值聚合,所以选择term
- "field": "brand", // 参与聚合的字段
- "size": 20 // 希望获取的聚合结果数量
- }
- }
- }
- }
我们可以指定order属性,自定义聚合的排序方式:
- GET /hotel/_search
- {
- "size": 0,
- "aggs": {
- "brandAgg": {
- "terms": {
- "field": "brand",
- "order": {
- "_count": "asc" // 按照_count升序排列
- },
- "size": 20
- }
- }
- }
- }
默认情况下,Bucket聚合是对索引库的所有文档做聚合,但真实场景下,用户会输入搜索条件,因此聚合必须是对搜索结果聚合。那么聚合必须添加限定条件。
我们可以限定要聚合的文档范围,只要添加query条件即可:
- GET /hotel/_search
- {
- "query": {
- "range": {
- "price": {
- "lte": 200 // 只对200元以下的文档聚合
- }
- }
- },
- "size": 0,
- "aggs": {
- "brandAgg": {
- "terms": {
- "field": "brand",
- "size": 20
- }
- }
- }
- }
我们需要对桶内的酒店做运算,获取每个品牌的用户评分的min、max、avg等值。
这就要用到Metric聚合了,例如stat聚合:就可以获取min、max、avg等结果。
语法如下:
- GET /hotel/_search
- {
- "size": 0,
- "aggs": {
- "brandAgg": {
- "terms": {
- "field": "brand",
- "size": 20
- },
- "aggs": { // 是brands聚合的子聚合,也就是分组后对每组分别计算
- "score_stats": { // 聚合名称
- "stats": { // 聚合类型,这里stats可以计算min、max、avg等
- "field": "score" // 聚合字段,这里是score
- }
- }
- }
- }
- }
- }
语法如下:
- GET /hotel/_search
- {
- "size": 0,
- "aggs": {
- "brandAgg": {
- "terms": {
- "field": "brand",
- "size": 20,
- "order": {
- "score_stats.avg": "desc" // 根据平均分排序
- }
- },
- "aggs": { // 是brands聚合的子聚合,也就是分组后对每组分别计算
- "score_stats": { // 聚合名称
- "stats": { // 聚合类型,这里stats可以计算min、max、avg等
- "field": "score" // 聚合字段,这里是score
- }
- }
- }
- }
- }
- }
- public class _01聚合查询 {
-
- private RestHighLevelClient client;
-
- @Test
- void aggregation() throws IOException {
- // 1.准备Request
- SearchRequest request = new SearchRequest("hotel");
- // 2.准备DSL
- String aggregationName = "brandAgg";
- request.source()
- // 2.1 设置size(0:清除文档数据)
- .size(0)
- // 2.2 聚合查询
- .aggregation(AggregationBuilders
- // 聚合名称
- .terms(aggregationName)
- // 查询字段
- .field("brand")
- // 查询数量
- .size(10)
- );
- // 3.发送请求
- SearchResponse response = client.search(request, RequestOptions.DEFAULT);
- // 4.解析响应
- handleResponse(response, aggregationName);
- }
-
- private void handleResponse(SearchResponse response, String aggregationName) {
- // 4.解析响应
- Aggregations aggregations = response.getAggregations();
- // 4.1.根据聚合名称获取聚合结果
- Terms aggregateTerms = aggregations.get(aggregationName);
- // 4.2.获取buckets
- List<? extends Terms.Bucket> buckets = aggregateTerms.getBuckets();
- // 4.3.遍历
- for (Terms.Bucket bucket : buckets) {
- // 获取key
- String key = bucket.getKeyAsString();
- System.out.println(key);
- }
- }
-
- @BeforeEach
- void setUp() {
- this.client = new RestHighLevelClient(RestClient.builder(
- HttpHost.create("http://192.168.116.129:9200")
- ));
- }
-
- @AfterEach
- void tearDown() throws IOException {
- this.client.close();
- }
- }
要实现根据字母做补全,就必须对文档按照拼音分词。在GitHub上恰好有elasticsearch的拼音分词插件。地址:GitHub - medcl/elasticsearch-analysis-pinyin: This Pinyin Analysis plugin is used to do conversion between Chinese characters and Pinyin.
默认的拼音分词器会将每个汉字单独分为拼音,而我们希望的是每个词条形成一组拼音,需要对拼音分词器做个性化定制,形成自定义分词器。
elasticsearch中分词器(analyzer)的组成包含三部分:
character filters:在tokenizer之前对文本进行处理。例如删除字符、替换字符
tokenizer:将文本按照一定的规则切割成词条(term)。例如keyword,就是不分词;还有ik_smart
tokenizer filter:将tokenizer输出的词条做进一步处理。例如大小写转换、同义词处理、拼音处理等
文档分词时会依次由这三部分来处理文档:
声明自定义分词器的语法如下:
- PUT /test
- {
- "settings": {
- "analysis": {
- "analyzer": { // 自定义分词器
- "my_analyzer": { // 分词器名称
- "tokenizer": "ik_max_word",
- "filter": "py"
- }
- },
- "filter": { // 自定义tokenizer filter
- "py": { // 过滤器名称
- "type": "pinyin", // 过滤器类型,这里是pinyin
- "keep_full_pinyin": false,
- "keep_joined_full_pinyin": true,
- "keep_original": true,
- "limit_first_letter_length": 16,
- "remove_duplicated_term": true,
- "none_chinese_pinyin_tokenize": false
- }
- }
- }
- },
- "mappings": {
- "properties": {
- "name": {
- "type": "text",
- "analyzer": "my_analyzer",
- "search_analyzer": "ik_smart"
- }
- }
- }
- }
测试:
总结:
如何使用拼音分词器?
下载pinyin分词器
解压并放到elasticsearch的plugin目录
重启即可
如何自定义分词器?
创建索引库时,在settings中配置,可以包含三部分
character filter
tokenizer
filter
拼音分词器注意事项?
为了避免搜索到同音字,搜索时不要使用拼音分词器
elasticsearch提供了Completion Suggester查询来实现自动补全功能。这个查询会匹配以用户输入内容开头的词条并返回。为了提高补全查询的效率,对于文档中字段的类型有一些约束:
参与补全查询的字段必须是completion类型。
字段的内容一般是用来补全的多个词条形成的数组。
比如,一个这样的索引库:
- // 创建索引库
- PUT test
- {
- "mappings": {
- "properties": {
- "title":{
- "type": "completion"
- }
- }
- }
- }
然后插入下面的数据:
- // 示例数据
- POST test/_doc
- {
- "title": ["Sony", "WH-1000XM3"]
- }
- POST test/_doc
- {
- "title": ["SK-II", "PITERA"]
- }
- POST test/_doc
- {
- "title": ["Nintendo", "switch"]
- }
查询的DSL语句如下:
- // 自动补全查询
- GET /test/_search
- {
- "suggest": {
- "title_suggest": {
- "text": "s", // 关键字
- "completion": {
- "field": "title", // 补全查询的字段
- "skip_duplicates": true, // 跳过重复的
- "size": 10 // 获取前10条结果
- }
- }
- }
- }
现在,我们的hotel索引库还没有设置拼音分词器,需要修改索引库中的配置。但是我们知道索引库是无法修改的,只能删除然后重新创建。
另外,我们需要添加一个字段,用来做自动补全,将brand、suggestion、city等都放进去,作为自动补全的提示。
因此,总结一下,我们需要做的事情包括:
修改hotel索引库结构,设置自定义拼音分词器
修改索引库的name、all字段,使用自定义分词器
索引库添加一个新字段suggestion,类型为completion类型,使用自定义的分词器
给HotelDoc类添加suggestion字段,内容包含brand、business
重新导入数据到hotel库
- // 酒店数据索引库
- PUT /hotel
- {
- "settings": {
- "analysis": {
- "analyzer": {
- "text_anlyzer": {
- "tokenizer": "ik_max_word",
- "filter": "py"
- },
- "completion_analyzer": {
- "tokenizer": "keyword",
- "filter": "py"
- }
- },
- "filter": {
- "py": {
- "type": "pinyin",
- "keep_full_pinyin": false,
- "keep_joined_full_pinyin": true,
- "keep_original": true,
- "limit_first_letter_length": 16,
- "remove_duplicated_term": true,
- "none_chinese_pinyin_tokenize": false
- }
- }
- }
- },
- "mappings": {
- "properties": {
- "id":{
- "type": "keyword"
- },
- "name":{
- "type": "text",
- "analyzer": "text_anlyzer",
- "search_analyzer": "ik_smart",
- "copy_to": "all"
- },
- "address":{
- "type": "keyword",
- "index": false
- },
- "price":{
- "type": "integer"
- },
- "score":{
- "type": "integer"
- },
- "brand":{
- "type": "keyword",
- "copy_to": "all"
- },
- "city":{
- "type": "keyword"
- },
- "starName":{
- "type": "keyword"
- },
- "business":{
- "type": "keyword",
- "copy_to": "all"
- },
- "location":{
- "type": "geo_point"
- },
- "pic":{
- "type": "keyword",
- "index": false
- },
- "all":{
- "type": "text",
- "analyzer": "text_anlyzer",
- "search_analyzer": "ik_smart"
- },
- "suggestion":{
- "type": "completion",
- "analyzer": "completion_analyzer"
- }
- }
- }
- }
HotelDoc中要添加一个字段,用来做自动补全,内容可以是酒店品牌、城市、商圈等信息。按照自动补全字段的要求,最好是这些字段的数组。
因此我们在 HotelDoc中 添加一个 suggestion 字段,类型为List<String>
,然后将brand、city、business等信息放到里面。
代码如下:
- @Data
- @NoArgsConstructor
- public class HotelDoc {
- private Long id;
- private String name;
- private String address;
- private Integer price;
- private Integer score;
- private String brand;
- private String city;
- private String starName;
- private String business;
- private String location;
- private String pic;
- private Object distance;
- private Boolean isAD;
- private List<String> suggestion;
-
- public HotelDoc(Hotel hotel) {
- this.id = hotel.getId();
- this.name = hotel.getName();
- this.address = hotel.getAddress();
- this.price = hotel.getPrice();
- this.score = hotel.getScore();
- this.brand = hotel.getBrand();
- this.city = hotel.getCity();
- this.starName = hotel.getStarName();
- this.business = hotel.getBusiness();
- this.location = hotel.getLatitude() + ", " + hotel.getLongitude();
- this.pic = hotel.getPic();
- // 组装suggestion
- if(this.business.contains("/")){
- // business有多个值,需要切割
- String[] arr = this.business.split("/");
- // 添加元素
- this.suggestion = new ArrayList<>();
- this.suggestion.add(this.brand);
- Collections.addAll(this.suggestion, arr);
- }else {
- this.suggestion = Arrays.asList(this.brand, this.business);
- }
- }
- }
重新执行之前编写的导入数据功能,可以看到新的酒店数据中包含了suggestion:
之前我们学习了自动补全查询的DSL,而没有学习对应的JavaAPI,这里给出一个示例:
而自动补全的结果也比较特殊,解析的代码如下:
查看前端页面,可以发现当我们在输入框键入时,前端会发起ajax请求:
返回值是补全词条的集合,类型为List<String>
1)在 HotelController
中添加新接口,接收新的请求:
- @GetMapping("suggestion")
- public List<String> getSuggestions(@RequestParam("key") String prefix) {
- return hotelService.getSuggestions(prefix);
- }
2)在IhotelService
中添加方法:
List<String> getSuggestions(String prefix);
3)在 HotelService
中实现该方法:
- @Override
- public List<String> getSuggestions(String prefix) {
- try {
- // 1.准备Request
- SearchRequest request = new SearchRequest("hotel");
- // 2.准备DSL
- request.source().suggest(new SuggestBuilder().addSuggestion(
- "suggestions",
- SuggestBuilders.completionSuggestion("suggestion")
- .prefix(prefix)
- .skipDuplicates(true)
- .size(10)
- ));
- // 3.发起请求
- SearchResponse response = client.search(request, RequestOptions.DEFAULT);
- // 4.解析结果
- Suggest suggest = response.getSuggest();
- // 4.1.根据补全查询名称,获取补全结果
- CompletionSuggestion suggestions = suggest.getSuggestion("suggestions");
- // 4.2.获取options
- List<CompletionSuggestion.Entry.Option> options = suggestions.getOptions();
- // 4.3.遍历
- List<String> list = new ArrayList<>(options.size());
- for (CompletionSuggestion.Entry.Option option : options) {
- String text = option.getText().toString();
- list.add(text);
- }
- return list;
- } catch (IOException e) {
- throw new RuntimeException(e);
- }
- }
elasticsearch中的酒店数据来自于mysql数据库,因此mysql数据发生改变时,elasticsearch也必须跟着改变,这个就是elasticsearch与mysql之间的数据同步。
常见的数据同步方案有三种:
同步调用
优点:实现简单,粗暴
缺点:业务耦合度高
异步通知
优点:低耦合,实现难度一般
缺点:依赖mq的可靠性
监听binlog
优点:完全解除服务间耦合
缺点:开启binlog增加数据库负担、实现复杂度高
导入课前资料提供的hotel-admin项目:
运行后,访问 http://localhost:8099
其中包含了酒店的CRUD功能:
MQ结构如图:
在hotel-admin、hotel-demo中引入rabbitmq的依赖:
- <!--amqp-->
- <dependency>
- <groupId>org.springframework.boot</groupId>
- <artifactId>spring-boot-starter-amqp</artifactId>
- </dependency>
在hotel-admin和hotel-demo中的cn.itcast.hotel.constatnts
包下新建一个类MqConstants
:
- package cn.itcast.hotel.constatnts;
-
- public class MqConstants {
- /**
- * 交换机
- */
- public final static String HOTEL_EXCHANGE = "hotel.topic";
- /**
- * 监听新增和修改的队列
- */
- public final static String HOTEL_INSERT_QUEUE = "hotel.insert.queue";
- /**
- * 监听删除的队列
- */
- public final static String HOTEL_DELETE_QUEUE = "hotel.delete.queue";
- /**
- * 新增或修改的RoutingKey
- */
- public final static String HOTEL_INSERT_KEY = "hotel.insert";
- /**
- * 删除的RoutingKey
- */
- public final static String HOTEL_DELETE_KEY = "hotel.delete";
- }
在hotel-demo中,定义配置类,声明队列、交换机:
- @Configuration
- public class MqConfig {
- @Bean
- public TopicExchange topicExchange(){
- return new TopicExchange(MqConstants.HOTEL_EXCHANGE, true, false);
- }
-
- @Bean
- public Queue insertQueue(){
- return new Queue(MqConstants.HOTEL_INSERT_QUEUE, true);
- }
-
- @Bean
- public Queue deleteQueue(){
- return new Queue(MqConstants.HOTEL_DELETE_QUEUE, true);
- }
-
- @Bean
- public Binding insertQueueBinding(){
- return BindingBuilder.bind(insertQueue()).to(topicExchange()).with(MqConstants.HOTEL_INSERT_KEY);
- }
-
- @Bean
- public Binding deleteQueueBinding(){
- return BindingBuilder.bind(deleteQueue()).to(topicExchange()).with(MqConstants.HOTEL_DELETE_KEY);
- }
- }
在hotel-admin中的增、删、改业务中分别发送MQ消息:
hotel-demo接收到MQ消息要做的事情包括:
新增消息:根据传递的hotel的id查询hotel信息,然后新增一条数据到索引库
删除消息:根据传递的hotel的id删除索引库中的一条数据
1)首先在hotel-demo的cn.itcast.hotel.service
包下的IHotelService
中新增新增、删除业务
- void deleteById(Long id);
- void insertById(Long id);
2)给hotel-demo中的cn.itcast.hotel.service.impl
包下的HotelService中实现业务:
- @Override
- public void deleteById(Long id) {
- try {
- // 1.准备Request
- DeleteRequest request = new DeleteRequest("hotel", id.toString());
- // 2.发送请求
- client.delete(request, RequestOptions.DEFAULT);
- } catch (IOException e) {
- throw new RuntimeException(e);
- }
- }
-
- @Override
- public void insertById(Long id) {
- try {
- // 0.根据id查询酒店数据
- Hotel hotel = getById(id);
- // 转换为文档类型
- HotelDoc hotelDoc = new HotelDoc(hotel);
-
- // 1.准备Request对象
- IndexRequest request = new IndexRequest("hotel").id(hotel.getId().toString());
- // 2.准备Json文档
- request.source(JSON.toJSONString(hotelDoc), XContentType.JSON);
- // 3.发送请求
- client.index(request, RequestOptions.DEFAULT);
- } catch (IOException e) {
- throw new RuntimeException(e);
- }
- }
3)编写监听器
在hotel-demo中的cn.itcast.hotel.mq
包新增一个类:
- package cn.itcast.hotel.mq;
-
- import cn.itcast.hotel.constants.MqConstants;
- import cn.itcast.hotel.service.IHotelService;
- import org.springframework.amqp.rabbit.annotation.RabbitListener;
- import org.springframework.beans.factory.annotation.Autowired;
- import org.springframework.stereotype.Component;
-
- @Component
- public class HotelListener {
-
- @Autowired
- private IHotelService hotelService;
-
- /**
- * 监听酒店新增或修改的业务
- * @param id 酒店id
- */
- @RabbitListener(queues = MqConstants.HOTEL_INSERT_QUEUE)
- public void listenHotelInsertOrUpdate(Long id){
- hotelService.insertById(id);
- }
-
- /**
- * 监听酒店删除的业务
- * @param id 酒店id
- */
- @RabbitListener(queues = MqConstants.HOTEL_DELETE_QUEUE)
- public void listenHotelDelete(Long id){
- hotelService.deleteById(id);
- }
- }
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。