官网https://www.elastic.co/cn/what-is/elasticsearch
**全文搜索属于最常见的需求,开源的 Elasticsearch 是目前全文搜索引擎的首选。
它可以快速地储存、搜索和分析海量数据。**维基百科、Stack Overflow、Github 都采用它。
Elastic 是 Lucene 的封装,提供了 REST API (天然的跨平台)的操作接口,开箱即用。
官方文档:https://www.elastic.co/guide/en/elasticsearch/reference/current/index.html
官方中文:https://www.elastic.co/guide/cn/elasticsearch/guide/current/foreword_id.html
社区中文:
https://es.xiaoleilu.com/index.html
http://doc.codingdict.com/elasticsearch/0/
是什么?
一个全文搜索引擎。
有什么用?
海量数据全文搜索、存储、分析。
解决什么问题?
当下技术存储数据查找搜索慢。
场景
在8.0版本正式删除类型概念。
#elasticSearch
1、Index(索引)
相当于MySQL的库概念
2、Type(类型)
相当于MySQL的Table概念
3、Document(文档)
文档为 JSON 格式,Document 就像是 MySQL 中的某个 Table 里面的内容
4、倒排索引机制
使用分词技术分词,存储分词和相关文档记录。能极大提高相关查询效率。
docker pull elasticsearch:7.4.2
存储和检索数据
docker pull kibana:7.4.2
可视化检索数据
1.ElasticSearch
# 创建ES配置目录
mkdir -p /home/hacah/dockerdata/elasticsearch/config
mkdir -p /home/hacah/dockerdata/elasticsearch/data
echo "http.host: 0.0.0.0" >> /home/hacah/dockerdata/elasticsearch/config/elasticsearch.yml
# 保证权限
chmod -R 777 /home/hacah/dockerdata/elasticsearch/
# 启动
docker run --name elasticsearch -p 9200:9200 -p 9300:9300 \
-e "discovery.type=single-node" \
-e ES_JAVA_OPTS="-Xms64m -Xmx512m" \
-v /home/hacah/dockerdata/elasticsearch/config/elasticsearch.yml:/usr/share/elasticsearch/config/elasticsearch.yml \
-v /home/hacah/dockerdata/elasticsearch/data:/usr/share/elasticsearch/data \
-v /home/hacah/dockerdata/elasticsearch/plugins:/usr/share/elasticsearch/plugins \
-d elasticsearch:7.4.2
特别注意:
-e ES_JAVA_OPTS=“-Xms64m -Xmx512m” \ 测试环境下,设置 ES 的初始内存和最大内存。
2.Kibana
docker run --name kibana -e ELASTICSEARCH_HOSTS=http://192.168.31.158:9200 -p 5601:5601 \
-d kibana:7.4.2
_cat
使用GET /_cat/nodes
:查看所有节点GET /_cat/health
:查看 es 健康状况GET /_cat/master
:查看主节点GET /_cat/indices
:查看所有索引指定保存在哪个索引的哪个类型下,指定用哪个唯一标识。
PUT customer/external/1 是在 customer 索引下的 external 类型下保存 1 号数据
保存可以使用PUT或POST请求
POST :可以新增可以修改。如果不指定 id,会自动生成 id,指定 id 就会修改这个数据,并新增版本号。
# 不指定id
http://192.168.31.158:9200/customer/external
# 指定id
http://192.168.31.158:9200/customer/external/2
PUT: 可以新增可以修改,但一定需要指定id,不指定 id 会报错。由于 PUT 需要指定 id,我们一般都用来做修改操作,请求url方式与post一样。
GET customer/external/1
{
"_index": "customer",//在哪个索引
"_type": "external",//在哪个类型
"_id": "1",//记录 id
"_version": 2,//版本号
"_seq_no": 1,//并发控制字段,每次更新就会+1,用来做乐观锁
"_primary_term": 1,//同上,主分片重新分配,如重启,就会变化
"found": true,
"_source": { //真正的内容
"name": "John Doe"
}
}
POST有两种操作:
POST customer/external/1/_update
{
"doc":{
"name": "John de" // 更新内容
}
}
其中的doc结构是固定的,更新的数据在其中。
第二种:
POST customer/external/1
{
"name": "John dt" // 更新内容
}
如何确定使用哪种呢?
基于场景,对于大并发更新,不带 update;
对于大并发查询偶尔更新,带 update;对比更新,重新计算分配规则。
PUT只能使用不带_update的更新且用法与POST类似。
# 删除文档
DELETE customer/external/1
# 删除索引
DELETE customer
bulk用于批量操作,使用操作如下:
POST customer/external/_bulk
{ action: { metadata }}\n
{ request body}\n
url添加_bulk,请求体中两条数据为一个操作。
比如:
POST /_bulk
{ "delete": { "_index": "website", "_type": "blog", "_id": "123" }}
{ "create": { "_index": "website", "_type": "blog", "_id": "123" }}
{ "title":"My first blog post" }
{ "index":{ "_index": "website", "_type": "blog" }}
{ "title":"My second blog post" }
{ "update": { "_index": "website", "_type": "blog", "_id": "123", "_retry_on_conflict" : 3} }
{ "doc" : {"title" : "My updated blog post"} }
在kibana执行以上指令。
bulk API 以此按顺序执行所有的 action(动作)。如果一个单个的动作因任何原因而失败,
它将继续处理它后面剩余的动作。当 bulk API 返回时,它将提供每个动作的状态。
顾客银行账户信息的虚构的 JSON文档样本
POST bank/account/_bulk
https://github.com/elastic/elasticsearch/blob/7.4/docs/src/test/resources/accounts.json
执行上面的数据指令。
ES 支持两种基本方式检索 :
uri参数检索
# 检索 bank 下所有信息,包括 type 和 docs
GET bank/_search
# 请求参数方式检索
GET bank/_search?q=*&sort=account_number:asc
# 得到的响应属性解析
took - Elasticsearch 执行搜索的时间(毫秒)
time_out - 告诉我们搜索是否超时
_shards - 告诉我们多少个分片被搜索了,以及统计了成功/失败的搜索分片
hits - 搜索结果
hits.total - 搜索结果
hits.hits - 实际的搜索结果数组(默认为前 10 的文档)
sort - 结果的排序 key(键)(没有则按 score 排序)
score 和 max_score –相关性得分和最高得分(全文检索用)
请求体进行检索
GET bank/_search
{
"query": {
"match_all": {}
},
"sort": [
{
"account_number": {
"order": "desc"
}
}
]
}
使用请求工具就改为POST请求。
Elasticsearch 提供了一个可以执行查询的 Json 风格的 DSL(domain-specific language 领域特
定语言)。这个被称为 Query DSL。
一个查询语句的典型结构:
{
QUERY_NAME: {
ARGUMENT: VALUE,
ARGUMENT: VALUE,...
}
}
如果是针对某个字段,那么它的结构如下:
{
QUERY_NAME: {
FIELD_NAME: {
ARGUMENT: VALUE,
ARGUMENT: VALUE,...
}
}
}
使用例子:
GET bank/_search
{
"query": {
"match_all": {}
},
"from": 0,
"size": 5,
"sort": [
{
"account_number": {
"order": "desc"
}
}
],
"_source": ["age","balance"]
}
query 定义如何查询,
match_all 查询类型【代表查询所有的所有】,es 中可以在 query 中组合非常多的查
询类型完成复杂查询
除了 query 参数之外,我们也可以传递其它的参数以改变查询结果。如 sort,size
from+size 限定,完成分页功能
sort 排序,多字段排序,会在前序字段相等时后续字段内部排序,否则以前序为准
"_source"定义返回字段
可以分为数值匹配和字符串全文索引,数值匹配会查询完全相同的数据,而字符串全文索引会按分词查询相关度的数据(需要使用单词查询)。
基本类型(非字符串),精确匹配:
GET bank/_search
{
"query": {
"match": {
"balance": "16418"
}
}
}
字符串全文索引
GET bank/_search
{
"query": {
"match": {
"address": "Road 263"
}
}
}
查询出相关数据与相关度得分。
短语匹配会把查询值当做不可分割的整体查询,即对比match来说不进行分词查询,把短语作为整体查询出相关度。
GET bank/_search
{
"query": {
"match_phrase": {
"address": "Aviation Road"
}
}
}
查询包含Aviation Road
的数据。
match的多字段版本
GET bank/_search
{
"query": {
"multi_match": {
"query": "Road",
"fields": ["address", "state"]
}
}
}
查询 address或者state两个字段的匹配Road数据。
复合语句可以合并任何其它查询语句,包括复合语句,复合语句之间可以互相嵌套,可以表达非常复杂的逻辑。
GET bank/_search
{
"query": {
"bool": {
"must": [
{"match": {
"address": "Road"
}},
{"match": {
"gender": "M"
}}
],
"must_not": [
{"match_phrase": {
"city": "Calpine"
}}
],
"should": [
{"match": {
"age": "26"
}}
]
}
}
}
官方文档
使用filter和正常的匹配都能够查询自己想要的结果。filter不同的是不会对过滤结果产生得分。
GET bank/_search
{
"query": {
"bool": {
"must": [
{"match": {
"address": "Road"
}}
],
"filter": {
"range": {
"age": {
"gte": 10,
"lte": 30
}
}
}
}
}
}
对比:
GET bank/_search
{
"query": {
"bool": {
"must": [
{"match": {
"address": "Road"
}},
{"range": {
"age": {
"gte": 10,
"lte": 30
}
}}
]
}
}
}
}
官方文档:https://www.elastic.co/guide/en/elasticsearch/reference/8.5/query-dsl-term-query.html
用与查找基于精确值的查找。使用类似match。所以精确值我们使用term查询,文本字段值使用匹配match查询。
GET bank/_search
{
"query": {
"bool": {
"must": [
{"match": {
"address": "Road"
}},
{"term": {
"age": {
"value": "26"
}
}}
]
}
}
}
字符串使用.keywork精确匹配,语法:字段.keywork
,可用于match或term等之下。
GET bank/_search
{
"query": {
"bool": {
"must": [
{"match": {
"address": "Road"
}},
{"match": {
"firstname.keyword": "Hardin"
}}
]
}
}
}
聚合提供了从数据中分组和提取数据的能力。最简单的聚合方法大致等于 SQL GROUP
BY 和 SQL 聚合函数。
搜索时可以执行查询和多个聚合,聚合里也能嵌套聚合。一次返回多个结果。
完成一下例子:
GET bank/_search
{
"query": {
"match": {
"address": "mill"
}
},
"size": 0,
"aggs": {
"age_group": {
"terms": {
"field": "age",
"size": 10
}
},
"age_avg":{
"avg": {
"field": "age"
}
}
}
}
size:0
不显示搜索数据
aggs:执行聚合。聚合语法如下
"aggs": {
"aggs_name 这次聚合的名字,方便展示在结果集中": {
"AGG_TYPE 聚合的类型(avg,term,terms)": {}
}
}
GET bank/_search
{
"aggs": {
"agg_group": {
"terms": {
"field": "age",
"size": 100
},
"aggs": {
"balance_avg": {
"avg": {
"field": "balance"
}
}
}
}
},
"size": 1
}
GET bank/_search
{
"aggs": {
"agg_group": {
"terms": {
"field": "age",
"size": 100
},
"aggs": {
"age_group": {
"terms": {
"field": "gender.keyword",
"size": 100
},"aggs": {
"blan_avg": {
"avg": {
"field": "balance"
}
}
}
},
"avg_bl":{
"avg": {
"field": "balance"
}
}
}
}
},
"size": 1
}
Mapping 是用来定义一个文档(document),以及它所包含的属性(field)是如何存储和索引的。
比如,使用 mapping 来定义:
官方文档
查看 mapping 信息:
GET bank/_mapping
可以查看具体属性的类型。
字段类型有一下几种:
关系型数据库中两个数据表示是独立的,即使他们里面有相同名称的列也不影响使用,但ES
中不是这样的。elasticsearch是基于Lucene开发的搜索引擎,而ES中不同type下名称相同
的filed最终在Lucene中的处理方式是一样的。
Elasticsearch 7.x
Elasticsearch 8.x
解决:
创建索引并指定映射
PUT /my_index
{
"mappings": {
"properties": {
"age": {"type": "integer"},
"email":{"type": "keyword"},
"name":{"type": "text"}
}
}
}
PUT /my_index/_mapping
{
"properties":{
"employee-id":{
"type": "keyword",
"index": false
}
}
}
已经存在的索引字段类型是不能更新的。但可以使用创建新的索引并把数据迁移过去的方式更新索引。
POST /_reindex
{
"source": {
"index": "users"
},
"dest": {
"index": "new_users"
}
}
其中users是源索引,new_users是新索引,其他不变。
映射文档
一个 tokenizer(分词器)接收一个字符流,将之分割为独立的 tokens(词元,通常是独立
的单词),然后输出 tokens 流。
例如,whitespace tokenizer 遇到空白字符时分割文本。它会将文本 “Quick brown fox!” 分割
为 [Quick, brown, fox!]。
该 tokenizer(分词器)还负责记录各个 term(词条)的顺序或 position 位置(用于 phrase 短
语和 word proximity 词近邻查询),以及 term(词条)所代表的原始 word(单词)的 start
(起始)和 end(结束)的 character offsets(字符偏移量)(用于高亮显示搜索的内容)。
Elasticsearch 提供了很多内置的分词器,可以用来构建 custom analyzers(自定义分词器)。
地址:https://github.com/medcl/elasticsearch-analysis-ik
下载ik分词插件
https://github.com/medcl/elasticsearch-analysis-ik/releases/download/v7.4.2/elasticsearch-anal
ysis-ik-7.4.2.zip
移动到es 容器内部 plugins 目录,解压压缩包
确认是否安装好了分词器
在容器内部的elasticsearch的bin目录下
elasticsearch-plugin list
即可列出系统的分词器
注意:需要重启才能使用新的分词器。
POST _analyze
{
"text": "我是个中国人"
}
POST _analyze
{
"analyzer": "ik_smart",
"text": "我是中国人"
}
POST _analyze
{
"analyzer": "ik_max_word",
"text": "我是中国人"
}
修改/usr/share/elasticsearch/plugins/ik/config/中的 IKAnalyzer.cfg.xml
IK Analyzer 扩展配置
http://192.168.128.130/fenci/myword.txt
在下面这个标签里能够配置远程文件地址来扩张自定义分词。
http://192.168.128.130/fenci/myword.txt
我们可以采用Nginx搭建这个提供分词文件的服务。[[技术总结#Docker安装Nginx | Nginx服务搭建教程]]
服务地址:http://192.168.31.158/es/fenci.txt
配置文件:
IK Analyzer 扩展配置
http://192.168.31.158/es/fenci.txt
分词文件:
网络的
键盘侠
我们把配置文件修改之后,分词文件也把分词加上了,重启es,之后就能测试扩展的分词了。
POST _analyze
{
"analyzer": "ik_smart",
"text": "网络的键盘侠"
}
结果:
{
"tokens" : [
{
"token" : "网络的",
"start_offset" : 0,
"end_offset" : 3,
"type" : "CN_WORD",
"position" : 0
},
{
"token" : "键盘侠",
"start_offset" : 3,
"end_offset" : 6,
"type" : "CN_WORD",
"position" : 1
}
]
}
网络上很多相关的工具能够使用Java操作ElasticSearch,常用如下:
1.从9300端口通过TCP操作
springboot 版本不同, transport-api.jar 不同,不能适配 es 版本
而且9300端口7.x 已经不建议使用,8 以后就要废弃。
2.从9200端口通过HTTP操作
最终选择 Elasticsearch-Rest-Client(elasticsearch-rest-high-level-client)
https://www.elastic.co/guide/en/elasticsearch/client/java-rest/current/java-rest-high.html
<dependency>
<groupId>org.elasticsearch.clientgroupId>
<artifactId>elasticsearch-rest-high-level-clientartifactId>
<version>7.4.2version>
dependency>
package com.atguigu.gulimall.search.config;
import org.apache.http.HttpHost;
import org.elasticsearch.client.HttpAsyncResponseConsumerFactory;
import org.elasticsearch.client.RequestOptions;
import org.elasticsearch.client.RestClient;
import org.elasticsearch.client.RestHighLevelClient;
import org.elasticsearch.common.inject.BindingAnnotation;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* 使用引入步骤:
* 1.导入依赖
* 2.配置、代码
* 3.测试
*
* @author Hacah
* @date 2022/11/4 11:46
*/
@Configuration
public class ElasticSearchConfig {
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();
}
@Bean
public RestHighLevelClient ElasticSearchClient(){
RestHighLevelClient client = new RestHighLevelClient(
RestClient.builder(
new HttpHost("192.168.31.158", 9200, "http")));
return client;
}
}
/**
* 测试存储ES
* 更新也行
*/
@Test
public void indexData() throws IOException {
IndexRequest indexRequest = new IndexRequest("users");
indexRequest.id("1");
User user = new User();
user.setUsername("list");
user.setAge(12);
user.setEmail("[email protected]");
String jsonString = JSON.toJSONString(user);
indexRequest.source(jsonString, XContentType.JSON);
// 提交
IndexResponse index = restHighLevelClient.index(indexRequest, ElasticSearchConfig.COMMON_OPTIONS);
System.out.println(index);
}
class User{
private String username;
private Integer age;
private String email;
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public Integer getAge() {
return age;
}
public void setAge(Integer age) {
this.age = age;
}
public String getEmail() {
return email;
}
public void setEmail(String email) {
this.email = email;
}
}
搜索测试:
/**
* 搜索信息
* https://www.elastic.co/guide/en/elasticsearch/client/java-rest/current/java-rest-high-search.html
*
* # 搜索 address 中包含 mill 的所有人的年龄分布以及平均年龄,但不显示这些人的详情
* GET bank/_search
* {
* "query": {
* "match": {
* "address": "mill"
* }
* },
* "size": 10,
* "aggs": {
* "age_group": {
* "terms": {
* "field": "age",
* "size": 10
* }
* },
* "age_avg":{
* "avg": {
* "field": "age"
* }
* }
* }
* }
*/
@Test
public void searchData() throws IOException {
SearchRequest searchRequest = new SearchRequest();
SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder();
// query
searchSourceBuilder.query(QueryBuilders.matchQuery("address", "mill")).size(10);
// aggs
TermsAggregationBuilder termsAggregationBuilder = AggregationBuilders.terms("age_group").field("age").size(10);
AvgAggregationBuilder avgAggregationBuilder = AggregationBuilders.avg("age_avg").field("age");
searchSourceBuilder.aggregation(termsAggregationBuilder);
searchSourceBuilder.aggregation(avgAggregationBuilder);
System.out.println("查询参数: "+searchSourceBuilder.toString());
searchRequest.source(searchSourceBuilder);
SearchResponse search = restHighLevelClient.search(searchRequest, ElasticSearchConfig.COMMON_OPTIONS);
System.out.println("查询结果: "+search.toString());
SearchHits hits = search.getHits();
TotalHits totalHits = hits.getTotalHits();
System.out.println("命中数量: "+totalHits);
for (SearchHit hit : hits.getHits()) {
System.out.println("每一个数据:"+hit.getSourceAsString());
}
// 统计数据
Aggregations aggregations = search.getAggregations();
StringBuilder sjoin = new StringBuilder();
for (Aggregation aggregation : aggregations.asList()) {
String name = aggregation.getName();
sjoin.append(name + " ");
}
System.out.println("统计的名称有:"+sjoin);
// age_group
Terms ageGroup = aggregations.get("age_group");
List<? extends Terms.Bucket> buckets = ageGroup.getBuckets();
for (Terms.Bucket bucket : buckets) {
String keyAsString = bucket.getKeyAsString();
long docCount = bucket.getDocCount();
System.out.println("age类型是:"+keyAsString+",数量是:"+docCount);
}
// age_avg
Avg ageAvg = aggregations.get("age_avg");
double value = ageAvg.getValue();
System.out.println("age_avg平均:"+value);
}