ES
1.基本概念
1.1 Index(索引)
动词,相当于MySQL的insert
名词,相当于MySQL中的Database
1.2 Type(类型)
在Index(索引中),可以定义一个或者多个类型。
类似于MySQL中的table。每一种类型的数据放在一起。
1.3 Document(文档)
保存在某个索引下,某种类型的有一个数据。文档是JSON形式的,Document就像是MySQL中的某个Table里面的内容。
1.4 倒排索引
2. Docker安装
2.1 下载镜像文件
docker pull elasticsearch:7.4.2
docker pull kibana:7.4.2
2.2创建实例
2.2.1 安装ElasticSearch
mkdir -p /data/elasticsearch/config
mkdir -p /data/elasticsearch/data
echo "http.host: 0.0.0.0">>/data/elasticsearch/config/elasticsearch.yml
docker run --name elasticsearch -p 9200:9200 -p 9300:9300 \
-e "discovery.type=single-node" \
-e ES_JAVA_OPTS="-Xms64m -Xmx128m" \
-v /data/elasticsearch/config/elasticsearch.yml:/usr/share/elasticsearch/config/elasticsearch.yml \
-v /data/elasticsearch/data:/usr/share/elasticsearch/data \
-v /data/elasticsearch/plugins:/usr/share/elasticsearch/plugins \
-d elasticsearch:7.4.2
chmod -R 777 /data/elasticsearch/
9200是发送http请求es的端口,9300是es在分布式集群状态下节点之间的通信端口,\是换行,
-e指定参数,single-node指定es单节点运行,ES_JAVA_OPTS指定es初始内存和最大内存,如果不指定会占用全部内存导致整个虚拟机卡死
-v挂载
-d,后台启动es用es7.4.2镜像
chmod -R 777 /data/elasticsearch/ 是给文件下所有用户的读写权限
看见这个信息说明安装成功。
{
"name" : "a89ff87659af",
"cluster_name" : "elasticsearch",
"cluster_uuid" : "AC6ze4rTRFWouH11lZUtbg",
"version" : {
"number" : "7.4.2",
"build_flavor" : "default",
"build_type" : "docker",
"build_hash" : "2f90bbf7b93631e52bafb59b3b049cb44ec25e96",
"build_date" : "2019-10-28T20:40:44.881551Z",
"build_snapshot" : false,
"lucene_version" : "8.2.0",
"minimum_wire_compatibility_version" : "6.8.0",
"minimum_index_compatibility_version" : "6.0.0-beta1"
},
"tagline" : "You Know, for Search"
}
2.2.1 安装kibana
# kibana指定了了ES交互端口9200 # 5600位kibana主页端口
docker run --name kibana -e ELASTICSEARCH_HOSTS=http://124.220.26.66:9200/ -p 5601:5601 -d kibana:7.4.2
# 设置开机启动kibana
docker update kibana --restart=always
3.初步索引
3.1 _cat
GET /_cat/nodes 查看所有节点
GET /_cat/health 查看es健康状况
GET /_cat/master 查看主节点
GET /_cat/indices 查看所有索引
3.2 索引一个文档(保存)
保存一个数据,保存在哪个索引的哪个类型下,指定用哪一个唯一标识。
PUT /customer/external/1 首次是新增,多次调用带id是更新(PUT请求必须带id)
POST /customer/external 是一个新增操作,多次调用带id是新增,不带id是更新
3.3 查询文档
GET /customer/external/1
{
"_index": "customer", //在哪个索引
"_type": "external", //在哪个类型
"_id": "1", //记录id
"_version": 2 //版本号
"_seq_no": 1, //并发控制字段,每次更新就会+1,用来做乐观锁
"_primary_term": 1,//同上,主分片重新分配,如重启,就会变化
"found": true,
"_source": { //真正的内容
"name": "张三"
}
}
保存更新(乐观锁形式 CAS)
PUT /customer/external/1?if_seq_no=7&if_primary_term=1
3.4 更新文档
POST /customer/external/1/_update 会对比原来数据,与原来一样就什么都不做,version版本号和seq_no都不变
{
"doc":{
"name":"张三"
}
}
POST /customer/external/1 会不断更新,不检查版本号
{
"name":"张三"
}
3.5 删除文档&索引
DELETE /customer/external/1 删除文档
DELETE /customer 删除索引
3.6 bulk批量API
POST /customer/external/_bulk
{"index":{"_id":1}}
{"name:":"张三1"}
{"index":{"_id":2}}
{"name:":"张三2"}
4.进阶搜索
4.1 Search API
ES支持两种基本方式检索:
- 一种是通过REST request URI 发送搜索参数(uri+检索参数)
- 另一种是通过使用REST request body 来发送它们(uri+请求体)
GET bank/_search?q=*&sort=account_number:asc
GET bank/_search
{
"query": {
"match_all": {}
},
"sort": [
{
"account_number": {
"order": "desc"
}
}
]
}
4.2 Query DSL
ElasticSearch提供了一个可以执行的JSON风格的DSL(domain-specific language 领域特定语言),这个被称为Query DSL,该查询语言非常全面,并且刚开始的时候感觉有点复杂,真正学好它的方法就是从一些基础案例开始的。
完整的语法结构:
{
QUERY_NAME:{
ARGUMENT:VALUE,
ARGUMENT:VALUE,...
}
}
如果是针对某个字段,那么它的结构为
{
QUERY_NAME:{
FIELD_NAME:{
ARGUMENT:VALUE,
ARGUMENT:VALUE,...
}
}
}
4.2.1 match
上面我们用到来的match_all是匹配所有的数据,而我们现在要讲的match是条件匹配
如果对应的字段是基本类型(非字符串类型),则是精确匹配。
GET bank/_search
{
"query":{
"match":{
"account_number":20
}
}
}
match返回的是 account_number:20的记录
如果对应的字段是字符串类型,则是全文检索
GET bank/_search
{
"query":{
"match":{
"address":"mill"
}
}
}
match返回的就是address中包含mill字符串的记录
4.2.2 match_phrase
将需要匹配的值当成一个整体单词(不分词)进行检索,短语匹配
GET bank/_search
{
"query":{
"match_phrase":{
"address":"mill road"
}
}
}
查询出address中包含 mill road的所有记录,并给出相关性得分
4.2.3 multi_match 多字段匹配
GET bank/_search
{
"query":{
"multi_match":{
"query":"mill road",
"fields":["address","state"]
}
}
}
查询出state或者address中包含 mill road的记录
4.2.4 复合查询
布尔查询又叫组合查询,bool用来实现复合查询,
bool
把各种其它查询通过 must
(与)、must_not
(非)、should
(或)的方式进行组合
复合语句可以合并任何其他查询语句,包括复合语句也可以合并,了解这一点很重要,这意味着,复合语句之间可以相互嵌套,可以表达非常复杂的逻辑。
GET /bank/_search
{
"query": {
"bool": {
"must": [
{ "match": { "age": "40" } }
],
"must_not": [
{ "match": { "state": "ID" } }
]
}
}
}
4.2.5 filter 结果过滤
并不是所有的查询都需要产生分数,特别是那些仅用于"filtering"的文档,为了不计算分数,ElasticSearch会自动检查场景并且优化查询的执行。
GET /bank/_search
{
"query": {
"bool": {
"must": { "match_all": {} },
"filter": {
"range": {
"balance": {
"gte": 20000,
"lte": 30000
}
}
}
}
}
}
4.2.6 term
和match一样,匹配某个属性的值,全文检索字段用match,其他非text字段匹配用term
GET bank/_search
{
"query":{
"term":{
"account_number":20
}
}
}
4.3 Mapping
Mapping 是用来定义一个文档(document),以及它所包含的属性(field)是如何存储和索引的。比如,使用 mapping 来定义:
- 哪些字符串属性应该被看做全文本属性(full text fields)。
- 哪些属性包含数字,日期或者地理位置。
- 文档中的所有属性是否都能被索引(_all 配置)。
- 日期的格式。
- 自定义映射规则来执行动态添加属性。
使用动态映射和显式映射来定义数据。每种方法根据您在数据之旅中所处的阶段提供不同的优势。例如,显式映射不想使用默认值的字段,或者获得对创建哪些字段的更大控制。然后您可以允许Elasticsearch 动态添加其他字段。
Es7 及以上移除了 type 的概念。
- 关系型数据库中两个数据表示是独立的,即使他们里面有相同名称的列也不影响使用, 但 ES 中不是这样的。elasticsearch 是基于 Lucene 开发的搜索引擎,而 ES 中不同 type下名称相同的 filed 最终在 Lucene 中的处理方式是一样的。
- 两个不同 type 下的两个 user_name,在 ES 同一个索引下其实被认为是同一个 filed, 你必须在两个不同的 type 中定义相同的 filed 映射。否则,不同 type 中的相同字段 名称就会在处理中出现冲突的情况,导致 Lucene 处理效率下降。
- 去掉 type 就是为了提高 ES 处理数据的效率。
Elasticsearch 7.x
- URL 中的 type 参数为可选。比如,索引一个文档不再要求提供文档类型。
Elasticsearch 8.x
- 不再支持 URL 中的 type 参数。 解决:
1)、将索引从多类型迁移到单类型,每种类型文档一个独立索引
2)、将已存在的索引下的类型数据,全部迁移到指定位置即可。详见数据迁移
4.3.1 创建映射
PUT /my_index
{
"mappings": {
"properties": {
"age":{"type": "text"},
"email":{"type": "keyword"},
"name":{"type": "text"}
}
}
}
4.3.2 添加映射
PUT /my-index/_mapping
{ "properties":
{ "employee-id":
{ "type": "keyword", "index": false
}
}
}
4.3.3 更新映射
对于已经存在的映射字段,我们不能更新。更新必须创建新的索引进行数据迁移。(因为已经建立好了索引字段)
4.3.4 数据迁移
POST _reindex
{
"source": {
"index": "bank",
"type": "account"
},
"dest": {
"index": "newbank"
}
}
4.4 分词器
分词器是es中的一个组件,通俗意义上理解,就是将一段文本按照一定的逻辑,分析成多个词语,同时对这些词语进行常规化的一种工具;ES会将text格式的字段按照分词器进行分词,并编排成倒排索引,正是因为如此,es的查询才如此之快。
ES的中文分词器需要下载插件安装使用的,将下载的插件解压到ik目录下,安装好后记得重启es容器。
下载地址:
4.5自定义分词器
通过nginx反向代理一个远程词库,来实现自定义分词。
docker run -p 80:80 --name nginx \
-v /data/nginx/html:/usr/share/nginx/html \
-v /data/nginx/logs:/var/logs/nginx \
-v /data/nginx/conf:/etc/nginx \
-d nginx:1.10
5 Elasticsearch-RestClient
1.9300:TCP
- springboot版本不同,transport-api.jar不同,不能适配es版本
- 7.x已经不建议使用,8以后就要废弃
2.9200:HTTP
- JestClient:非官方,更新慢
- RestTemplate:模拟HTTP请求,ES很多操作需要自己封装,麻烦
- HttpClient: 同上
- Elasticsearch-Rest-Client:官方RestClient,封装了ES操作,API层次分明,上手简单。
5.1 导入依赖
<dependency>
<groupId>org.elasticsearch.client</groupId>
<artifactId>elasticsearch-rest-high-level-client</artifactId>
<version>7.4.2</version>
</dependency>
5.2 编写配置
/**
* 1.导入依赖
* 2.编写配置,给容器注入一个RestHighLevelClient
* 3.参照API
*/
@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 restHighLevelClient(){
RestHighLevelClient client = new RestHighLevelClient(
RestClient.builder(new HttpHost("hostname", port, "http")));
return client;
}
}
5.3 代码测试
package com.atjm.gulimall.search;
import com.alibaba.fastjson.JSONObject;
import com.atjm.gulimall.search.config.ElasticsearchConfig;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.elasticsearch.action.index.IndexRequest;
import org.elasticsearch.action.index.IndexResponse;
import org.elasticsearch.action.search.SearchRequest;
import org.elasticsearch.action.search.SearchResponse;
import org.elasticsearch.client.RestHighLevelClient;
import org.elasticsearch.common.xcontent.XContentType;
import org.elasticsearch.index.query.QueryBuilders;
import org.elasticsearch.search.SearchHit;
import org.elasticsearch.search.aggregations.Aggregation;
import org.elasticsearch.search.aggregations.AggregationBuilders;
import org.elasticsearch.search.aggregations.Aggregations;
import org.elasticsearch.search.aggregations.bucket.terms.Terms;
import org.elasticsearch.search.aggregations.metrics.Avg;
import org.elasticsearch.search.builder.SearchSourceBuilder;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;
import javax.validation.constraints.NotBlank;
import java.io.IOException;
import java.util.List;
import java.util.Map;
@RunWith(SpringRunner.class)
@SpringBootTest
public class GulimallSearchApplicationTests {
@Autowired
private RestHighLevelClient restHighLevelClient;
/**
* 测试存储数据到es
*/
@Test
public void indexData() throws IOException {
IndexRequest indexRequest = new IndexRequest("users");
//indexRequest.id("1").source("username","zhangsan","age",18);
User user = new User("张三","男",18);
//要保存的内容
indexRequest.source(JSONObject.toJSONString(user), XContentType.JSON);
//执行操作
IndexResponse index = restHighLevelClient.index(indexRequest, ElasticsearchConfig.COMMON_OPTIONS);
//提取响应数据
System.out.println(index);
}
/**
* 测试查询es
* @throws IOException
*/
@Test
public void searchData() throws IOException {
//1.创建检索请求
SearchRequest searchRequest = new SearchRequest();
//指定索引
searchRequest.indices("bank");
//指定DSL、检索条件
SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder();
//1.1 构造检索条件
// searchSourceBuilder.query();
// searchSourceBuilder.from();
// searchSourceBuilder.size();
// searchSourceBuilder.aggregation();
searchSourceBuilder.query(QueryBuilders.matchQuery("address","mill"));
//1.2 按照年龄的值分布聚合
searchSourceBuilder.aggregation(AggregationBuilders.terms("ageAgg").field("age").size(10));
//1.3 计算平均薪资
searchSourceBuilder.aggregation(AggregationBuilders.avg("balanceAvg").field("balance"));
System.err.println(searchSourceBuilder);
searchRequest.source(searchSourceBuilder);
//2.执行检索
SearchResponse searchResponse = restHighLevelClient.search(searchRequest, ElasticsearchConfig.COMMON_OPTIONS);
//3.分析结果
System.err.println(searchResponse);
//3.1 获取所有查到的数据
SearchHit[] hits = searchResponse.getHits().getHits();
/**
* "_index" : "users",
* "_type" : "_doc",
* "_id" : "fmhpvIYBD8pZWX53Nj0P",
* "_score" : 1.0,
* "_source" : {}
*/
for (SearchHit hit : hits) {
Map<String, Object> sourceAsMap = hit.getSourceAsMap();
String sourceAsString = hit.getSourceAsString();
}
//3.2获取检索到的分析信息
Aggregations aggregations = searchResponse.getAggregations();
// List<Aggregation> aggregations1 = aggregations.asList();
// for (Aggregation aggregation : aggregations1) {
// String name = aggregation.getName();
// String type = aggregation.getType();
// Map<String, Object> metaData = aggregation.getMetaData();
// }
Terms aggAgg = aggregations.get("ageAgg");
for (Terms.Bucket bucket : aggAgg.getBuckets()) {
String keyAsString = bucket.getKeyAsString();
System.out.println("年龄:"+keyAsString);
}
Avg balanceAvg = aggregations.get("balanceAvg");
System.out.println("平均薪资:"+balanceAvg);
}
@Data
@AllArgsConstructor
@NoArgsConstructor
class User{
private String username;
private String gender;
private Integer age;
}
}