本文为总结性笔记,适合你在浏览了一些教程后快速上手进行操作,后续视情况逐渐补充细节以及结合代码操作的内容
如果你不熟悉docker,可以借助搜索引擎查询如何本地安装ES以及kibana,如果你熟悉docker,可以使用下边的docker-compose 快速搭建一个学习环境。
新建docker-compose.yml
version: '2.2'
services:
es01:
image: docker.elastic.co/elasticsearch/elasticsearch:7.15.2
container_name: es01
environment:
- node.name=es01
- cluster.name=es-docker-cluster
- discovery.seed_hosts=es02,es03
- cluster.initial_master_nodes=es01,es02,es03
- bootstrap.memory_lock=true
- "ES_JAVA_OPTS=-Xms512m -Xmx512m"
- http.cors.enabled=true
- http.cors.allow-origin=http://localhost:9100
ulimits:
memlock:
soft: -1
hard: -1
volumes:
- data01:/usr/share/elasticsearch/data
ports:
- 9200:9200
networks:
- elastic
es02:
image: docker.elastic.co/elasticsearch/elasticsearch:7.15.2
container_name: es02
environment:
- node.name=es02
- cluster.name=es-docker-cluster
- discovery.seed_hosts=es01,es03
- cluster.initial_master_nodes=es01,es02,es03
- bootstrap.memory_lock=true
- "ES_JAVA_OPTS=-Xms512m -Xmx512m"
- http.cors.enabled=true
- http.cors.allow-origin=http://localhost:9100
ulimits:
memlock:
soft: -1
hard: -1
volumes:
- data02:/usr/share/elasticsearch/data
networks:
- elastic
es03:
image: docker.elastic.co/elasticsearch/elasticsearch:7.15.2
container_name: es03
environment:
- node.name=es03
- cluster.name=es-docker-cluster
- discovery.seed_hosts=es01,es02
- cluster.initial_master_nodes=es01,es02,es03
- bootstrap.memory_lock=true
- "ES_JAVA_OPTS=-Xms512m -Xmx512m"
- http.cors.enabled=true
- http.cors.allow-origin=http://localhost:9100
ulimits:
memlock:
soft: -1
hard: -1
volumes:
- data03:/usr/share/elasticsearch/data
networks:
- elastic
kibana:
image: docker.elastic.co/kibana/kibana:7.15.2
container_name: kibana
environment:
ELASTICSEARCH_HOSTS: '["http://es01:9200","http://es02:9200","http://es03:9200"]'
SERVER_NAME: kibana.example.org
ports:
- "5601:5601"
networks:
- elastic
elasticsearch-head:
image: mobz/elasticsearch-head:5
container_name: elasticsearch-head
ports:
- "9100:9100"
networks:
- elastic
depends_on:
- es01
- es02
- es03
volumes:
data01:
driver: local
data02:
driver: local
data03:
driver: local
networks:
elastic:
driver: bridge
在docker-compose.yml同级目录下运行命令
docker-compost up -d
(这里省略本地安装docker,请根据操作系统自行安装)运行成功后,将创建一个ES集群,包含三个ES节点,同时启动一个Kibana实例用于便捷操作ES RESTfulAPI、一个elasticsearch-head 实例用于观察索引分片情况
# 浏览器访问 http://localhost:9200/_cluster/health?pretty=true 查看ES集群情况
{
"name": "es01",
"cluster_name": "es-docker-cluster",
"cluster_uuid": "3z5dkkWEQt2qaF0ZT9oLKg",
"version": {
"number": "7.15.2",
"build_flavor": "default",
"build_type": "docker",
"build_hash": "93d5a7f6192e8a1a12e154a2b81bf6fa7309da0c",
"build_date": "2021-11-04T14:04:42.515624022Z",
"build_snapshot": false,
"lucene_version": "8.9.0",
"minimum_wire_compatibility_version": "6.8.0",
"minimum_index_compatibility_version": "6.0.0-beta1"
},
"tagline": "You Know, for Search"
}
# 浏览器访问 http://localhost:5601/app/dev_tools#/console 进入kibana控制台
# 浏览器访问 http://localhost:9100/ 可以访问head 查看索引分片分布情况等信息
ElasticSearch | 关系型数据库 |
---|---|
索引(Index)+类型(Type) | 表 (Table) |
文档(Document) | 行 (Row) |
文档字段(Document Field) | 列(Column) |
文档ID (Document ID) | 主键 (Primary Key) |
curl http://localhost:9200/_cluster/health?pretty
{
"cluster_name": "es-docker-cluster",
"status": "green",
"timed_out": false,
"number_of_nodes": 3, // 节点数 3
"number_of_data_nodes": 3, // data节点数 3
"active_primary_shards": 7,
"active_shards": 14,
"relocating_shards": 0,
"initializing_shards": 0,
"unassigned_shards": 0,
"delayed_unassigned_shards": 0,
"number_of_pending_tasks": 0,
"number_of_in_flight_fetch": 0,
"task_max_waiting_in_queue_millis": 0,
"active_shards_percent_as_number": 100.0
}
GET 查询、 POST 新增、 PUT 更新、 DELETE 删除
# 新增一条文档,ES自动为其创建索引
curl -X POST -H "Content-Type: application/json" -d '{"title":"零基础学ES","description":"自学ES入门图书","price":68.88}' "http://localhost:9200/book_store/_doc?prettty"
{
"_index" : "book_store", // 索引 文档容器,文档存储逻辑单元,相当于关系数据库中的表
"_type" : "_doc", // ES早期版本中,一个索引可以包含多个类型,一个类型下可以有多个文档,7.0 之后一个索引只有一个类型,名字也必须是_doc,逐渐抛弃了类型的概念
"_id" : "zNu8e4kBHcnL53ZwWuyr",// 文档ID 文档:数据读写最基本单元
"_version" : 1,
"result" : "created",
"_shards" : {
"total" : 2,
"successful" : 1,
"failed" : 0
},
"_seq_no" : 0,
"_primary_term" : 1
}
## 获取索引的信息,并以表格形式显示这些信息
GET _cat/indices?v
# 结果
health status index uuid pri rep docs.count docs.deleted store.size pri.store.size
green open book_store cv-hzdVFS8KtG4gFmdJGjA 1 1 6 2 36.2kb 16kb
green open test_array -6luiCI0QGm9_QaYQwgvhw 1 1 2 0 13.6kb 3.2kb
# 列信息
health:显示索引的健康状态。可能的值有:green(绿色,表示正常)、yellow(黄色,表示部分分片可用)、red(红色,表示所有分片都不可用)。
status:显示索引的状态。可能的值有:open(开放,表示索引是可写的)、closed(关闭,表示索引不可写)、readonly(只读,表示索引是只读的)。
index:显示索引的名称。
uuid:显示索引的唯一标识符。
pri:显示主分片的数量。
rep:显示复制分片的数量。
docs.count:显示索引中文档(文档记录)的总数。
docs.deleted:显示已删除的文档数量。
store.size:显示索引在磁盘上的总存储大小。
pri.store.size:显示主分片在磁盘上的存储大小。
# 终端curl
curl -X GET "http://localhost:9200/_cluster/health?pretty=true"
# kibana 控制台
GET _cluster/health?pretty
PUT 不能用于不指定ID的新增操作,只能用于指定ID情况下新增或更新
POST 既可以不指定ID新增,同时也可以指定ID新增或者更新
# 新增文档时指定文档ID
PUT /book_store/_doc/{文档ID}
{
"field1": "value1",
"field2": "value2",
...
}
# 新增文档时使用ES默认随机ID
# 相比于自定义文档ID的命令,用POST方法替换PUT方法,且没有提供文档ID
POST /book_store/_doc
{
"field1": "value1",
"field2": "value2",
...
}
ES中的文档是不可改变的,我们无法直接修改一个文档的内容,如果需要修改一个已存在的文档,只能用一个新的文档替换掉原来的旧文档
PUT /book_store/_doc/9787111213826
{
"ISBN": "9787111213826",
"title": "JAVA编程思想",
"description": "JAVA学习经典,殿堂级著作",
"price": 77.88,
"author": "Bruce Eckel",
"publisher": "机械工业出版社",
"stock": 231
}
# 返回结果
{
"_index" : "book_store",
"_type" : "_doc",
"_id" : "9787111213826",
"_version" : 18,
"result" : "updated",
"_shards" : {
"total" : 2,
"successful" : 2,
"failed" : 0
},
"_seq_no" : 28,
"_primary_term" : 1
}
# 即使只更新文档部门字段值,仍然需要提供整个文档的信息,如果请求体只包含部分字段信息,则新文档只会包含请求的字段(效果相当于先删后增)
PUT /book_store/_doc/9787111213826
{
"price": 77.88 // 如果新文档中只包含要修改的字段price
}
# 获取文档查看修改结果
GET /book_store/_doc/9787111213826
{
"_index" : "book_store",
"_type" : "_doc",
"_id" : "9787111213826",
"_version" : 19,
"_seq_no" : 29,
"_primary_term" : 1,
"found" : true,
"_source" : {
"price" : 77.88 // 这本书的文档就只剩下price一个属性了
}
}
PUT /book_store/_doc/9787111213826
{
"doc": {
"price": 55.66
}
}
为了实现部分更新文档功能,ES会先读取旧文档,再基于旧文档和请求中的doc参数构造新文档,最后使用新文档替换原有的旧文档。
由于这三个步骤全是在ES服务端完成,相比于客户端完成这三部更新操作,可以有效降低网络开销以及更新冲突发生的概率
GET /book_store/_doc/{文档Id}
{
"_index" : "book_store",
"_type" : "_doc",
"_id" : "zNu8e4kBHcnL53ZwWuyr",
"_version" : 1,
"_seq_no" : 0,
"_primary_term" : 1,
"found" : true,// 表示成功找到对应文档
"_source" : {
"title" : "零基础学ES",
"description" : "自学ES入门图书",
"price" : 68.88
}
}
# 文档不存在返回示例
{
"_index" : "book_store",
"_type" : "_doc",
"_id" : "FWmJw30BAtxnt_qoj81E",
"found" : false
}
# _source 指定ES结果中只包含哪些字段
GET /book_store/_doc/{文档ID}?_source=title,desctiption
{
"_index" : "book_store",
"_type" : "_doc",
"_id" : "97876381260",
"_version" : 8,
"_seq_no" : 8,
"_primary_term" : 1,
"found" : true,
"_source" : {
"description" : "零基础自学JAVA编程的入门图书",
"title" : "零基础学Java"
}
}
# 如果你对文档的元数据不感兴趣,只想获取其真正的业务数据,那么可以使用_source
GET /book_store/_doc/{文档ID}/_source
{
"ISBN" : "9085115891807",
"title" : "Python编程,从入门到实践",
"description" : "零基础学Python编程教程书籍",
"price" : 82.3,
"author" : "Eric Matthes",
"publisher" : "人民邮电出版社",
"stock" : 121
}
GET /book_store/_search
{
"query": {
"match": {
"title": "java"
}
},
"highlight": {
"fields": {
"title": {}
}
}
}
HEAD /book_store/_doc/{文档ID}
# 如果文档存在会收到200 - OK,如果文档不存在则收到404 Not Found
DELETE /book_store/_doc/{文档ID}
# 返回结果
{
"_index" : "book_store",
"_type" : "_doc",
"_id" : "FWmJw30BAtxnt_qoj8IE",
"_version" : 8,
"result" : "deleted",// 表示文档成功删除
"_shards" : {
...
},
"_seq_no" : 23,
"_primary_term" : 2
}
批量读写可以有效降低多次请求的网络开销,提高ES处理效率
注意如果数据量过大,ES服务器需要消耗大量资源,可能导致ES无法响应其他客户端的请求,严重时会导致ES服务器卡死
一次批量读写中,数据量大小保持在5 -15M 左右比较合适
具体要根据ES服务器的硬件配置、单个文档数据的大小、以及操作复杂度等因素共同决定。
# mget api 接收一个docs数组参数,数组中每个元素都要包含index和id两个参数,用于唯一确定一个文档
GET /_mget
{
"docs" : [
{
"_index": "book_store",
"_id": "97876381260"
},
{
"_index": "book_store",
"_id": "47879081231",
"_source": ["title", "description"]
},
{
"_index": "web_log",
"_id": "NVkiaX0BOkDWX_gQlsYf"
}
]
}
# 返回结果
{
"docs" : [
{
"_index" : "book_store",
"_type" : "_doc",
"_id" : "97876381260",
"_version" : 8,
"_seq_no" : 8,
"_primary_term" : 1,
"found" : true,
"_source" : {
"title" : "零基础学Java",
"description" : "零基础自学JAVA编程的入门图书",
"ISBN" : "97876381260",
"price" : 55.66,
"publisher" : "极客军营",
"author": "poype",
"stock": 121
}
},
{
"_index" : "book_store",
"_type" : "_doc",
"_id" : "47879081231",
"_version" : 1,
"_seq_no" : 11,
"_primary_term" : 1,
"found" : true,
"_source" : {
"description" : "JAVA学习圣经",
"title" : "JAVA编程思想"
}
},
{
"_index" : "web_log",
"_type" : "_doc",
"_id" : "NVkiaX0BOkDWX_gQlsYf",
"_version" : 1,
"_seq_no" : 0,
"_primary_term" : 1,
"found" : true,
"_source" : {
"api" : "/hello",
"time" : "2021-11-29T12:58:17",
"level" : "INFO",
"content" : "this is a test, no any error",
"rt" : 30
}
}
]
}
# 命令格式
{action : {metadata}}
{request body}
{action : {metadata}}
{request body}
action 可以包含 create update index delete 四种操作类型
create
创建一个文档,如果文档已存在则报错
update
更新部分文档字段值,文档不存在则报错
index
创建一个新文档,如果文档已存在,用新的文档替换旧文档
delete
删除一个文档
除了删除,其他操作需要提供request body
POST /_bulk
{"delete": {"_index": "web_log", "_id": "NVkiaX0BOkDWX_gQlsYf"}} # 删除指定文档
{"create": {"_index": "book_store", "_id": "97876381260"}} # 创建一个文档 如果报错不会影响后边的操作执行
{"title": "零基础学Java", "description": "零基础", "price": 66.88}
{"index": {"_index": "book_store"}} # 创建一个新文档,或替换
{"title": "数据结构", "description": "计算机专业必修课", "price": 36.88}
{"update": {"_index": "book_store", "_id": "97876381260"}}# 更新文档部分字段值时,不需要提供完整文档字段
{"doc": {"price": 37.8}}
# 执行结果
{
"took" : 117,
"errors" : true,
"items" : [
{
"delete" : {
"_index" : "web_log",
"_type" : "_doc",
"_id" : "NVkiaX0BOkDWX_gQlsYf",
"_version" : 2,
"result" : "deleted",
"_shards" : {
"total" : 2,
"successful" : 2,
"failed" : 0
},
"_seq_no" : 2,
"_primary_term" : 1,
"status" : 200
}
},
{ # 虽然在执行到第二个create action时就报错了,但报错的action并不会影响后续action命令的执行
"create" : {
"_index" : "book_store",
"_type" : "_doc",
"_id" : "97876381260",
"status" : 409,
"error" : {
"type" : "version_conflict_engine_exception",
"reason" : "[97876381260]: version conflict, document already exists (current version [8])",
"index_uuid" : "lIZ3zfYCTduJY7rQA-ekxw",
"shard" : "0",
"index" : "book_store"
}
}
},
{
"index" : {
"_index" : "book_store",
"_type" : "_doc",
"_id" : "OVl-bn0BOkDWX_gQfcY9",
"_version" : 1,
"result" : "created",
"_shards" : {
"total" : 2,
"successful" : 2,
"failed" : 0
},
"_seq_no" : 12,
"_primary_term" : 1,
"status" : 201
}
},
{
"update" : {
"_index" : "book_store",
"_type" : "_doc",
"_id" : "97876381260",
"_version" : 9,
"result" : "updated",
"_shards" : {
"total" : 2,
"successful" : 2,
"failed" : 0
},
"_seq_no" : 13,
"_primary_term" : 1,
"status" : 200
}
}
]
}
ES全文搜索是根据关键字与文档的相关性筛选搜索结果的,不是精确匹配
ES为搜索结果中的每个文档都赋予了一个相关性分数(_score),匹配程度越高相关性分数越高
ES中相关性分数算法是TF/IDF 算法 term frequency/inverse document frequency
Term frequency
关键词在一个文档中出现的频率越高,那么这个关键词与该文档的匹配程度就越高。
Doc_1文档同时包含了两个搜索关键词,Doc_2文档只包含了一个搜索关键词,所以Doc_1文档的匹配程度就要高于Doc_2文档。或者一篇文章中有10个词,结果8个词都是给定的关键词,那么该文章的匹配程度也一定很高。
Inverse document frequency
一个关键词出现在越多的文档,那么这个关键词对于相关性分数的贡献就越低。
Doc_1和Doc_2两个文档中都包含“is”这个词,那么“is”这个词对两个文档的相关性分数贡献都很低。在计算相关性分数时,如果所有的文档都包含一个词,那么就与所有文档都不包含这个词的结果是一样的。例如英文中的“is”、“a”、“the”,中文中的“呢”、“的”、“是”等词对相关性分数贡献都很低。在ES中,将这类对搜索没有帮助的词称作停用词(stopword),通常不会将这类词放入倒排索引中。
Field-length
文档字段内容的长度越长,则关键字与该文档匹配的可能性越低。
例如,一篇文章的标题有8个词,你有一个关键词与其匹配;相比于一篇文章的内容有800个词,你有一个关键词与其匹配;一定是前者的匹配程度更高。所以一个关键词出现在文章标题中贡献的相关性分数一定大于这个词出现在文章内容中贡献的相关性分数
字符过滤器(Character filter):
对文本中的特殊字符进行转换或过滤。例如通常我们要将文本中的HTML标签过滤掉,避免通过ul、li等标签也能搜索到目标文档。
单词切分器(Tokenizer):
将一段全文本切分成多个单词(term)。对于英文,可以简单的使用空格作为分隔符对一个句子进行切分。但对于中文就复杂的多。
单词过滤器(Token filter):
切分好的单词还要经过一系列Token filter的处理。
例如将所有英文单词转换为小写,无论我们输入的关键字是Python或是python,是Java或是JAVA都能正常支持搜索。
又例如过滤掉一些停用词(stopword),像英文中的“a”、“the”、“is”,中文中的“呢”、“的”、“是”,这些词对搜索几乎没有意义,所以也不会被 放进倒排索引中。
再例如做一些近义词的替换或者增加一些相近的词,比如将“猜测”转换为“推测”、“才干”转换为“才能”。又比如给“蚂蚁金服”增加一个“支付宝”的近义词,这样使用两个关键词就都可以搜索到目标文档。
用于测试分词效果
GET _analyze
{
"analyzer": "standard",
"text": "Python is better for programming beginners than PHP"
}
# 返回结果
{
"tokens" : [
{
"token" : "python",
"start_offset" : 0, # 这个单词在句子中的起始位置
"end_offset" : 6, #这个单词在句子中的结束位置
"type" : "" , # 词的类型
"position" : 0 # 这个词在句子中是第几个词
},
{
"token" : "is",
"start_offset" : 7,
"end_offset" : 9,
"type" : "" ,
"position" : 1
},
{
"token" : "better",
"start_offset" : 10,
"end_offset" : 16,
"type" : "" ,
"position" : 2
}
...
]
}
GET /index_name/_doc/document_id/_termvectors
{
"fields": ["your_text_field"]
}
# 测试一个索引内的局部分词器,格式:GET /{index_name}/_analyze
GET /test_custom_analyzer/_analyze
{
"analyzer": "my_chinese_analyzer", # 自定义分词器,具体Setting配置参考局部分词器章节描述
"text": "《Java编程思想》是Java领域极具影响力和价值的经典著作"
}
standard、english、spanish、Italian、french、german
进入es安装目录
./bin/elasticsearch-plugin install https://github.com/medcl/elasticsearch-analysis-ik/releases/download/v7.15.2/elasticsearch-analysis-ik-7.15.2.zip
安装后重启ES即可
ik_smart 最少词数量切分,通常更贴近语意,建议
ik_max_word 尽可能切分出更多数量的词
GET _analyze
{
"char_filter": ["html_strip"],// 字符过滤器,html_strip是Elasticsearch内置的Character filter,专门用于过滤HTML标签。
"tokenizer": "ik_smart",// 单词切分器,使用IK
"filter": [{"type": "stop", "stopwords": ["了", "的"]}], // 单词过滤器,stopword 过滤掉对搜索意义不大的停用词
"text": "今天太冷了
"
}
# 结果
今天, 太冷
# 直接创建索引的命令如下,重点关注mappings
PUT /index_name
{
"settings": {
...
},
"mappings": {
"properties": {
"field1": {
"type": "some_data_type",
"other_attr": "xxx"
},
"field2": {
"type": "some_data_type",
"other_attr": "xxx"
},
...
}
}
}
# 删除一个索引
DELETE /index_name
整数类型
byte、short、integer、long、unsigned_long
浮点数类型
float、double、half_float、scaled_float
布尔类型
boolean
时间类型
date
字符串类型
text、keyword
类型 | 描述 |
---|---|
byte | 有符号8位整数类型 |
short | 有符号16位整数类型 |
integer | 有符号32位整数类型 |
long | 有符号64位整数类型 |
unsigned_long | 无符号64位整数类型 |
类型 | 描述 |
---|---|
half_float | 有符号16位浮点数类型 |
float | 有符号32位浮点数类型 |
double | 有符号64位浮点数类型 |
scaled_float | long类型支持的浮点数,结合缩放因子在整数和浮点数之间转换 |
其中,scaled_float类型要详细解释下。如果使用scaled_float类型创建索引,还需要提供缩放因子scaling_factor。例如通过如下命令创建一个含有price字段的索引:
PUT /test_scaled_float
{
"mappings": {
"properties": {
"price": {
"type": "scaled_float",
"scaling_factor": 100
}
}
}
}
scaled_float类型字段的值虽然是浮点数,但在Elasticsearch底层却是以long类型存储的整数,这个整数的值是用字段原始值乘以scaling_factor再四舍五入得到的。例如保存下面一个文档:
PUT /test_scaled_float/_doc/1
{
"price": "99.98"
}
这里给price提供的原始是99.98,但在Elasticsearch中存储的值是99.98乘以100,即9988这个值。
又例如保存下面的文档,price的值有3位小数:
PUT /test_scaled_float/_doc/2
{
"price": "99.988"
}
99.988乘以100后是9998.8,再四舍五入得到9999,所以这个price字段值在Elasticsearch中存储是9999。
当使用scaled_float类型的字段作为搜索条件时,也会使用相同的方式将浮点数转换为整数再执行搜索操作。
下面两个搜索例子,搜索条件的值与保存文档时提供的浮点数原始值并不相同,但由于它们转换成整数后的值是相同的,所以仍然可以匹配到对应的文档。
# 99.994乘以100再四舍五入后的值是9999
GET /test_scaled_float/_doc/_search
{
"query": {
"match": {
"price": 99.994
}
}
}
# 99.985乘以100再四舍五入后的值是9999
GET /test_scaled_float/_doc/_search
{
"query": {
"match": {
"price": 99.985
}
}
}
#上面两个搜索命令都返回如下结果
{
...
"hits" : {
"total" : {
"value" : 1,
"relation" : "eq"
},
"max_score" : 1.0,
"hits" : [
{
"_index" : "test_scaled_float",
"_type" : "_doc",
"_id" : "2",
"_score" : 1.0,
"_source" : {
"price" : "99.988"
}
}
]
}
}
可以看到虽然ElasticSearch在底层存储的是整数9999,但在查询结果中展示的还是保存文档时提供的原始值。
相比于传统的浮点数类型double和float,由于scaled_float类型底层是使用整型数据存储,整型数据比浮点型数据更容易压缩,所以使用scaled_float类型存储浮点数有利于存储空间的高效利用。
Elasticsearch的布尔类型字段除了可以接受true和false两个值外,还可以接受能够转换为布尔类型的字符串参数。
真值:true、“true”
假值:false、“false”、“”(空字符串)
PUT /test_boolean
{
"mappings": {
"properties": {
"flag": {
"type": "boolean"
}
}
}
}
可以通过下面三种方式表示一个时间:
具有时间格式的字符串,例如"2021-12-07"或"2021-12-07T13:34:30Z"。
一个整型数字,表示从1970-01-01 00:00:00开始经过的毫秒数。
一个整型数字,表示从1970-01-01 00:00:00开始经过的秒数(需要配置)。
PUT /test_date
{
"mappings": {
"properties": {
"current": {
"type": "date",
"format": "yyyy-MM-dd HH:mm:ss" # 可以明确为date类型提供一个字符串格式
}
}
}
}
# 下面两个命令以两种不同的时间格式在索引中保存两个文档
# 时间戳的格式,从1970-01-01 00:00:00开始所经过的毫秒数
PUT /test_date/_doc/1
{
"current": 1638854757524
}
# 时间格式字符串
PUT /test_date/_doc/2
{
"current": "2021-12-07T13:34:30Z"
}
# 使用新的时间格式 (mapping 中添加 "format": "yyyy-MM-dd HH:mm:ss")
PUT /test_date/_doc/1
{
"current": "2021-12-07 13:34:30"
}
字符串类型主要包括text和keyword两个类型,它们之间的区别是 text类型字段支持全文搜索,而keyword 类型字段只能精确匹配
通常像身份证号、邮箱地址、电话号码、邮编、状态码等需要精确匹配的字段,使用keyword作为其类型。当保存一个文档时,Elasticsearch不会对keyword类型的字段进行分词,所以keyword类型的字段只能支持精确搜索,不支持全文搜索。
keyword类型的字段在存储文档时不需要分词器处理,所以文档写入性能更高
text适用于非结构化类型的字符串,例如文章的内容、商品描述信息、日志的内容等等。在保存一个新文档时,Elasticsearch会对text类型的字段进行分词处理,并为其建立倒排索引。所以text类型的字段支持全文搜索。
text类型字段最重要的属性就是analyzer,该属性指定使用哪个分词器对字符串进行分词。下面代码创建了一个包含text类型字段的索引,并明确指定使用ik_smart分词器对该字段进行分词处理:
PUT /test_text
{
"mappings": {
"properties": {
"description": {
"type": "text",
"analyzer": "ik_smart"
}
}
}
}
保存一个文档时,如果该文档对应的索引不存在,Elasticsearch会根据文档中各个字段的值推测其数据类型,并自动创建一个索引。如下代码用于测试自动创建的索引使用什么样的数据类型:
PUT /test_default_type/_doc/1
{
"integer_number": 123,
"float_number": 3.14,
"str_value": "test",
"bool_value": true,
"str_bool_value": "true",
"date_value": "2021-12-07T13:34:30Z"
}
# 查看test_default_type的mapping结构
GET /test_default_type/_mapping
Elasticsearch自动创建的索引会使用如下规则推测数据类型:
如果字段的值是整数,该字段默认为long类型。
如果字段的值是浮点数,该字段默认为float类型。
如果字段的值是true或false,该字段就为boolean类型。
如果字段的值是字符串"true"或"false",则该字段仍作为普通字符串处理,参考第6条规则。
如果字段的值是符合时间格式的字符串,则该字段被推测为date类型。
如果字段的值是字符串,该字段默认为text类型。且拥有一个叫做keyword的子字段,其类型是keyword类型
除了前面提到的数字、布尔、字符串等基本类型外,JSON中还支持数组、对象等复杂数据类型。这两个复杂类型在Elasticsearch中也是被支持的。
在Elasticsearch中,没有为数组增加专门的类型。但任何一个字段都可以包含0个、1个或多个值。例如,下面的代码将一个数组赋值给一个普通字段:
# array_field字段就是普通的integer类型
PUT /test_array
{
"mappings": {
"properties": {
"array_field": {
"type": "integer"
}
}
}
}
# 将数组赋值给array_field字段
PUT /test_array/_doc/1
{
"array_field": [1, 2, 3, 4]
}
就像text类型的字符串被分词器切分成一个个词,再将每个词放进索引中一样,数组中的每个元素都被作为一个term保存在索引中。所以可以通过下面的搜索命令找到该文档:
GET /test_array/_doc/_search
{
"query": {
"match": {
"array_field": 3
}
}
}
数组中的所有元素都必须是相同的类型(包含不同类型时,新增/更新不会报错,需要客户端规避)。保存一个包含数组字段的文档时,如果是由Elasticsearch自动为其创建索引,则会用数组中第一个元素的类型作为整个数组字段的类型。
PUT /test_object
{
"mappings": {
"properties": {
"product_name": {
"type": "text"
},
"price": {
"type": "object",
"properties": {
"currency": {
"type": "keyword"
},
"amount": {
"type": "double"
}
}
}
}
}
}
# 创建两个测试文档
PUT /test_object/_doc/1
{
"product_name": "Java编程思想",
"price": {
"currency": "RMB",
"amount": 108
}
}
PUT /test_object/_doc/2
{
"product_name": "Thinking in Java",
"price": {
"currency": "USD",
"amount": 38.52
}
}
我们可以将整个文档看成是根对象(root object),将price字段看成是根对象内部的子对象(inner object)。可以使用“点操作符”引用一个子对象字段,例如下面命令根据子对象中的currency字段搜索对应的文档:
# 使用“点操作符”引用一个子对象字段
GET /test_object/_doc/_search
{
"query": {
"match": {
"price.currency": "RMB"
}
}
}
除了数据类型,对一个字段最重要的就是index 属性了,当一个字段的index属性为true时,Elasticsearch会为该字段创建索引,进而该字段可以支持搜索(精确匹配或全文搜索)。当一个字段的index属性为false时,Elasticsearch不会为该字段建立任何索引,也就导致该字段无法支持搜索。
# 下面创建一个索引,该索引中password字段的index属性被设置为false:
PUT /test_index_attribute
{
"mappings": {
"properties": {
"password": {
"type": "keyword",
"index": false
}
}
}
}
PUT /test_index_attribute/_doc/1
{
"password": "123"
}
GET /test_index_attribute/_doc/_search
{
"query": {
"match": {
"password": "123"
}
}
}
# 通常情况,密码一定是不能用作搜索条件的,所以将其index属性设置为false,以避免为passowrd字段创建索引。
# 如果执行下面的搜索命令,Elasticsearch会返回错误信息:
# 报错信息
{
"error" : {
"root_cause" : [
{
"type" : "query_shard_exception",
"reason" : "failed to create query: Cannot search on field [password] since it is not indexed.",
"index_uuid" : "aQ9TRcsMQWKXoNwZWz8B0w",
"index" : "test_index_attribute"
}
],
...
},
"status" : 400
}
默认情况下,保存一个文档时,如果文档中包含一个在mapping中没有定义的字段,则Elasticsearch会自动在mapping中增加那个字段。
自动扩展索引的mapping结构虽然对软件开发过程比较友好,但在软件发布到线上后,我们还是希望索引的mapping结构能够保持稳定,以避免异常数据破坏索引结构,或者系统的隐患迟迟得不到发现。
可以通过dynamic参数控制索引的mapping是否可以动态扩展新的字段,它有三个取值,分别是"true"、“false”、“strict”:
“true”:
dynamic参数的默认值就是"true",它表示索引的mapping结构可以动态扩展字段。
“false”:
索引的mapping结构不能扩展字段,当文档中包含新的字段时,Elasticsearch会忽略新的字段。
“strict”:
索引的mapping结构不能扩展字段,当文档中包含新的字段时,Elasticsearch会抛异常。
# dynamic参数是false的索引
PUT /test_dynamic_mapping
{
"mappings": {
"dynamic": "false",
"properties": {
"one": {
"type": "keyword"
}
}
}
}
# 保存一个测试文档,包含一个新的字段two
PUT /test_dynamic_mapping/_doc/1
{
"one": "123",
"two": "456"
}
# 查看索引的mapping结构
GET /test_dynamic_mapping/_mapping
# ES返回如下结果,mapping结构保持不变
{
"test_dynamic_mapping" : {
"mappings" : {
"dynamic" : "false",
"properties" : {
"one" : {
"type" : "keyword"
}
}
}
}
}
有时候,我们希望索引的mapping结构在整体上是稳定的,但在某一个字段下面可以动态扩展。可以使用dynamic参数精确指定单个字段的动态扩展类型。
# 整个mapping的dynamic参数是strict,但在stash字段下的dynamic参数是true
PUT /test_dynamic_mapping
{
"mappings": {
"dynamic": "strict",
"properties": {
"one": {
"type": "keyword"
},
"stash": {
"type": "object",
"dynamic": true
}
}
}
}
# 在stach字段下面自动增加one字段
PUT /test_dynamic_mapping/_doc/1
{
"one": "123",
"stash": {
"one": 123
}
}
# 在stach字段下面自动增加two字段
PUT /test_dynamic_mapping/_doc/2
{
"one": "123",
"stash": {
"two": 456
}
}
# 查看索引的mapping结构,stash字段下面被自动增加了两个字段
{
"test_dynamic_mapping" : {
"mappings" : {
"dynamic" : "strict",
"properties" : {
"one" : {
"type" : "keyword"
},
"stash" : {
"dynamic" : "true",
"properties" : {
"one" : {
"type" : "long"
},
"two" : {
"type" : "long"
}
}
}
}
}
}
}
有时候,如果可以通过多种方式索引一个字段会非常有用,例如让一个字段同时具有中文分词器和拼音分词器,这样就可以让该字段同时支持中文搜索和拼音搜索。又例如让一个字段同时具有text类型和keyword类型的特点,text类型可以支持全文搜索,而keyword类型可以用于排序、聚合、精确匹配等操作。
可以通过给目标字段增加子字段的方式实现这些需求。下面代码创建的索引中,title字段下包含了一个子字段:
# title字段具有一个叫keyword的子字段
PUT /test_sub_field
{
"mappings": {
"properties": {
"title": {
"type": "text",
"analyzer": "ik_smart",
"fields" : {
"keyword" : {
"type" : "keyword",
"ignore_above" : 256
}
}
}
}
}
}
PUT /test_sub_field/_doc/1
{
"title": "程序员的自我修养"
}
在keyword类型中有一个ignore_above属性,它表示当字符串长度超过ignore_above属性的值时,就不再为该字段建立索引,以避免消耗过多的存储空间。但如果不为字段建立索引,该字段也就无法支持搜索。Elasticsearch默认会为text类型的字段建立keyword类型的子字段,其子字段的ignore_above属性值默认为256。
由于title字段具有子字段,所以可以通过下面两种方式对其进行搜索,第一种是利用title字段本身执行全文搜索,第二种是利用title字段的子字段执行精确搜索:
# 通过title字段本身执行全文搜索
GET /test_sub_field/_doc/_search
{
"query": {
"term": {
"title": "修养"
}
}
}
# 通过子字段执行精确搜索
GET /test_sub_field/_doc/_search
{
"query": {
"term": {
"title.keyword": "程序员的自我修养"
}
}
}
Elasticsearch支持在索引内部定义一个局部分词器,局部分词器可以应用在该索引内的任何一个text类型字段。
下面代码创建一个索引,该索引中包含一个自定义的局部分词器,并将该分词器应用在description字段上:
# 在索引内部定义了一个局部分词器my_chinese_analyzer
# 相比于原版ik_smart分词器,增加了过滤html标签的char_filter和删除stopword的filter
PUT /test_custom_analyzer
{
"settings": {
"analysis": {
"filter": {
"chinese_stopwords": {
"type": "stop",
"stopwords": ["了", "的", "是", "和"]
}
},
"analyzer": {
"my_chinese_analyzer": {
"type": "custom",
"char_filter": ["html_strip"],
"tokenizer": "ik_smart",
"filter": ["chinese_stopwords"]
}
}
}
},
"mappings": {
"properties": {
"description": {
"type": "text",
"analyzer": "my_chinese_analyzer"
}
}
}
}
可以通过下面命令验证一个索引内局部分词器的分词效果:
# 测试一个索引内的局部分词器,格式:GET /{index_name}/_analyze
GET /test_custom_analyzer/_analyze
{
"analyzer": "my_chinese_analyzer",
"text": "《Java编程思想》是Java领域极具影响力和价值的经典著作"
}
# 分词结果
java, 编程, 思想, 领域, 极具, 影响力, 价值, 经典著作
可以看到所有的stopword都已经被删除了。此时如果再用”是“、”的“等词作为搜索条件,将搜索不到任何结果。
对一个字段来说,最重要的两个属性就是其数据类型和index属性,一个字段要支持搜索,就必须为该字段建立索引,index属性就是用来控制是否为已给字段建立索引,进而控制该字段是否可以支持搜索
数据类型除了表示一个字段的类型和数据长度外,还决定着一个字段要以什么方式建立索引,所有数据类型可以分为两大类型-- 精确值和全文本。text类型是全文本,其他所有类型都是精确值。
为精确值类型字段建立索引,在索引中保存的是字段本身的值
而为全文本类型字段建立索引,需要先对该字段本身进行分词处理得到一组词(term),再根据这组词建立索引,这样建立的索引也就是倒排索引
当需要更新索引Mapping时,由于ES一旦创建索引并定义映射则默认情况下不允许更改已经存在的字段映射的,意味着,一旦索引被创建,字段类型和属性将不能再修改,ES提供了一些策略来处理这种情况
重建索引(Reindex):
这是最安全和推荐的方法。您可以创建一个新的索引,并在新索引中定义所需的新映射。然后使用 Elasticsearch 的 Reindex API 将数据从旧索引重新索引到新索引中。这样可以确保新索引具有新的映射,并且不会丢失现有的数据。
动态映射(Dynamic Mapping):
Elasticsearch 允许自动检测和创建映射,这被称为动态映射。当您索引一个新文档并包含之前未见过的字段时,Elasticsearch 将自动为该字段创建映射。动态映射的行为可以通过配置动态映射设置来调整。
Update By Query API:
在某些情况下,您可能只需更改字段的属性,而不是其类型。在这种情况下,您可以使用 Update By Query API 来执行批量更新操作,以更改字段的属性。但请注意,Update By Query API 不允许更改字段类型。
建议使用第一种,以下是示例
# 首先,创建一个新的索引,并定义所需的新映射:
PUT /new_index
{
"mappings": {
"properties": {
"new_field": {
"type": "text"
},
"existing_field": {
"type": "keyword"
}
}
}
}
# 使用 Reindex API 将数据从旧索引重新索引到新索引
POST /_reindex
{
"source": {
"index": "old_index"
},
"dest": {
"index": "new_index"
}
}
请注意,重建索引可能会消耗一定的资源,特别是在处理大量数据时,因此建议在维护时间段或负载较低的时候执行该操作。
查询表达式DSL(Domain Specific Language)用于执行各类搜索命令
根据是否需要分词处理,查询DSL分成两大类,分别是Term查询和Match查询。
Term 查询直接将查询参数本身作为条件执行搜索操作
Match 查询会先对查询参数做分词处理(根据搜索字段使用相同的分词方式处理),再执行搜索操作
# 创建一个测试索引,message字段是text类型,采用ik_smart分词器
PUT /news
{
"mappings": {
"properties": {
"message": {
"type": "text",
"analyzer": "ik_smart"
}
}
}
}
# 保存一个测试文档
PUT /news/_doc/1
{
"message": "90后博导5年来发表SCI论文60余篇"
}
# term搜索,使用大写SCI作为搜索参数,无法搜索到测试文档
GET /news/_doc/_search
{
"query": {
"term": {
"message": "SCI"
}
}
}
# term搜索,换成小写sci作为搜索参数,能够搜索到测试文档
GET /news/_doc/_search
{
"query": {
"term": {
"message": "sci"
}
}
}
# term搜索,虽然搜索参数与测试文档的内容完全一致,但无法搜索到目标文档
GET /news/_doc/_search
{
"query": {
"term": {
"message": "90后博导5年来发表SCI论文60余篇"
}
}
}
当在news索引中保存文档时,message字段的值会先经过分词器的处理再为其建立倒排索引。测试文档的message字段被分词后的结果如下:
90, 后, 博导, 5, 年来, 发表, sci, 论文, 60, 余, 篇
所以在为message字段建立的倒排索引中,既不包含大写的“SCI”,也不包含文档的原始值“90后博导5年来发表SCI论文60余篇”。
Term查询不会对查询参数做任何的分词处理,所以用“SCI”和“90后博导5年来发表SCI论文60余篇”作为搜索条件无法找到对应的测试文档。
# mathc查询可以搜索到测试文档
GET /news/_doc/_search
{
"query": {
"match": {
"message": "博士毕业要求至少两篇SCI论文"
}
}
}
# mathc查询的结果
{
...
"hits" : {
...
"max_score" : 0.5753642,
"hits" : [
{
"_index" : "news",
"_type" : "_doc",
"_id" : "1",
"_score" : 0.5753642,
"_source" : {
"message" : "90后博导5年来发表SCI论文60余篇"
}
}
]
}
}
Match查询会先对查询参数做分词处理,再执行搜索。由于索引的mapping指定message字段使用ik_smart分词器,所以Elasticsearch执行这个Match查询时也会先用ik_smart分词器处理message查询参数,message查询参数被分词后的结果如下:
博士, 毕业, 要求, 至少, 两篇, sci, 论文
用这组词在message字段的倒排索引中查找,发现有”sci“和”论文“两个词可以匹配,并对其计算相关性算分得到0.5753642,所以得到了上面的查询结果
大多数情况,我们可以使用Match查询执行所有类型字段的搜索操作。如果搜索的是text类型字段,Match查询会先分词再搜索。但如果搜索的是精确值类型的字段,例如数字、日期、keyword字符串等类型的字段,Match查询会直接使用查询参数本身的值进行搜索。所以当搜索精确值类型的字段时,Match查询和Term查询的执行效果是一样的。
range查询用于搜索字段的值在某个范围内的文档。例如搜索书的库存数量在某个范围内的文档
# 搜索stock < 200的文档
GET /book_store/_doc/_search
{
"query": {
"range": {
"stock": {
"lt": 200
}
}
}
}
# 搜索stock >= 300的文档
GET /book_store/_doc/_search
{
"query": {
"range": {
"stock": {
"gte": 300 # 大于等于(Greater than or equal to)
}
}
}
}
# 搜索200 < stock < 400的文档
GET /book_store/_doc/_search
{
"query": {
"range": {
"stock": {
"gt": 200, # 大于(Greater than)
"lt": 400 # 小于(Less than)
}
}
}
}
截至目前,我们看到的所有搜索表达式中都只包含一个搜索条件。可很多时候,搜索条件可能不止一个。
book_store 索引mapping如下
PUT /book_store
{
"mappings": {
"properties": {
"ISBN": {
"type": "keyword"
},
"title": {
"type": "text",
"analyzer": "ik_smart"
},
"description": {
"type": "text",
"analyzer": "ik_smart"
},
"price": {
"type": "scaled_float",
"scaling_factor": 100
},
"author": {
"type": "text"
},
"publisher": {
"type": "text"
},
"stock": {
"type": "long"
}
}
}
}
搜索需求如下
搜索跟Java相关的书,即书名中包含Java;
由于是大批量采购,所以书的库存数量必须大于等于200;
书的作者不能是特朗普,毕竟懂王人比较离谱,书也不能怎么样;
如果书的描述中包含“经典”、“著作”等形容词,要把该书的优先级升高,即排在搜索结果的前面;
bool查询有如下子句
must
必须匹配的条件,就像SQL中的AND
must_not
不能匹配的条件,就像SQL中的NOT
should
如果匹配,则增加匹配文档的相关性分数,如果不匹配,也没有影响
# bool查询命令
GET /book_store/_doc/_search
{
"query": {
"bool": {
"must": [
{
"match": {
"title": "Java"
}
},
{
"range": {
"stock": {
"gte": 200
}
}
}
],
"must_not": {
"match": {
"author": "特朗普"
}
},
"should": [
{
"term": { # 这里之所以使用term是因为不需要对查询参数进行分词处理
"description": "经典"
}
},
{
"term": {
"description": "著作"
}
}
]
}
}
}
默认情况下,搜索结果会按照每个文档的相关性分数进行排序
使用sort参数可以指定搜索结果的排序方式,它与query参数是在同一个级别的。query参数负责指定搜索条件,sort参数算是对它的补充说明
# 根据库存数量排序搜索结果
# 删除query参数中的should子句(因为不再根据相关性分数对搜索结果进行排序),增加了sort参数
GET /book_store/_doc/_search
{
"query": {
"bool": {
"must": [
{
"match": {
"title": "Java"
}
},
{
"range": {
"stock": {
"gte": 200
}
}
}
],
"must_not": {
"match": {
"author": "特朗普"
}
}
}
},
"sort": {
"stock": {
"order": "desc"
}
}
}
分页搜索时,我们可以使用 from 和 size 参数来控制返回的结果集的起始位置和大小。from 参数表示从结果集中的第几条开始返回(0表示第一条),size 参数表示返回的文档数量
GET /book_store/_search
{
"from": 0,
"size": 1,
"sort": [
{
"stock": {
"order": "desc"
}
}
],
"query": {
"bool": {
"must": [
{
"match": {
"title": "Java"
}
},{
"range": {
"stock": {
"gte": 200
}
}
}
],
"must_not": [
{
"match": {
"author": "特朗普"
}
}
]
}
}
}