ElasticSearch-全文检索快速入门

ElasticSearch-全文检索快速入门

官网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/

是什么?

一个全文搜索引擎。

有什么用?

海量数据全文搜索、存储、分析。

解决什么问题?

当下技术存储数据查找搜索慢。

场景

  • 大量数据搜索
  • 日志记录分析

ElasticSearch-全文检索快速入门_第1张图片

在8.0版本正式删除类型概念。

#elasticSearch

一、基本概念

1、Index(索引)

相当于MySQL的库概念

2、Type(类型)

相当于MySQL的Table概念

3、Document(文档)

文档为 JSON 格式,Document 就像是 MySQL 中的某个 Table 里面的内容

4、倒排索引机制

使用分词技术分词,存储分词和相关文档记录。能极大提高相关查询效率。

ElasticSearch-全文检索快速入门_第2张图片

二、Docker 安装 ES

1、下载镜像文件

docker pull elasticsearch:7.4.2
存储和检索数据
docker pull kibana:7.4.2
可视化检索数据

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

三、基础使用

1._cat使用
  • GET /_cat/nodes:查看所有节点
  • GET /_cat/health:查看 es 健康状况
  • GET /_cat/master:查看主节点
  • GET /_cat/indices:查看所有索引
2、索引一个文档(保存)

指定保存在哪个索引的哪个类型下,指定用哪个唯一标识。

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一样。

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": "John Doe"
    }
}
4、更新文档

POST有两种操作:

POST customer/external/1/_update
{
    "doc":{  
    	"name": "John de"  // 更新内容
    }
}

其中的doc结构是固定的,更新的数据在其中。

第二种:

POST customer/external/1
{
    	"name": "John dt"  // 更新内容
}

如何确定使用哪种呢?

基于场景,对于大并发更新,不带 update;

对于大并发查询偶尔更新,带 update;对比更新,重新计算分配规则。

PUT只能使用不带_update的更新且用法与POST类似。

5、删除文档&索引
# 删除文档
DELETE customer/external/1
# 删除索引
DELETE customer
6、bulk 批量 API

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 返回时,它将提供每个动作的状态。

7、样本测试数据

顾客银行账户信息的虚构的 JSON文档样本

POST bank/account/_bulk

https://github.com/elastic/elasticsearch/blob/7.4/docs/src/test/resources/accounts.json

执行上面的数据指令。

四、进阶检索

1、SearchAPI

ES 支持两种基本方式检索 :

  • 一个是通过使用 REST request URI
    发送搜索参数(uri+检索参数)
  • 另一个是通过使用
    REST request body 来发送它们(uri+请求体)
检索信息

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请求。

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,...
        }
    }
}

使用例子:

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"定义返回字段

1 match(匹配查询)

可以分为数值匹配和字符串全文索引,数值匹配会查询完全相同的数据,而字符串全文索引会按分词查询相关度的数据(需要使用单词查询)。

基本类型(非字符串),精确匹配:

GET bank/_search
{
  "query": {
    "match": {
      "balance": "16418"
    }
  }
}

字符串全文索引

GET bank/_search
{
  "query": {
    "match": {
      "address": "Road 263"
    }
  }
}

查询出相关数据与相关度得分。

2 match_phrase(短语匹配)

短语匹配会把查询值当做不可分割的整体查询,即对比match来说不进行分词查询,把短语作为整体查询出相关度。

GET bank/_search
{
  "query": {
    "match_phrase": {
      "address": "Aviation Road"
    }
  }
}

查询包含Aviation Road的数据。

3 multi_match(多字段匹配)

match的多字段版本

GET bank/_search
{
  "query": {
    "multi_match": {
      "query": "Road",
      "fields": ["address", "state"]
    }
  }
}

查询 address或者state两个字段的匹配Road数据。

4 bool(复合查询)

复合语句可以合并任何其它查询语句,包括复合语句,复合语句之间可以互相嵌套,可以表达非常复杂的逻辑。

ElasticSearch-全文检索快速入门_第3张图片

  • must:必须达到 must 列举的所有条件
  • should:应该达到 should 列举的条件,如果达到会增加相关文档的评分,不改变查询结果
  • must_not 必须不是指定的情况
