Lucene 是一个开源、免费、高性能、纯 Java 编写的全文检索引擎,可以算作是开源领域最好的全文检索工具包。
在实际开发中,Lucene 几乎适用于任何需要全文检索的场景,所以 Lucene 先后发展出好多语言版本,例如 C++、C#、Python 等。
早在 2005 年,Lucene 就升级为 Apache 顶级开源项目。它的作者是 Doug Cutting,有的人可能没听过这这个人,不过你肯定听过他的另一个大名鼎鼎的作品 Hadoop。
不过需要注意的是,Lucene 只是一个工具包,并非一个完整的搜索引擎,开发者可以基于 Lucene 来开发完整的搜索引擎。比较著名的有 Solr、ElasticSearch,不过在分布式和大数据环境下,ElasticSearch 更胜一筹。
Lucene 主要有如下特点:
ElasticSearch 是一个分布式、可扩展、近实时性的高性能搜索与数据分析引擎。ElasticSearch 基于 Java 编写,通过进一步封装 Lucene,将搜索的复杂性屏蔽起来,开发者只需要一套简单的 RESTful API 就可以操作全文检索。
ElasticSearch 在分布式环境下表现优异,这也是它比较受欢迎的原因之一。它支持 PB 级别的结构化或非结构化海量数据处理
整体上来说,ElasticSearch 有三大功能:
ElasticSearch 的主要特点:
* soft nproc 4096
* hard nproc 4096
* soft nofile 65536
* hard nofile 65536
vm.max_map_count=655360
首先打开 Es 官网,找到 Elasticsearch:
将下载的文件解压,解压后的目录含义如下:
目录 | 含义 |
---|---|
modules | 依赖模块目录 |
lib | 第三方依赖库 |
logs | 输出日志目录 |
plugins | 插件目录 |
bin | 可执行文件目录 |
config | 配置文件目录 |
data | 数据存储目录 |
启动方式:
进入到 bin 目录下,直接执行 ./elasticsearch 启动即可。
看到 started 表示启动成功。
默认监听的端口是 9200,所以浏览器直接输入 localhost:9200 可以查看节点信息。
节点的名字以及集群(默认是 elasticsearch)的名字,我们都可以自定义配置。
打开 config/elasticsearch.yml 文件,可以配置集群名称以及节点名称。配置方式如下:
cluster.name: javaboy-es
node.name: master
配置完成后,保存配置文件,并重启 es。重启成功后,刷新浏览器 localhost:9200 页面,就可以看到最新信息。
Elasticsearch-head 插件,可以通过可视化的方式查看集群信息。
Chrome 直接在 App Store 搜索 Elasticsearch-head,点击安装即可。
四个步骤:
git clone git://github.com/mobz/elasticsearch-head.git
cd elasticsearch-head
npm install
npm run start
注意,此时看不到集群数据。原因在于这里通过跨域的方式请求集群数据的,默认情况下,集群不支持跨域,所以这里就看不到集群数据。
解决办法如下,修改 es 的 config/elasticsearch.yml 配置文件,添加如下内容,使之支持跨域:
http.cors.enabled: true
http.cors.allow-origin: "*"
配置完成后,重启 es,此时 head 上就有数据了。
首先修改 master 的 config/elasticsearch.yml 配置文件:
cluster.name: javaboy-es #(自定义的集群名称)
node.name: master #(自定义的节点名称)
node.master: true #(作为master节点)
network.host: 127.0.0.1 #(主机IP地址)
配置完成后,重启 master。
将 es 的压缩包解压两份,分别命名为 slave01 和 slave02,代表两个从机。
分别对其进行配置。
slave01/config/elasticsearch.yml:
cluster.name: javaboy-es #(自定义的集群名称,同master节点的配置一致)
node.name: slave01 #(自定义的节点名称)
network.host: 127.0.0.1 #(主机IP地址)
http.port: 9201 #(端口号)
discovery.zen.ping.unicast.hosts: ["127.0.0.1"] #(找到master节点)
slave02/config/elasticsearch.yml:
cluster.name: javaboy-es #(自定义的集群名称,同master节点的配置一致)
node.name: slave02 #(自定义的节点名称)
network.host: 127.0.0.1 #(主机IP地址)
http.port: 9202 #(端口号)
discovery.zen.ping.unicast.hosts: ["127.0.0.1"] #(找到master节点)
然后分别启动 slave01 和 slave02。启动后,可以在 head 插件上查看集群信息。
Kibana 是一个 Elastic 公司推出的一个针对 es 的分析以及数据可视化平台,可以搜索、查看存放在 es 中的数据。
安装步骤如下:
Kibana 安装好之后,首次打开时,可以选择初始化 es 提供的测试数据,也可以不使用。
cluster.name: el-cluster
node.name: es01
#path.data: /home/es/es01/data
#path.logs: /home/es/es01/logs
bootstrap.memory_lock: false
bootstrap.system_call_filter: false
network.host: 0.0.0.0
http.port: 9200
transport.tcp.port: 9300
discovery.seed_hosts: ["192.168.220.**0","192.168.220.**1","192.168.220.**2"]
cluster.initial_master_nodes: ["192.168.220.**0]
http.cors.enabled: true
http.cors.allow-origin: "*"
cluster.name: el-cluster
node.name: es02
#path.data: /home/es/es02/data
#path.logs: /home/es/es02/logs
bootstrap.memory_lock: false
bootstrap.system_call_filter: false
network.host: 0.0.0.0
http.port: 9200
transport.tcp.port: 9300
discovery.seed_hosts: ["192.168.220.**0","192.168.220.**1","192.168.220.**2"]
cluster.initial_master_nodes: ["192.168.220.**0]
http.cors.enabled: true
http.cors.allow-origin: "*"
cluster.name: el-cluster
node.name: es03
#path.data: /home/es/es03/data
#path.logs: /home/es/es03/logs
bootstrap.memory_lock: false
bootstrap.system_call_filter: false
network.host: 0.0.0.0
http.port: 9200
transport.tcp.port: 9300
discovery.seed_hosts: ["192.168.220.**0","192.168.220.**1","192.168.220.**2"]
cluster.initial_master_nodes: ["192.168.220.**0]
http.cors.enabled: true
http.cors.allow-origin: "*"
一个或者多个安装了 es 节点的服务器组织在一起,就是集群,这些节点共同持有数据,共同提供搜索服务。
一个集群有一个名字,这个名字是集群的唯一标识,该名字成为 cluster name,默认的集群名称是 elasticsearch,具有相同名称的节点才会组成一个集群。
可以在 config/elasticsearch.yml 文件中配置集群名称:
cluster.name: javaboy-es
在集群中,节点的状态有三种:绿色、黄色、红色:
集群中的一个服务器就是一个节点,节点中会存储数据,同时参与集群的索引以及搜索功能。一个节点想要加入一个集群,只需要配置一下集群名称即可。默认情况下,如果我们启动了多个节点,多个节点还能够互相发现彼此,那么它们会自动组成一个集群,这是 es 默认提供的,但是这种方式并不可靠,有可能会发生脑裂现象。所以在实际使用中,建议一定手动配置一下集群信息。
索引可以从两方面来理解:
名词
具有相似特征文档的集合。
动词
索引数据以及对数据进行索引操作。
类型是索引上的逻辑分类或者分区。在 es6 之前,一个索引中可以有多个类型,从 es7 开始,一个索引中,只能有一个类型。在 es6.x 中,依然保持了兼容,依然支持单 index 多个 type 结构,但是已经不建议这么使用。
一个可以被索引的数据单元。例如一个用户的文档、一个产品的文档等等。文档都是 JSON 格式的。
索引都是存储在节点上的,但是受限于节点的空间大小以及数据处理能力,单个节点的处理效果可能不理想,此时我们可以对索引进行分片。当我们创建一个索引的时候,就需要指定分片的数量。每个分片本身也是一个功能完善并且独立的索引。
默认情况下,一个索引会自动创建 1 个分片,并且为每一个分片创建一个副本。
副本也就是备份,是对主分片的一个备份。
集群中对索引的定义信息,例如索引的分片数、副本数等等。
Mapping 保存了定义索引字段的存储类型、分词方式、是否存储等信息。
字段分词方式的定义。
ElasticSearch 核心功能就是数据检索,首先通过索引将文档写入 es。查询分析则主要分为两个步骤:
- 词条化:分词器将输入的文本转为一个一个的词条流。
- 过滤:比如停用词过滤器会从词条中去除不相干的词条(的,嗯,啊,呢);另外还有同义词过滤器、小写过滤器等。
ElasticSearch 中内置了多种分词器可以供使用。
在 Es 中,使用较多的中文分词器是 elasticsearch-analysis-ik,这个是 es 的一个第三方插件,代码托管在 GitHub 上:
两种使用方式:
第一种:
第二种:
./bin/elasticsearch-plugin install https://github.com/medcl/elasticsearch-analysis-ik/releases/download/v7.9.3/elasticsearch-analysis-ik-7.9.3.zip
es 重启成功后,首先创建一个名为 test 的索引:
接下来,在该索引中进行分词测试:
在 es/plugins/ik/config 目录下,新建 ext.dic 文件(文件名任意),在该文件中可以配置自定义的词库。
如果有多个词,换行写入新词即可。
然后在 es/plugins/ik/config/IKAnalyzer.cfg.xml 中配置扩展词典的位置:
也可以配置远程词库,远程词库支持热更新(不用重启 es 就可以生效)。
热更新只需要提供一个接口,接口返回扩展词即可。
具体使用方式如下,新建一个 Spring Boot 项目,引入 Web 依赖即可。然后在 resources/stastic 目录下新建 ext.dic 文件,写入扩展词:
接下来,在 es/plugins/ik/config/IKAnalyzer.cfg.xml 文件中配置远程扩展词接口:
配置完成后,重启 es ,即可生效。
热更新,主要是响应头的 Last-Modified
或者 ETag
字段发生变化,ik 就会自动重新加载远程扩展
在 head 插件中,选择 索引选项卡,然后点击新建索引。新建索引时,需要填入索引名称、分片数以及副本数。
索引创建成功后,如下图:
0、1、2、3、4 分别表示索引的分片,粗框表示主分片,细框表示副本(点一下框,通过 primary 属性可以查看是主分片还是副本)。.kibana 索引只有一个分片和一个副本,所以只有 0。
可以通过 postman 发送请求,也可以通过 kibana 发送请求,由于 kibana 有提示,所以这里采用 kibana。
创建索引请求:
PUT book
创建成功后,可以查看索引信息:
需要注意两点:
索引创建好之后,可以修改其属性。
例如修改索引的副本数:
PUT book/_settings
{
"number_of_replicas": 2
}
修改成功后,如下:
更新分片数也是一样。
索引创建成功后,可以向索引中写入文档:
PUT book/_doc/1
{
"title":"三国演义"
}
写入成功后,可以在 head 插件中查看:
默认情况下,索引是具备读写权限的,当然这个读写权限可以关闭。
例如,关闭索引的写权限:
PUT book/_settings
{
"blocks.write": true
}
关闭之后,就无法添加文档了。关闭了写权限之后,如果想要再次打开,方式如下:
PUT book/_settings
{
"blocks.write": false
}
其他类似的权限有:
head 插件查看方式如下:
请求查看方式如下:
GET book/_settings
也可以同时查看多个索引信息:
GET book,test/_settings
也可以查看所有索引信息:
GET _all/_settings
head 插件可以删除索引:
请求删除如下:
DELETE test
删除一个不存在的索引会报错。
关闭索引:
POST book/_close
打开索引:
POST book/_open
当然,可以同时关闭/打开多个索引,多个索引用 , 隔开,或者直接使用 _all 代表所有索引。
索引复制,只会复制数据,不会复制索引配置。
POST _reindex
{
"source": {"index":"book"},
"dest": {"index":"book_new"}
}
复制的时候,可以添加查询条件。
可以为索引创建别名,如果这个别名是唯一的,该别名可以代替索引名称。
POST /_aliases
{
"actions": [
{
"add": {
"index": "book",
"alias": "book_alias"
}
}
]
}
添加结果如下:
将 add 改为 remove 就表示移除别名:
POST /_aliases
{
"actions": [
{
"remove": {
"index": "book",
"alias": "book_alias"
}
}
]
}
查看某一个索引的别名:
GET /book/_alias
查看某一个别名对应的索引(book_alias 表示一个别名):
GET /book_alias/_alias
可以查看集群上所有可用别名:
GET /_alias
首先新建一个索引。
然后向索引中添加一个文档:
PUT blog/_doc/1
{
"title":"6. ElasticSearch 文档基本操作",
"date":"2020-11-05",
"content":"微信公众号**江南一点雨**后台回复 **elasticsearch06** 下载本笔记。首先新建一个索引。"
}
1 表示新建文档的 id。
添加成功后,响应的 json 如下:
{
"_index" : "blog",
"_type" : "_doc",
"_id" : "1",
"_version" : 1,
"result" : "created",
"_shards" : {
"total" : 2,
"successful" : 2,
"failed" : 0
},
"_seq_no" : 0,
"_primary_term" : 1
}
添加成功后,可以查看添加的文档:
当然,添加文档时,也可以不指定 id,此时系统会默认给出一个 id,如果不指定 id,则需要使用 POST 请求,而不能使用 PUT 请求。
POST blog/_doc
{
"title":"666",
"date":"2020-11-05",
"content":"微信公众号**江南一点雨**后台回复 **elasticsearch06** 下载本笔记。首先新建一个索引。"
}
Es 中提供了 GET API 来查看存储在 es 中的文档。使用方式如下:
GET blog/_doc/RuWrl3UByGJWB5WucKtP
上面这个命令表示获取一个 id 为 RuWrl3UByGJWB5WucKtP 的文档。
如果获取不存在的文档,会返回如下信息:
{
"_index" : "blog",
"_type" : "_doc",
"_id" : "2",
"found" : false
}
如果仅仅只是想探测某一个文档是否存在,可以使用 head 请求:
如果文档不存在,响应如下:
如果文档存在,响应如下:
当然也可以批量获取文档。
GET blog/_mget
{
"ids":["1","RuWrl3UByGJWB5WucKtP"]
}
这里可能有小伙伴有疑问,GET 请求竟然可以携带请求体?
某些特定的语言,例如 JavaScript 的 HTTP 请求库是不允许 GET 请求有请求体的,实际上在 RFC7231 文档中,并没有规定 GET 请求的请求体该如何处理,这样造成了一定程度的混乱,有的 HTTP 服务器支持 GET 请求携带请求体,有的 HTTP 服务器则不支持。虽然 es 工程师倾向于使用 GET 做查询,但是为了保证兼容性,es 同时也支持使用 POST 查询。例如上面的批量查询案例,也可以使用 POST 请求。
注意,文档更新一次,version 就会自增 1。
可以直接更新整个文档:
PUT blog/_doc/RuWrl3UByGJWB5WucKtP
{
"title":"666"
}
这种方式,更新的文档会覆盖掉原文档。
大多数时候,我们只是想更新文档字段,这个可以通过脚本来实现。
POST blog/_update/1
{
"script": {
"lang": "painless",
"source":"ctx._source.title=params.title",
"params": {
"title":"666666"
}
}
}
更新的请求格式:POST {index}/_update/{id}
在脚本中,lang 表示脚本语言,painless 是 es 内置的一种脚本语言。source 表示具体执行的脚本,ctx 是一个上下文对象,通过 ctx 可以访问到 _source、_title 等。
也可以向文档中添加字段:
POST blog/_update/1
{
"script": {
"lang": "painless",
"source":"ctx._source.tags=[\"java\",\"php\"]"
}
}
添加成功后的文档如下:
通过脚本语言,也可以修改数组。例如再增加一个 tag:
POST blog/_update/1
{
"script":{
"lang": "painless",
"source":"ctx._source.tags.add(\"js\")"
}
}
当然,也可以使用 if else 构造稍微复杂一点的逻辑。
POST blog/_update/1
{
"script": {
"lang": "painless",
"source": "if (ctx._source.tags.contains(\"java\")){ctx.op=\"delete\"}else{ctx.op=\"none\"}"
}
}
通过条件查询找到文档,然后再去更新。
例如将 title 中包含 666 的文档的 content 修改为 888。
POST blog/_update_by_query
{
"script": {
"source": "ctx._source.content=\"888\"",
"lang": "painless"
},
"query": {
"term": {
"title":"666"
}
}
}
从索引中删除一个文档。
删除一个 id 为 TuUpmHUByGJWB5WuMasV 的文档。
DELETE blog/_doc/TuUpmHUByGJWB5WuMasV
如果在添加文档时指定了路由,则删除文档时也需要指定路由,否则删除失败。
查询删除是 POST 请求。
例如删除 title 中包含 666 的文档:
POST blog/_delete_by_query
{
"query":{
"term":{
"title":"666"
}
}
}
也可以删除某一个索引下的所有文档:
POST blog/_delete_by_query
{
"query":{
"match_all":{
}
}
}
es 中通过 Bulk API 可以执行批量索引、批量删除、批量更新等操作。
首先需要将所有的批量操作写入一个 JSON 文件中,然后通过 POST 请求将该 JSON 文件上传并执行。
例如新建一个名为 aaa.json 的文件,内容如下:
首先第一行:index 表示要执行一个索引操作(这个表示一个 action,其他的 action 还有 create,delete,update)。_index 定义了索引名称,这里表示要创建一个名为 user 的索引,_id 表示新建文档的 id 为 666。
第二行是第一行操作的参数。
第三行的 update 则表示要更新。
第四行是第三行的参数。
注意,结尾要空出一行。
aaa.json 文件创建成功后,在该目录下,执行请求命令,如下:
curl -XPOST "http://localhost:9200/user/_bulk" -H "content-type:application/json" --data-binary @aaa.json
执行完成后,就会创建一个名为 user 的索引,同时向该索引中添加一条记录,再修改该记录,最终结果如下:
es 是一个分布式系统,当我们存储一个文档到 es 上之后,这个文档实际上是被存储到 master 节点中的某一个主分片上。
例如新建一个索引,该索引有两个分片,0个副本,如下:
接下来,向该索引中保存一个文档:
PUT blog/_doc/a
{
"title":"a"
}
文档保存成功后,可以查看该文档被保存到哪个分片中去了:
GET _cat/shards/blog?v
查看结果如下:
index shard prirep state docs store ip node
blog 1 p STARTED 0 208b 127.0.0.1 slave01
blog 0 p STARTED 1 3.6kb 127.0.0.1 master
从这个结果中,可以看出,文档被保存到分片 0 中。
那么 es 中到底是按照什么样的规则去分配分片的?
es 中的路由机制是通过哈希算法,将具有相同哈希值的文档放到一个主分片中,分片位置的计算方式如下:
shard=hash(routing) % number_of_primary_shards
routing 可以是一个任意字符串,es 默认是将文档的 id 作为 routing 值,通过哈希函数根据 routing 生成一个数字,然后将该数字和分片数取余,取余的结果就是分片的位置。
默认的这种路由模式,最大的优势在于负载均衡,这种方式可以保证数据平均分配在不同的分片上。但是他有一个很大的劣势,就是查询时候无法确定文档的位置,此时它会将请求广播到所有的分片上去执行。另一方面,使用默认的路由模式,后期修改分片数量不方便。
当然开发者也可以自定义 routing 的值,方式如下:
PUT blog/_doc/d?routing=javaboy
{
"title":"d"
}
如果文档在添加时指定了 routing,则查询、删除、更新时也需要指定 routing。
GET blog/_doc/d?routing=javaboy
自定义 routing 有可能会导致负载不均衡,这个还是要结合实际情况选择。
典型场景:
对于用户数据,我们可以将 userid 作为 routing,这样就能保证同一个用户的数据保存在同一个分片中,检索时,同样使用 userid 作为 routing,这样就可以精准的从某一个分片中获取数据。
当我们使用 es 的 API 去进行文档更新时,它首先读取原文档出来,然后对原文档进行更新,更新完成后再重新索引整个文档。不论你执行多少次更新,最终保存在 es 中的是最后一次更新的文档。但是如果有两个线程同时去更新,就有可能出问题。
要解决问题,就是锁。
悲观锁
很悲观,每一次去读取数据的时候,都认为别人可能会修改数据,所以屏蔽一切可能破坏数据完整性的操作。关系型数据库中,悲观锁使用较多,例如行锁、表锁等等。
乐观锁
很乐观,每次读取数据时,都认为别人不会修改数据,因此也不锁定数据,只有在提交数据时,才会检查数据完整性。这种方式可以省去锁的开销,进而提高吞吐量。
在 es 中,实际上使用的就是乐观锁。
es6.7之前
在 es6.7 之前,使用 version+version_type 来进行乐观并发控制。根据前面的介绍,文档每被修改一个,version 就会自增一次,es 通过 version 字段来确保所有的操作都有序进行。
version 分为内部版本控制和外部版本控制。
es 自己维护的就是内部版本,当创建一个文档时,es 会给文档的版本赋值为 1。
每当用户修改一次文档,版本号就回自增 1。
如果使用内部版本,es 要求 version 参数的值必须和 es 文档中 version 的值相当,才能操作成功。
也可以维护外部版本。
在添加文档时,就指定版本号:
PUT blog/_doc/1?version=200&version_type=external
{
"title":"2222"
}
以后更新的时候,版本要大于已有的版本号。
现在使用 if_seq_no 和 if_primary_term 两个参数来做并发控制。
seq_no 不属于某一个文档,它是属于整个索引的(version 则是属于某一个文档的,每个文档的 version 互不影响)。现在更新文档时,使用 seq_no 来做并发。由于 seq_no 是属于整个 index 的,所以任何文档的修改或者新增,seq_no 都会自增。
现在就可以通过 seq_no 和 primary_term 来做乐观并发控制。
PUT blog/_doc/2?if_seq_no=5&if_primary_term=1
{
"title":"6666"
}
倒排索引是 es 中非常重要的索引结构,是从文档词项到文档 ID 的一个映射过程。
我们在关系型数据库中见到的索引,就是“正排索引”。
关系型数据库中的索引如下,假设我有一个博客表:
我们可以针对这个表建立索引(正排索引):
当我们通过 id 或者标题去搜索文章时,就可以快速搜到。
但是如果我们按照文章内容的关键字去搜索,就只能去内容中做字符匹配了。为了提高查询效率,就要考虑使用倒排索引。
倒排索引就是以内容的关键字建立索引,通过索引找到文档 id,再进而找到整个文档。
一般来说,倒排索引分为两个部分:
当我们去索引一个文档时,就回建立倒排索引,搜索时,直接根据倒排索引搜索。
映射就是 Mapping,它用来定义一个文档以及文档所包含的字段该如何被存储和索引。所以,它其实有点类似于关系型数据库中表的定义。
顾名思义,就是自动创建出来的映射。es 根据存入的文档,自动分析出来文档中字段的类型以及存储方式,这种就是动态映射。
举一个简单例子,新建一个索引,然后查看索引信息:
在创建好的索引信息中,可以看到,mappings 为空,这个 mappings 中保存的就是映射信息。
现在我们向索引中添加一个文档,如下:
PUT blog/_doc/1
{
"title":"1111",
"date":"2020-11-11"
}
文档添加成功后,就会自动生成 Mappings:
可以看到,date 字段的类型为 date,title 的类型有两个,text 和 keyword。
默认情况下,文档中如果新增了字段,mappings 中也会自动新增进来。
有的时候,如果希望新增字段时,能够抛出异常来提醒开发者,这个可以通过 mappings 中 dynamic 属性来配置。
dynamic 属性有三种取值:
具体配置方式如下,创建索引时指定 mappings(这其实就是静态映射):
PUT blog
{
"mappings": {
"dynamic":"strict",
"properties": {
"title":{
"type": "text"
},
"age":{
"type":"long"
}
}
}
}
然后向 blog 中索引中添加数据:
PUT blog/_doc/2
{
"title":"1111",
"date":"2020-11-11",
"age":99
}
在添加的文档中,多出了一个 date 字段,而该字段没有预定义,所以这个添加操作就回报错:
{
"error" : {
"root_cause" : [
{
"type" : "strict_dynamic_mapping_exception",
"reason" : "mapping set to strict, dynamic introduction of [date] within [_doc] is not allowed"
}
],
"type" : "strict_dynamic_mapping_exception",
"reason" : "mapping set to strict, dynamic introduction of [date] within [_doc] is not allowed"
},
"status" : 400
}
动态映射还有一个日期检测的问题。
例如新建一个索引,然后添加一个含有日期的文档,如下:
PUT blog/_doc/1
{
"remark":"2020-11-11"
}
添加成功后,remark 字段会被推断是一个日期类型。
此时,remark 字段就无法存储其他类型了。
PUT blog/_doc/1
{
"remark":"javaboy"
}
此时报错如下:
{
"error" : {
"root_cause" : [
{
"type" : "mapper_parsing_exception",
"reason" : "failed to parse field [remark] of type [date] in document with id '1'. Preview of field's value: 'javaboy'"
}
],
"type" : "mapper_parsing_exception",
"reason" : "failed to parse field [remark] of type [date] in document with id '1'. Preview of field's value: 'javaboy'",
"caused_by" : {
"type" : "illegal_argument_exception",
"reason" : "failed to parse date field [javaboy] with format [strict_date_optional_time||epoch_millis]",
"caused_by" : {
"type" : "date_time_parse_exception",
"reason" : "Failed to parse with all enclosed parsers"
}
}
},
"status" : 400
}
要解决这个问题,可以使用静态映射,即在索引定义时,将 remark 指定为 text 类型。也可以关闭日期检测。
PUT blog
{
"mappings": {
"date_detection": false
}
}
此时日期类型就回当成文本来处理。
略。
es 中动态映射类型推断方式如下:
scaled_float 举例:
PUT product
{
"mappings": {
"properties": {
"name":{
"type": "text"
},
"price":{
"type": "scaled_float",
"scaling_factor": 100
}
}
}
}
由于 JSON 中没有日期类型,所以 es 中的日期类型形式就比较多样:
es 内部将时间转为 UTC,然后将时间按照 millseconds-since-the-epoch 的长整型来存储。
自定义日期类型:
PUT product
{
"mappings": {
"properties": {
"date":{
"type": "date"
}
}
}
}
这个能够解析出来的时间格式比较多。
PUT product/_doc/1
{
"date":"2020-11-11"
}
PUT product/_doc/2
{
"date":"2020-11-11T11:11:11Z"
}
PUT product/_doc/3
{
"date":"1604672099958"
}
上面三个文档中的日期都可以被解析,内部存储的是毫秒计时的长整型数。
JSON 中的 “true”、“false”、true、false 都可以。
二进制接受的是 base64 编码的字符串,默认不存储,也不可搜索。
PUT product
{
"mappings": {
"properties": {
"date":{
"type": "date"
},
"price":{
"type":"float_range"
}
}
}
}
插入文档的时候,需要指定范围的界限:
PUT product
{
"mappings": {
"properties": {
"date":{
"type": "date"
},
"price":{
"type":"float_range"
}
}
}
}
指定范围的时,可以使用 gt、gte、lt、lte。
es 中没有专门的数组类型。默认情况下,任何字段都可以有一个或者多个值。需要注意的是,数组中的元素必须是同一种类型。
添加数组是,数组中的第一个元素决定了整个数组的类型。
由于 JSON 本身具有层级关系,所以文档包含内部对象。内部对象中,还可以再包含内部对象。
PUT product/_doc/2
{
"date":"2020-11-11T11:11:11Z",
"ext_info":{
"address":"China"
}
}
nested 是 object 中的一个特例。
如果使用 object 类型,假如有如下一个文档:
{
"user":[
{
"first":"Zhang",
"last":"san"
},
{
"first":"Li",
"last":"si"
}
]
}
由于 Lucene 没有内部对象的概念,所以 es 会将对象层次扁平化,将一个对象转为字段名和值构成的简单列表。即上面的文档,最终存储形式如下:
{
"user.first":["Zhang","Li"],
"user.last":["san","si"]
}
扁平化之后,用户名之间的关系没了。这样会导致如果搜索 Zhang si 这个人,会搜索到。
此时可以 nested 类型来解决问题,nested 对象类型可以保持数组中每个对象的独立性。nested 类型将数组中的每一饿对象作为独立隐藏文档来索引,这样每一个嵌套对象都可以独立被索引。
{
{
"user.first":"Zhang",
"user.last":"san"
},{
"user.first":"Li",
"user.last":"si"
}
}
优点
文档存储在一起,读取性能高。
缺点
更新父或者子文档时需要更新更个文档。
使用场景:
geo_point 就是一个坐标点,定义方式如下:
PUT people
{
"mappings": {
"properties": {
"location":{
"type": "geo_point"
}
}
}
}
创建时指定字段类型,存储的时候,有四种方式:
PUT people/_doc/1
{
"location":{
"lat": 34.27,
"lon": 108.94
}
}
PUT people/_doc/2
{
"location":"34.27,108.94"
}
PUT people/_doc/3
{
"location":"uzbrgzfxuzup"
}
PUT people/_doc/4
{
"location":[108.94,34.27]
}
注意,使用数组描述,先经度后纬度。
地址位置转 geo_hash:http://www.csxgame.top/#/
指定 geo_shape 类型:
PUT people
{
"mappings": {
"properties": {
"location":{
"type": "geo_shape"
}
}
}
}
添加文档时需要指定具体的类型:
PUT people/_doc/1
{
"location":{
"type":"point",
"coordinates": [108.94,34.27]
}
}
如果是 linestring,如下:
PUT people/_doc/2
{
"location":{
"type":"linestring",
"coordinates": [[108.94,34.27],[100,33]]
}
}
存储 IP 地址,类型是 ip:
PUT blog
{
"mappings": {
"properties": {
"address":{
"type": "ip"
}
}
}
}
添加文档:
PUT blog/_doc/1
{
"address":"192.168.91.1"
}
搜索文档:
GET blog/_search
{
"query": {
"term": {
"address": "192.168.0.0/16"
}
}
}
用于统计字符串分词后的词项个数。
PUT blog
{
"mappings": {
"properties": {
"title":{
"type": "text",
"fields": {
"length":{
"type":"token_count",
"analyzer":"standard"
}
}
}
}
}
}
相当于新增了 title.length 字段用来统计分词后词项的个数。
添加文档:
PUT blog/_doc/1
{
"title":"zhang san"
}
可以通过 token_count 去查询:
GET blog/_search
{
"query": {
"term": {
"title.length": 2
}
}
}
定义文本字段的分词器。默认对索引和查询都是有效的。
假设不用分词器,我们先来看一下索引的结果,创建一个索引并添加一个文档:
PUT blog
PUT blog/_doc/1
{
"title":"定义文本字段的分词器。默认对索引和查询都是有效的。"
}
查看词条向量(term vectors)
GET blog/_termvectors/1
{
"fields": ["title"]
}
查看结果如下:
{
"_index" : "blog",
"_type" : "_doc",
"_id" : "1",
"_version" : 1,
"found" : true,
"took" : 0,
"term_vectors" : {
"title" : {
"field_statistics" : {
"sum_doc_freq" : 22,
"doc_count" : 1,
"sum_ttf" : 23
},
"terms" : {
"义" : {
"term_freq" : 1,
"tokens" : [
{
"position" : 1,
"start_offset" : 1,
"end_offset" : 2
}
]
},
"分" : {
"term_freq" : 1,
"tokens" : [
{
"position" : 7,
"start_offset" : 7,
"end_offset" : 8
}
]
},
"和" : {
"term_freq" : 1,
"tokens" : [
{
"position" : 15,
"start_offset" : 16,
"end_offset" : 17
}
]
},
"器" : {
"term_freq" : 1,
"tokens" : [
{
"position" : 9,
"start_offset" : 9,
"end_offset" : 10
}
]
},
"字" : {
"term_freq" : 1,
"tokens" : [
{
"position" : 4,
"start_offset" : 4,
"end_offset" : 5
}
]
},
"定" : {
"term_freq" : 1,
"tokens" : [
{
"position" : 0,
"start_offset" : 0,
"end_offset" : 1
}
]
},
"对" : {
"term_freq" : 1,
"tokens" : [
{
"position" : 12,
"start_offset" : 13,
"end_offset" : 14
}
]
},
"引" : {
"term_freq" : 1,
"tokens" : [
{
"position" : 14,
"start_offset" : 15,
"end_offset" : 16
}
]
},
"效" : {
"term_freq" : 1,
"tokens" : [
{
"position" : 21,
"start_offset" : 22,
"end_offset" : 23
}
]
},
"文" : {
"term_freq" : 1,
"tokens" : [
{
"position" : 2,
"start_offset" : 2,
"end_offset" : 3
}
]
},
"是" : {
"term_freq" : 1,
"tokens" : [
{
"position" : 19,
"start_offset" : 20,
"end_offset" : 21
}
]
},
"有" : {
"term_freq" : 1,
"tokens" : [
{
"position" : 20,
"start_offset" : 21,
"end_offset" : 22
}
]
},
"本" : {
"term_freq" : 1,
"tokens" : [
{
"position" : 3,
"start_offset" : 3,
"end_offset" : 4
}
]
},
"查" : {
"term_freq" : 1,
"tokens" : [
{
"position" : 16,
"start_offset" : 17,
"end_offset" : 18
}
]
},
"段" : {
"term_freq" : 1,
"tokens" : [
{
"position" : 5,
"start_offset" : 5,
"end_offset" : 6
}
]
},
"的" : {
"term_freq" : 2,
"tokens" : [
{
"position" : 6,
"start_offset" : 6,
"end_offset" : 7
},
{
"position" : 22,
"start_offset" : 23,
"end_offset" : 24
}
]
},
"索" : {
"term_freq" : 1,
"tokens" : [
{
"position" : 13,
"start_offset" : 14,
"end_offset" : 15
}
]
},
"认" : {
"term_freq" : 1,
"tokens" : [
{
"position" : 11,
"start_offset" : 12,
"end_offset" : 13
}
]
},
"词" : {
"term_freq" : 1,
"tokens" : [
{
"position" : 8,
"start_offset" : 8,
"end_offset" : 9
}
]
},
"询" : {
"term_freq" : 1,
"tokens" : [
{
"position" : 17,
"start_offset" : 18,
"end_offset" : 19
}
]
},
"都" : {
"term_freq" : 1,
"tokens" : [
{
"position" : 18,
"start_offset" : 19,
"end_offset" : 20
}
]
},
"默" : {
"term_freq" : 1,
"tokens" : [
{
"position" : 10,
"start_offset" : 11,
"end_offset" : 12
}
]
}
}
}
}
}
可以看到,默认情况下,中文就是一个字一个字的分,这种分词方式没有任何意义。如果这样分词,查询就只能按照一个字一个字来查,像下面这样:
GET blog/_search
{
"query": {
"term": {
"title": "定"
}
}
}
无意义!!!
所以,我们要根据实际情况,配置合适的分词器。
给字段设定分词器:
PUT blog
{
"mappings": {
"properties": {
"title":{
"type":"text",
"analyzer": "ik_smart"
}
}
}
}
存储文档:
PUT blog/_doc/1
{
"title":"定义文本字段的分词器。默认对索引和查询都是有效的。"
}
查看词条向量:
GET blog/_termvectors/1
{
"fields": ["title"]
}
查询结果如下:
{
"_index" : "blog",
"_type" : "_doc",
"_id" : "1",
"_version" : 1,
"found" : true,
"took" : 1,
"term_vectors" : {
"title" : {
"field_statistics" : {
"sum_doc_freq" : 12,
"doc_count" : 1,
"sum_ttf" : 13
},
"terms" : {
"分词器" : {
"term_freq" : 1,
"tokens" : [
{
"position" : 4,
"start_offset" : 7,
"end_offset" : 10
}
]
},
"和" : {
"term_freq" : 1,
"tokens" : [
{
"position" : 8,
"start_offset" : 16,
"end_offset" : 17
}
]
},
"字段" : {
"term_freq" : 1,
"tokens" : [
{
"position" : 2,
"start_offset" : 4,
"end_offset" : 6
}
]
},
"定义" : {
"term_freq" : 1,
"tokens" : [
{
"position" : 0,
"start_offset" : 0,
"end_offset" : 2
}
]
},
"对" : {
"term_freq" : 1,
"tokens" : [
{
"position" : 6,
"start_offset" : 13,
"end_offset" : 14
}
]
},
"文本" : {
"term_freq" : 1,
"tokens" : [
{
"position" : 1,
"start_offset" : 2,
"end_offset" : 4
}
]
},
"有效" : {
"term_freq" : 1,
"tokens" : [
{
"position" : 11,
"start_offset" : 21,
"end_offset" : 23
}
]
},
"查询" : {
"term_freq" : 1,
"tokens" : [
{
"position" : 9,
"start_offset" : 17,
"end_offset" : 19
}
]
},
"的" : {
"term_freq" : 2,
"tokens" : [
{
"position" : 3,
"start_offset" : 6,
"end_offset" : 7
},
{
"position" : 12,
"start_offset" : 23,
"end_offset" : 24
}
]
},
"索引" : {
"term_freq" : 1,
"tokens" : [
{
"position" : 7,
"start_offset" : 14,
"end_offset" : 16
}
]
},
"都是" : {
"term_freq" : 1,
"tokens" : [
{
"position" : 10,
"start_offset" : 19,
"end_offset" : 21
}
]
},
"默认" : {
"term_freq" : 1,
"tokens" : [
{
"position" : 5,
"start_offset" : 11,
"end_offset" : 13
}
]
}
}
}
}
}
然后就可以通过词去搜索了:
GET blog/_search
{
"query": {
"term": {
"title": "索引"
}
}
}
查询时候的分词器。默认情况下,如果没有配置 search_analyzer,则查询时,首先查看有没有 search_analyzer,有的话,就用 search_analyzer 来进行分词,如果没有,则看有没有 analyzer,如果有,则用 analyzer 来进行分词,否则使用 es 默认的分词器。
normalizer 参数用于解析前(索引或者查询)的标准化配置。
比如,在 es 中,对于一些我们不想切分的字符串,我们通常会将其设置为 keyword,搜索时候也是使用整个词进行搜索。如果在索引前没有做好数据清洗,导致大小写不一致,例如 javaboy 和 JAVABOY,此时,我们就可以使用 normalizer 在索引之前以及查询之前进行文档的标准化。
先来一个反例,创建一个名为 blog 的索引,设置 author 字段类型为 keyword:
PUT blog
{
"mappings": {
"properties": {
"author":{
"type": "keyword"
}
}
}
}
添加两个文档:
PUT blog/_doc/1
{
"author":"javaboy"
}
PUT blog/_doc/2
{
"author":"JAVABOY"
}
然后进行搜索:
GET blog/_search
{
"query": {
"term": {
"author": "JAVABOY"
}
}
}
大写关键字可以搜到大写的文档,小写关键字可以搜到小写的文档。
如果使用了 normalizer,可以在索引和查询时,分别对文档进行预处理。
normalizer 定义方式如下:
PUT blog
{
"settings": {
"analysis": {
"normalizer":{
"my_normalizer":{
"type":"custom",
"filter":["lowercase"]
}
}
}
},
"mappings": {
"properties": {
"author":{
"type": "keyword",
"normalizer":"my_normalizer"
}
}
}
}
在 settings 中定义 normalizer,然后在 mappings 中引用。
测试方式和前面一致。此时查询的时候,大写关键字也可以查询到小写文档,因为无论是索引还是查询,都会将大写转为小写
boost 参数可以设置字段的权重。
boost 有两种使用思路,一种就是在定义 mappings 的时候使用,在指定字段类型时使用;另一种就是在查询时使用。
实际开发中建议使用后者,前者有问题:如果不重新索引文档,权重无法修改。
mapping 中使用 boost(不推荐):
PUT blog
{
"mappings": {
"properties": {
"content":{
"type": "text",
"boost": 2
}
}
}
}
另一种方式就是在查询的时候,指定 boost
GET blog/_search
{
"query": {
"match": {
"content": {
"query": "你好",
"boost": 2
}
}
}
}
coerce 用来清除脏数据,默认为 true。
例如一个数字,在 JSON 中,用户可能写错了:
{"age":"99"}
或者 :
{"age":"99.0"}
这些都不是正确的数字格式。
通过 coerce 可以解决该问题。
默认情况下,以下操作没问题,就是 coerce 起作用:
PUT blog
{
"mappings": {
"properties": {
"age":{
"type": "integer"
}
}
}
}
POST blog/_doc
{
"age":"99.0"
}
如果需要修改 coerce ,方式如下:
PUT blog
{
"mappings": {
"properties": {
"age":{
"type": "integer",
"coerce": false
}
}
}
}
POST blog/_doc
{
"age":99
}
当 coerce 修改为 false 之后,数字就只能是数字了,不可以是字符串,该字段传入字符串会报错。
这个属性,可以将多个字段的值,复制到同一个字段中。
定义方式如下:
PUT blog
{
"mappings": {
"properties": {
"title":{
"type": "text",
"copy_to": "full_content"
},
"content":{
"type": "text",
"copy_to": "full_content"
},
"full_content":{
"type": "text"
}
}
}
}
PUT blog/_doc/1
{
"title":"你好江南一点雨",
"content":"当 coerce 修改为 false 之后,数字就只能是数字了,不可以是字符串,该字段传入字符串会报错。"
}
GET blog/_search
{
"query": {
"term": {
"full_content": "当"
}
}
}
es 中的搜索主要是用到倒排索引,doc_values 参数是为了加快排序、聚合操作而生的。当建立倒排索引的时候,会额外增加列式存储映射。
doc_values 默认是开启的,如果确定某个字段不需要排序或者不需要聚合,那么可以关闭 doc_values。
大部分的字段在索引时都会生成 doc_values,除了 text。text 字段在查询时会生成一个 fielddata 的数据结构,fieldata 在字段首次被聚合、排序的时候生成。
doc_values 默认开启,fielddata 默认关闭。
doc_values 演示:
PUT users
PUT users/_doc/1
{
"age":100
}
PUT users/_doc/2
{
"age":99
}
PUT users/_doc/3
{
"age":98
}
PUT users/_doc/4
{
"age":101
}
GET users/_search
{
"query": {
"match_all": {}
},
"sort":[
{
"age":{
"order": "desc"
}
}
]
}
由于 doc_values 默认时开启的,所以可以直接使用该字段排序,如果想关闭 doc_values ,如下:
PUT users
{
"mappings": {
"properties": {
"age":{
"type": "integer",
"doc_values": false
}
}
}
}
PUT users/_doc/1
{
"age":100
}
PUT users/_doc/2
{
"age":99
}
PUT users/_doc/3
{
"age":98
}
PUT users/_doc/4
{
"age":101
}
GET users/_search
{
"query": {
"match_all": {}
},
"sort":[
{
"age":{
"order": "desc"
}
}
]
}
es 默认会索引所有的字段,但是有的字段可能只需要存储,不需要索引。此时可以通过 enabled 字段来控制:
PUT blog
{
"mappings": {
"properties": {
"title":{
"enabled": false
}
}
}
}
PUT blog/_doc/1
{
"title":"javaboy"
}
GET blog/_search
{
"query": {
"term": {
"title": "javaboy"
}
}
}
设置了 enabled 为 false 之后,就可以再通过该字段进行搜索了。
日期格式。format 可以规范日期格式,而且一次可以定义多个 format。
PUT users
{
"mappings": {
"properties": {
"birthday":{
"type": "date",
"format": "yyyy-MM-dd||yyyy-MM-dd HH:mm:ss"
}
}
}
}
PUT users/_doc/1
{
"birthday":"2020-11-11"
}
PUT users/_doc/2
{
"birthday":"2020-11-11 11:11:11"
}
igbore_above 用于指定分词和索引的字符串最大长度,超过最大长度的话,该字段将不会被索引,这个字段只适用于 keyword 类型。
PUT blog
{
"mappings": {
"properties": {
"title":{
"type": "keyword",
"ignore_above": 10
}
}
}
}
PUT blog/_doc/1
{
"title":"javaboy"
}
PUT blog/_doc/2
{
"title":"javaboyjavaboyjavaboy"
}
GET blog/_search
{
"query": {
"term": {
"title": "javaboyjavaboyjavaboy"
}
}
}
ignore_malformed 可以忽略不规则的数据,该参数默认为 false。
PUT users
{
"mappings": {
"properties": {
"birthday":{
"type": "date",
"format": "yyyy-MM-dd||yyyy-MM-dd HH:mm:ss"
},
"age":{
"type": "integer",
"ignore_malformed": true
}
}
}
}
PUT users/_doc/1
{
"birthday":"2020-11-11",
"age":99
}
PUT users/_doc/2
{
"birthday":"2020-11-11 11:11:11",
"age":"abc"
}
PUT users/_doc/2
{
"birthday":"2020-11-11 11:11:11aaa",
"age":"abc"
}
这个是针对 _all 字段的,但是在 es7 中,该字段已经被废弃了。
index 属性指定一个字段是否被索引,该属性为 true 表示字段被索引,false 表示字段不被索引。
PUT users
{
"mappings": {
"properties": {
"age":{
"type": "integer",
"index": false
}
}
}
}
PUT users/_doc/1
{
"age":99
}
GET users/_search
{
"query": {
"term": {
"age": 99
}
}
}
index_options 控制索引时哪些信息被存储到倒排索引中(用在 text 字段中),有四种取值:
norms 对字段评分有用,text 默认开启 norms,如果不是特别需要,不要开启 norms。
在 es 中,值为 null 的字段不索引也不可以被搜索,null_value 可以让值为 null 的字段显式的可索引、可搜索:
PUT users
{
"mappings": {
"properties": {
"name":{
"type": "keyword",
"null_value": "javaboy_null"
}
}
}
}
PUT users/_doc/1
{
"name":null,
"age":99
}
GET users/_search
{
"query": {
"term": {
"name": "javaboy_null"
}
}
}
被解析的 text 字段会将 term 的位置考虑进去,目的是为了支持近似查询和短语查询,当我们去索引一个含有多个值的 text 字段时,会在各个值之间添加一个假想的空间,将值隔开,这样就可以有效避免一些无意义的短语匹配,间隙大小通过 position_increment_gap 来控制,默认是 100。
PUT users
PUT users/_doc/1
{
"name":["zhang san","li si"]
}
GET users/_search
{
"query": {
"match_phrase": {
"name": {
"query": "sanli"
}
}
}
}
sanli
搜索不到,因为两个短语之间有一个假想的空隙,为 100。GET users/_search
{
"query": {
"match_phrase": {
"name": {
"query": "san li",
"slop": 101
}
}
}
}
可以通过 slop 指定空隙大小。
也可以在定义索引的时候,指定空隙:
PUT users
{
"mappings": {
"properties": {
"name":{
"type": "text",
"position_increment_gap": 0
}
}
}
}
PUT users/_doc/1
{
"name":["zhang san","li si"]
}
GET users/_search
{
"query": {
"match_phrase": {
"name": {
"query": "san li"
}
}
}
}
similarity 指定文档的评分模型,默认有三种:
默认情况下,字段会被索引,也可以搜索,但是不会存储,虽然不会被存储的,但是 _source 中有一个字段的备份。如果想将字段存储下来,可以通过配置 store 来实现。
term_vectors 是通过分词器产生的信息,包括:
fields 参数可以让同一字段有多种不同的索引方式。例如:
PUT blog
{
"mappings": {
"properties": {
"title":{
"type": "text",
"fields": {
"raw":{
"type":"keyword"
}
}
}
}
}
}
PUT blog/_doc/1
{
"title":"javaboy"
}
GET blog/_search
{
"query": {
"term": {
"title.raw": "javaboy"
}
}
}
PUT blog
{
"mappings": {
"dynamic_templates": [
{
"long2integer": {
"match_mapping_type": "long",
"mapping": {
"type": "integer"
}
}
}
]
}
}
PUT blog/_doc/1
{
"count":99
}
PUT blog
{
"mappings": {
"dynamic_templates":[
{
"string2long": {
"match_mapping_type": "string",
"match": "num_*",
"unmatch": "*_text",
"mapping": {
"type": "integer"
}
}
}
]
}
}
PUT blog/_doc/1
{
"num_count": "99",
"num_text": "javaboy"
}
PUT blog/_doc/2
{
"num_count": "99",
"num_text":"javaboy",
"num_aaa": 999
}
PUT blog/_doc/1
{
"num_count": "99",
"num_text": "javaboy"
}