一、简介
全文搜索属于最常见的需求,开源的 Elasticsearch (以下简称 Elastic)是目前全文搜索引擎的首选。它可以快速地储存、搜索和分析海量数据。 Elastic 的底层是开源库 Lucene。但是,你没法直接用 Lucene,必须自己写代码去调用它的接口。Elastic 是 Lucene 的封装,提供了 REST API 的操作接口,开箱即用。
二、安装
官网:www.elastic.co/cn/elastics…
1. 使用 docker 安装
# 1. 拉取镜像
# 存储和检索数据
docker pull elasticsearch:7.4.2
# 可视化检索数据
docker pull kibana:7.4.2
# 2. 配置挂载数据文件夹
# 创建配置文件目录
mkdir -p /mydata/elasticsearch/config
# 创建数据目录
mkdir -p /mydata/elasticsearch/data
# 将/mydata/elasticsearch/文件夹中文件都可读可写
chmod -R 777 /mydata/elasticsearch/
# 配置任意机器可以访问 elasticsearch
echo "http.host: 0.0.0.0" >/mydata/elasticsearch/config/elasticsearch.yml
# 3. 编写 docker-compose 文件
version: "3.8"
services:
elasticsearch:
image: elasticsearch:7.4.2
ports:
- "9200:9200"
- "9300:9300"
environment:
discovery.type: "single-node"
ES_JAVA_OPTS: "-Xms64m -Xmx512m"
volumes:
- "/mydata/elasticsearch/config/elasticsearch.yml:/usr/share/elasticsearch/config/elasticsearch.yml"
- "/mydata/elasticsearch/data:/usr/share/elasticsearch/data"
- "/mydata/elasticsearch/plugins:/usr/share/elasticsearch/plugins"
restart: always
kibana:
image: kibana:7.4.2
ports:
- "5601:5601"
environment:
ELASTICSEARCH_HOSTS: "http://10.211.55.12:9200" # 注意,这里要指定上面es的地址
restart: always
2. 查看 ES 是否安装成功
访问 IP:9200 看到返回的 json 数据说明启动成功。
3. 查看 Kibana 是否安装成功
访问 IP:5601 测试:
4. 汉化
进入容器,打开 Kibana 配置文件:config/kibana.yml,在最后一行添加如下代码:
# 进入容器
docker exec -it dockercompose_kibana_1 /bin/bash
# 打开 Kibana 配置文件
vi config/kibana.yml
#在文件最后一行添加
i18n.locale: "zh-CN"
# 退出容器后重启容器
docker restart dockercompose_kibana_1
5. 向 es 发送请求
Kibana 左侧菜单栏中有个 Dev Tools(开发工具),可以在这里写请求操作 es,并且有自动补全的提示,后续的话,我在这里写的请求
三、基本概念
==Elasticsearch 是面向文档的,一切都是 JSON==
索引(Index)
Elastic 会索引所有字段,经过处理后写入一个反向索引(Inverted Index)。查找数据的时候,直接查找该索引。
所以,Elastic 数据管理的顶层单位就叫做 Index(索引)。它是单个数据库的同义词。每个 Index (即数据库)的名字必须是小写。
- 动词,相当于 mysql 的
insert - 名词,相当于 mysql 的
database
类型(Type)(过时)
在 Index(索引)中,可以定义一个或多个类型。 类似于 MySQL 的 Table,每一种类 型的数据存放在一起。 在 Elasticsearch8.0 之后,Type 类型被移除。
文档(Document)
存储在 Elasticsearch 中的主要实体叫文档(Document)。用关系型数据库来类比的话,一个文档相当于数据库表中的一行记录。Elasticsearch 和 MongoDB 中的文档类似,都可以有不同的结构,但 Elasticsearch 的文档中,相同字段必须有相同类型。文档由多个字段组成,每个字段可能多次出现在一个文档里,这样的字段叫多值字段(multivalued)。每个字段的类型,可以是文本、数值、日期等。字段类型也可以是复杂类型,一个字段包含其他子文档或者数组。
映射(Mapping)
- 所有文档写进索引之前都会先进行分析,如何将输入的文本分割为词条、哪些词条又会被过滤,这种行为叫做映射。一般由用户自己定义规则。详见第七章。
分片与副本(Shards & Replicas)
四、ElasticSearch 使用
1. _cat(查看ES信息)
/_cat/nodes:查看所有节点/_cat/health:查看ES健康状况/_cat/master:查看主节点信息/_cat/indicies:查看所有索引
2. 索引的CRUD
创建索引,PUT请求:
PUT localhost:9200/{索引名}
{
// 常用的创建规则,根据需要配置
"settings":{
// 一些设置
"index":{
"number_of_shards":"2", // 分片数
"number_of_replicas":"0" // 副本数
}
},
"mappings": {
"properties": {
// 这里配置字段的类型
"字段名": {
"type" : "字段类型"
}
}
}
}
删除索引,DELETE 请求:
DELETE localhost:9200/{索引名}
修改索引,GET 请求:
查询索引,GET 请求:
GET /{索引名}
3. 查询出的文档结构字段说明
在 Elasticsearch 中,文档以 JSON 格式进行存储,可以是复杂的结构
元数据(metadata)
查询一个文档时,返回的不只文档的数据。它还包含了元数据(metadata)——关于文档的信息。常见的元数据有:
| 节点 | 说明 |
|---|---|
| _index | 文档存储的索引 |
| _type | 文档代表的类,推荐是 _doc |
| _id | 文档的唯一标识 |
| _score | 文档的分数,当查询出多条结果时,匹配度越高则分值越高 |
| _source | 文档中的数据 |
| _version | 文档的版本号,增删改时会自增 1 |
| _source | 文档的唯一标识 |
| _source | 文档的唯一标识 |
| found | 是否存在数据 |
例如:这是一条查询语句查询出的结果:
{
"_index" : "test3",
"_type" : "_doc",
"_id" : "5",
"_version" : 17,
"_seq_no" : 25,
"_primary_term" : 1,
"found" : true,
"_source" : {
"name" : "赵六",
"age" : 26
}
}
4. 文档的 CRUD
类型名称推荐使用 _doc
| method | url地址 | 描述 |
|---|---|---|
| PUT | localhost:9200 / { 索引名称 } / { 类型名称 } / { 文档id } | 创建文档(指定文档id) |
| POST | localhost:9200 / { 索引名称 } / { 类型名称 } | 创建文档(随机文档id) |
| DELETE | localhost:9200 / { 索引名称 } / { 类型名称 } / { 文档id } | 删除文档 |
| POST | localhost:9200 / { 索引名称 } / _update / { 文档id } | 修改文档 |
| GET | localhost:9200 / { 索引名称 } / { 类型名称 } / { 文档id } | 通过文档 id 查询文档 |
| POST | localhost:9200 / { 索引名称 } / _search | 查询所有数据 |
添加文档,PUT请求:
PUT /test3/_doc/5
{
"name": "张三",
"age": 23,
"desc": "法外狂徒",
}
删除文档,DELETE请求:
DELETE /test3/_doc/5
修改文档,PUT请求或者POST请求:
- PUT请求,通过覆盖的方式进行更新,如果有字段没有值,那么值就为空:
PUT /test3/_doc/5
{
"name": "李四",
}
- POST请求,如下代码,将要修改的字段放到 doc 中。使用该方法只会修改指定的值,其余值不做修改。
POST /test3/_update/5
{
"doc": {
"age": 100
}
}
==注意==:每次对同一个 id 值进行增、删、改时,该 id 对应的文档中的 _version 都会加 1 ,用于乐观锁
几种更新文档的区别
在上面索引文档即保存文档的时候介绍,还有两种更新文档的方式:
- 当PUT请求带id,且有该id数据存在时,会更新文档;
- 当POST请求带id,与PUT相同,该id数据已经存在时,会更新文档; 这两种请求类似,即带id,且数据存在,就会执行更新操作。 类比:
- 请求体的报文格式不同,_update方式要修改的数据要包裹在 doc 键下
- _update方式不会重复更新,数据已存在不会更新,版本号不会改变,另两种方式会重复更新(覆盖原来数据),版本号会改变
- 这几种方式在更新时都可以增加属性,PUT请求带id更新和POST请求带id更新,会直接覆盖原来的数据,不会在原来的属性里面新增属性
查询文档,GET请求(简单的查询):
根据 id 查询:
GET test3/_doc/5
GET test3/_search?q=name:李四
5. bulk-批量操作数据
语法格式:
{action:{metadata}} # 例如index保存记录,update更新
{request body }
{action:{metadata}}
{request body }
- 指定索引和类型的批量操作
POST /customer/external/_bulk
{"index":{"_id":"1"}}
{"name":"John Doe"}
{"index":{"_id":"2"}}
{"name":"John Doe"}
- 对所有索引执行批量操作
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"}}
{"doc":{"title":"my updated blog post"}}
- 这里的批量操作,当发生某一条执行发生失败时,其他的数据仍然能够接着执行,也就是说彼此之间是独立的。
- bulk api 以此按顺序执行所有的 action(动作)。如果一个单个的动作因任何原因失败,它将继续处理它后面剩余的动作。
- 当 bulk api 返回时,它将提供每个动作的状态(与发送的顺序相同),所以您可以检查是否一个指定的动作是否失败了。
6. 检索示例介绍
下面的请求都是在 Kibana dev-tools 操作
请求接口
GET /bank/_search
{
"query": {
"match_all": {}
},
"sort": [
{
"account_number": "asc"
}
]
}
# query 查询条件
# sort 排序条件
结果
{
"took" : 7,
"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" : "bradshawmckenzie@euron.com",
"city" : "Hobucken",
"state" : "CO"
},
"sort" : [
0
]
},
...
]
}
}
响应字段解释
- took:Elasticsearch运行查询所用的时间(毫秒)
- timed_out:搜索请求是否超时
- _shards:搜索了多少个碎片,以及成功、失败或跳过的碎片的详细信息
- max_score:找到的最相关文档的分数
- hits.total.value:找到了多少匹配的文件
- hits.sort:文档的排序位置(不按相关性分数排序时)
- hits._score:文档的相关性得分(使用match_all时不适用)
响应结果说明
Elasticsearch 默认会分页返回10条数据,不会一下返回所有数据。
请求方式说明
ES支持两种基本方式检索;
- 通过REST request uri 发送搜索参数 (uri +检索参数);
- 通过REST request body 来发送它们(uri+请求体); 也就是说除了上面示例的请求接口,根据请求体进行检索外; 还可以用GET请求参数的方式检索:
GET bank/_search?q=*&sort=account_number:asc
# q=* 查询所有
# sort=account_number:asc 按照account_number进行升序排列
五、Query DSL
本小节参考官方文档:Query DSL
1. match查询
match 查询是一个标准查询,不管你需要全文本查询还是精确查询基本上都要用到它。
match 会先分析文档,然后再通过分析的文档进行查询
模糊查询-文本字符串
如果你使用 match 查询一个全文本字段,它会在真正查询之前用分析器先分析 match 一下查询字符:
# 查找匹配 address 包含 mill 或 lane 的数据
GET bank/_search
{
"query": {
"match": {
"address": "mill lane"
}
}
}
match 即全文检索,对检索字段进行分词匹配,会按照响应的评分 _score 排序,原理是倒排索引。
如果用 match 下指定了一个确切值,在遇到数字,日期,布尔值或者 not_analyzed 的字符串时,它将为你搜索你
给定的值:
精确查询-基本数据类型(非文本)
GET bank/_search
{
"query": {
"match": {
"account_number": 20
}
}
}
# 查找匹配 account_number 为 20 的数据 非文本推荐使用 term
term 不会对查询的条件进行分词,keyword 不会对存储的数据进行分词
精确匹配
如果不希望对 text 进行分词查询,则使用
GET bank/_search
{
"query": {
"match": {
"address.keyword": "288 Mill Street"
}
}
}
2. match_phrase-短语匹配
将需要匹配的值当成一整个单词(不分词)进行检索
GET bank/_search
{
"query": {
"match_phrase": {
"address": "mill lane"
}
}
}
# 这里会检索 address 匹配包含短语 mill lane 的数据
3. multi_math-多字段匹配
GET bank/_search
{
"query": {
"multi_match": {
"query": "mill",
"fields": [
"city",
"address"
]
}
}
}
# 检索 city 或 address 匹配包含 mill 的数据,会对查询条件分词
4. term查询
term 主要用于精确匹配哪些值,用于非 text 字段,比如数字,日期,布尔值。
避免使用 term 查询文本字段,默认情况下,Elasticsearch 会通过 analysis 分词将文本字段的值拆分为一部分,这使精确匹配文本字段的值变得困难。如果要查询文本字段值,请使用 match 查询代替。
{ "term": { "age": 26 }}
{ "term": { "date": "2014-09-01" }}
{ "term": { "public": true }}
{ "term": { "tag": "full_text" }}
POST 127.0.0.1:9200/itcast/person/_search
{
"query":{
"term":{
"age":20
}
}
}
5. terms查询
terms 跟 term 有点类似,但 terms 允许指定多个匹配条件。 如果某个字段指定了多个值,那么文档需要一起去做匹配:
POST 127.0.0.1:9200/itcast/person/_search
{
"query":{
"terms":{
"age" : [20,21]
}
}
}
6. range查询
range 过滤允许我们按照指定范围查找一批数据: 范围操作符包含:
gt:大于gte:大于等于lt:小于lte:小于等于
POST test3/_search
{
"query":{
"range":{
"age":{
"gte":20,
"lte":22
}
}
}
}
7. exists 查询
exists 查询可以用于查找文档中是否包含指定字段或没有某个字段,类似于SQL语句中的 IS_NULL 条件
POST 127.0.0.1:9200/itcast/person/_search
{
"query":{
"exists":{
"field": "hobby"
}
}
}
# 找出存在 hobby 字段的数据
8. bool查询
bool 查询可以用来合并多个条件查询结果的布尔逻辑,它包含一下操作符:
must:多个查询条件的完全匹配,相当于and。must_not:多个查询条件的相反匹配,相当于not。should:至少有一个查询条件匹配,相当于or。 这些参数可以分别继承一个查询条件或者一个查询条件的数组:
GET bank/_search
{
"query": {
"bool": {
"must": [
{
"match": {
"gender": "M"
}
},
{
"match": {
"address": "mill"
}
}
]
}
}
}
# 查询 gender 为 M 且 address 包含 mill 的数据
在boolean查询中,must, should 和must_not 元素都被称为查询子句。文档是否符合每个“must”或“should”子句中的标准,决定了文档的“相关性得分”。 得分越高,文档越符合您的搜索条件。 默认情况下,Elasticsearch 返回根据这些相关性得分排序的文档。
“must_not”子句中的条件被视为“filter”。它影响文档是否包含在结果中,但不影响文档的评分方式。还可以显式地指定任意过滤器来包含或排除基于结构化数据的文档。
9. 过滤查询 filter
并不是所有的查询都需要产生分数,特别是哪些仅用于 filtering 过滤的文档。为了不计算分数,elasticsearch 会自动检查场景并且优化查询的执行。
filter 对结果进行过滤,且==不计算相关性得分==。
GET bank/_search
{
"query": {
"bool": {
"must": [
{
"match": {
"address": "mill"
}
}
],
"filter": {
"range": {
"balance": {
"gte": "10000",
"lte": "20000"
}
}
}
}
}
}
# 这里先是查询所有匹配 address 包含 mill 的文档,
# 然后再根据 10000<=balance<=20000 进行过滤查询结果
10. sort 文档排序
GET test3/_search
{
"sort": [
{
"字段名": {
"order": "desc" // asc 为升序
}
}
]
}
11. 分页 form/size
这两个参数与 MySQL 中 limit 中的两个参数相同
- from:跳过几个文档
- size:选取多少个文档
GET test3/_search
{
"from": 0,
"size": 20
}
12. 嵌套查询:nested
当一个字段中是一个对象(json字符串)时,需要使用 nested 嵌套查询,嵌套查询搜索嵌套字段对象时,就像它们被索引为单独的文档一样。如果对象与搜索匹配,嵌套查询将返回根父文档。
GET /my-index-000001/_search
{
"query": {
"nested": {
"path": "obj1",
"query": {
"bool": {
"must": [
{ "match": { "obj1.name": "blue" } },
{ "range": { "obj1.count": { "gt": 5 } } }
]
}
},
"score_mode": "avg"
}
}
}
13. 高亮
通过 highlight 来对匹配出的字段设置高亮显示。
| 属性 | 说明 |
|---|---|
| fields | 要高亮的字段 |
| pre_tags | 前缀,默认是<em> |
| post_tags | 后缀,默认是</em> |
GET test3/_search
{
"query": {
"match": {
"name": "张三"
}
},
"highlight": {
"pre_tags": "<p class='key' style='color: red'>",
"post_tags": "</p>",
"fields": {
"name": {}
}
}
}
六、聚合(Aggregation)
官方文档:www.elastic.co/guide/en/el… 聚合语法
GET /my-index-000001/_search
{
"aggs":{
"aggs_name":{ # 这次聚合的名字,方便展示在结果集中
"AGG_TYPE":{ # 聚合的类型(avg,term,terms)
"field": "my-field"
}
}
}
}
示例1-搜索address中包含mill的所有人的年龄分布以及平均年龄
terms聚合:比如性别有男、女,就会创建两个桶,分别存放男女的信息。默认会搜集doc_count的信息,即记录有多少男生,有多少女生,然后返回给客户端,这样就完成了一个 terms 的统计。
GET bank/_search
{
"query": {
"match": {
"address": "Mill"
}
},
"aggs": {
"ageAgg": {
"terms": {
"field": "age",
"size": 10
}
},
"ageAvg": {
"avg": {
"field": "age"
}
},
"balanceAvg": {
"avg": {
"field": "balance"
}
}
},
"size": 0
}
# 先 query 再 match,会先查找数据,再对查找出后的结果进行聚合
# "ageAgg": { --- 聚合名为 ageAgg
# "terms": { --- 聚合类型为 terms,按字段分别创建桶,分别存放不同字段的信息。默认会搜集doc_count的信息,即记录每个桶有多少数据
# "field": "age", --- 聚合字段为 age
# "size": 10 --- 取聚合后前十个数据
# }
# },
# ------------------------
# "ageAvg": { --- 聚合名为 ageAvg
# "avg": { --- 聚合类型为 avg 求平均值
# "field": "age" --- 聚合字段为 age
# }
# },
# ------------------------
# "balanceAvg": { --- 聚合名为 balanceAvg
# "avg": { --- 聚合类型为 avg 求平均值
# "field": "balance" --- 聚合字段为 balance
# }
# }
# ------------------------
# "size": 0 --- 不显示命中结果,只看聚合信息
结果:
{
"took" : 11,
"timed_out" : false,
"_shards" : {
"total" : 1,
"successful" : 1,
"skipped" : 0,
"failed" : 0
},
"hits" : {
"total" : {
"value" : 4,
"relation" : "eq"
},
"max_score" : null,
"hits" : [ ]
},
"aggregations" : {
"ageAgg" : {
"doc_count_error_upper_bound" : 0,
"sum_other_doc_count" : 0,
"buckets" : [
{
"key" : 38,
"doc_count" : 2
},
{
"key" : 28,
"doc_count" : 1
},
{
"key" : 32,
"doc_count" : 1
}
]
},
"ageAvg" : {
"value" : 34.0
},
"balanceAvg" : {
"value" : 25208.0
}
}
}
示例2-按照年龄聚合,并且求这些年龄段的这些人的平均薪资(每个年龄的平均薪资)
GET bank/_search
{
"query": {
"match_all": {}
},
"aggs": {
"ageAgg": {
"terms": {
"field": "age",
"size": 100
},
"aggs": {
"ageAvg": {
"avg": {
"field": "balance"
}
}
}
}
},
"size": 0
}
结果(数据太多,只取了3条):
{
"took" : 4,
"timed_out" : false,
"_shards" : {
"total" : 1,
"successful" : 1,
"skipped" : 0,
"failed" : 0
},
"hits" : {
"total" : {
"value" : 1000,
"relation" : "eq"
},
"max_score" : null,
"hits" : [ ]
},
"aggregations" : {
"ageAgg" : {
"doc_count_error_upper_bound" : 0,
"sum_other_doc_count" : 820,
"buckets" : [
{
"key" : 31,
"doc_count" : 61,
"ageAvg" : {
"value" : 28312.918032786885
}
},
{
"key" : 39,
"doc_count" : 60,
"ageAvg" : {
"value" : 25269.583333333332
}
},
{
"key" : 26,
"doc_count" : 59,
"ageAvg" : {
"value" : 23194.813559322032
}
}
]
}
}
}
示例3-查出所有年龄分布,并且这些年龄段中性别为M的平均薪资和F的平均薪资以及这个年龄段的总体平均薪资
GET bank/_search
{
"query": {
"match_all": {}
},
"aggs": {
"ageAgg": {
"terms": {
"field": "age",
"size": 100
},
"aggs": {
"genderAgg": {
"terms": {
"field": "gender.keyword"
},
"aggs": {
"balanceAvg": {
"avg": {
"field": "balance"
}
}
}
},
"ageBalanceAvg": {
"avg": {
"field": "balance"
}
}
}
}
},
"size": 0
}
# "field": "gender.keyword" gender是txt没法聚合 必须加.keyword精确替代
结果(数据太多,只取了2条)
{
"took" : 11,
"timed_out" : false,
"_shards" : {
"total" : 1,
"successful" : 1,
"skipped" : 0,
"failed" : 0
},
"hits" : {
"total" : {
"value" : 1000,
"relation" : "eq"
},
"max_score" : null,
"hits" : [ ]
},
"aggregations" : {
"ageAgg" : {
"doc_count_error_upper_bound" : 0,
"sum_other_doc_count" : 879,
"buckets" : [
{
"key" : 31,
"doc_count" : 61,
"genderAgg" : {
"doc_count_error_upper_bound" : 0,
"sum_other_doc_count" : 0,
"buckets" : [
{
"key" : "M",
"doc_count" : 35,
"balanceAvg" : {
"value" : 29565.628571428573
}
},
{
"key" : "F",
"doc_count" : 26,
"balanceAvg" : {
"value" : 26626.576923076922
}
}
]
},
"ageBalanceAvg" : {
"value" : 28312.918032786885
}
},
{
"key" : 39,
"doc_count" : 60,
"genderAgg" : {
"doc_count_error_upper_bound" : 0,
"sum_other_doc_count" : 0,
"buckets" : [
{
"key" : "F",
"doc_count" : 38,
"balanceAvg" : {
"value" : 26348.684210526317
}
},
{
"key" : "M",
"doc_count" : 22,
"balanceAvg" : {
"value" : 23405.68181818182
}
}
]
},
"ageBalanceAvg" : {
"value" : 25269.583333333332
}
}
]
}
}
}
七、Mapping映射
1. Mapping介绍
Maping 是用来定义一个文档(document),以及它所包含的属性(field)是如何存储和索引的。
比如:使用maping来定义:
- 哪些字符串属性应该被看做全文本属性(full text fields);
- 哪些属性包含数字,日期或地理位置;
- 文档中的所有属性是否都嫩被索引(all 配置);
- 日期的格式;
- 自定义映射规则来执行动态添加属性;
查看mapping信息
GET bank/_mapping
{
"bank" : {
"mappings" : {
"properties" : {
"account_number" : {
"type" : "long"
},
"address" : {
"type" : "text",
"fields" : {
"keyword" : {
"type" : "keyword",
"ignore_above" : 256
}
}
},
"age" : {
"type" : "long"
},
"balance" : {
"type" : "long"
},
"email" : {
"type" : "text",
"fields" : {
"keyword" : {
"type" : "keyword",
"ignore_above" : 256
}
}
},
"firstname" : {
"type" : "text",
"fields" : {
"keyword" : {
"type" : "keyword",
"ignore_above" : 256
}
}
},
"gender" : {
"type" : "text",
"fields" : {
"keyword" : {
"type" : "keyword",
"ignore_above" : 256
}
}
},
"lastname" : {
"type" : "text",
"fields" : {
"keyword" : {
"type" : "keyword",
"ignore_above" : 256
}
}
},
"state" : {
"type" : "text",
"fields" : {
"keyword" : {
"type" : "keyword",
"ignore_above" : 256
}
}
}
}
}
}
}
2. 新版本type移除
ElasticSearch7-去掉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参数。
- 解决: 将索引从多类型迁移到单类型,每种类型文档一个独立索引 将已存在的索引下的类型数据,全部迁移到指定位置即可。详见数据迁移
3. 属性类型
参考:官方属性类型
常见:
| 类型 | 数据类型 |
|---|---|
| 字符串 | text、keyword |
| 整型 | byte、short、integer、long |
| 浮点 | float、double |
| 日期 | date |
| 布尔值 | boolean |
| 二进制 | binary |
- ==text 类型==:当一个字段是要被全文搜索的,比如Email内容、产品描述,应该使用text类型。设置text类型以后,字段内容会被分析,在生成倒排索引以前,字符串会被分词器分成一个一个词项。text类型的字段 不用于排序,很少用于聚合。
- ==keyword类型==:适用于索引结构化的字段,比如email地址、主机名、状态码和标签。如果字段需要进行过滤(比如查找已发布博客中 status 属性为 published 的文章)、排序、聚合。keyword 类型的字段==只能通过精确值搜索到==。
映射操作
参考:创建映射操作
创建索引映射
创建索引并指定属性的映射规则(相当于新建表并指定字段和字段类型)
PUT /my_index
{
"mappings": {
"properties": {
"age": {
"type": "integer"
},
"email": {
"type": "keyword"
},
"name": {
"type": "text"
}
}
}
}
结果:
{
"acknowledged" : true,
"shards_acknowledged" : true,
"index" : "my_index"
}
给已有映射增加字段
PUT /my_index/_mapping
{
"properties": {
"employee-id": {
"type": "keyword",
"index": false
}
}
}
# 这里的 "index": false,表明新增的字段不能被检索。默认是true
# https://www.elastic.co/guide/en/elasticsearch/reference/7.x/mapping-index.html
结果:
{
"acknowledged" : true
}
查看映射
GET /my_index/_mapping
# 查看某一个字段的映射
GET /my_index/_mapping/field/employee-id
结果:
{
"my_index" : {
"mappings" : {
"properties" : {
"age" : {
"type" : "integer"
},
"email" : {
"type" : "keyword"
},
"employee-id" : {
"type" : "keyword",
"index" : false
},
"name" : {
"type" : "text"
}
}
}
}
}
# index false 表示不能被索引找到
更新映射
www.elastic.co/guide/en/el… 对于已经存在的字段映射,我们不能更新。更新必须创建新的索引,进行数据迁移。
数据迁移
数据迁移步骤:
- 创建新的索引,并创建 Mapping 映射
- 使用下面的方法进行数据迁移
迁移方式分为两种,一种是7和7之后去掉type的情况,一种是包含type 迁移的情况。
无type数据迁移
POST reindex [固定写法]
{
"source":{
"index":"twitter" # 指定原索引
},
"dest":{
"index":"new_twitters" #指定目标索引
}
}
有type数据迁移
POST reindex [固定写法]
{
"source":{
"index":"twitter", # 指定原索引
"twitter":"twitter" # 指定type
},
"dest":{
"index":"new_twitters" #指定目标索引
}
}
八、IK分词器
1. 简介
一个 tokenizer(分词器)接收一个字符流,将之分割为独立的 tokens(词元,通常是独立的单词),然后输出 tokens 流。
例如:whitespace tokenizer 遇到空白字符时分割文本。它会将文本“Quick brown fox!”分割为[Quick,brown,fox!]。
该 tokenizer(分词器)还负责记录各个 terms(词条)的顺序或 position位置(用于phrase短语和word proximity词近邻查询),以及 term(词条)所代表的原始 word(单词)的 start(起始)和 end(结束)的 character offsets(字符串偏移量)(用于高亮显示搜索的内容)。
elasticsearch 提供了很多内置的分词器,可以用来构建 custom analyzers(自定义分词器)。
查看分词:
POST _analyze
{
"analyzer": "standard",
"text": "The 2 QUICK Brown-Foxes jumped over the lazy dog's bone."
}
# "analyzer": "standard" 指定分词器类型,standard 为默认的标准分词器
默认的分词器一般都是针对于英文,对于中文我们需要安装额外的分词器来进行分词。
2. 安装IK分词器
- 下载
- IK 分词器属于 Elasticsearch 的插件,所以 IK 分词器的安装目录是 Elasticsearch 的 plugins 目录,在我们使用Docker启动
Elasticsearch时,已经将该目录挂载到主机的/mydata/elasticsearch/plugins目录。 - IK 分词器的版本需要跟 Elasticsearch 的版本对应,当前选择的版本为 7.4.2(tag 为7.4.2),下载地址为:Github Release 或访问:镜像地址
# 进入挂载的插件目录 /mydata/elasticsearch/plugins
cd /mydata/elasticsearch/plugins
# 安装 wget 下载工具
yum install -y wget
# 下载对应版本的 IK 分词器(这里是7.4.2)
wget https://github.com/medcl/elasticsearch-analysis-ik/releases/download/v7.4.2/elasticsearch-analysis-ik-7.4.2.zip
# 解压到 plugins 目录下的 ik 目录
unzip elasticsearch-analysis-ik-7.4.2.zip -d ik
# 删除下载的压缩包
rm -f elasticsearch-analysis-ik-7.4.2.zip
# 修改文件夹访问权限
chmod -R 777 ik/
# 重启 Elasticsearch
docker restart elasticsearch
- 测试 ik 分词器
GET my_index/_analyze
{
"analyzer": "ik_max_word",
"text":"蔡徐坤"
}
3. 自定义扩展分词库
我们在 nginx 中自定义分词文件,通过配置 es 的 ik 配置文件来远程调用 nginx 中的分词文件来实现自定义扩展词库。 注:默认 nginx 请求的是 数据目录的 html 静态目录 nginx 安装参考:docker 安装 nginx
-
nginx 中自定义分词文件
echo "蔡徐坤" > /mydata/nginx/html/fenci.txtnginx默认请求地址为ip:port/fenci.txt如果想要增加新的词语,只需要在该文件追加新的行并保存新的词语即可。
-
给 es 配置自定义词库
# 1. 打开并编辑 ik 插件配置文件
vim /mydata/elasticsearch/plugins/ik/config/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>
<!-- 用户可以在这里配置远程扩展字典 -->
<!-- 在这里指定nginx中的文件 -->
<entry key="remote_ext_dict">http://192.168.163.131/fenci.txt</entry>
<!--用户可以在这里配置远程扩展停止词字典-->
<!-- <entry key="remote_ext_stopwords">words_location</entry> -->
</properties>
- 重启 elasticsearch 容器
docker restart elasticsearch
- 测试自定义词库
GET my_index/_analyze
{
"analyzer": "ik_max_word",
"text":"蔡徐坤"
}
九、集成 SpringBoot
官方文档:戳这里,进去后选择自己 ES 相应的版本,然后选择 Java High Level REST Client 查看文档
这里以 7.4 版本的 es 为例:
1. 引入依赖
<properties>
<!--springboot父依赖中有默认的elasticsearch版本,改成自己相应的版本-->
<!--改springboot中默认管理的es版本-->
<elasticsearch.version>7.4.2</elasticsearch.version>
</properties>
<!-- 导入es的high-level-client 注意要改spring-boot中的es管理版本-->
<dependency>
<groupId>org.elasticsearch.client</groupId>
<artifactId>elasticsearch-rest-high-level-client</artifactId>
<version>7.4.2</version>
</dependency>
配置 Elasticsearch 客户端: www.elastic.co/guide//en/e… 配置 Elasticsearch 选项:www.elastic.co/guide//en/e…
@Configuration
public class ElasticsearchConfig {
//注入初始化客户端,方便之后使用
@Bean
public RestHighLevelClient restHighLevelClient() {
RestHighLevelClient client = new RestHighLevelClient(
RestClient.builder(
// 当有多个主机时,这里配置多个主机,
new HttpHost("localhost", 9200, "http"),
new HttpHost("localhost", 9300,"http")));
return client;
}
// 配置操作es时的选项
private 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();
}
}
2. 测试数据存储
官方文档:www.elastic.co/guide/en/el…
@Autowired
RestHighLevelClient client;
@Test
void indexData() throws IOException {
IndexRequest indexRequest = new IndexRequest("users");
indexRequest.id("1");
// json 字符串
indexRequest.source("{" +
"\"user\":\"kimchy\"," +
"\"postDate\":\"2013-01-30\"," +
"\"message\":\"trying out Elasticsearch\"" +
"}", XContentType.JSON);
// KV 键值对
// indexRequest.source("username", "zhangsan", "age", 12, "address", "sz");
// 同步执行
client.index(indexRequest, MallElasticSearchConfig.COMMON_OPTIONS);
}
3. 测试检索
官方文档:www.elastic.co/guide/en/el…
检索地址中带有 mill 的人员年龄分布和平均薪资
@Autowired
RestHighLevelClient client;
@Test
void searchData() throws IOException {
// 1. 创建检索请求
SearchRequest searchRequest = new SearchRequest();
// 指定索引
searchRequest.indices("bank");
// 指定 DSL 检索条件
SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder();
// 1.1 构建检索条件 address 包含 mill
searchSourceBuilder.query(QueryBuilders.matchQuery("address", "mill"));
// 1.2 按照年龄值分布进行聚合
TermsAggregationBuilder ageAgg = AggregationBuilders.terms("ageAgg").field("age").size(10);
searchSourceBuilder.aggregation(ageAgg);
// 1.3 计算平均薪资
AvgAggregationBuilder balanceAvg = AggregationBuilders.avg("balanceAvg").field("balance");
searchSourceBuilder.aggregation(balanceAvg);
System.out.println("检索条件:" + searchSourceBuilder.toString());
searchRequest.source(searchSourceBuilder);
// 2. 执行检索, 获得响应
SearchResponse searchResponse = client.search(searchRequest, MallElasticSearchConfig.COMMON_OPTIONS);
// 3. 分析结果
// 3.1 获取所有查到的记录
SearchHits hits = searchResponse.getHits();
SearchHit[] searchHits = hits.getHits();
for (SearchHit hit : searchHits) {
// 数据字符串
String jsonString = hit.getSourceAsString();
System.out.println(jsonString);
// 可以通过 json 转换成实体类对象
// Account account = JSON.parseObject(jsonString, Account.class);
}
// 3.2 获取检索的分析信息(聚合数据等)
Aggregations aggregations = searchResponse.getAggregations();
// for (Aggregation aggregation : aggregations.asList()) {
// System.out.println("当前聚合名:" + aggregation.getName());
// }
Terms ageAgg1 = aggregations.get("ageAgg");
for (Terms.Bucket bucket : ageAgg1.getBuckets()) {
String keyAsString = bucket.getKeyAsString();
System.out.println("年龄:" + keyAsString + " 岁的有 " + bucket.getDocCount() + " 人");
}
Avg balanceAvg1 = aggregations.get("balanceAvg");
System.out.println("平均薪资: " + balanceAvg1.getValue());
}