GET bank/_search
{
  "query": {
    "bool": {
      "must": [
        {"match": {
          "address": "Road"
        }},
      {"match": {
        "gender": "M"
      }}
      ],
      "must_not": [
        {"match_phrase": {
          "city": "Calpine"
        }}
      ],
      "should": [
        {"match": {
          "age": "26"
        }}
      ]
    }
  }
}
filter(结果过滤)

官方文档

使用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
          }
        }}
      ]
      }
    }
  }
}
5 term

官方文档: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"
        }}
      ]
    }
  }
}
6 aggregations(执行聚合)

聚合提供了从数据中分组和提取数据的能力。最简单的聚合方法大致等于 SQL GROUP
BY 和 SQL 聚合函数。

搜索时可以执行查询和多个聚合,聚合里也能嵌套聚合。一次返回多个结果。

完成一下例子:

  • 搜索 address 中包含 mill 的所有人的年龄分布以及平均年龄,但不显示这些人的详情
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
}
  • 查出所有年龄分布,并且这些年龄段中 M 的平均薪资和 F 的平均薪资以及这个年龄段的总体平均薪资
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
}

3、Mapping

Mapping 是用来定义一个文档(document),以及它所包含的属性(field)是如何存储和索引的。

比如,使用 mapping 来定义:

  • 哪些字符串属性应该被看做全文本属性(full text fields)。
  • 哪些属性包含数字,日期或者地理位置。
  • 文档中的所有属性是否都能被索引(_all 配置)。
  • 日期的格式。
  • 自定义映射规则来执行动态添加属性。

官方文档

查看 mapping 信息:

GET bank/_mapping

可以查看具体属性的类型。

字段类型有一下几种:

ElasticSearch-全文检索快速入门_第4张图片

新版本改变去掉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. 将已存在的索引下的类型数据,全部迁移到指定位置即可。详见数据迁移
1 创建映射

创建索引并指定映射

PUT /my_index
{
  "mappings": {
    "properties": {
      "age": {"type": "integer"},
      "email":{"type": "keyword"},
      "name":{"type": "text"}
    }
  }
}
2 添加新的字段映射
PUT /my_index/_mapping
{
  "properties":{
    "employee-id":{
      "type": "keyword",
      "index": false
    }
  }
}
3 更新映射

已经存在的索引字段类型是不能更新的。但可以使用创建新的索引并把数据迁移过去的方式更新索引。

数据迁移
POST /_reindex
{
  "source": {
    "index": "users"
  },
  "dest": {
    "index": "new_users"
  }
}

其中users是源索引,new_users是新索引,其他不变。

映射文档

4、分词

一个 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(自定义分词器)。

1 安装ik分词器

地址: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
    即可列出系统的分词器
    

注意:需要重启才能使用新的分词器。

2 测试分词器
POST _analyze
{
  "text": "我是个中国人"
}
POST _analyze
{
  "analyzer": "ik_smart",
  "text": "我是中国人"
}
POST _analyze
{
  "analyzer": "ik_max_word",
  "text": "我是中国人"
}

3 自定义词库

修改/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
    }
  ]
}

五、SpringBoot整合ElasticSearch

网络上很多相关的工具能够使用Java操作ElasticSearch,常用如下:

1.从9300端口通过TCP操作

  • spring-data-elasticsearch:transport-api.jar;

springboot 版本不同, transport-api.jar 不同,不能适配 es 版本

而且9300端口7.x 已经不建议使用,8 以后就要废弃。

2.从9200端口通过HTTP操作

  • JestClient:非官方,更新慢
  • RestTemplate:模拟发 HTTP 请求,ES 很多操作需要自己封装,麻烦
  • HttpClient:同上
  • Elasticsearch-Rest-Client:官方 RestClient,封装了 ES 操作,API 层次分明,上手简单

最终选择 Elasticsearch-Rest-Client(elasticsearch-rest-high-level-client)
https://www.elastic.co/guide/en/elasticsearch/client/java-rest/current/java-rest-high.html

1.引入配置

<dependency>
    <groupId>org.elasticsearch.clientgroupId>
    <artifactId>elasticsearch-rest-high-level-clientartifactId>
    <version>7.4.2version>
dependency>

2.配置

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;
    }




}

3.测试使用

/**
 * 测试存储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);


}

你可能感兴趣的:(框架,elasticsearch,全文检索,搜索引擎,笔记)