Elasticsearch 是一个分布式、RESTful 风格的搜索和数据分析引擎,能够解决不断涌现出的各种用例。 作为 Elastic Stack 的核心,它集中存储您的数据,帮助您发现意料之中以及意料之外的情况。
MySQL中也有检索数据的方式,可是为什么不适合呢?因为术业有专攻,MySQL是专攻于数据的持久化的存储与管理,但是要做海量数据的检索和分析,Elasticsearch更在行。
Elasticsearch是面向文档的,这意味着它可以存储整个对象或文档。然而它不仅仅是存储,还会索引每个文档的内容使之可以被搜索。在Elasticsearch中,你可以对文档(而非成行成列的数据)进行索引、搜索、排序、过滤。Elasticsearch比传统关系型数据库如下:
关系型数据库:Database --> Table --> Row --> Column
Elasticsearch:Index --> Type --> Document --> Field
动词,相当于MySQL的insert
,
名称,相当于MySQL的Database
。
在index(索引)中,可以定义一个或多个类型。
类似于MySQL中的Table;每一种类型的数据放在一起。
保存在某个索引(Index)下,某种类型(Type)的一个数据(Document),文档是Json格式的,Document就像是MySQL中的某个Table里面的内容。
// 安装elasticsearch
docker pull elasticsearch:7.4.2
// 安装Elasticsearch的开源分析和可视化平台
docker pull kibana:7.4.2
// 提前创建相关目录,为了将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/share/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
然后再外部访问到此数据才算成功
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
GET / _cat/nodes:查看集群中所有节点
GET / _cat/health:查看es健康状况
GET / _cat/master:查看主节点
GET / _cat/indecis:查看所有索引(类似于 show databases;)
保存一个数据,保存在哪个索引的哪个类型下,指定用哪一个唯一标识。
// 在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的话,那也是更新操作。
GET / customer/external/1
老版是使用版本号做乐观锁,新版使用了序列号。
使用乐观锁更新的话,需要在保存语句后加上 ?if_seq_no=0&if_primary_term=1
// 带_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更新” 效果相同。
DELETE / customer/external/1
DELETE / customer
删除不存在的文档,result会显示“not_found”
POST /customer/external/_bulk
{"index":{"_id":"1"}}
{"name": "John Doe"}
{"index":{"_id":"2"}}
{"name": "Jane Doe"}
两行为一个整体。
每一个整体都是独立操作的,不同于事务。无论前面的是否成功,后面的依然执行。
// 语法格式
{action:{metadata}}\n
{requestbody }
{action:{metadata}}\n
{requestbody }
POST /bank/account/_bulk
测试数据:https://gitee.com/xlh_blog/common_content/blob/master/es%E6%B5%8B%E8%AF%95%E6%95%B0%E6%8D%AE.json#
es支持两种基本方式检索:
_search
开始语句 | 含义 |
---|---|
GET /bank/_search | 检索bank下所有信息,包括type和docs |
GET /bank/_search?q=*&sort=account_number:asc | 请求参数方式检索 |
q=* :查询所有
sort=account_number:asc :安装account_number字段排序,升序排序
asc :升序排序
GET /bank/_search
{
"query":{
"match_all":{}
},
"sort":[
{
"account_number":"asc"
}
]
}
官方文档: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", ...]
}
这种匹配是全文检索。
若传了两个单词,则会 按空格分词 + 全文检索
_score :得分
max_score :最高得分
全文检索会按照得分进行排序
将需要匹配的值当成一个整体单词,不分词进行匹配。
这个也是会进行 按空格分词 + 检索
bool用来做复合查询:
复合语句可以合并任何其他查询语句,包括复合语句,了解这一点是很重要的。这就意味着,复合语句之间可以互相嵌套,可以表达非常复杂的逻辑。
bool中的条件是且的关系。
一个match中只能写一个条件,多个条件要多个match。
满足should的能够提高相关性得分。must和must_not也能够提供相关性得分。
should、must、must_not都能够筛选,并且提供相关性得分。
filter也是过滤,功能类似于must,但是不提供相关性得分。
和match一样,匹配某个属性的值。全文检索用match,其他非text字段使用term更好。
聚合提供了从数据中分组和提取数据的能力。最简单的聚合方法大致等于SQL GROUP BY和SQL聚合函数。在Elasticsearch中,有执行搜索并返回hits(命中结果),并且同时返回聚合结果,把一个响应中的hits分隔开的能力。这是非常强大且有效的,你可以执行查询和多个聚合,并且在一次使用中得到各自的返回结果,使用一次简洁和简化的API来避免网络往返。
terms中的size表示这种分布可能有多少种情况。若实际情况的个数大于size值,则只显示size值的数量。
多重聚合。
# 查出所有年龄分布,并且这些年龄段中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"
}
}
}
}
作用:定义数据库中表的结构,通过mapping来控制索引存储数据的位置。不做映射直接创建索引的话,索引会帮你自动映射,但是映射的类型不一定跟你想的一样。
创建索引不建议再带上类型(type)了。
PUT /my_index/_mapping
{
"properties" :{
"employee-id":{
"type": "keyword",
"index": false
}
}
}
对于已经存在的映射字段,我们不能更新。更新必须创建新的索引并进行数据迁移。
// 数据迁移,固定写法。
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(自定义分词器)。
标准分词器,但是它是支持英文的,不太支持中文。
所以我们需要一款能够识别中文的分词器,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_smart
和 ik_max_word
,其中 ik_smart 为最少切分,ik_max_word为最细粒度划分
从效果可以看出来,比上面的standard分词器要好很多。
ik分词器默认的词库并不支持一些新的词汇,比如“尚硅谷”,ik分词器的词库中是没有的。所以很多网络用语、新型词汇,ik分词器都不能识别,我们就需要自己来扩展它的词库。
随便创建并启动一个nginx实例,只是为了复制出配置
docker pull nginx:1.10
docker run -p 80:80 --name nginx -d
将容器内的配置文件拷贝到当前目录,并修改文件夹名称
// 先创建一个文件夹 /mydata/nginx
mkdir /mydata/nginx
// 进入该文件夹
cd /mydata/nginx
// 拷贝目录
docker container cp nginx:/etc/nginx .
// 修改拷贝出来的文件夹名称为conf
mv nginx conf
终止原容器,并删除原容器
docker stop
docker rm
创建新的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:
403是因为这个nginx没有设置index.html
。所以我们到挂载的目录中新建一个index.html
。
在nginx
的html
目录下,新建一个es
目录,在es
目录中新建一个fenci.txt
,里面写入自定义的词库,然后保存退出。这时候使用路径可以直接访问该文件。
修改配置文件,配置远程自定义字典
远程字典的位置就是刚才能够直接访问的词库地址。
重启es。
docker restart
测试
如果分词失败的很可能是编码问题。需要将虚拟机内部的编码改为utf-8的编码方式。
具体操作可参考:https://blog.csdn.net/dreaming317/article/details/120839655
操作库的选择
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/%E5%88%9B%E5%BB%BA%E6%A3%80%E7%B4%A2%E6%9C%8D%E5%8A%A1.gif)]
引入依赖。我的elasticsearch
的版本是7.4.2
的,所以依赖的版本也要相同。
<dependency>
<groupId>org.elasticsearch.clientgroupId>
<artifactId>elasticsearch-rest-high-level-clientartifactId>
<version>7.4.2version>
dependency>
打开maven可以看到,elasticsearch
的版本却不是7.4.2
,所以需要修改elasticsearch
的版本
SpringBoot已经对elasticsearch的版本做了管理。
直接在pom.xml
中添加指定版本号
<properties>
<java.version>1.8java.version>
<elasticsearch.version>7.4.2elasticsearch.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);
}