一、Elasticsearch介绍

Elasticsearch是一个全文检索服务器

1 全文检索

全文检索是一种非结构化数据的搜索方式
结构化数据:指具有固定格式固定长度的数据,如数据库中的字段。
在这里插入图片描述
非结构化数据:指格式和长度不固定的数据,如电商网站的商品详情。
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
结构化数据一般存入数据库,使用sql语句即可快速查询。但由于非结构化数据的数据量大且格式不固定,我们需要采用全文检索的方式进行搜索。全文检索通过建立倒排索引加快搜索效率。

2 倒排索引

  1. 索引:将数据中的一部分信息提取出来,重新组织成一定的数据结构,我们可以根据该结构进行快速搜索,这样的结构称之为索引。
    索引即目录,例如字典会将字的拼音提取出来做成目录,通过目录即可快速找到字的位置。
    在这里插入图片描述
    索引分为正排索引和倒排索引。
  2. 正排索引(正向索引):将文档id建立为索引,通过id快速可以快速查找数据。如数据库中的主键就会创建正排索引。
    在这里插入图片描述
  3. 倒排索引(反向索引):非结构化数据中我们往往会根据关键词查询数据。此时我们将数据中的关键词建立为索引,指向文档的 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服务

  1. 解压elasticsearch压缩文件
    在这里插入图片描述

  2. 修改es服务器config目录下的yml文件,加入以下配置,用于连接ES服务:
    在这里插入图片描述

http.cors.enabled: true 
http.cors.allow-origin: "*"

在这里插入图片描述

  1. 启动bin/elasticsearch.bat
    在这里插入图片描述

  2. 访问 http://127.0.0.1:9200
    5.

2 安装kibana

ES需要一个图形化管理软件方便我们操作,此处我们安装kibana。

  1. 解压kibana压缩文件
  2. 启动bin/kibana.bat
  3. 访问http://127.0.0.1:5601
    在这里插入图片描述

3 安装head

我们也可以使用head插件作为ES的图形化管理软件,head插件是使用Javascript语言开发的。在安装head插件前,需要先安装JS的运行环境nodejs,和JS项目构建工具Grunt。
1. 安装nodejs

  1. 运行nodejs安装包
  2. 查看版本:cmd控制台输入
node -v

2. 安装Grunt

  1. 配置镜像:在cmd控制台输入
npm config set registry https://registry.npm.taobao.org
  1. 下载安装:在cmd控制台输入
npm install -g grunt-cli

3. 安装head插件

  1. 解压elasticsearch-head-master.zip
  2. phantomjs-2.1.1-windows.zip文件复制
    C:\Users\Administrator\AppData\Local\Temp\phantomjs
  3. 在head插件解压路径下打开cmd控制台输入
npm install
  1. 运行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 搭建集群

  1. 复制三个elasticsearch服务
    注:复制时要删除data目录。
  2. 修改每个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"]
  1. 启动各个节点服务器
    注:搭建集群时一定配置JAVA_HOME环境变量,ES7对应的JDK版本为11。

  2. 测试:访问http://localhost:9202/_cat/nodes查看是否集群搭建成功。
    在这里插入图片描述

  3. head访问集群:访问集群中的任意一个节点即可。
    在这里插入图片描述

  4. kibana访问集群:修改kibana.yml,添加如下配置

# 该集群的所有节点 
elasticsearch.hosts: 
["http://localhost:9200","http://localhost:9201","http://localhost:9202"]

3 测试集群状态

  1. 在集群中创建一个索引
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 
			} 
		} 
	} 
}
  1. 查看集群状态
# 查看集群健康状态 
GET /_cat/health?v 
# 查看索引状态 
GET /_cat/indices?v 
# 查看分片状态 
GET /_cat/shards?v

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

4 故障应对&水平扩容

  1. 关闭一个节点,可以发现ES集群可以自动进行故障应对。
    在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述

  2. 重新打开该节点,可以发现ES集群可以自动进行水平扩容。
    在这里插入图片描述

  3. 分片数不能改变,但是可以改变每个分片的副本数:

PUT /索引/_settings 
{ 
	"number_of_replicas": 副本数 
}

在这里插入图片描述
在这里插入图片描述

5 Linux搭建Elasticsearch

接下载我们在CentOS8系统环境下搭建ES集群:

5.1 准备工作

  1. 准备一台搭载有CentOS8系统的虚拟机,使用XShell连接虚拟机
  2. 关闭防火墙,方便kibana连接集群:
#关闭防火墙: 
systemctl stop firewalld.service 
#禁止防火墙自启动: 
systemctl disable firewalld.service
  1. 配置最大可创建文件数大小
#打开系统文件: 
vim /etc/sysctl.conf 
#添加以下配置: 
vm.max_map_count=655360 
#配置生效: 
sysctl -p
  1. 由于ES不能以root用户运行,我们需要创建一个非root用户,此处创建一个名为es的用户:
#创建用户: 
useradd es

5.2 搭建ES集群

  1. 使用rz命令将linux版的ES上传至虚拟机
  2. 解压第一个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
  1. 解压第二个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
  1. 修改两个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"]
  1. 启动两个ES节点:
#切换为es用户: 
su es 
#进入第一个节点: 
cd /usr/local/elasticsearch1/bin/ 
#后台启动第一个节点: 
./elasticsearch -d 
#进入第二个节点: 
cd /usr/local/elasticsearch2/bin/ 
#后台启动第二个节点: 
./elasticsearch -d

5.3 连接ES集群

  1. 测试集群:
http://虚拟机IP:9200/_cat/nodes
  1. kibana链接集群:修改kibana.yml,添加如下配置
elasticsearch.hosts:  ["http://虚拟机IP:9200","http://虚拟机IP:9201"]
  1. 启动kibana

七、原生JAVA操作ES

原生JAVA可以对ES的索引和文档进行操作,但操作较复杂,我们了解即可。

1 搭建项目

  1. 创建maven项目
  2. 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 索引操作

  1. 创建空索引
// 创建空索引
    @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 文档操作

  1. 新增&修改文档
 // 新增/修改文档
    @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();
    }
  1. 根据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();
    }
  1. 删除文档
// 删除文档
    @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 查询操作

  1. 查询所有文档
// 查询所有文档
    @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();
    }
  1. 根据关键词查询文档
 // 根据关键词查询文档
    @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用户进行以下操作

  1. 关闭es服务
#查看es进程号 ps -ef | grep elastic 
#关闭es进程 
kill -9 进程号
  1. 使用 rz 命令将 ik 和 pinyin 分词器的压缩文件上传至 es 安装路径下的 plugins 目录。
  2. 解压分词器
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
  1. 删除压缩文件
rm -rf elasticsearch-analysis-ik-7.12.1.zip 
rm -rf elasticsearch-analysis-pinyin-7.12.1.zip
  1. 启动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是结果类型。
  1. 普通查询:
    @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);
        }
    }
  1. 复杂条件查询
    @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);
        }
    }
  1. 分页查询
    @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());
    }

  1. 结果排序
    @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中

  1. 解压logstash-7.12.1-windows-x86_64.zip
    logstash要和elastisearch版本一致
  2. 在解压路径下的/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}" 
	} 
}
  1. 在解压路径下打开cmd黑窗口,运行命令:
bin\logstash -f config\mysql.conf
  1. 测试自动补齐
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>

练习源码:https://gitee.com/cutelili/elastic-search
ElasticStack

Logo

为开发者提供学习成长、分享交流、生态实践、资源工具等服务,帮助开发者快速成长。

更多推荐