Elasticsearch简介
1 概念
Elasticsearch 是一个分布式、RESTful 风格的搜索和数据分析引擎,能够解决不断涌现出的各种用例。 作为 Elastic Stack 的核心,它集中存储您的数据,帮助您发现意料之中以及意料之外的情况。
2 和MySQL的检索对比
MySQL中也有检索数据的方式,可是为什么不适合呢?因为术业有专攻,MySQL是专攻于数据的持久化的存储与管理,但是要做海量数据的检索和分析,Elasticsearch更在行。
3 基本概念
概述
Elasticsearch是面向文档的,这意味着它可以存储整个对象或文档。然而它不仅仅是存储,还会索引每个文档的内容使之可以被搜索。在Elasticsearch中,你可以对文档(而非成行成列的数据)进行索引、搜索、排序、过滤。Elasticsearch比传统关系型数据库如下:
关系型数据库:Database --> Table --> Row --> Column
Elasticsearch:Index --> Type --> Document --> Field
核心概念
Index(索引)
动词,相当于MySQL的insert
,
名称,相当于MySQL的Database
。
Type(类型)
在index(索引)中,可以定义一个或多个类型。
类似于MySQL中的Table;每一种类型的数据放在一起。
Document(文档)
保存在某个索引(Index)下,某种类型(Type)的一个数据(Document),文档是Json格式的,Document就像是MySQL中的某个Table里面的内容。
倒排索引机制
安装Elasticsearch
1 下载镜像文件
// 安装elasticsearch
docker pull elasticsearch:7.4.2
// 安装Elasticsearch的开源分析和可视化平台
docker pull kibana:7.4.2
2 创建实例
ElasticSearch
// 提前创建相关目录,为了将elasticsearch的配置和数据挂载到w
// -p:创建多级不存在的目录
mkdir -p /mydata/elasticsearch/config
mkdir -p /mydata/elasticsearch/data
// 允许被远程的任何机器访问(注意:冒号后面需要一个空格——yml的格式)
echo "http.host: 0.0.0.0" >> /mydata/elasticsearch/config/elasticsearch.yml
// 9200:接收api请求的端口,9300:分布式集群下节点之间的通信端口
docker run --name elasticsearch -p 9200:9200 -p 9300:9300 \
-e "discovery.type=single-node" \
-e ES_JAVA_OPTS="-Xms64m -Xmx128m" \
-v /mydata/elasticsearch/config/elasticsearch.yml:/usr/share/elasticsearch/config/elasticsearch.yml \
-v /mydata/elasticsearch/data:/usr/elasticsearch/data \
-v /mydata/elasticsearch/plugins:/usr/share/elasticsearch/plugins \
-d elasticsearch:7.4.2
属性 | 含义 |
---|---|
"discovery.type=single-node" | 单节点启动 |
ES_JAVA_OPTS="-Xms64m -Xmx128m" | -Xms64m:启动时占用64M,最大128M。(不指定的话,elasticsearch启动后会将所有内存占掉) |
注:想要外部访问虚拟机内的elasticsearch,可能还需要开放防火墙的部分端口
// 查看防火墙状态
systemctl status firewalld
// 开启一个端口
// 1.添加端口,--permanent表示永久生效,没有此参数重启后会无效
firewall-cmd --zone=public --add-port=9200/tcp --permanent
// 2.添加端口外部访问权限
firewall-cmd --add-port=9200/tcp
// 3.重新载入,添加端口后重新载入才能生效
firewall-cmd --reload
然后再外部访问到此数据才算成功
Kibana
docker run --name kibana -e ELASTICSEARCH_HOSTS=http://192.168.121.138:9200 \
-p 5601:5601 \
-d kibana:7.4.2
http://192.168.121.138记得要改成自己的虚拟机地址
kibana可视化页面汉化方法:https://blog.csdn.net/qq_42184699/article/details/90934018
初步检索
1 _cat
GET / _cat/nodes:查看集群中所有节点
GET / _cat/health:查看es健康状况
GET / _cat/master:查看主节点
GET / _cat/indecis:查看所有索引(类似于 show databases;)
2 索引一个文档(保存)
保存一个数据,保存在哪个索引的哪个类型下,指定用哪一个唯一标识。
// 在customer索引下的external类型下保存1号数据
POST(PUT) / customer/external/1
// 请求体
{
"name": "John Doe"
}
POST和PUT都可以。
POST新增。如果不指定id,会自动生成id。指定id就会修改这个数据,并新增版本号。
PUT可以新增也可以修改。PUT必须指定id,由于PUT需要指定id,不指定id会报错,所以一般用于修改。
对POST请求来说,不指定id,发送多少次都是新增操作。
而对于PUT请求来说,需要指定id,发送多次,是更新操作。POST指定id的话,那也是更新操作。
3 查询文档
GET / customer/external/1
老版是使用版本号做乐观锁,新版使用了序列号。
使用乐观锁更新的话,需要在保存语句后加上 ?if_seq_no=0&if_primary_term=1
4 更新文档
// 带_update的POST更新
POST / customer/external/1/_update
// 请求体
{
"doc":{
"name": "John Doe1"
}
}
带_update的POST更新,在更新之前会对比原来的数据,如果更新的数据与原来一样的话,就什么都不做,version、seq_no都不变。
// 不带_update的POST更新
POST / customer/external/1
// 请求体
{
"name": "John Doe2"
}
不带_update的POST更新,不会做对比,而是直接更新,version、seq_no都增加。
// 不带_update的PUT更新
PUT / customer/external/1
// 请求体
{
"name": "John Doe3"
}
不带_update的PUT更新和 “不带_update的POST更新” 效果相同。
5 删除文档
DELETE / customer/external/1
DELETE / customer
删除不存在的文档,result会显示“not_found”
6 批量操作
POST /customer/external/_bulk
{"index":{"_id":"1"}}
{"name": "John Doe"}
{"index":{"_id":"2"}}
{"name": "Jane Doe"}
两行为一个整体。
每一个整体都是独立操作的,不同于事务。无论前面的是否成功,后面的依然执行。
// 语法格式
{action:{metadata}}\n
{requestbody }
{action:{metadata}}\n
{requestbody }
7 导入样本测试数据
POST /bank/account/_bulk
测试数据:https://gitee.com/xlh_blog/common_content/blob/master/es测试数据.json#
进阶检索
1 SearchAPI
es支持两种基本方式检索:
- 一个是通过 REST request URI 发送搜索参数(uri + 检索参数)
- 另一个是通过使用 REST request body 来发送它们(uri + 请求体)
检索信息
- 一切检索从
_search
开始
语句 | 含义 |
---|---|
GET /bank/_search | 检索bank下所有信息,包括type和docs |
GET /bank/_search?q=*&sort=account_number:asc | 请求参数方式检索 |
q=* :查询所有
sort=account_number:asc :安装account_number字段排序,升序排序
asc :升序排序
- uri + 请求体进行检索
GET /bank/_search
{
"query":{
"match_all":{}
},
"sort":[
{
"account_number":"asc"
}
]
}
2 Query DSL
官方文档:https://www.elastic.co/guide/en/elasticsearch/reference/7.17/getting-started.html
基本语法格式
Elasticsearch提供了一个可以执行查询的Json风格的DSL(domain-specific language 领域特定语言)。这个被称为Query DSL。该查询语言非常全面,并且刚开始的时候感觉有点复杂,真正学好它的方法是从一些基础的示例开始的。
- 一个查询语句的典型结构:
{
QUERY_NAME:{
ARGUMENT:VALUE,
ARGUMENT:VALUE,
...
}
}
- 如果是针对某个字段,那么它的结构如下:
{
QUERY_NAME:{
FIELD_NAME:{
ARGUMENT:VALUE,
ARGUMENT:VALUE,
...
}
}
}
返回部分字段
{
"_source": ["field", ...]
}
match匹配查询
这种匹配是全文检索。
- 匹配数值类型是精确匹配
- 匹配字符串类型是模糊查询
若传了两个单词,则会 按空格分词 + 全文检索
_score :得分
max_score :最高得分
全文检索会按照得分进行排序
match_phrase短语匹配
将需要匹配的值当成一个整体单词,不分词进行匹配。
multi_match多字段匹配
这个也是会进行 按空格分词 + 检索
bool复合查询
bool用来做复合查询:
复合语句可以合并任何其他查询语句,包括复合语句,了解这一点是很重要的。这就意味着,复合语句之间可以互相嵌套,可以表达非常复杂的逻辑。
bool中的条件是且的关系。
一个match中只能写一个条件,多个条件要多个match。
满足should的能够提高相关性得分。must和must_not也能够提供相关性得分。
filter过滤
should、must、must_not都能够筛选,并且提供相关性得分。
filter也是过滤,功能类似于must,但是不提供相关性得分。
term精确匹配
和match一样,匹配某个属性的值。全文检索用match,其他非text字段使用term更好。
aggregation执行聚合
聚合提供了从数据中分组和提取数据的能力。最简单的聚合方法大致等于SQL GROUP BY和SQL聚合函数。在Elasticsearch中,有执行搜索并返回hits(命中结果),并且同时返回聚合结果,把一个响应中的hits分隔开的能力。这是非常强大且有效的,你可以执行查询和多个聚合,并且在一次使用中得到各自的返回结果,使用一次简洁和简化的API来避免网络往返。
- 搜索address中包含mill的所有人的年龄分布以及平均年龄,但是不显示这些人的详情。
terms中的size表示这种分布可能有多少种情况。若实际情况的个数大于size值,则只显示size值的数量。
- 按照年龄聚合,并且请求这些年龄段的这些人的平均薪资。
- 查出所有年龄分布,并且这些年龄段中M的平均薪资和F的平均薪资以及这个年龄段的总体平均薪资。
多重聚合。
# 查出所有年龄分布,并且这些年龄段中M的平均薪资和F的平均薪资以及这个年龄段的总体平均薪资。
GET /bank/_search
{
"query": {
"match_all": {}
},
"aggs": {
"ageAgg": {
"terms": {
"field": "age",
"size": 100
},
"aggs": {
"genderAgg": {
"terms": {
"field": "gender.keyword",
"size": 2
},
"aggs": {
"balanceAgg": {
"avg": {
"field": "balance"
}
}
}
}
}
},
"totalAvgAgg":{
"avg": {
"field": "balance"
}
}
}
}
映射
1 Elasticsearch7中已经去掉了type概念
2 创建索引并指定映射
作用:定义数据库中表的结构,通过mapping来控制索引存储数据的位置。不做映射直接创建索引的话,索引会帮你自动映射,但是映射的类型不一定跟你想的一样。
- 定义Index下的字段名(Field Name)
- 定义字段的类型,比如integer、text、keyword。(text会全表检索、keyword是精确检索)
- 定义倒排索引相关的配置,比如documentId、记录position、打分等。
创建索引不建议再带上类型(type)了。
3 在索引内添加一个新的映射字段
PUT /my_index/_mapping
{
"properties" :{
"employee-id":{
"type": "keyword",
"index": false
}
}
}
4 修改映射&数据迁移
修改映射
对于已经存在的映射字段,我们不能更新。更新必须创建新的索引并进行数据迁移。
数据迁移
// 数据迁移,固定写法。
POST _reindex
{
"source": {
"index": "bank"
},
"dest": {
"index": "newbank"
}
}
// 由于我们是用老版本(索引下带类型)迁移到新版本(无类型),所以需要加一个type
POST _reindex
{
"source": {
"index": "bank",
"type": "account"
},
"dest": {
"index": "newbank"
}
}
想一个问题,为什么我们经常重启es,但是它里面的数据不丢失呢?
因为我们在创建容器的时候就将它的数据挂载到外部了,所以重启容器,数据不会丢失。
分词
一个 tokenizer(分词器)接收一个字符流,将之分割为独立的 tokens(词元,通常是独立的单词),然后输出 tokens 流。
例如,tokenizer 遇到空白字符时分割文本。它会将文本“Quick brown fox!”分割为[Quick, brown, fox!]。
该 tokenizer(分词器)还负责记录各个 term(词条)的顺序或 position 位置(用于phrase短语和word proximity 词近邻查询),以及 term(词条)所代表的原始 word(单词)的start(起始)和end(结束)的character offsets(字符偏移量)(用于高亮显示搜索的内容)。Elasticsearch提供了很多内置的分词器,可以用来构建custom analyzers(自定义分词器)。
standard分词器
标准分词器,但是它是支持英文的,不太支持中文。
所以我们需要一款能够识别中文的分词器,ik分词器。
ik分词器
安装ik分词器
下载链接:https://github.com/medcl/elasticsearch-analysis-ik/releases?page=7
要选择跟elasticsearch
相同的版本
-
进入Linux虚拟机,进入和docker容器挂载到外面的
elasticsearch
的plugins
目录中,创建一个文件夹ik
,并进入文件夹mkdir ik cd ik
-
下载ik分词器的压缩包
wget https://github.com/medcl/elasticsearch-analysis-ik/releases/download/v7.4.2/elasticsearch-analysis-ik-7.4.2.zip
-
解压并删除压缩包
unzip elasticsearch-analysis-ik-7.4.2.zip rm -f elasticsearch-analysis-ik-7.4.2.zip
-
重启容器(重启之前可以进入容器内部康康是否安装成功)
使用ik分词器
ik分词器提供了两个分词算法ik_smart
和 ik_max_word
,其中 ik_smart 为最少切分,ik_max_word为最细粒度划分
从效果可以看出来,比上面的standard分词器要好很多。
自定义扩展词库
ik分词器默认的词库并不支持一些新的词汇,比如“尚硅谷”,ik分词器的词库中是没有的。所以很多网络用语、新型词汇,ik分词器都不能识别,我们就需要自己来扩展它的词库。
安装nginx
-
随便创建并启动一个nginx实例,只是为了复制出配置
docker pull nginx:1.10 docker run -p 80:80 --name nginx -d <nginx容器id>
-
将容器内的配置文件拷贝到当前目录,并修改文件夹名称
// 先创建一个文件夹 /mydata/nginx mkdir /mydata/nginx // 进入该文件夹 cd /mydata/nginx // 拷贝目录 docker container cp nginx:/etc/nginx . // 修改拷贝出来的文件夹名称为conf mv nginx conf
-
终止原容器,并删除原容器
docker stop <nginx容器id> docker rm <nginx容器id>
-
创建新的nginx容器
docker run -p 80:80 --name nginx \ -v /mydata/nginx/html:/usr/share/nginx/html \ -v /mydata/nginx/logs:/var/log/nginx \ -v /mydata/nginx/conf:/etc/nginx \ -d <nginx容器id>
此时尝试访问nginx:
403是因为这个nginx没有设置index.html
。所以我们到挂载的目录中新建一个index.html
。
配置自定义词库
-
在
nginx
的html
目录下,新建一个es
目录,在es
目录中新建一个fenci.txt
,里面写入自定义的词库,然后保存退出。这时候使用路径可以直接访问该文件。 -
修改配置文件,配置远程自定义字典
远程字典的位置就是刚才能够直接访问的词库地址。
-
重启es。
docker restart <es容器id>
-
测试
如果分词失败的很可能是编码问题。需要将虚拟机内部的编码改为utf-8的编码方式。
具体操作可参考:https://blog.csdn.net/dreaming317/article/details/120839655
springboot整合
操作库的选择
9200:HTTP
JestClient:非官方,更新慢
RestTemplate:模拟HTTP请求,es很多操作需要自己封装,麻烦
HttpClient:同上
Elasticsearch-Rest-Client:官方RestClient,封装了es操作,API层次分明,上手简单
整合
-
再单独创建一个检索服务
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-6kZsgpmG-1665370313097)(https://img-beg.oss-cn-hangzhou.aliyuncs.com/img/创建检索服务.gif)]
-
引入依赖。我的
elasticsearch
的版本是7.4.2
的,所以依赖的版本也要相同。<dependency> <groupId>org.elasticsearch.client</groupId> <artifactId>elasticsearch-rest-high-level-client</artifactId> <version>7.4.2</version> </dependency>
-
打开maven可以看到,
elasticsearch
的版本却不是7.4.2
,所以需要修改elasticsearch
的版本SpringBoot已经对elasticsearch的版本做了管理。
-
直接在
pom.xml
中添加指定版本号<properties> <java.version>1.8</java.version> <elasticsearch.version>7.4.2</elasticsearch.version> </properties>
-
编写配置,给容器中注入一个
RestHighLevelClient
。package com.example.gulimall.search.config; import org.apache.http.HttpHost; import org.elasticsearch.client.RestClient; import org.elasticsearch.client.RestClientBuilder; import org.elasticsearch.client.RestHighLevelClient; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @Configuration public class GulimallElasticsearchConfig { @Bean public RestHighLevelClient esRestClient(){ RestClientBuilder builder = null; builder = RestClient.builder(new HttpHost("192.168.121.138", 9200, "http")); RestHighLevelClient client = new RestHighLevelClient(builder); return client; } }
-
测试
@SpringBootTest public class GulimallSearchApplicationTests { @Autowired private RestHighLevelClient client; @Test public void contextLoads() { System.out.println(client); } }
注意pom.xml中的各种配置,特别是SpringBoot和SpringCloud的版本冲突问题。
测试保存数据
官网Java API文档:https://www.elastic.co/guide/en/elasticsearch/client/java-rest/7.4/java-rest-high-document-index.html
所有的RestHighLevelClient都可以使用RequestOptions来定义你的所有请求,并且其不会改变Elasticsearch执行请求的方式。所以我们需要定义一个RequestOptions。
- 定义
RequestOptions
。
@Configuration
public class GulimallElasticsearchConfig {
public static final RequestOptions COMMON_OPTIONS;
static {
RequestOptions.Builder builder = RequestOptions.DEFAULT.toBuilder();
// builder.addHeader("Authorization", "Bearer " + TOKEN);
// builder.setHttpAsyncResponseConsumerFactory(
// new HttpAsyncResponseConsumerFactory
// .HeapBufferedResponseConsumerFactory(30 * 1024 * 1024 * 1024));
COMMON_OPTIONS = builder.build();
}
}
- 测试
@SpringBootTest
public class GulimallSearchApplicationTests {
@Autowired
private RestHighLevelClient client;
@Test
public void indexData() throws IOException {
// 一个参数:索引的名称,不存在会自动创建
IndexRequest request = new IndexRequest("users");
// 指定要插入数据的id,不指定则自动生成
request.id("1");
// 实例化一个user
User user = new User();
user.setName("李四");
user.setAge(22);
user.setGender("男");
// 将user对象解析为json
String userJson = JSON.toJSONString(user);
// 构建文档(这个json就是文档)
request.source(userJson, XContentType.JSON);
// 同步执行,使用定义的RequestOptions
IndexResponse indexResponse = client.index(request, GulimallElasticsearchConfig.COMMON_OPTIONS);
System.out.println(indexResponse);
}
@Data
static class User{
private String name;
private String gender;
private int age;
}
}
测试检索数据
官方文档:https://www.elastic.co/guide/en/elasticsearch/client/java-rest/7.4/java-rest-high-search.html
// 搜索address中包含mill的所有人的年龄分布以及平均年龄
GET /bank/_search
{
"query": {
"match": {
"address": "mill"
}
},
"aggs": {
"ageAggs": {
"terms": {
"field": "age",
"size": 10
}
},
"ageAvg":{
"avg": {
"field": "age"
}
}
}
}
使用Java来完成上方的检索
@Test
public void searchData() throws IOException{
// 指定查询bank索引
SearchRequest searchRequest = new SearchRequest("bank");
SearchSourceBuilder builder = new SearchSourceBuilder();
// 查询address中包含mill的所有人
MatchQueryBuilder matchQuery = QueryBuilders.matchQuery("address", "mill");
// 查询年龄分布。使用term聚合函数,函数名称为“ageAggs”;
// terms中的size表示这种分布可能有多少种情况。若实际情况的个数大于size值,则只显示size值的数量。
TermsAggregationBuilder ageAggs = AggregationBuilders.terms("ageAggs");
ageAggs.field("age");
ageAggs.size(10);
// 查询平均年龄。
AvgAggregationBuilder ageAvg = AggregationBuilders.avg("ageAvg");
ageAvg.field("age");
builder.query(matchQuery).aggregation(ageAggs).aggregation(ageAvg);
// 构建文档
searchRequest.source(builder);
// 同步执行
SearchResponse response = client.search(searchRequest, GulimallElasticsearchConfig.COMMON_OPTIONS);
System.out.println(response);
}
和MP的条件构造器挺相似的。
获取解析信息
/**
* 搜索address中包含mill的所有人的年龄分布以及平均年龄
*/
@Test
public void searchData() throws IOException {
// 指定查询bank索引
SearchRequest searchRequest = new SearchRequest("bank");
SearchSourceBuilder builder = new SearchSourceBuilder();
// 查询address中包含mill的所有人
MatchQueryBuilder matchQuery = QueryBuilders.matchQuery("address", "mill");
// 查询年龄分布。使用term聚合函数,函数名称为“ageAggs”;
// terms中的size表示这种分布可能有多少种情况。若实际情况的个数大于size值,则只显示size值的数量。
TermsAggregationBuilder ageAggs = AggregationBuilders.terms("ageAggs");
ageAggs.field("age");
ageAggs.size(10);
// 查询平均年龄。
AvgAggregationBuilder ageAvg = AggregationBuilders.avg("ageAvg");
ageAvg.field("age");
builder.query(matchQuery).aggregation(ageAggs).aggregation(ageAvg);
// 构建文档
searchRequest.source(builder);
// 同步执行
SearchResponse response = client.search(searchRequest, GulimallElasticsearchConfig.COMMON_OPTIONS);
// System.out.println(response);
SearchHit[] searchHits = response.getHits().getHits();
for (SearchHit hit : searchHits) {
// hit.getIndex();hit.getId();hit.getScore();
// _source
String sourceString = hit.getSourceAsString();
Account account = JSON.parseObject(sourceString, Account.class);
System.out.println("account=" + account);
}
// 获取聚合结果
Aggregations aggregations = response.getAggregations();
// lterms#ageAggs
Terms termsAgeAvg = aggregations.get("ageAggs");
for (Terms.Bucket bucket : termsAgeAvg.getBuckets()) {
String key = bucket.getKeyAsString();
long docCount = bucket.getDocCount();
System.out.println("年龄:" + key + ",出现次数:" + docCount);
}
// avg#ageAvg
Avg avgAgeAvg = aggregations.get("ageAvg");
double value = avgAgeAvg.getValue();
System.out.println("平均年龄:" + value);
}