一、Elasticsearch介绍
Elasticsearch是一个全文检索服务器
1 全文检索
全文检索是一种非结构化数据的搜索方式。
结构化数据:指具有固定格式固定长度的数据,如数据库中的字段。
非结构化数据:指格式和长度不固定的数据,如电商网站的商品详情。
结构化数据一般存入数据库,使用sql
语句即可快速查询。但由于非结构化数据的数据量大且格式不固定,我们需要采用全文检索的方式进行搜索。全文检索通过建立倒排索引加快搜索效率。
2 倒排索引
- 索引:将数据中的一部分信息提取出来,重新组织成一定的数据结构,我们可以根据该结构进行快速搜索,这样的结构称之为索引。
索引即目录,例如字典会将字的拼音提取出来做成目录,通过目录即可快速找到字的位置。
索引分为正排索引和倒排索引。 - 正排索引(正向索引):将文档
id
建立为索引,通过id
快速可以快速查找数据。如数据库中的主键就会创建正排索引。
- 倒排索引(反向索引):非结构化数据中我们往往会根据关键词查询数据。此时我们将数据中的关键词建立为索引,指向文档的 id,这样的索引称为倒排索引。
创建倒排索引流程:
3 Elasticsearch的出现
多年前,一个刚结婚的名叫Shay的失业开发者,跟着妻子去了伦敦,他的妻子在那里学习厨师。
Shay使用全文检索工具——lucene
,给他的妻子做一个食谱搜索引擎。
但Lucene
的操作非常复杂,且Lucene是一个单机软件,不支持联网访问。因此 Shay基于Lucene开发了开源项目 Elasticsearch
。Elasticsearch本质是一个java语言开发的web项目,我们可以通过RESTful
风格的接口访问该项目内部的Lucene,从而让全文搜索变得简单。
从此以后,Elasticsearch 已经成为了 Github 上最活跃的项目之一, Elastic公司已经开始围绕Elasticsearch提供商业服务,并开发新的特性。并且Elasticsearch 将永远开源并对所有人可用。
4 Elasticsearch应用场景
- 2013年初,GitHub抛弃了Solr,采取Elasticsearch来做PB级的搜索。GitHub使用Elasticsearch搜 索20TB 的数据,包括13亿文件和1300亿行代码。
- 维基百科:以Elasticsearch为基础的核心搜索架构。
- 百度:百度目前广泛使用Elasticsearch作为文本数据分析,采集百度所有服务器上的各类指标数据
及用户自定义数据。目前覆盖百度内部20多个业务线(包括casio、云分析、网盟、预测、文库、
直达号、钱包、风控等),单集群最大100台机器,200个ES节点,每天导入30TB+数据 - 新浪使用ES分析处理32亿条实时日志。
- 阿里使用ES构建自己的日志采集和分析体系。
- 我们可以使用ES实现全站搜索,线上商城系统的搜索,分析日志等功能。
5 Elasticsearch对比Solr
Solr也是基于Lucene的一款全文搜索引擎,下面是他们的对比。
- Solr利用 Zookeeper 进行分布式管理,而 Elasticsearch自身带有分布式协调管理功能;
- Solr支持更多格式的数据,而Elasticsearch仅支持 json文件格式;
- Solr官方提供的功能更多,而Elasticsearch本身更注重于核心功能,高级功能多由第三方插件提供;
- Solr在传统的搜索应用中表现好于Elasticsearch,但在处理实时搜索应用时效率明显低于Elasticsearch。
目前Elasticsearch的市场占有率越来越高,Spring从2020年起也已经停止Spring Data Solr的维护,更多的公司使用Elasticsearch作为搜索引擎。
6 Elasticsearch数据结构
文档(Document):文档是可被查询的最小数据单元,一个 Document 就是一条数据。类似于关系型
数据库中的记录的概念。
类型(Type):具有一组共同字段的文档定义成一个类型,类似于关系型数据库中的数据表的概念。
索引(Index):索引是多种类型文档的集合,类似于关系型数据库中的库的概念。
域(Fied):文档由多个域组成,类似于关系型数据库中的字段的概念。
Elasticsearch跟关系型数据库中概念的对比:
注:ES7.X之后删除了type的概念,一个索引不会代表一个库,而是代表一张表。我们这里使用ES7.12,所以目前的ES中概念对比为:
二、Elasticsearch安装
1 安装ES服务
-
解压elasticsearch压缩文件
-
修改es服务器config目录下的yml文件,加入以下配置,用于连接ES服务:
http.cors.enabled: true
http.cors.allow-origin: "*"
-
启动
bin/elasticsearch.bat
-
访问
http://127.0.0.1:9200
2 安装kibana
ES需要一个图形化管理软件方便我们操作,此处我们安装kibana。
- 解压kibana压缩文件
- 启动
bin/kibana.bat
- 访问
http://127.0.0.1:5601
3 安装head
我们也可以使用head插件作为ES的图形化管理软件,head插件是使用Javascript语言开发的。在安装head插件前,需要先安装JS的运行环境nodejs,和JS项目构建工具Grunt。
1. 安装nodejs
- 运行nodejs安装包
- 查看版本:cmd控制台输入
node -v
2. 安装Grunt
- 配置镜像:在cmd控制台输入
npm config set registry https://registry.npm.taobao.org
- 下载安装:在cmd控制台输入
npm install -g grunt-cli
3. 安装head插件
- 解压
elasticsearch-head-master.zip
- 把
phantomjs-2.1.1-windows.zip
文件复制
C:\Users\Administrator\AppData\Local\Temp\phantomjs
中 - 在head插件解压路径下打开cmd控制台输入
npm install
- 运行head插件:在解压路径下打开cmd控制台输入
grunt server
4. 访问 http://127.0.0.1:9100
三、Elasticsearch常用操作
Elasticsearch是使用Restful
风格的http
请求访问的,请求参数和返回值都是Json
格式的,我们可以使用kibana
发送http
请求操作ES
。
1 索引操作
- 创建没有结构的索引
路径:ip地址:端口号/索引名
注:在kibana中所有的请求都会省略ip地址:端口号,之后的路径我们省略写ip地址:端口号请求方式:PUT - 为索引添加结构:
POST /索引名/_mapping
{
"properties":{
"域名1":{
"type":域的类型,
"store":是否存储,
"index":是否创建索引,
"analyzer":分词器
},
"域名2":{
...
}
}
}
- 创建有结构的索引
PUT /索引名
{
"mappings":{
"properties":{
"域名1":{
"type":域的类型,
"store":是否单独存储,
"index":是否创建索引,
"analyzer":分词器
},
"域名2":{
...
}
}
}
}
域的类型:
index
:该域是否创建索引。只有值设置为true,才能根据该域的关键词查询文档。
根据关键词查询文档:
GET /索引名/_search
{
"query":{
"term":{
搜索字段: 关键字
}
}
}
store
:是否单独存储。如果设置为true,则该域能够单独查询。
单独查询某个域:
GET /索引名/_search
{
"stored_fields": ["域名"]
}
- 删除索引
DELETE /索引名
2 文档操作
- 新增/修改文档
id值不写时自动生成文档 id,id 和已有 id重复时修改文档
POST /索引/_doc/[id值]
{
"field名":field值
}
- 根据id查询查询文档
GET /索引/_doc/id值
- 删除文档
DELETE /索引/_doc/id值
- 根据 id 批量查询文档
GET /索引/_mget
{
"docs":[
{"_id":id值},
{"_id":id值}
]
}
- 查询所有文档
GET /索引/_search
{
"query": {
"match_all": {}
}
}
- 修改文档部分字段
POST /索引/_doc/[id值]/_update
{
"doc":{
域名:值
}
}
注:
ElasticSearch执行删除操作时,ES先标记文档为 deleted 状态,而不是直接物理删除。当ES存储空间不足或工作空闲时,才会执行物理删除操作。
ElasticSearch执行修改操作时,ES 不会真的修改Document中的数据,而是标记ES中原有的文档为deleted状态,再创建一个新的文档来存储数据。
四、分词器
ES文档的数据拆分成一个个有完整含义的关键词,并将关键词与文档对应,这样就可以通过关键词查询文档。要想正确的分词,需要选择合适的分词器。
1 默认分词器
standard analyzer:Elasticsearch 默认分词器,根据空格和标点符号对英文进行分词,会进行单词的
大小写转换。
默认分词器是英文分词器,对中文的分词是一字一词。
- 查看分词效果
GET /_analyze {
"text":测试语句,
"analyzer":分词器
}
默认中文查不出来
查看分词效果
默认英文可以查出
2 IK分词器
1. 概念
IKAnalyzer是一个开源的,基于java语言开发的轻量级的中文分词工具包。提供了两种分词算法:
ik_smart
:最少切分ik_max_word
:最细粒度划分
2. 安装
- 解压
elasticsearch-analysis-ik
,将解压后的文件夹拷贝到 elasticsearch 的 plugins 目录下。
ik分词器的版本要和es版本保持一致。 - 重启es。
3. 词典
IK分词器根据词典进行分词,词典文件在IK分词器的 config 目录中。
-
main.dic:IK 中内置的词典。记录了 IK 统计的所有中文单词。
-
IKAnalyzer.cfg.xml :用于配置自定义词库。
ext_dict
:自定义扩展词库,是对 main.dic 文件的扩展。ext_stopwords
:自定义停用词。
ik的所有的 dic 词库文件,必须使用UTF-8 字符集。不建议使用记事本编辑,记事本使用的是GBK字符集。
4. 测试分词器效果
GET /_analyze
{
"text":测试语句,
"analyzer":ik_smart/ik_max_word
}
3 拼音分词器
1. 概念
拼音分词器可以将中文分成对应的全拼,全拼首字母等。
2. 安装
- 解压
elasticsearch-analysis-pinyin
,将解压后的文件夹拷贝到 elasticsearch 的 plugins 目录下。
注:拼音分词器的版本要和es版本保持一致。 - 重启es。
3. 测试分词效果
GET /_analyze
{
"text":测试语句,
"analyzer":pinyin
}
4 自定义分词器
真实开发中我们往往需要对一段内容既进行文字分词,又进行拼音分词,此时我们需要自定义ik+pinyin
分词器。
4.1 创建自定义分词器
- 在创建索引时自定义分词器
PUT /索引名
{
"settings" : {
"analysis" : {
"analyzer" : {
"ik_pinyin" : { //自定义分词器名
"tokenizer":"ik_max_word", // 基本分词器
"filter":"pinyin_filter" // 配置分词器过滤
}
},
"filter" : { // 分词器过滤时配置另一个分词器,相当于同时使用两个分词器
"pinyin_filter" : {
"type" : "pinyin", // 另一个分词器 // 拼音分词器的配置
"keep_separate_first_letter" : false, // 是否分词每个字的首字母
"keep_full_pinyin" : true, // 是否分词全拼
"keep_original" : true, // 是否保留原始输入
"remove_duplicated_term" : true // 是否删除重复项
}
}
}
},
"mappings":{
"properties":{
"域名1":{
"type":域的类型,
"store":是否单独存储,
"index":是否创建索引,
"analyzer":分词器
},
"域名2":{
...
}
}
}
}
4.2 测试自定义分词器
GET /索引/_analyze
{
"text": "你好百战程序员",
"analyzer": "ik_pinyin"
}
五、Elasticsearch搜索文档
请求路径:/索引/_search
请求方式:GET
{
"query":{
搜索方式:搜索参数
}
}
搜索前我们添加一些示例数据
{
"id":2,
"name":"美羊羊",
"info":"美羊羊是羊村最漂亮的人"
}
{
"id":3,
"name":"懒羊羊",
"info":"懒羊羊的成绩不是很好"
}
{
"id":4,
"name":"小灰灰",
"info":"小灰灰的年纪比较小"
}
{
"id":5,
"name":"沸羊羊",
"info":"沸羊羊喜欢美羊羊"
}
{
"id":6,
"name":"村长",
"info":"村长德高望重"
}
{
"id":7,
"name":"灰太狼",
"info":"灰太狼是小灰灰的父亲,每次都会说我一定会回来的"
}
1 搜索方式
- match_all:查询所有数据
搜索参数:
{}
- match:全文检索。将查询条件分词后再进行搜索。
搜索参数:
{
搜索字段:搜索条件
}
- match_phrase:短语检索。搜索条件不做任何分词解析,在搜索字段对应的倒排索引中精确匹配。
搜索参数:
{
搜索字段:搜索条件
}
- range:范围搜索。对数字类型的字段进行范围搜索
搜索参数:
{
搜索字段:{
"gte":最小值,
"lte":最大值
}
}gt/lt:大于/小于
gte/lte:大于等于/小于等于
- term/terms:单词/词组搜索。搜索条件不做任何分词解析,在搜索字段对应的倒排索引中精确匹配
term参数:
{
搜索字段: 搜索条件
}
terms参数:
{
搜索字段: [搜索条件1,搜索条件2]
}
补充:在搜索时关键词有可能会输入错误,ES搜索提供了自动纠错功能,即ES的模糊查询。使用match方式可以实现模糊查询。模糊查询对中文的支持效果一般,我们使用英文数据测试模糊查询。
请求体:
{
"query": {
"match": {
"域名": {
"query": 搜索条件,
"fuzziness": 最多错误字符数,不能超过2
}
}
}
}
2 复合搜索
路径: /索引/_search
请求方式:GET
请求体:
{
"query": {
"bool": {
// 必须满足的条件
"must": [
搜索方式:搜索参数,
搜索方式:搜索参数
],
// 多个条件有任意一个满足即可
"should": [
搜索方式:搜索参数,
搜索方式:搜索参数
],
// 必须不满足的条件
"must_not":[
搜索方式:搜索参数,
搜索方式:搜索参数
]
}
}
}
3 结果排序
ES中默认使用相关度分数实现排序,可以通过搜索语法定制化排序。
请求体:
{
"query": 搜索条件,
"sort": [
{
"字段1":{
"order":"asc"
}
},
{
"字段2":{
"order":"desc"
}
}
]
}
由于ES对text 类型字段数据会做分词处理,使用哪一个单词做排序都是不合理的,所以 ES中默认不允许对text 类型的字段做排序。如果需要使用字符串做结果排序,可以使用 keyword 类型的字段作为排序依据,因为 keyword 字段不做分词处理。
4 分页查询
请求体:
{
"query": 搜索条件,
"from": 起始下标,
"size": 查询记录数
}
5 高亮查询
在进行关键字搜索时,搜索出的内容中的关键字会显示不同的颜色,称之为高亮。如:
为什么在网页中关键字会显示不同的颜色,我们通过开发者工具查看网页源码:
我们可以在关键字左右加入标签字符串,数据传入前端即可完成高亮显示,ES可以对查询出的内容中关键字部分进行标签和样式的设置。
请求体:
{
"query":搜索条件,
"highlight":{
"fields": {
"高亮显示的字段名": {
// 返回高亮数据的最大长度
"fragment_size":100,
// 返回结果最多可以包含几段不连续的文字
"number_of_fragments":5
}
},
"pre_tags":["前缀"],
"post_tags":["后缀"]
}
}
6 SQL查询
在ES7之后,支持SQL语句查询文档:
GET /_sql?format=txt
{
"query": SQL语句
}
开源版本的ES并不支持通过Java操作SQL进行查询,如果需要操作 SQL查询,则需要氪金(购买白金版)
六、Elasticsearch集群
1 概念
在单台ES服务器上,随着一个索引内数据的增多,会产生存储、效率、安全等问题。
此时我们可以采用ES集群,将单个索引的数据分成几份,每份数据还拥有不同的副本,分别存储在不同的物理机器上,从而可以实现高可用、容错性等。
节点(node):一个节点是集群中的一台服务器,是集群的一部分。它存储数据,参与集群的索引和搜索功能。集群中有一个为主节点,主节点通过ES内部选举产生。
集群(cluster):一组节点组织在一起称为一个集群,它们共同持有整个的数据,并一起提供索引和搜索功能。
分片(shards):ES可以把完整的索引分成多个分片,分别存储在不同的节点上。
副本(replicas):ES可以为每个分片创建副本,提高查询效率,保证在分片数据丢失后的恢复。
注:分片的数量只能在索引创建时指定,索引创建后不能再更改分片数量,但可以改变副本的数量。
为保证节点发生故障后集群的正常运行,ES不会将某个分片和它的副本存在同一台节点上。
2 搭建集群
- 复制三个elasticsearch服务
注:复制时要删除data
目录。 - 修改每个es服务的
cong/elasticsearch.yml
文件
#集群名称,保证唯一
cluster.name: my_elasticsearch
#节点名称,必须不一样
node.name: node1 /node2/node3
#可以访问该节点的ip地址
network.host: 0.0.0.0
#该节点端口号
http.port: 9200 /9201/9202
#集群间通信端口号
transport.tcp.port: 9300 /9301/9302
#候选主节点的设备地址
discovery.seed_hosts: ["127.0.0.1:9300","127.0.0.1:9301","127.0.0.1:9302"]
#候选主节点的节点名
cluster.initial_master_nodes: ["node1", "node2", "node3"]
-
启动各个节点服务器
注:搭建集群时一定配置JAVA_HOME环境变量,ES7对应的JDK版本为11。 -
测试:访问
http://localhost:9202/_cat/nodes
查看是否集群搭建成功。
-
head访问集群:访问集群中的任意一个节点即可。
-
kibana访问集群:修改
kibana.yml
,添加如下配置
# 该集群的所有节点
elasticsearch.hosts:
["http://localhost:9200","http://localhost:9201","http://localhost:9202"]
3 测试集群状态
- 在集群中创建一个索引
PUT /product
{
"settings": {
"number_of_shards": 5, // 分片数
"number_of_replicas": 1 // 每个分片的副本数
},
"mappings": {
"properties": {
"id": {
"type": "integer",
"store": true,
"index": true
},
"productName": {
"type": "text",
"store": true,
"index": true
},
"productDesc": {
"type": "text",
"store": true,
"index": true
}
}
}
}
- 查看集群状态
# 查看集群健康状态
GET /_cat/health?v
# 查看索引状态
GET /_cat/indices?v
# 查看分片状态
GET /_cat/shards?v
4 故障应对&水平扩容
-
关闭一个节点,可以发现ES集群可以自动进行故障应对。
-
重新打开该节点,可以发现ES集群可以自动进行水平扩容。
-
分片数不能改变,但是可以改变每个分片的副本数:
PUT /索引/_settings
{
"number_of_replicas": 副本数
}
5 Linux搭建Elasticsearch
接下载我们在CentOS8系统环境下搭建ES集群:
5.1 准备工作
- 准备一台搭载有CentOS8系统的虚拟机,使用XShell连接虚拟机
- 关闭防火墙,方便kibana连接集群:
#关闭防火墙:
systemctl stop firewalld.service
#禁止防火墙自启动:
systemctl disable firewalld.service
- 配置最大可创建文件数大小
#打开系统文件:
vim /etc/sysctl.conf
#添加以下配置:
vm.max_map_count=655360
#配置生效:
sysctl -p
- 由于ES不能以root用户运行,我们需要创建一个非root用户,此处创建一个名为es的用户:
#创建用户:
useradd es
5.2 搭建ES集群
- 使用rz命令将linux版的ES上传至虚拟机
- 解压第一个ES节点:
#解压:
tar -zxvf elasticsearch-7.12.1-linux-x86_64.tar.gz
#重命名:
mv elasticsearch-7.12.1 elasticsearch1
#移动文件夹:
mv elasticsearch1 /usr/local/
#es用户取得该文件夹权限:
chown -R es:es /usr/local/elasticsearch1
- 解压第二个ES节点:
#解压:
tar -zxvf elasticsearch-7.12.1-linux-x86_64.tar.gz
#重命名:
mv elasticsearch-7.12.1 elasticsearch2
#移动文件夹:
mv elasticsearch2 /usr/local/
#es用户取得该文件夹权限:
chown -R es:es /usr/local/elasticsearch2
- 修改两个ES节点的elasticsearch.yml文件:
#进入节点一配置:
vim /usr/local/elasticsearch1/config/elasticsearch.yml
#集群名称,保证唯一
cluster.name: my_elasticsearch
#节点名称,必须不一样
node.name: node1
#可以访问该节点的ip地址
network.host: 0.0.0.0
#该节点服务端口号
http.port: 9200
#集群间通信端口号
transport.tcp.port: 9300
#候选主节点的设备地址
discovery.seed_hosts: ["127.0.0.1:9300","127.0.0.1:9301"]
#候选主节点的节点名
cluster.initial_master_nodes: ["node1", "node2"]
#进入节点二配置:
vim /usr/local/elasticsearch2/config/elasticsearch.yml
#集群名称,保证唯一
cluster.name: my_elasticsearch
#节点名称,必须不一样
node.name: node2
#可以访问该节点的ip地址
network.host: 0.0.0.0
#该节点服务端口号
http.port: 9201
#集群间通信端口号
transport.tcp.port: 9301
#候选主节点的设备地址
discovery.seed_hosts: ["127.0.0.1:9300","127.0.0.1:9301"]
#候选主节点的节点名
cluster.initial_master_nodes: ["node1", "node2"]
- 启动两个ES节点:
#切换为es用户:
su es
#进入第一个节点:
cd /usr/local/elasticsearch1/bin/
#后台启动第一个节点:
./elasticsearch -d
#进入第二个节点:
cd /usr/local/elasticsearch2/bin/
#后台启动第二个节点:
./elasticsearch -d
5.3 连接ES集群
- 测试集群:
http://虚拟机IP:9200/_cat/nodes
- kibana链接集群:修改kibana.yml,添加如下配置
elasticsearch.hosts: ["http://虚拟机IP:9200","http://虚拟机IP:9201"]
- 启动kibana
七、原生JAVA操作ES
原生JAVA可以对ES的索引和文档进行操作,但操作较复杂,我们了解即可。
1 搭建项目
- 创建maven项目
- maven项目引入以下依赖:
<dependencies>
<dependency>
<groupId>org.elasticsearch</groupId>
<artifactId>elasticsearch</artifactId>
<version>7.12.1</version>
</dependency>
<dependency>
<groupId>org.elasticsearch.client</groupId>
<artifactId>elasticsearch-rest-high-level-client</artifactId>
<version>7.12.1</version>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.12</version>
</dependency>
</dependencies>
2 索引操作
- 创建空索引
// 创建空索引
@Test
public void createIndex() throws IOException {
// 创建客户端对象,链接ES
RestHighLevelClient client = new RestHighLevelClient(
RestClient.builder(new HttpHost("192.168.1.58",9200,"http")));
// 创建请求对象
CreateIndexRequest request = new CreateIndexRequest("student");
request.settings(Settings.builder()
.put("index.number_of_shards",5)
.put("index.number_of_replicas",1)
);
// 发送请求
CreateIndexResponse response = client.indices().create(request, RequestOptions.DEFAULT);
// 输出返回结果
System.out.println(response.index());
// 关闭客户端
client.close();
}
2. 给索引添加结构
@Test
public void mappingIndex() throws IOException {
// 创建客户端对象,链接ES
RestHighLevelClient client = new RestHighLevelClient(
RestClient.builder(new HttpHost("192.168.1.58",9200,"http")));
// 创建请求对象
PutMappingRequest request = new PutMappingRequest("student");
request.source("{\"properties\": {" +
" \"id\": {" +
" \"type\": \"integer\"," +
" \"store\": true," +
" \"index\": true" +
" }," +
" \"name\": {" +
" \"type\": \"text\"," +
" \"store\": true," +
" \"index\": true" +
" }," +
" \"info\": {" +
" \"type\": \"text\"," +
" \"store\": true," +
" \"index\": true" +
" }" +
" }}", XContentType.JSON);
// 发送请求
AcknowledgedResponse response = client.indices().putMapping(request, RequestOptions.DEFAULT);
// 输出返回结果
System.out.println(response.isAcknowledged());
// 关闭客户端
client.close();
}
3. 删除索引
@Test
public void deleteIndex() throws IOException {
// 创建客户端对象,链接ES
RestHighLevelClient client = new RestHighLevelClient(
RestClient.builder(new HttpHost("192.168.1.58",9200,"http")));
// 创建请求对象
DeleteIndexRequest request = new DeleteIndexRequest("student");
// 发送请求
AcknowledgedResponse response = client.indices().delete(request, RequestOptions.DEFAULT);
// 输出返回结果
System.out.println(response.isAcknowledged());
// 关闭客户端
client.close();
}
3 文档操作
- 新增&修改文档
// 新增/修改文档
@Test
public void addDocument() throws IOException {
// 创建客户端对象,链接ES
RestHighLevelClient client = new RestHighLevelClient(
RestClient.builder(new HttpHost("192.168.1.58",9200,"http")));
// 创建请求对象
IndexRequest request = new IndexRequest("student").id("2");
request.source(XContentFactory.jsonBuilder()
.startObject()
.field("id",2)
.field("name","cat1")
.field("info","cat1 is a good girl")
.endObject());
// 发送请求
IndexResponse response = client.index(request, RequestOptions.DEFAULT);
// 输出返回结果
System.out.println(response.status());
// 关闭客户端
client.close();
}
- 根据id查询文档
// 根据id查询文档
@Test
public void findById() throws IOException {
// 创建客户端对象,链接ES
RestHighLevelClient client = new RestHighLevelClient(
RestClient.builder(new HttpHost("192.168.1.58",9200,"http")));
// 创建请求对象
GetRequest request = new GetRequest("student", "2");
// 发送请求
GetResponse response = client.get(request, RequestOptions.DEFAULT);
// 输出返回结果
System.out.println(response.getSourceAsString());
// 关闭客户端
client.close();
}
- 删除文档
// 删除文档
@Test
public void deleteDocument() throws IOException {
// 创建客户端对象,链接ES
RestHighLevelClient client = new RestHighLevelClient(
RestClient.builder(new HttpHost("192.168.1.58",9200,"http")));
// 创建请求对象
DeleteRequest request = new DeleteRequest("student", "2");
// 发送请求
DeleteResponse response = client.delete(request, RequestOptions.DEFAULT);
// 输出返回结果
System.out.println(response.status());
// 关闭客户端
client.close();
}
4 查询操作
- 查询所有文档
// 查询所有文档
@Test
public void queryAllDocument() throws IOException {
// 创建客户端对象,链接ES
RestHighLevelClient client = new RestHighLevelClient(
RestClient.builder(new HttpHost("192.168.1.58",9200,"http")));
// 创建查询条件的请求体
SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder();
searchSourceBuilder.query(QueryBuilders.matchAllQuery());
// 创建请求对象
SearchRequest request = new SearchRequest("student").source(searchSourceBuilder);
// 发送请求
SearchResponse response = client.search(request, RequestOptions.DEFAULT);
// 输出返回结果
for (SearchHit hit:response.getHits()) {
System.out.println(hit.getSourceAsString());
}
// 关闭客户端
client.close();
}
- 根据关键词查询文档
// 根据关键词查询文档
@Test
public void queryTermDocument() throws IOException {
// 创建客户端对象,链接ES
RestHighLevelClient client = new RestHighLevelClient(
RestClient.builder(new HttpHost("192.168.1.58",9200,"http")));
// 创建查询条件的请求体
SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder();
searchSourceBuilder.query(QueryBuilders.termQuery("info","boy"));
// 创建请求对象
SearchRequest request = new SearchRequest("student").source(searchSourceBuilder);
// 发送请求
SearchResponse response = client.search(request, RequestOptions.DEFAULT);
// 输出返回结果
for (SearchHit hit:response.getHits()) {
System.out.println(hit.getSourceAsString());
}
// 关闭客户端
client.close();
}
八、Spring Data Elasticsearch
Spring Data ElasticSearch是Spring对原生JAVA操作Elasticsearch封装之后的产物。它通过对原生API的封装,使得JAVA程序员可以简单的对Elasticsearch进行操作。
1 快速入门
1.1 linux环境配置分词器
不要使用root用户进行以下操作
- 关闭es服务
#查看es进程号 ps -ef | grep elastic
#关闭es进程
kill -9 进程号
- 使用
rz
命令将 ik 和 pinyin 分词器的压缩文件上传至 es 安装路径下的 plugins 目录。 - 解压分词器
unzip elasticsearch-analysis-ik-7.12.1.zip -d analysis-ik-7.12.1
unzip elasticsearch-analysis-pinyin-7.12.1.zip -d analysis-pinyin-7.12.1
- 删除压缩文件
rm -rf elasticsearch-analysis-ik-7.12.1.zip
rm -rf elasticsearch-analysis-pinyin-7.12.1.zip
- 启动es服务
1.2 搭建项目
创建SpringBoot项目,加入Spring Data Elasticsearch起步依赖:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-elasticsearch</artifactId>
</dependency>
如果idea版本较低,还需补充以下依赖:
<dependency>
<groupId>org.junit.platform</groupId>
<artifactId>junit-platform-launcher</artifactId>
<scope>test</scope>
</dependency>
写配置文件:
spring:
elasticsearch:
rest:
uris: http://192.168.1.58:9200, http://192.168.1.58:9201
1.3 创建实体类
一个实体类的所有对象都会存入ES的一个索引中,所以我们在创建实体类时关联ES索引。如果ES中没有该索引则会自动建索引。
package com.baizhan.springdataes.product;
import org.springframework.data.annotation.Id;
import org.springframework.data.elasticsearch.annotations.Document;
import org.springframework.data.elasticsearch.annotations.Field;
import org.springframework.data.elasticsearch.annotations.FieldType;
@Document(indexName = "product",shards = 3,replicas = 1,createIndex = true)
public class Product {
@Id
@Field(type = FieldType.Integer,store = true,index = true)
private Integer id;
@Field(type = FieldType.Text,store = true,index = true,analyzer = "ik_smart",searchAnalyzer = "ik_smart")
private String productName;
@Field(type = FieldType.Text,store = true,index = true,analyzer = "ik_smart",searchAnalyzer = "ik_smart")
private String productDesc;
public Product() {
}
public Product(Integer id, String productName, String productDesc) {
this.id = id;
this.productName = productName;
this.productDesc = productDesc;
}
public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
public String getProductName() {
return productName;
}
public void setProductName(String productName) {
this.productName = productName;
}
public String getProductDesc() {
return productDesc;
}
public void setProductDesc(String productDesc) {
this.productDesc = productDesc;
}
@Override
public String toString() {
return "Product{" +
"id=" + id +
", productName='" + productName + '\'' +
", productDesc='" + productDesc + '\'' +
'}';
}
}
@Document:标记在类上,标记实体类为文档对象,一般有如下属性:
indexName:对应索引的名称
shards:分片数量
replicas:副本数量
createIndex:是否自动创建索引
@Id:标记在成员变量上,标记一个字段为主键,该字段的值会同步到ES该文档的id值。
@Field:标记在成员变量上,标记为文档中的域,一般有如下属性:
type:域的类型
index:是否索引,默认是 true
store:是否单独存储,默认是 false
analyzer:分词器
searchAnalyzer:搜索时的分词器
1.4 创建Repository接口
创建Repository接口继承ElasticsearchRepository,该接口提供了文档的增删改查方法
public interface ProductRepository extends ElasticsearchRepository<Product,Integer> {
@Query("{" +
" \"match\": {" +
" \"productDesc\": \"?0\"" +
" }" +
" }")
List<Product> findByProductDescMatch(String keyword);
@Query("{" +
" \"match\": {" +
" \"productDesc\": {" +
" \"query\": \"?0\"," +
" \"fuzziness\": 1" +
" }" +
" }" +
"}")
List<Product> findByProductDescFuzzy(String keyword);
List<Product> findByProductName(String productName);
List<Product> findByProductNameOrProductDesc(String productName,String productDesc);
List<Product> findByIdBetween(Integer startId,Integer endId);
Page<Product> findByProductDesc(String productDesc, Pageable pageable);
}
1.5 测试方法
编写测试类,注入Repository接口并测试Repository接口的增删改查方法
@SpringBootTest
class SpringdataesApplicationTests {
@Autowired
private ProductRepository repository;
@Test
public void addDocument() {
Product product = new Product(1, "iphone12", "iphone12是苹果最新款手机!");
repository.save(product);
}
@Test
public void addDocument1() {
// 添加一些数据
repository.save(new Product(2, "三体1", "三体1是优秀的科幻小说"));
repository.save(new Product(3, "三体2", "三体2是优秀的科幻小说"));
repository.save(new Product(4, "三体3", "三体3是优秀的科幻小说"));
repository.save(new Product(5, "elasticsearch", "elasticsearch是基于lucene开发的优秀的搜索引擎"));
}
@Test
public void updateDocument() {
Product product = new Product(1, "iphone13", "iphone13是苹果最新款手机!");
repository.save(product);
}
@Test
public void deleteDocument1() {
repository.deleteById(1);
}
@Test
public void findDocument1() {
Optional<Product> product = repository.findById(1);
System.out.println(product.get());
}
@Test
public void findDocument() {
Iterable<Product> all = repository.findAll();
for (Product product : all) {
System.out.println(product);
}
}
}
2 SpringDataES查询方式
@Test
public void addDocument1() {
// 添加一些数据
repository.save(new Product(2, "三体1", "三体1是优秀的科幻小说"));
repository.save(new Product(3, "三体2", "三体2是优秀的科幻小说"));
repository.save(new Product(4, "三体3", "三体3是优秀的科幻小说"));
repository.save(new Product(5, "elasticsearch", "elasticsearch是基于lucene开发的优秀的搜索引擎"));
}
2.1 使用Repository继承的方法查询文档
2.2 使用DSL语句查询文档
ES通过json类型的请求体查询文档,方法如下:
GET /索引/_search
{
"query":{
搜索方式:搜索参数
}
}
query后的 json对象称为DSL语句,我们可以在接口方法上使用@Query
注解自定义DSL语句查询。
@Query("{" +
" \"match\": {" +
" \"productDesc\": \"?0\"" +
" }" +
" }")
List<Product> findByProductDescMatch(String keyword);
@Query("{" +
" \"match\": {" +
" \"productDesc\": {" +
" \"query\": \"?0\"," +
" \"fuzziness\": 1" +
" }" +
" }" +
"}")
List<Product> findByProductDescFuzzy(String keyword);
2.3 按照规则命名方法进行查询
- 只需在Repository接口中按照SpringDataES的规则命名方法,该方法就能完成相应的查询。
- 规则:查询方法以
findBy
开头,涉及查询条件时,条件的属性用条件关键字连接。
List<Product> findByProductName(String productName);
List<Product> findByProductNameOrProductDesc(String productName,String productDesc);
List<Product> findByIdBetween(Integer startId,Integer endId);
测试
@Autowired
private ProductRepository repository;
@Test
public void t1() {
List<Product> list = repository.findByProductDescMatch("我喜欢用苹果手机写elasticsearch代码");
System.out.println(list);
}
@Test
public void t2() {
System.out.println(repository.findByProductDescFuzzy("elasticsearc"));
}
@Test
public void t3() {
System.out.println(repository.findByProductName("elasticsearch"));
}
@Test
public void t4() {
System.out.println(repository.findByProductNameOrProductDesc("elasticsearch","手机"));
}
@Test
public void t5() {
System.out.println(repository.findByIdBetween(1,3));
}
2.4 分页查询
使用继承或自定义的方法时,在方法中添加 Pageable类型的参数,返回值为Page类型即可进行分页查询。
@Test
public void t6() {
// 测试继承的方法:
// 参数1:页数 参数2:每页条数
Pageable pageable = PageRequest.of(0,3);
Page<Product> page = repository.findAll(pageable);
System.out.println(page.getContent());
System.out.println(page.getTotalElements());
System.out.println(page.getTotalPages());
}
@Test
public void t7() {
// 自定义方法
Pageable pageable = PageRequest.of(1,3);
// 测试自定义方法
Page<Product> page = repository.findByProductDesc("优秀",pageable);
System.out.println(page.getContent());
System.out.println(page.getTotalElements());
System.out.println(page.getTotalPages());
}
2.5 结果排序
使用继承或自定义的方法时,在方法中添加Sort类型的参数即可进行结果排序。
@Test
public void t8(){
// 结果排序
Sort sort = Sort.by(Sort.Direction.DESC,"id");
Iterable<Product> all = repository.findAll(sort);
for (Product product : all) {
System.out.println(product);
}
}
@Test
public void t9(){
// 测试分页加排序
Sort sort = Sort.by(Sort.Direction.DESC,"id");
Pageable pageable = PageRequest.of(0,3,sort);
Page<Product> page = repository.findByProductDesc("优秀", pageable);
System.out.println(page.getContent());
System.out.println(page.getTotalElements());
System.out.println(page.getTotalPages());
}
、
2.6 ElasticsearchRestTemplate
SpringDataElasticsearch提供了一个工具类ElasticsearchRestTemplate,我们注入该类对象也能对ES
进行操作。
2.6.1 操作索引
@Autowired
private ElasticsearchRestTemplate template;
@Test
public void t1(){
// 获得操作索引对象
IndexOperations indexOperations = template.indexOps(Student.class);
// 创建索引,注:该方法无法设置索引结构,框架遗留bug,不推荐使用。
indexOperations.create();
}
@Test
public void t2(){
IndexOperations indexOperations = template.indexOps(Student.class);
// 删除索引
indexOperations.delete();
}
2.6.2 增删改文档
template操作文档的常用方法:
- save():新增/修改文档
- delete():删除文档
@Test
public void testDocument() {
Product product = new Product(1004, "Elasticsearch是一个实时的分布式搜索和分析引 擎", "它底层封装了Lucene框架,可以提供分布式全文检索服 务。");
template.save(product);
template.delete("1004",Product.class);
}
2.6.3 查询文档
template 的 search方法可以查询文档:
SearchHits<T> search(Query query, Class<T> clazz):查询文档,query是查询条件对象, clazz是结果类型。
- 普通查询:
@Autowired
private ElasticsearchRestTemplate template;
@Test
public void t1(){
// 构建查询条件
// MatchAllQueryBuilder queryBuilder = QueryBuilders.matchAllQuery();
// TermQueryBuilder queryBuilder = QueryBuilders.termQuery("productDesc", "手机");
MatchQueryBuilder queryBuilder = QueryBuilders.matchQuery("productDesc", "我喜欢看科幻小说");
NativeSearchQuery query = new NativeSearchQueryBuilder().withQuery(queryBuilder).build();
// 查询
SearchHits<Product> search = template.search(query, Product.class);
// 打印查询结果
for (SearchHit<Product> productSearchHit : search) {
Product product = productSearchHit.getContent();
System.out.println(product);
}
}
- 复杂条件查询
@Test
public void t2(){
String productName = "elasticsearch";
String productDesc = "优秀";
// 构建查询条件
BoolQueryBuilder boolQueryBuilder = QueryBuilders.boolQuery();
if(productName == null && productDesc == null){
MatchAllQueryBuilder queryBuilder = QueryBuilders.matchAllQuery();
boolQueryBuilder.must(queryBuilder);
}else{
if(productName != null){
MatchQueryBuilder queryBuilder1 = QueryBuilders.matchQuery("productName", productName);
boolQueryBuilder.must(queryBuilder1);
}
if(productDesc != null){
MatchQueryBuilder queryBuilder1 = QueryBuilders.matchQuery("productDesc", productDesc);
boolQueryBuilder.must(queryBuilder1);
}
}
NativeSearchQuery query = new NativeSearchQueryBuilder().withQuery(boolQueryBuilder).build();
// 查询
SearchHits<Product> search = template.search(query, Product.class);
// 打印查询结果
for (SearchHit<Product> productSearchHit : search) {
Product product = productSearchHit.getContent();
System.out.println(product);
}
}
- 分页查询
@Test
public void t3(){
// 构建查询条件
MatchAllQueryBuilder queryBuilder = QueryBuilders.matchAllQuery();
// 分页条件
Pageable pageable = PageRequest.of(0,3);
NativeSearchQuery query = new NativeSearchQueryBuilder()
.withQuery(queryBuilder)
.withPageable(pageable)
.build();
// 查询
SearchHits<Product> search = template.search(query, Product.class);
// 封装为page对象
List<Product> content = new ArrayList();
for (SearchHit<Product> productSearchHit : search) {
content.add(productSearchHit.getContent());
}
Page<Product> page = new PageImpl(content,pageable,search.getTotalHits());
// 打印分页对象
System.out.println(page.getTotalElements());
System.out.println(page.getTotalPages());
System.out.println(page.getContent());
}
- 结果排序
@Test
public void t4(){
// 构建查询条件
MatchAllQueryBuilder queryBuilder = QueryBuilders.matchAllQuery();
// 分页条件
Pageable pageable = PageRequest.of(0,3);
// 排序条件
SortBuilder sortBuilder = SortBuilders.fieldSort("id").order(SortOrder.DESC);
NativeSearchQuery query = new NativeSearchQueryBuilder()
.withQuery(queryBuilder)
.withPageable(pageable)
.withSort(sortBuilder)
.build();
// 查询
SearchHits<Product> search = template.search(query, Product.class);
// 封装为page对象
List<Product> content = new ArrayList();
for (SearchHit<Product> productSearchHit : search) {
content.add(productSearchHit.getContent());
}
Page<Product> page = new PageImpl(content,pageable,search.getTotalHits());
// 打印分页对象
System.out.println(page.getTotalElements());
System.out.println(page.getTotalPages());
System.out.println(page.getContent());
}
九、Elasticsearch优化
ES的优化即通过调整参数使得读写性能更快
1 磁盘选择
磁盘通常是服务器的瓶颈。Elasticsearch重度使用磁盘,磁盘的效率越高,Elasticsearch的执行效率就越高。这里有一些优化磁盘的技巧:
- 使用SSD(固态硬盘),它比机械磁盘优秀多了。
- 使用RAID0模式(将连续的数据分散到多个硬盘存储,这样可以并行进行IO操作),代价是一块硬盘发生故障就会引发系统故障。
- 不要使用远程挂载的存储。
2 分片策略
分片和副本数并不是越多越好。每个分片的底层都是一个Lucene索引,会消耗一定的系统资源。且搜索请求需要命中索引中的所有分片,分片数过多会降低搜索性能。索引的分片数需要架构师和技术人员对业务的增长有预先的判断,一般来说我们遵循以下原则:
- 每个分片占用的硬盘容量不超过ES的最大JVM的堆空间设置(一般设置不超过32G)。比如:如果索引的总容量在500G左右,那分片数量在16个左右即可。
- 分片数一般不超过节点数的3倍。比如:如果集群内有10个节点,则分片数不超过30个。
- 推迟分片分配:节点中断后集群会重新分配分片。但默认集群会等待一分钟来查看节点是否重新加入。我们可以设置等待的时长,减少重新分配的次数:
PUT /索引/_settings
{
"settings":{
"index.unassianed.node_left.delayed_timeout":"5m"
}
}
- 减少副本数量:进行写入操作时,需要把写入的数据都同步到副本,副本越多写入的效率就越慢。我们进行大批量进行写入操作时可以先设置副本数为0,写入完成后再修改回正常的状态。
3 内存设置
ES默认占用内存是4GB,我们可以修改config/jvm.option
设置ES的堆内存大小,Xms表示堆内存的初始大小,Xmx表示可分配的最大内存。
- Xmx和Xms的大小设置为相同的,可以减轻伸缩堆大小带来的压力。
- Xmx和Xms不要超过物理内存的50%,因为ES内部的Lucene也要占据一部分物理内存。
- Xmx和Xms不要超过 32GB,由于Java语言的特性,堆内存超过32G会浪费大量系统资源,所以在内存足够的情况下,最终我们都会采用设置为31G:
-Xms 31g
-Xmx 31g
例如:在一台128GB内存的机器中,我们可以创建两个节点,每个节点分配31GB内存。
十、Elasticsearch案例
1 需求说明
接下来我们使用ES模仿百度搜索,即自动补全+搜索引擎效果:
2 自动补全
es为我们提供了关键词的自动补全功能:
GET /索引/_search
{
"suggest": {
"prefix_suggestion": {// 自定义推荐名
"prefix": "elastic",// 被补全的关键字
"completion": {
"field": "productName",// 查询的域
"skip_duplicates": true, // 忽略重复结果
"size": 10 //最多查询到的结果数
}
}
}
}
注:自动补全对性能要求极高,ES不是通过倒排索引来实现的,所以需要将对应的查询字段类型设置completion
。
PUT /product1
{
"mappings":{
"properties":{
"id":{
"type":"integer",
"store":true,
"index":true
},
"productName":{
"type":"completion"
},
"productDesc":{
"type":"text",
"store":true,
"index":true
}
}
}
}
POST /product1/_doc
{
"id":1,
"productName":"elasticsearch1",
"productDesc":"elasticsearch1 is a good search engine"
}
{
"id":2,
"productName":"elasticsearch2",
"productDesc":"elasticsearch2 is a good search engine"
}
{
"id":3,
"productName":"elasticsearch3",
"productDesc":"elasticsearch3 is a good search engine"
}
3 创建索引
PUT /news
{
"settings": {
"number_of_shards": 3,
"number_of_replicas": 1,
"analysis": {
"analyzer": {
"ik_pinyin": {
"tokenizer": "ik_smart",
"filter": "pinyin_filter"
},
"tag_pinyin": {
"tokenizer": "keyword",
"filter": "pinyin_filter"
}
},
"filter": {
"pinyin_filter": {
"type": "pinyin",
"keep_joined_full_pinyin": true,
"keep_original": true,
"remove_duplicated_term": true
}
}
}
},
"mappings": {
"properties": {
"id": {
"type": "integer",
"index": true
},
"title": {
"type": "text",
"index": true,
"analyzer": "ik_pinyin",
"search_analyzer": "ik_smart"
},
"content": {
"type": "text",
"index": true,
"analyzer": "ik_pinyin",
"search_analyzer": "ik_smart"
},
"url": {
"type": "keyword",
"index": true
},
"tags": {
"type": "completion",
"analyzer": "tag_pinyin",
"search_analyzer": "tag_pinyin"
}
}
}
}
4 将mysql表数据复制到索引中
使用logstash工具可以将mysql数据复制到es中
- 解压
logstash-7.12.1-windows-x86_64.zip
logstash要和elastisearch版本一致 - 在解压路径下的
/config
中创建 mysql.conf 文件,文件写入以下脚本内容:
input {
jdbc {
jdbc_driver_library => "E:\logstash-7.12.1\lib\mysql-connector-java- 5.1.37-bin.jar"
jdbc_driver_class => "com.mysql.jdbc.Driver"
jdbc_connection_string => "jdbc:mysql:///news"
jdbc_user => "root"
jdbc_password => "root"
schedule => "* * * * *"
jdbc_default_timezone => "Asia/Shanghai"
statement => "SELECT * FROM news;"
}
}
filter {
mutate {
split => {"tags" => ","}
}
}
output {
elasticsearch {
hosts => ["192.168.1.58:9200","192.168.1.58:9201"]
index => "news"
document_id => "%{id}"
}
}
- 在解压路径下打开cmd黑窗口,运行命令:
bin\logstash -f config\mysql.conf
- 测试自动补齐
GET /news/_search
{
"suggest": {
"my_suggest": {
"prefix": "li",
"completion": {
"field": "tags",
"skip_duplicates": true,
"size": 10
}
}
}
}
5 后端实现
5.1 项目搭建
创建springboot项目,加入SpringDataElasticsearch和SpringMVC的起步依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-elasticsearch</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
写配置文件:
spring:
elasticsearch:
rest:
uris: http://192.168.1.58:9200, http://192.168.1.58:9201
5.2 创建实体类和Repository接口
实体类:
@Document(indexName = "news")
public class News {
@Id
@Field
private Integer id;
@Field
private String title;
@Field
private String content;
@Field
private String url;
@CompletionField
@Transient
private Completion tags;
public News() {
}
public News(Integer id, String title, String content, String url, Completion tags) {
this.id = id;
this.title = title;
this.content = content;
this.url = url;
this.tags = tags;
}
public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
public String getTitle() {
return title;
}
public void setTitle(String title) {
this.title = title;
}
public String getContent() {
return content;
}
public void setContent(String content) {
this.content = content;
}
public String getUrl() {
return url;
}
public void setUrl(String url) {
this.url = url;
}
public Completion getTags() {
return tags;
}
public void setTags(Completion tags) {
this.tags = tags;
}
@Override
public String toString() {
return "News{" +
"id=" + id +
", title='" + title + '\'' +
", content='" + content + '\'' +
", url='" + url + '\'' +
", tags=" + tags +
'}';
}
}
Repository接口:
public interface NewsRepository extends ElasticsearchRepository<News, Integer> {
}
5.3 创建service类
创建service类,提供自动补齐和搜索关键字功能:
注入repository 和 template类:
@Service
public class NewsService {
@Autowired
private ElasticsearchRestTemplate template;
@Autowired
private NewsRepository repository;
}
自动补齐:
// 自动补齐
public List<String> autoSuggest(String keyword) {
// 创建请求
SuggestBuilder suggestBuilder = new SuggestBuilder();
// 请求体
SuggestionBuilder suggestionBuilder = SuggestBuilders
.completionSuggestion("tags")
.prefix(keyword)
.skipDuplicates(true)
.size(10);
suggestBuilder.addSuggestion("prefix_suggestion", suggestionBuilder);
// 发送请求
SearchResponse response = template.suggest(suggestBuilder, IndexCoordinates.of("news"));
// 处理结果
List<String> result = response
.getSuggest()
.getSuggestion("prefix_suggestion")
.getEntries()
.get(0)
.getOptions()
.stream()
.map(Suggest.Suggestion.Entry.Option::getText)
.map(Text::toString)
.collect(Collectors.toList());
return result;
}
搜索关键字(带有高亮显示):
在repository接口中添加方法:
public interface NewsRepository extends ElasticsearchRepository<News, Integer> {
@Highlight(fields = {@HighlightField(name = "title"), @HighlightField(name = "content")})
List<SearchHit<News>> findByTitleMatchesOrContentMatches(String title, String content);
}
service类中调用该方法:
public List<News> highLightSearch(String keyword) {
List<SearchHit<News>> result = repository.findByTitleMatchesOrContentMatches(keyword, keyword);
List<News> newsList = new ArrayList();
for (SearchHit<News> newsSearchHit : result) {
News news = newsSearchHit.getContent();
if (newsSearchHit.getHighlightFields().get("title") != null) {
news.setTitle(newsSearchHit.getHighlightFields().get("title").get(0));
}
if (newsSearchHit.getHighlightFields().get("content") != null) {
news.setContent(newsSearchHit.getHighlightFields().get("content").get(0));
}
newsList.add(news);
}
return newsList;
}
5.4 创建Controller类
@RestController
public class NewController {
@Autowired
private NewsService newsService;
@GetMapping("/autoSuggest")
public List<String> autoSuggest(String term) {
return newsService.autoSuggest(term);
}
@GetMapping("/highLightSearch")
public List<News> highLightSearch(String term) {
return newsService.highLightSearch(term);
}
}
6 前端页面
我们使用jqueryUI 中的 autocomplete插件完成项目的前端实现。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>结果</title>
<link rel="stylesheet" type="text/css" href="css/jquery-ui.min.css"/>
<link rel="stylesheet" type="text/css" href="css/bootstrap.min.css"/>
<script src="js/jquery-2.1.1.min.js"></script>
<script src="js/jquery-ui.min.js"></script>
<style>
body {
padding-left: 14px;
padding-top: 14px;
}
ul, li {
list-style: none;
padding: 0;
}
li {
padding-bottom: 16px;
}
a, a:link, a:visited, a:hover, a:active {
text-decoration: none;
}
em {
color: #ff0000;
font-style: normal;
}
</style>
</head>
<body>
<div>
<input id="newsTag" class="form-control" style="display: inline; width: 50%;" name="keyword">
<button class="btn btn-primary" onclick="search()">搜索一下</button>
</div>
<hr>
<div>
<ul id="news"></ul>
</div>
</body>
<script>
$("#newsTag").autocomplete({
source: "/autoSuggest", // 请求路径
delay: 100, //请求延迟
minLength: 1 //最少输入多少字符向服务器发送请求
})
function search() {
var term = $("#newsTag").val();
$.get("/highLightSearch", {term: term}, function (data) {
var str = "";
for (var i = 0; i < data.length; i++) {
var document = data[i];
str += "<li>" +
" <h4>" +
" <a href='" + document.url + "' target='_blank'>" + document.title + "</a>" +
" </h4> " +
" <p>" + document.content + "</p>" +
" </li>";
}
$("#news").html(str);
})
}
</script>
</html>