开源的Elasticsearch是目前全文搜索引擎的首选。它可以快速地存储、搜索和分析海量数据
官方文档
1、Index (索引)
动词,相当于MySQL中的insert
名词,相当于MySQL中的Database
2、Type (类型)
在Index(索引)中,可以定义一个或多个类型。
类似于MySQL中的Table,每一种类型的数据放在一起
3、Document(文档)
保存在某个索引(Index)下,某种类型(Type)的一条数据(Document),文档是JSON格式的,Document就像是MySQL中的某个Table里面的内容。
4、倒排索引机制
# 安装elasticsearch
docker pull elasticsearch:7.4.2
# 安装kibana(可视化)
docker pull kibana:7.4.2
# 创建外部目录 将es数据挂载出来
mkdir -p /Users/jinchengming/mydata/elasticsearch/config
mkdir -p /Users/jinchengming/mydata/elasticsearch/data
# es配置文件 即将来外部任何机器都可以访问
echo "http.host: 0.0.0.0">>/Users/jinchengming/mydata/elasticsearch/config/elasticsearch.yml
# 启动 elasticsearch
# 9200 外部访问api的端口 9300集群部署时 节点间通信端口
docker run --name elasticsearch -p 9200:9200 -p 9300:9300 -e "discovery.type=single-node" -e ES_JAVA_OPTS="-Xms64m -Xmx512m" -v /Users/jinchengming/mydata/elasticsearch/config/elasticsearch.yml:/usr/share/elasticsearch/config/elasticsearch.yml -v /Users/jinchengming/mydata/elasticsearch/data:/usr/share/elasticsearch/data -v /Users/jinchengming/mydata/elasticsearch/plugins:/usr/share/elasticsearch/plugins -d elasticsearch:7.4.2
简单命令
# 返回的所有节点 如果是集群部署 前面带* 表示当前节点是主节点
# localhost:9200/_cat/nodes
127.0.0.1 50 94 1 0.22 0.08 0.06 dilm * fa09e7661fc2
# 查看es健康状况
# localhost:9200/_cat/health
# 查看主节点
# localhost:9200/_cat/master
# 查看所有索引
# localhost:9200/_cat/indices
# 启动kibana
docker run --name kibana -e ELASTICSEARCH_HOSTS=http://127.0.0.1:9200 -p 5601:5601 -d kibana:7.4.2
# 经过测试 网络不通 日志输出访问不到127.0.1的es服务
# 通过指定容器内ip
docker run --name kibana -e ELASTICSEARCH_HOSTS=http://172.17.0.3:9200 -p 5601:5601 -d kibana:7.4.2
# 查看容器内ip
jinchengming@MacBook-Pro elasticsearch % docker inspect --format='{{.Name}} - {{range.NetworkSettings.Networks}}{{.IPAddress}}{{end}}' $(docker ps -aq)
/kibana - 172.17.0.4
/elasticsearch - 172.17.0.3
/rabbitmq3.7.7 - 172.17.0.2
/mysql01 - 172.17.0.2
/mongodb - 172.17.0.3
成功访问
汉化
进入容器后 ,编辑/usr/local/kibana/config
文件夹下的kibana.yml
文件,新增下面这个配置
i18n.locale: "zh-CN"
语法:请求方式:PUT
案例中的请求表示在customer索引下的external类型下保存一号数据 ,数据在请求体中,是个json
相同的id 再保存一次就是更新操作,result由created变为updated,version加1
关于请求方式
PUT 和 POST都可以
POST新增。如果不指定id,会自动生成id。指定id就会修改这个数据,并新增版本号
PUT可以新增可以修改,PUT必须指定id,由于PUT需要指定id,我们一般都用来做修改操作,不指定id会报错。
GET customer/external/1
返回值:
{
"_index": "customer", // 索引
"_type": "external", // 类型
"_id": "1", // id
"_version": 2, // 版本
"_seq_no": 1, // 并发控制字段,每次更新就会+1,用来做乐观锁
"_primary_term": 1, // 同上,主分片重新分配,如重启,就会变化
"found": true, // 是否查询到数据
"_source": { // 数据内容
"name": "chengming"
}
}
除了上面的POST和PUT带上id可以更新文档外,还可以直接请求上用_update表示更新操作,数据要加上doc将数据包起来
区别:_update操作更新是会检查数据,如果数据和原数据相同 ,则version和_seq_no都不会增加
elasticsearch中没有删除类型的操作
请求中使用_bulk
表示批量,请求体中两行为一条数据,第一行为操作类型和条件,第二行为数据
批量新增数据
`postman不方便测试这种操作,采用kibana测试
可以看出右边的items是对每一条数据新增的单独统计,所以不同于mysql的批量更新,没有事务,每个操作是独立的
导入官网提供的测试数据
测试数据下载地址
成功导入后,查看当前es中所有索引,可以看出刚才导入成功1000条数据
ES支持两种基本方式检索:
uri+检索参数
# 查询bank索引下所有(*)数据,并按照account_number升序
GET bank/_search?q=*&sort=account_number:asc
查询结果:
{
"took" : 30, // 查询时间 :毫秒
"timed_out" : false, // 是否超时
"_shards" : { // 集群相关
"total" : 1,
"successful" : 1,
"skipped" : 0,
"failed" : 0
},
"hits" : {
"total" : {
"value" : 1000,
"relation" : "eq"
},
"max_score" : null, // 得分 查询全部 不存在分数
"hits" : [ // 命中的记录
{
"_index" : "bank",
"_type" : "account",
"_id" : "0",
"_score" : null,
"_source" : { // 数据源信息
"account_number" : 0,
"balance" : 16623,
"firstname" : "Bradshaw",
"lastname" : "Mckenzie",
"age" : 29,
"gender" : "F",
"address" : "244 Columbus Place",
"employer" : "Euron",
"email" : "[email protected]",
"city" : "Hobucken",
"state" : "CO"
},
"sort" : [
0
]
},
...
...
...
]
uri+请求体
GET bank/_search
{
"query": {
"match_all": {}
},
"sort": [
{
"account_number": "asc"
},
{
"balance": "desc"
}
],
// 相当于分页操作
"from": 10,
"size": 10,
"_source": ["balance","account_number"] // 指定返回字段
}
上面案例中的排序规则是简写,完整写法如下
匹配所有
GET bank/_search
{
"query": {
"match_all": {}
},
"sort": [
{
"balance": {
"order": "desc" // 针对balance降序排列
}
}
]
}
全文检索,会对检索条件进行分词匹配,按照倒排索引机制排序
GET bank/_search
{
"query": {
"match": {
"address": "mill lane"
}
}
}
短语匹配,将需要匹配的值当成一个整体单词(不分词)进行检索
GET bank/_search
{
"query": {
"match_phrase": {
"address": "mill lane"
}
}
}
此时返回结果只有一条"address" : "198 Mill Lane"的数据,而match返回多条。
多字段匹配
GET bank/_search
{
"query": {
"multi_match": {
"query": "mill",
"fields": ["state","address"] // 在state或者address中包含mill
}
}
}
复合查询
GET bank/_search
{
"query": {
"bool": { // 复合查询
"must": [ // 必须满足
{
"match": {
"gender": "M"
}
},
{
"match": {
"address": "mill"
}
}
],
"must_not": [ // 必须不满足
{
"match": {
"age": 18
}
}
],
"should": [ // 应该满足,即如果满足,得分高
{"match": {
"lastname": "Wallace"
}}
]
}
}
}
结果过滤 : 并不是所有的查询都需要产生分数,特别是那些仅用于“filtering”(过滤)的文档,为了不计算分数 ES会自动检查场景并且优化查询的执行
即,filter过滤不会贡献分数,仅过滤
GET bank/_search
{
"query": {
"bool": {
"must": [
{
"match": {
"address": "mill"
}
}
],
"filter": {
"range": {
"balance": {
"gte": 10000,
"lte": 20000
}
}
}
}
}
}
和match一样,匹配某个属性的值,全文检索字段用match,其他非text字段匹配用term
GET bank/_search
{
"query": {
"term": {
"age": {
"value": 20
}
}
}
}
如果是地址这种(文本字段)需要检索的,用match 但是年龄(非文本字段)这种需要精确匹配的建议用term
使用match时,如果需要精确匹配,可以使用.keyword
GET bank/_search
{
"query": {
"match": {
"address.keyword":"789 Madison Street"
}
}
}
feild.keyword不同于match_phrase,前者精确匹配,后者为短语匹配,只是不分词而且
执行聚合,聚合提供了从数据中分组和提取数据的能力。最简单的聚合方法大致等于 SQL GROUP BY 和 SQL 聚合函数。
在ES中,有执行搜索返回的hits(命中结果),并且同时返回聚合结果。
把一个响应中的所有命中分割开的能力,这是非常强大且有效的,可以执行查询和多个聚合,并且在一次使用中得到各自的(任何一个的)返回结果,使用一次简洁和简化的API来避免网络往返。
# 搜索address中包含mill的所有人的年龄分布以及平均年龄、平均工资
GET bank/_search
{
"query": {
"match": {
"address": "mill"
}
},
"size": 0, // 不显示搜索数据
"aggs": {
"ageAgg": {
"terms": {
"field": "age",
"size": 10
}
},
"ageAvg":{
"avg": {
"field": "age"
}
},
"balanceAvg":{
"avg": {
"field": "balance"
}
}
}
}
返回结果:
处理匹配返回的hits外,还返回了 aggregations信息
如:返回的年龄分布中,再求个年龄的平均工资
GET bank/_search
{
"query": {
"match_all": {}
},
"aggs": {
"ageAgg": {
"terms": {
"field": "age",
"size": 100
},
"aggs": {
"balanceAvg": {
"avg": {
"field": "balance"
}
}
}
}
}
}
返回结果:
升级:上述分布结果中,再分布求出男和女的平均工资
在对gender(文本字段)进行聚合时,必须使用精确匹配,即使用keyword
GET bank/_search
{
"query": {
"match_all": {}
},
"aggs": {
"ageAgg": {
"terms": {
"field": "age",
"size": 100
},
"aggs": {
"genderAgg": {
"terms": {
"field": "gender.keyword"
},
"aggs": {
"genderBalanceAvg": {
"avg": {
"field": "balance"
}
}
}
},
"balanceAvg": {
"avg": {
"field": "balance"
}
}
}
}
}
}
# 创建索引时指定映射
PUT /my_index
{
"mappings": {
"properties": {
"age":{"type": "integer"},
"email":{"type": "keyword"}, // 只能精确匹配
"name":{"type": "text"}, // 会分词检索
}
}
}
PUT /my_index/_mapping
{
"properties": {
"employee-id":{
"type": "long",
"index":false // 创建和添加时,默认都是true,表示当前字段是否参与检索
}
}
}
对于已存在的映射字段,不可以修改type,如果一定要改,只能创建新的索引,然后进行数据迁移
# 建立新的索引
PUT /newbank
{
"mappings": {
"properties": {
"account_number": {
"type": "long"
},
"address": {
"type": "text"
},
"age": {
"type": "integer"
},
"balance": {
"type": "long"
},
"city": {
"type": "keyword"
},
"email": {
"type": "keyword"
},
"employer": {
"type": "keyword"
},
"firstname": {
"type": "text"
},
"gender": {
"type": "keyword"
},
"lastname": {
"type": "text",
"fields": {
"keyword": {
"type": "keyword",
"ignore_above": 256
}
}
},
"state": {
"type": "keyword"
}
}
}
}
# 数据迁移
POST _reindex
{
"source": {
"index": "bank",
"type": "account" // 如果这个索引存在type,需要指定 (6.0版本后放弃type)
},
"dest": {
"index": "newbank"
}
}
一个tokenizer
(分词器)接收一个字符流,将之分割为独立的tokens
(词元,通常是独立的单词),然后输出tokens
流。
例如:whitespace tokenizer
遇到空白字符时分割文本。它会将文本“Quick brown fox!”分割为[Quick,brown,fox!]
该tokenizer
(分词器)还负责记录各个term
(词条)的顺序或position
位置(用于phrase短语和word proximity词近邻查询),以及term
(词条)所代表的原始word(
单词)的start
(起始)和end
(结束)的character offsets
(字符偏移量) (用于高亮显示搜索的内容)。
ES 提供了很多内置的分词器,可以用来构建custom analyzers
(自定义分词器)
POST _analyze
{
"analyzer": "standard", // 标准分词器
"text": "I love you"
}
内置的分词器都是支持英文分词的,要支持中文分词,需要安装自己的分词器,一般使用开源的ik分词器
github地址
安装踩坑(mac下安装的):
"Caused by: java.nio.file.FileSystemException: /usr/share/elasticsearch/plugins/.DS_Store/plugin-descriptor.properties: Not a directory",
这应该是开发者的锅,他们在gitignore中没有包括.DS_Store ,找到.DS_Store文件,删掉重启就可以了
ls –a
rm .DS_Store
# 运行一个容器 目的是把配置文件模板拷贝出来
jinchengming@MacBook-Pro mydata % docker run -p 80:80 --name nginx -d nginx:1.10
jinchengming@MacBook-Pro mydata % docker container cp nginx:/etc/nginx .
# 重构目录
jinchengming@MacBook-Pro mydata % mv nginx conf
jinchengming@MacBook-Pro mydata % ls
conf elasticsearch
jinchengming@MacBook-Pro mydata % mkdir nginx
jinchengming@MacBook-Pro mydata % mv conf nginx/
# 先删除原容器 重新运行一个新的容器
docker run -p 80:80 --name nginx -v /Users/jinchengming/mydata/nginx/html:/usr/share/nginx/html -v /Users/jinchengming/mydata/nginx/logs:/var/log/nginx -v /Users/jinchengming/mydata/nginx/conf:/etc/nginx -d nginx:1.10
# 可以在html下新建一个index.html 即可通过127.0.0.1访问到
# 在html同级新建一个es目录,并新建fenci.txt 存放自建分词 可以通过127.0.0.1/es/fenci.txt访问到,乱码可忽略
# 进入es的配置文件,扩展自己的分词库
jinchengming@MacBook-Pro mydata % cd elasticsearch
jinchengming@MacBook-Pro elasticsearch % ls
config data plugins
jinchengming@MacBook-Pro elasticsearch % cd plugins
jinchengming@MacBook-Pro plugins % ls
analysis-ik
jinchengming@MacBook-Pro plugins % cd analysis-ik
jinchengming@MacBook-Pro analysis-ik % ls
commons-codec-1.9.jar config httpclient-4.5.2.jar plugin-descriptor.properties
commons-logging-1.2.jar elasticsearch-analysis-ik-7.4.2.jar httpcore-4.4.4.jar plugin-security.policy
jinchengming@MacBook-Pro analysis-ik % cd config
jinchengming@MacBook-Pro config % ls
IKAnalyzer.cfg.xml extra_single_word.dic extra_single_word_low_freq.dic main.dic quantifier.dic suffix.dic
extra_main.dic extra_single_word_full.dic extra_stopword.dic preposition.dic stopword.dic surname.dic
# 修改后的配置,指定的ip为容器内nginx的IP
jinchengming@MacBook-Pro config % cat IKAnalyzer.cfg.xml
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE properties SYSTEM "http://java.sun.com/dtd/properties.dtd">
<properties>
<comment>IK Analyzer 扩展配置</comment>
<!--用户可以在这里配置自己的扩展字典 -->
<entry key="ext_dict"></entry>
<!--用户可以在这里配置自己的扩展停止词字典-->
<entry key="ext_stopwords"></entry>
<!--用户可以在这里配置远程扩展字典 -->
<entry key="remote_ext_dict">http://172.17.0.4/es/fenci.txt</entry>
<!--用户可以在这里配置远程扩展停止词字典-->
<!-- <entry key="remote_ext_stopwords">words_location</entry> -->
</properties>
# 配置修改成功后 重启es
Java可以通过两种方式操作es
综上,我们选择Elasticsearch-Rest-Client,新建检索微服务,导入依赖
<dependency>
<groupId>org.elasticsearch.clientgroupId>
<artifactId>elasticsearch-rest-high-level-clientartifactId>
<version>7.4.2version>
dependency>
springboot本身也对es做了管理,所以需要自己单独指定项目中对应的es版本号,以下步骤跟踪查询当前版本的springboot管理的es版本
因为不是使用的springdata封装的es,所以我们要自己封装一个配置类
@Configuration
public class SkymailElasticSearchConfig {
/*
1. 导入依赖
2. 编写配置
*/
public static final RequestOptions COMMON_OPTIONS;
// options暂时还没加配置
static {
RequestOptions.Builder builder = RequestOptions.DEFAULT.toBuilder();
COMMON_OPTIONS = builder.build();
}
@Bean
public RestHighLevelClient esRestClient() {
RestHighLevelClient client = new RestHighLevelClient(RestClient.builder(
new HttpHost("localhost", 9200, "http")
));
return client;
}
}
测试:
@SpringBootTest
class SkymailSearchApplicationTests {
@Autowired
private RestHighLevelClient client;
@Test
void contextLoads() {
System.out.println(client);
}
/**
* 测试存储(更新)数据到es
*/
@Test
void indexData() throws IOException {
IndexRequest indexRequest = new IndexRequest("users");
indexRequest.id("1"); // 数据的id
User user = new User("chengming", 17, "男");
String userJson = JSON.toJSONString(user);
indexRequest.source(userJson, XContentType.JSON); // 要保存的数据
// 执行操作
IndexResponse index = client.index(indexRequest, SkymailElasticSearchConfig.COMMON_OPTIONS);
// 打印响应数据
System.out.println(index);
}
/**
* 测试复杂查询
*/
@Test
void searchData() throws IOException {
// 1. 创建检索请求
SearchRequest searchRequest = new SearchRequest();
// 指定索引
searchRequest.indices("bank");
// 指定DSL,即检索条件
SearchSourceBuilder builder = new SearchSourceBuilder();
// 构造检索条件
builder.query(QueryBuilders.matchQuery("address", "mill"));
// 聚合
// 1). 安装年龄分布聚合
TermsAggregationBuilder ageAgg = AggregationBuilders.terms("ageAgg").field("age").size(10);
builder.aggregation(ageAgg);
// 2). 计算平均工资
AvgAggregationBuilder balanceAvg = AggregationBuilders.avg("balanceAvg").field("balance");
builder.aggregation(balanceAvg);
System.out.println(builder.toString());
searchRequest.source(builder);
// 2. 执行检索
SearchResponse searchResponse = client.search(searchRequest, SkymailElasticSearchConfig.COMMON_OPTIONS);
// 3. 分析结果 searchResponse
System.out.println(searchResponse.toString());
SearchHits hits = searchResponse.getHits();
// 获取命中记录 hit
for (SearchHit hit : hits) {
String sourceAsString = hit.getSourceAsString();
Account account = JSON.parseObject(sourceAsString, Account.class);
System.out.println("account : " + account);
}
// 获取聚合数据(分析信息)
Aggregations aggregations = searchResponse.getAggregations();
for (Aggregation aggregation : aggregations) {
System.out.println(aggregation.getName());
}
// 或者直接根据聚合名 获取 聚合信息
Terms ageAgg1 = aggregations.get("ageAgg");
List<? extends Terms.Bucket> buckets = ageAgg1.getBuckets();
for (Terms.Bucket bucket : buckets) {
System.out.println("年龄:" + bucket.getKeyAsString() + " - 人数:" + bucket.getDocCount());
}
Avg balanceAvg1 = aggregations.get("balanceAvg");
System.out.println("平均薪资:" + balanceAvg1.getValueAsString());
}
}
@Data
@AllArgsConstructor
class User {
private String username;
private int age;
private String gender;
}
@Data
@ToString
class Account {
private int account_number;
private int balance;
private String firstname;
private String lastname;
private int age;
private String gender;
private String address;
private String employer;
private String email;
private String city;
private String state;
}
ElasticSearch7 去掉type概念