Elasticsearch(看这一篇就够了)
Elasticsearch是一个全文检索服务器全文检索是一种非结构化数据的搜索方式结构化数据:指具有固定格式固定长度的数据,如数据库中的字段。非结构化数据:指格式和长度不固定的数据,如电商网站的商品详情。结构化数据一般存入数据库,使用sql语句即可快速查询。但由于非结构化数据的数据量大且格式不固定,我们需要采用全文检索的方式进行搜索。全文检索通过建立倒排索引加快搜索效率。真实开发中我们往往需要对一
目录:
Elasticsearch
介绍
Elasticsearch是一个全文检索服务器
全文检索是一种非结构化数据的搜索方式
-
结构化数据:指具有固定格式固定长度的数据,如数据库中的字段。
-
非结构化数据:指格式和长度不固定的数据,如电商网站的商品详情。
结构化数据一般存入数据库,使用sql语句即可快速查询。但由于非结构化数据的数据量大且格式不固定,我们需要采用全文检索的方式进行搜索。全文检索通过建立倒排索引加快搜索效率。
正排索引和倒排索引
索引
将数据中的一部分信息提取出来,重新组织成一定的数据结构,我们可以根据该结构进行快速搜索,这样的结构称之为索引。
索引即目录,例如字典会将字的拼音提取出来做成目录,通过目录即可快速找到字的位置。
索引分为正排索引和倒排索引。
正排索引(正向索引)
将文档id建立为索引,通过id快速可以快速查找数据。如数据库中的主键就会创建正排索引。
倒排索引(反向索引)
非结构化数据中我们往往会根据关键词查询数据。此时我们将数据中的关键词建立为索引,指向文档数据,这样的索引称为倒排索引。
Elasticsearch安装
安装ES服务
准备工作
1.准备一台搭载有CentOS7系统的虚拟机,使用XShell连接虚拟机
2.关闭防火墙,方便访问ES
#关闭防火墙:
systemctl stop firewalld.service
#禁止防火墙自启动:
systemctl disable firewalld.service
3.配置最大可创建文件数大小
#打开系统文件:
vim /etc/sysctl.conf
#添加以下配置:
vm.max_map_count=655360
#配置生效:
sysctl -p
- 由于ES不能以root用户运行,我们需要创建一个非root用户,此处创建一个名为es的用户:
#创建用户:
useradd es
安装服务
-
使用rz命令将linux版的ES上传至虚拟机
-
解压ES
#解压:
tar -zxvf elasticsearch-8.10.4-linux-x86_64.tar.gz
#重命名:
mv elasticsearch-8.10.4 elasticsearch1
#移动文件夹:
mv elasticsearch1 /usr/local/
#es用户取得该文件夹权限:
chown -R es:es /usr/local/elasticsearch1
- 启动ES服务:
#切换为es用户:
su es
#进入ES安装文件夹:
cd /usr/local/elasticsearch1/bin/
#启动ES服务:
./elasticsearch
- 当启动成功,可以看到类似以下的日志输出。首次启动Elasticsearch,默认会启用安全配置功能,启用身份认证和授权,内置超级用户elastic,并生成默认密码,此时要注意保存,否则之后启动不会再显示。
# 重置默认密码:
cd /usr/local/elasticsearch1/bin/
./elasticsearch-reset-password -u elastic
# 自定义密码:
cd /usr/local/elasticsearch1/bin/
./elasticsearch-reset-password --username elastic -i
- 连接ES,查询ES服务是否启动成功
# 参数 --cacert指定了证书
curl --cacert /usr/local/elasticsearch1/config/certs/http_ca.crt -u elastic https://localhost:9200
安装kibana
Kibana是一款开源的数据分析和可视化平台,设计用于和Elasticsearch协作。我们可以使用Kibana对Elasticsearch索引中的数据进行搜索、查看、交互操作。
-
使用rz工具将Kibana压缩文件上传到Linux虚拟机
-
解压
tar -zxvf kibana-8.10.4-linux-x86_64.tar.gz -C /usr/local/
- 修改配置
# 进入Kibana解压路径
cd /usr/local/kibana-8.10.4/config
# 修改配置文件
vim kibana.yml
# 加入以下内容
# 主机IP,服务名
server.host: "虚拟机IP"
server.name: "kibana"
- 启动:
kibana不能以root用户运行,我们给es用户设置kibana目录的权限,并使用es用户运行kibana
# 给es用户设置kibana目录权限
chown -R es:es /usr/local/kibana-8.10.4/
# 切换为es用户
su es
# 启动kibana
cd /usr/local/kibana-8.10.4/bin/
./kibana
- 访问kibana:http://虚拟机IP:5601
首次访问Kibana管理台会提示输入ES生成的token秘钥,可以在ES首次启动日志中找。
如果token已失效或不正确,你也可以重新生成token。
# 进入es安装目录
cd /usr/local/elasticsearch1/bin
# 重新生成kibana的token
.elasticsearch-create-enrollment-token --scope kibana
紧接着输入登录账号 elastic,密码也同样是从ES首次启动日志中找。
- 点击
Management
=>Index Management
可以查看es索引信息。
索引操作
创建索引
Elasticsearch是使用RESTful风格的http请求访问操作的,请求参数和返回值都是Json格式的,我们可以使用kibana发送http请求操作ES。
创建没有结构的索引
路径:ip地址:端口号/索引名
注:在kibana中所有的请求都会省略ip地址:端口号,之后的路径我们省略写
ip地址:端口号
格式:
PUT /索引库名称
{
"mappings": {
"properties": {
"字段名":{
"type": "text",
"analyzer": "ik_smart"
},
"字段名2":{
"type": "keyword",
"index": "false"
},
"字段名3":{
"properties": {
"子字段": {
"type": "keyword"
}
}
},
// ...略
}
}
}
基本语法:
- 请求方式:PUT
- 请求路径:/索引库名,可以自定义
- 请求参数:mapping映射
格式:
为索引添加结构
POST /索引名/_mapping
{
"properties":{
"域名1":{
"type":域的类型,
"store":是否存储,
"index":是否创建索引,
"analyzer":分词器
},
"域名2":{
...
}
}
}
PUT /索引库名称
{
"mappings": {
"properties": {
"字段名":{
"type": "text",
"analyzer": "ik_smart"
},
"字段名2":{
"type": "keyword",
"index": "false"
},
"字段名3":{
"properties": {
"子字段": {
"type": "keyword"
}
}
},
// ...略
}
}
}
查询索引库
基本语法:
- 请求方式:GET
- 请求路径:/索引库名
- 请求参数:无
格式:
GET /索引库名
修改索引库
倒排索引结构虽然不复杂,但是一旦数据结构改变(比如改变了分词器),就需要重新创建倒排索引,这简直是灾难。因此索引库一旦创建,无法修改mapping
。
虽然无法修改mapping中已有的字段,但是却允许添加新的字段到mapping中,因为不会对倒排索引产生影响。
语法说明:
PUT /索引库名/_mapping
{
"properties": {
"新字段名":{
"type": "integer"
}
}
}
删除索引库
语法:
-
请求方式:DELETE
-
请求路径:/索引库名
-
请求参数:无
格式:
DELETE /索引库名
Elasticsearch常用操作
文档操作
新增文档
POST /索引/_doc/[id值]
{
"field名":field值
}
POST /索引库名/_doc/文档id
{
"字段1": "值1",
"字段2": "值2",
"字段3": {
"子属性1": "值3",
"子属性2": "值4"
},
// ...
}
示例:
POST /jjy/_doc/1
{
"info": "jjy最牛啦",
"email": "zy@itcast.cn",
"name": {
"firstName": "云",
"lastName": "赵"
}
}
注:id值不写时自动生成文档id,id和已有id重复时修改文档
查询文档
GET /索引/_doc/id值
示例:
GET /jjy/_doc/1
删除文档
DELETE /索引/_doc/id值
示例:
DELETE /jjy/_doc/1
根据id批量查询文档
GET /索引/_mget
{
"docs":[
{"_id":id值},
{"_id":id值}
]
}
示例:
GET /jjy/_mget
{
"docs":[
{"_id":1},
{"_id":2}
]
}
查询所有文档
GET /索引/_search
{
"query": {
"match_all": {}
}
}
示例:
GET /jjy/_search
{
"query": {
"match_all": {}
}
}
修改文档部分字段
POST /索引/_update/1/
{
"doc":{
域名:值
}
}
示例:
POST /jjy/_update/id值/
{
"doc":{
info:"jjy好厉害哦"
}
}
注:
Elasticsearch执行删除操作时,ES先标记文档为
deleted
状态,而不是直接物理删除。当ES存储空间不足或工作空闲时,才会执行物理删除操作。Elasticsearch执行修改操作时,ES不会真的修改Document中的数据,而是标记ES中原有的文档为deleted状态,再创建一个新的文档来存储数据。
域的属性
index
该域是否创建索引。只有值设置为true,才能根据该域的关键词查询文档。
// 根据关键词查询文档
GET /索引名/_search
{
"query":{
"term":{
搜索字段: 关键字
}
}
}
type
域的类型
核心类型 | 具体类型 |
---|---|
字符串类型 | text |
整数类型 | long, integer, short, byte |
浮点类型 | double, float |
日期类型 | date |
布尔类型 | boolean |
数组类型 | array |
对象类型 | object |
不分词的字符串 | keyword |
store
是否单独存储。如果设置为true,则该域能够单独查询。
// 单独查询某个域:
GET /索引名/_search
{
"stored_fields": ["域名"]
}
分词器
默认分词器
ES文档的数据拆分成一个个有完整含义的关键词,并将关键词与文档对应,这样就可以通过关键词查询文档。要想正确的分词,需要选择合适的分词器。
standard analyzer
:Elasticsearch默认分词器,根据空格和标点符号对英文进行分词,会进行单词的大小写转换。
- 查看分词效果
GET /_analyze
{
"text":测试语句,
"analyzer":分词器
}
IK分词器
IKAnalyzer是一个开源的,基于java语言开发的轻量级的中文分词工具包。提供了两种分词算法:
-
ik_smart:最少切分
-
ik_max_word:最细粒度划分
安装IK分词器
-
关闭es服务
-
使用rz命令将ik分词器上传至虚拟机
注:ik分词器的版本要和es版本保持一致。
- 解压ik分词器到elasticsearch的plugins目录下
unzip elasticsearch-analysis-ik-8.10.4.zip -d /usr/local/elasticsearch1/plugins/analysis-ik
- 启动ES服务
su es
#进入ES安装文件夹:
cd /usr/local/elasticsearch1/bin/
#启动ES服务:
./elasticsearch
测试分词器效果
GET /_analyze
{
"text":"测试语句",
"analyzer":"ik_smart/ik_max_word"
}
IK分词器词典
IK分词器根据词典进行分词,词典文件在IK分词器的config目录中。
-
main.dic:IK中内置的词典。记录了IK统计的所有中文单词。
-
IKAnalyzer.cfg.xml:用于配置自定义词库。
<properties>
<comment>IK Analyzer 扩展配置</comment>
<!--用户可以在这里配置自己的扩展字典 -->
<entry key="ext_dict">ext_dict.dic</entry>
<!--用户可以在这里配置自己的扩展停止词字典-->
<entry key="ext_stopwords">ext_stopwords.dic</entry>
<!--用户可以在这里配置远程扩展字典 -->
<!-- <entry key="remote_ext_dict">words_location</entry> -->
<!--用户可以在这里配置远程扩展停止词字典-->
<!-- <entry key="remote_ext_stopwords">words_location</entry> -->
</properties>
拼音分词器
拼音分词器可以将中文分成对应的全拼,全拼首字母等。
安装拼音分词器
-
关闭es服务
-
使用rz命令将拼音分词器上传至虚拟机
注:拼音分词器的版本要和es版本保持一致。
解压pinyin分词器到elasticsearch的plugins目录下
unzip elasticsearch-analysis-pinyin-8.10.4 -d /usr/local/elasticsearch1/plugins/analysis-pinyin
启动ES服务
su es
#进入ES安装文件夹:
cd /usr/local/elasticsearch1/bin/
#启动ES服务:
./elasticsearch
测试分词效果
GET /_analyze
{
"text":测试语句,
"analyzer":"pinyin"
}
自定义分词器
真实开发中我们往往需要对一段内容既进行文字分词,又进行拼音分词,此时我们需要自定义ik+pinyin分词器。
创建自定义分词器
在创建索引时自定义分词器
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":{
...
}
}
}
}
测试自定义分词器
GET /索引/_analyze
{
"text": "你好程序员",
"analyzer": "ik_pinyin"
}
Elasticsearch搜索文档
添加一些文档数据
PUT /students
{
"mappings":{
"properties":{
"id": {
"type": "integer",
"index": true
},
"name": {
"type": "text",
"store": true,
"index": true,
"analyzer": "ik_smart"
},
"info": {
"type": "text",
"store": true,
"index": true,
"analyzer": "ik_smart"
}
}
}
}
POST /students/_doc/
{
"id":1,
"name":"程序员",
"info":"I love baizhan"
}
POST /students/_doc/
{
"id":2,
"name":"美羊羊",
"info":"美羊羊是羊村最漂亮的人"
}
POST /students/_doc/
{
"id":3,
"name":"懒羊羊",
"info":"懒羊羊的成绩不是很好"
}
POST /students/_doc/
{
"id":4,
"name":"小灰灰",
"info":"小灰灰的年纪比较小"
}
POST /students/_doc/
{
"id":5,
"name":"沸羊羊",
"info":"沸羊羊喜欢美羊羊"
}
POST /students/_doc/
{
"id":6,
"name":"灰太狼",
"info":"灰太狼是小灰灰的父亲,每次都会说我一定会回来的"
}
搜索方式
match_all:查询所有文档
{
"query":{
"match_all":{}
}
}
match:全文检索。将查询条件分词后再进行搜索。
{
"query":{
"match":{
搜索字段:搜索条件
}
}
}
注:在搜索时关键词有可能会输入错误,ES搜索提供了自动纠错功能,即ES的模糊查询。使用match方式可以实现模糊查询。模糊查询对中文的支持效果一般,我们使用英文数据测试模糊查询。
{
"query": {
"match": {
"域名": {
"query": 搜索条件,
"fuzziness": 最多错误字符数,不能超过2
}
}
}
}
range:范围搜索。对数字类型的字段进行范围搜索
{
"query":{
"range":{
搜索字段:{
"gte":最小值,
"lte":最大值
}
}
}
}
gt/lt:大于/小于
gte/lte:大于等于/小于等于
match_phrase:短语检索。搜索条件不做任何分词解析,在搜索字段对应的倒排索引中精确匹配。
{
"query":{
"match_phrase":{
搜索字段:搜索条件
}
}
}
term/terms:单词/词组搜索。搜索条件不做任何分词解析,在搜索字段对应的倒排索引中精确匹配
{
"query":{
"term":{
搜索字段: 搜索条件
}
}
}
{
"query":{
"terms":{
搜索字段: [搜索条件1,搜索条件2]
}
}
}
复合搜索
GET /索引/_search
{
"query": {
"bool": {
// 必须满足的条件
"must": [
搜索方式:搜索参数,
搜索方式:搜索参数
],
// 多个条件有任意一个满足即可
"should": [
搜索方式:搜索参数,
搜索方式:搜索参数
],
// 必须不满足的条件
"must_not":[
搜索方式:搜索参数,
搜索方式:搜索参数
]
}
}
}
结果排序
ES中默认使用相关度分数实现排序,可以通过搜索语法定制化排序。
GET /索引/_search
{
"query": 搜索条件,
"sort": [
{
"字段1":{
"order":"asc"
}
},
{
"字段2":{
"order":"desc"
}
}
]
}
由于ES对text类型字段数据会做分词处理,使用哪一个单词做排序都是不合理的,所以 ES中默认不允许对text类型的字段做排序。如果需要使用字符串做结果排序,可以使用 keyword类型的字段作为排序依据,因为keyword字段不做分词处理。
高亮查询
在进行关键字搜索时,搜索出的内容中的关键字会显示不同的颜色,称之为高亮。
我们可以在关键字左右加入标签字符串,数据传入前端即可完成高亮显示,ES可以对查询出的内容中关键字部分进行标签和样式的设置。
GET /索引/_search
{
"query":搜索条件,
"highlight":{
"fields": {
"高亮显示的字段名": {
// 返回高亮数据的最大长度
"fragment_size":100,
// 返回结果最多可以包含几段不连续的文字
"number_of_fragments":5
}
},
"pre_tags":["前缀"],
"post_tags":["后缀"]
}
}
SQL查询
在ES7之后,支持SQL语句查询文档:
GET /_sql?format=txt
{
"query": SQL语句
}
开源版本的ES并不支持通过Java操作SQL进行查询,如果需要操作 SQL查询,则需要氪金(购买白金版)
SpringDaraES案例
Java原生代码可以操作Elasticsearch,但操作比较繁琐,类似于数据库中的JDBC,我们还需要将ES文档手动封装为Java对象。所以开发中我们一般使用框架操作Elasticsearch。
Spring Data ElasticSearch是JAVA操作Elasticsearch的框架。它通过对原生API的封装,使得JAVA程序员可以简单的对Elasticsearch进行操作。
使用Repository继承的方法查询文档
- 创建SpringBoot项目,加入Spring Data Elasticsearch起步依赖:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-elasticsearch</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<scope>provided</scope>
</dependency>
- 编写配置文件,连接elasticsearch
spring:
elasticsearch:
uris: https://192.168.0.187:9200
username: elastic
password: elastic
#日志格式
logging:
pattern:
console: '%d{HH:mm:ss.SSS} %clr(%-5level) --- [%-15thread] %cyan(%-50logger{50}):%msg%n'
- 创建配置类,跳过SSL证书检查。
从ES8开始,访问ES的协议从http变成了https,访问https请求需要SSL证书,在开发环境下我们不需要配置该证书,在项目中添加一个配置类,跳过SSL证书检查即可。
@Component
public class RestClientBuilderCustomizerImpl implements RestClientBuilderCustomizer {
@Override
public void customize(RestClientBuilder builder) {
}
/**
* 跳过SSL的证书检查
*/
@Override
public void customize(HttpAsyncClientBuilder builder) {
SSLContextBuilder sscb = SSLContexts.custom();
try {
sscb.loadTrustMaterial((chain, authType) -> {
return true;
});
} catch (NoSuchAlgorithmException e) {
throw new RuntimeException(e);
} catch (KeyStoreException e) {
throw new RuntimeException(e);
}
try {
builder.setSSLContext(sscb.build());
} catch (KeyManagementException | NoSuchAlgorithmException e) {
e.printStackTrace();
}
}
}
- 创建实体类:
// 一个实体类的所有对象都会存入ES的一个索引中,所以我们在创建实体类时关联ES索引
@Document(indexName = "product",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_max_word",searchAnalyzer = "ik_max_word")
private String productName;
@Field(type = FieldType.Text,store = true,index = true,analyzer = "ik_max_word",searchAnalyzer = "ik_max_word")
private String productDesc;
}
@Document:标记在类上,标记实体类为文档对象,一般有如下属性:
indexName:对应索引的名称
createIndex:是否自动创建索引
@Id:标记在成员变量上,标记一个字段为主键,该字段的值会同步到ES该文档的id值。
@Field:标记在成员变量上,标记为文档中的域,一般有如下属性:
type:域的类型
index:是否创建索引,默认是 true
store:是否单独存储,默认是 false
analyzer:分词器
searchAnalyzer:搜索时的分词器
- 创建Repository接口
// Repository接口继承ElasticsearchRepository,该接口提供了文档的增删改查方法
@Repository
public interface ProductRepository extends ElasticsearchRepository<Product,Integer> {
}
- 测试Repository接口的方法
@SpringBootTest
public class ProductRepositoryTest {
@Autowired
private ProductRepository repository;
@Test
public void addProduct(){
Product product = new Product(1, "iphone30", "iphone30是苹果最新手机");
repository.save(product);
}
@Test
public void updateProduct(){
Product product = new Product(1, "iphone31", "iphone31是苹果最新手机");
repository.save(product);
}
@Test
public void findAllProduct(){
Iterable<Product> all = repository.findAll();
for (Product product : all) {
System.out.println(product);
}
}
@Test
public void findProductById(){
Optional<Product> product = repository.findById(1);
System.out.println(product.get());
}
@Test
public void deleteProduct(){
repository.deleteById(1);
}
}
接下来我们讲解SpringDataES支持的查询方式,首先准备一些文档数据:
// 添加一些数据
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开发的优秀的搜索引擎"));
使用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);
按照规则命名方法查询文档
- 只需在Repository接口中按照一定的规则命名方法,该方法就能完成相应的查询。
- 规则:查询方法以findBy开头,涉及查询条件时,条件的属性用条件关键字连接。
关键字 | 命名规则 | 解释 | 示例 |
---|---|---|---|
and | findByField1AndField2 | 根据Field1和Field2 获得数据 | findByTitleAndContent |
or | findByField1OrField2 | 根据Field1或Field2 获得数据 | findByTitleOrContent |
is | findByField | 根据Field获得数据 | findByTitle |
not | findByFieldNot | 根据Field获得补集数据 | findByTitleNot |
between | findByFieldBetween | 获得指定范围的数据 | findByPriceBetween |
List<Product> findByProductName(String productName);
List<Product> findByProductNameOrProductDesc(String productName,String productDesc);
List<Product> findByIdBetween(Integer startId,Integer endId);
分页查询
在使用继承或自定义的方法时,在方法中添加Pageable类型的参数,返回值为Page类型即可进行分页查询。
// 测试继承的方法:
@Test
public void testFindPage(){
// 参数1:页数,参数2:每页条数
Pageable pageable = PageRequest.of(1, 3);
Page<Product> page = repository.findAll(pageable);
System.out.println("总条数"+page.getTotalElements());
System.out.println("总页数"+page.getTotalPages());
System.out.println("数据"+page.getContent());
}
// 自定义方法
Page<Product> findByProductDescMatch(String keyword, Pageable pageable);
// 测试自定义方法
@Test
public void testFindPage2(){
Pageable pageable = PageRequest.of(1, 2);
Page<Product> page = repository.findByProductDescMatch("我喜欢三体", pageable);
System.out.println("总条数"+page.getTotalElements());
System.out.println("总页数"+page.getTotalPages());
System.out.println("数据"+page.getContent());
}
结果排序
使用继承或自定义的方法时,在方法中添加Sort类型的参数即可进行结果排序。
// 结果排序
@Test
public void testFindSort(){
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 testFindPage2(){
Sort sort = Sort.by(Sort.Direction.DESC,"id");
Pageable pageable = PageRequest.of(0, 2,sort);
Page<Product> page = repository.findByProductDescMatch("我喜欢三体", pageable);
System.out.println("总条数"+page.getTotalElements());
System.out.println("总页数"+page.getTotalPages());
System.out.println("数据"+page.getContent());
}
template工具类
SpringDataElasticsearch提供了一个工具类ElasticsearchTemplate,我们使用该类对象也可以对ES进行操作。
操作索引
@SpringBootTest
public class TemplateTest {
@Autowired
private ElasticsearchTemplate template;
// 新增索引
@Test
public void addIndex() {
// 获得索引操作对象
IndexOperations indexOperations = template.indexOps(Product.class);
// 创建索引,注:该方法无法设置索引结构,不推荐使用
indexOperations.create();
}
// 删除索引
@Test
public void delIndex() {
// 获得索引操作对象
IndexOperations indexOperations = template.indexOps(Product.class);
// 删除索引
indexOperations.delete();
}
}
操作文档
// 新增/修改文档
@Test
public void addDocument() {
Product product = new Product(7, "es1", "es是一款优秀的搜索引擎");
template.save(product);
}
// 删除文档
@Test
public void delDocument() {
template.delete("7", Product.class);
}
// 根据id查询
@Test
public void findAllDocument() {
Product product = template.get("1", Product.class);
System.out.println(product);
}
查询文档
template的search方法可以查询文档:
SearchHits<T> search(Query query, Class<T> clazz):查询文档,query是查询条件对象,clazz是结果类型。
用法如下:
// 查询文档2
@Test
public void searchDocument2() {
String productName = "三体";
String productDesc = "优秀";
// 1.构建查询条件
NativeQueryBuilder nativeQueryBuilder = new NativeQueryBuilder();
// 如果没有传入参数,查询所有
if (productName == null && productDesc == null) {
nativeQueryBuilder.withQuery(Queries.matchAllQueryAsQuery());
} else {
BoolQuery.Builder boolQuery = QueryBuilders.bool();
if (productName != null) {
boolQuery.must(Queries.matchQueryAsQuery("productName", productName, null, null));
}
if (productDesc != null) {
boolQuery.must(Queries.matchQueryAsQuery("productDesc", productDesc, null, null));
}
nativeQueryBuilder.withQuery(boolQuery.build()._toQuery());
}
NativeQuery query = nativeQueryBuilder.build();
// 2.查询
SearchHits<Product> result = template.search(query, Product.class);
// 3.处理查询结果
for (SearchHit<Product> productSearchHit : result) {
Product product = productSearchHit.getContent();
System.out.println(product);
}
}
复杂条件查询
// 查询文档2
@Test
public void searchDocument2(){
String productName = "三体";
String productDesc = "优秀";
// 1.构造查询条件
NativeQueryBuilder nativeQueryBuilder = new NativeQueryBuilder();
// 如果没有传入参数,查询所有
if (productName == null && productDesc == null){
nativeQueryBuilder.withQuery(Queries.matchAllQueryAsQuery());
}else {
BoolQuery.Builder boolQuery = QueryBuilders.bool();
if (productName != null){
boolQuery.must(Queries.matchQueryAsQuery("productName",productName,null,null));
}
if (productDesc != null){
boolQuery.must(Queries.matchQueryAsQuery("productDesc",productDesc,null,null));
}
nativeQueryBuilder.withQuery(boolQuery.build()._toQuery());
}
NativeQuery query = nativeQueryBuilder.build();
// 2.查询
SearchHits<Product> result = template.search(query, Product.class);
// 3.处理查询结果
for (SearchHit<Product> productSearchHit : result) {
Product product = productSearchHit.getContent();
System.out.println(product);
}
}
分页查询
// 分页查询文档
@Test
public void searchDocumentPage() {
// 1.构建查询条件
Pageable pageable = PageRequest.of(0, 3);
NativeQuery query = new NativeQueryBuilder()
.withQuery(Queries.matchAllQueryAsQuery())
.withPageable(pageable)
.build();
// 2.查询
SearchHits<Product> result = template.search(query, Product.class);
// 3.处理查询结果
List<Product> content = new ArrayList();
for (SearchHit<Product> productSearchHit : result) {
Product product = productSearchHit.getContent();
content.add(product);
}
/**
* 封装Page对象,参数1:具体数据,参数2:分页条件对象,参数3:总条数
*/
Page<Product> page = new PageImpl(content, pageable, result.getTotalHits());
System.out.println(page.getTotalElements());
System.out.println(page.getTotalPages());
System.out.println(page.getContent());
}
结果排序
// 结果排序
@Test
public void searchDocumentSort() {
// 1.构建查询条件
NativeQuery query = new NativeQueryBuilder()
.withQuery(Queries.matchAllQueryAsQuery())
.withSort(Sort.by(Sort.Direction.DESC, "id"))
.build();
// 2.查询
SearchHits<Product> result = template.search(query, Product.class);
// 3.处理查询结果
for (SearchHit<Product> productSearchHit : result) {
Product product = productSearchHit.getContent();
System.out.println(product);
}
}
Elasticsearch集群
在单台ES服务器上,随着一个索引内数据的增多,会产生存储、效率、安全等问题。
- 假设项目中有一个500G大小的索引,但我们只有几台200G硬盘的服务器,此时是不可能将索引放入其中某一台服务器中的。
- 此时我们需要将索引拆分成多份,分别放入不同的服务器中,此时这几台服务器维护了同一个索引,我们称这几台服务器为一个集群,其中的每一台服务器为一个节点,每一台服务器中的数据称为一个分片。
- 此时如果某个节点故障,则会造成集群崩溃,所以每个节点的分片往往还会创建副本,存放在其他节点中,此时一个节点的崩溃就不会影响整个集群的正常运行。
节点(node):一个节点是集群中的一台服务器,是集群的一部分。它存储数据,参与集群的索引和搜索功能。集群中有一个为主节点,主节点通过ES内部选举产生。
集群(cluster):一组节点组织在一起称为一个集群,它们共同持有整个的数据,并一起提供索引和搜索功能。
分片(shards):ES可以把完整的索引分成多个分片,分别存储在不同的节点上。
副本(replicas):ES可以为每个分片创建副本,提高查询效率,保证在分片数据丢失后的恢复。
注:
分片的数量只能在索引创建时指定,索引创建后不能再更改分片数量,但可以改变副本的数量。
为保证节点发生故障后集群的正常运行,ES不会将某个分片和它的副本存在同一台节点上。
搭建集群
安装第一个ES节点
- 修改系统进程最大打开文件数
#修改系统文件
vim /etc/security/limits.conf
#添加如下内容
es soft nofile 65535
es hard nofile 131072
- 安装
#解压:
tar -zxvf elasticsearch-8.10.4-linux-x86_64.tar.gz
#重命名:
mv elasticsearch-8.10.4 myes1
#移动文件夹:
mv myes1 /usr/local/
#安装ik分词器
unzip elasticsearch-analysis-ik-8.10.4.zip -d /usr/local/myes1/plugins/analysis-ik
#安装拼音分词器
unzip elasticsearch-analysis-pinyin-8.10.4.zip -d /usr/local/myes1/plugins/analysis-pinyin
#es用户取得该文件夹权限:
chown -R es:es /usr/local/myes1
- 修改配置文件
#打开节点一配置文件:
vim /usr/local/myes1/config/elasticsearch.yml
配置如下信息:
#集群名称,保证唯一
cluster.name: my_elasticsearch
#节点名称,必须不一样
node.name: node1
#可以访问该节点的ip地址
network.host: 0.0.0.0
#该节点服务端口号
http.port: 9200
#集群间通信端口号
transport.port: 9300
#候选主节点的设备地址
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"]
#关闭安全认证
xpack.security.enabled: false
- 启动
#切换为es用户:
su es
#后台启动第一个节点:
ES_JAVA_OPTS="-Xms512m -Xmx512m" /usr/local/myes1/bin/elasticsearch -d
安装第二个ES节点
- 安装
#解压:
tar -zxvf elasticsearch-8.10.4-linux-x86_64.tar.gz
#重命名:
mv elasticsearch-8.10.4 myes2
#移动文件夹:
mv myes2 /usr/local/
#安装ik分词器
unzip elasticsearch-analysis-ik-8.10.4.zip -d /usr/local/myes2/plugins/analysis-ik
#安装拼音分词器
unzip elasticsearch-analysis-pinyin-8.10.4.zip -d /usr/local/myes2/plugins/analysis-pinyin
#es用户取得该文件夹权限:
chown -R es:es /usr/local/myes2
- 修改配置文件
#打开节点二配置文件:
vim /usr/local/myes2/config/elasticsearch.yml
配置如下信息:
#集群名称,保证唯一
cluster.name: my_elasticsearch
#节点名称,必须不一样
node.name: node2
#可以访问该节点的ip地址
network.host: 0.0.0.0
#该节点服务端口号
http.port: 9201
#集群间通信端口号
transport.port: 9301
#候选主节点的设备地址
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"]
#关闭安全认证
xpack.security.enabled: false
- 启动
#切换为es用户:
su es
#后台启动第二个节点:
ES_JAVA_OPTS="-Xms512m -Xmx512m" /usr/local/myes2/bin/elasticsearch -d
安装第三个ES节点
- 安装
#解压:
tar -zxvf elasticsearch-8.10.4-linux-x86_64.tar.gz
#重命名:
mv elasticsearch-8.10.4 myes3
#移动文件夹:
mv myes3 /usr/local/
#安装ik分词器
unzip elasticsearch-analysis-ik-8.10.4.zip -d /usr/local/myes3/plugins/analysis-ik
#安装拼音分词器
unzip elasticsearch-analysis-pinyin-8.10.4.zip -d /usr/local/myes3/plugins/analysis-pinyin
#es用户取得该文件夹权限:
chown -R es:es /usr/local/myes3
- 修改配置文件
#打开节点一配置文件:
vim /usr/local/myes3/config/elasticsearch.yml
配置如下信息:
#集群名称,保证唯一
cluster.name: my_elasticsearch
#节点名称,必须不一样
node.name: node3
#可以访问该节点的ip地址
network.host: 0.0.0.0
#该节点服务端口号
http.port: 9202
#集群间通信端口号
transport.port: 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"]
#关闭安全认证
xpack.security.enabled: false
- 启动
#切换为es用户:
su es
#后台启动第三个节点:
ES_JAVA_OPTS="-Xms512m -Xmx512m" /usr/local/myes3/bin/elasticsearch -d
测试集群
访问http://虚拟机IP:9200/_cat/nodes
查看集群是否搭建成功。
kibana连接es集群
- 在kibana中访问集群
# 打开kibana配置文件
vim /usr/local/kibana-8.10.4/config/kibana.yml
添加如下配置
# 该集群的所有节点
elasticsearch.hosts: ["http://127.0.0.1:9200","http://127.0.0.1:9201","http://127.0.0.1:9202"]
启动kibana
#切换为es用户:
su es
#启动kibana:
cd /usr/local/kibana-8.10.4/bin
./kibana
- 访问kibana:
http://虚拟机IP:5601
测试集群状态
- 在集群中创建一个索引
PUT /product1
{
"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
故障应对和水平扩容
-
关闭一个节点,可以发现ES集群可以自动进行故障应对。
-
重新打开该节点,可以发现ES集群可以自动进行水平扩容。
-
分片数不能改变,但是可以改变每个分片的副本数、
PUT /索引/_settings
{
"number_of_replicas": 副本数
}
内存设置
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内存。
磁盘选择
ES的优化即通过调整参数使得读写性能更快
磁盘通常是服务器的瓶颈。Elasticsearch重度使用磁盘,磁盘的效率越高,Elasticsearch的执行效率就越高。这里有一些优化磁盘的技巧:
- 使用SSD(固态硬盘),它比机械磁盘优秀多了。
- 使用RAID0模式(将连续的数据分散到多个硬盘存储,这样可以并行进行IO操作),代价是一块硬盘发生故障就会引发系统故障。
- 不要使用远程挂载的存储。
分片策略
分片和副本数并不是越多越好。每个分片的底层都是一个Lucene索引,会消耗一定的系统资源。且搜索请求需要命中索引中的所有分片,分片数过多会降低搜索性能。索引的分片数需要架构师和技术人员对业务的增长有预先的判断,一般来说我们遵循以下原则:
-
每个分片占用的硬盘容量不超过ES的最大JVM的堆空间设置(一般设置不超过32G)。比如:如果索引的总容量在500G左右,那分片数量在16个左右即可。
-
分片数一般不超过节点数的3倍。比如:如果集群内有10个节点,则分片数不超过30个。
-
推迟分片分配:节点中断后集群会重新分配分片。但默认集群会等待一分钟来查看节点是否重新加入。我们可以设置等待的时长,减少重新分配的次数:
PUT /索引/_settings
{
"settings":{
"index.unassianed.node_left.delayed_timeout":"5m"
}
}
- 减少副本数量:进行写入操作时,需要把写入的数据都同步到副本,副本越多写入的效率就越慢。我们进行大批量进行写入操作时可以先设置副本数为0,写入完成后再修改回正常的状态。
Elasticsearch案例
需求说明
使用ES模仿百度搜索,即自动补全+搜索引擎效果:
ES自动补全
GET /索引/_search
{
"suggest": {
"prefix_suggestion": {// 自定义推荐名
"prefix": "elastic",// 被补全的关键字
"completion": {
"field": "productName",// 查询的域
"skip_duplicates": true, // 忽略重复结果
"size": 10 //最多查询到的结果数
}
}
}
}
注:自动补全对性能要求极高,ES不是通过倒排索引来实现的,所以需要将对应的查询字段类型设置为completion。
PUT /product2
{
"mappings":{
"properties":{
"id":{
"type":"integer",
"store":true,
"index":true
},
"productName":{
"type":"completion"
},
"productDesc":{
"type":"text",
"store":true,
"index":true
}
}
}
}
POST /product2/_doc
{
"id":1,
"productName":"elasticsearch1",
"productDesc":"elasticsearch1 is a good search engine"
}
POST /product2/_doc
{
"id":2,
"productName":"elasticsearch2",
"productDesc":"elasticsearch2 is a good search engine"
}
POST /product2/_doc
{
"id":3,
"productName":"elasticsearch3",
"productDesc":"elasticsearch3 is a good search engine"
}
创建索引
PUT /news
{
"settings": {
"number_of_shards": 5,
"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"
}
}
}
}
mysql数据导入es
使用logstash工具可以将mysql数据同步到es中:
- 解压logstash-8.10.4-windows-x86_64
logstash要和elastisearch版本一致
- 在logstash解压路径下的/config中创建mysql.conf文件,文件写入以下脚本内容:
input {
jdbc {
jdbc_driver_library => "F:\Elasticsearch8\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 => ["http://192.168.0.187:9200","http://192.168.0.187:9201","http://192.168.0.187:9202"]
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
}
}
}
}
项目搭建
创建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>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
写配置文件:
# 连接elasticsearch
spring:
elasticsearch:
uris: 192.168.0.187:9200,192.168.0.187:9201,192.168.0.187:9202
# 日志格式
logging:
pattern:
console: '%d{HH:mm:ss.SSS} %clr(%-5level) --- [%-15thread] %cyan(%-50logger{50}):%msg%n'
创建实体类
@Document(indexName = "news")
@Data
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;
}
创建Repository接口
@Repository
public interface NewsRepository extends ElasticsearchRepository<News, Integer> {
}
自动补全功能
@Service
public class NewsService {
@Autowired
private ElasticsearchClient client;
// 自动补齐
public List<String> autoSuggest(String keyword) throws IOException {
// 1.自动补齐查询条件
Suggester suggester = Suggester.of(
s -> s.suggesters("prefix_suggestion", FieldSuggester.of(
fs -> fs.completion(
cs -> cs.skipDuplicates(true)
.size(10)
.field("tags")
)
)).text(keyword)
);
// 2.自动补齐查询
SearchResponse<Map> response = client.search(s -> s.index("news")
.suggest(suggester), Map.class);
// 3.处理查询结果
Map resultMap = response.suggest();
List<Suggestion> suggestionList = (List) resultMap.get("prefix_suggestion");
Suggestion suggestion = suggestionList.get(0);
List<CompletionSuggestOption> resultList = suggestion.completion().options();
List<String> result = new ArrayList<>();
for (CompletionSuggestOption completionSuggestOption : resultList) {
String text = completionSuggestOption.text();
result.add(text);
}
return result;
}
}
搜索关键字功能
在repository接口中添加高亮搜索关键字方法
// 高亮搜索关键字
@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);
// 处理结果,封装为News类型的集合
List<News> newsList = new ArrayList();
for (SearchHit<News> newsSearchHit : result) {
News news = newsSearchHit.getContent();
// 高亮字段
Map<String, List<String>> highlightFields = newsSearchHit.getHighlightFields();
if (highlightFields.get("title") != null){
news.setTitle(highlightFields.get("title").get(0));
}
if (highlightFields.get("content") != null){
news.setContent(highlightFields.get("content").get(0));
}
newsList.add(news);
}
return newsList;
}
创建Controller类
@RestController
public class NewsController {
@Autowired
private NewsService newsService;
@GetMapping("/autoSuggest")
public List<String> autoSuggest(String term){ // 前端使用jqueryUI,发送的参数默认名为term
return newsService.autoSuggest(term);
}
@GetMapping("/highLightSearch")
public List<News> highLightSearch(String term){
return newsService.highLightSearch(term);
}
}
前端页面
我们使用jqueryUI中的autocomplete插件完成项目的前端实现。
<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>
如果我的内容对你有帮助,请点赞,评论,收藏。创作不易,大家的支持就是我坚持下去的动力
开放原子开发者工作坊旨在鼓励更多人参与开源活动,与志同道合的开发者们相互交流开发经验、分享开发心得、获取前沿技术趋势。工作坊有多种形式的开发者活动,如meetup、训练营等,主打技术交流,干货满满,真诚地邀请各位开发者共同参与!
更多推荐
所有评论(0)