按数据分类的话,主要可以分为以下三类:
设想一个关于搜索的场景,假设我们要搜索一首诗句内容中带“前”字的古诗:
| name | content | author |
|---|---|---|
| 静夜思 | 床前明月光,疑是地上霜。举头望明月,低头思故乡。 | 李白 |
| 望庐山瀑布 | 日照香炉生紫烟,遥看瀑布挂前川。飞流直下三千尺,疑是银河落九天。 | 李白 |
思考:用传统关系型数据库和ES 实现会有什么差别?
如果用像 MySQL 这样的 RDBMS 来存储古诗的话,我们应该会去使用这样的 SQL 去查询:
select name from poems where content like "%前%"
这种我们称为顺序扫描法,需要遍历所有的记录进行匹配。不但效率低,而且不符合我们搜索时的期望,比如我们在搜索“ABCD"这样的关键词时,通常还希望看到"A","AB","CD",“ABC”的搜索结果。
全文检索是指,通过一个程序扫描文本中的每一个单词,针对单词建立索引,并保存该单词在文本中的位置、以及出现的次数。用户查询时,通过之前建立好的索引来查询,将索引中单词对应的文本位置、出现的次数返回给用户,因为有了具体文本的位置,所以就可以将具体内容读取出来了。

搜索原理简单概括的话可以分为这么几步:
索引就类似于目录,平时我们使用的都是索引,都是通过主键定位到某条数据,那么倒排索引呢,刚好相反,数据对应到主键。

| 文章ID | 文章标题 | 文章内容 |
|---|---|---|
| 1 | 浅析JAVA设计模式 | JAVA设计模式是每一个JAVA程序员都应该掌握的进阶知识 |
| 2 | JAVA多线程设计模式 | JAVA多线程与设计模式结合 |
假如,我们有一个站内搜索的功能,通过某个关键词来搜索相关的文章,那么这个关键词可能出现在标题中,也可能出现在文章内容中,那我们将会在创建或修改文章的时候,建立一个关键词与文章的对应关系表,这种,我们可以称之为倒排索引。
| 关键词 | 文章ID |
|---|---|
| JAVA | 1,2 |
| 设计模式 | 1,2 |
| 多线程 | 2 |
ElasticSearch(简称ES)是一个分布式、RESTful 风格的搜索和数据分析引擎,是用Java开发并且是当前最流行的开源的企业级搜索引擎,能够达到近实时搜索,稳定,可靠,快速,安装使用方便。客户端支持Java、.NET(C#)、PHP、Python、Ruby等多种语言。
官方网站: Elasticsearch 平台 — 大规模查找实时答案 | Elastic
下载地址:https://www.elastic.co/cn/downloads/past-releases#elasticsearch
当前搜索引擎最新排名:参考网站

5.x 新特性
6.x 新特性
7.x 新特性
8.x 新特性
Solr 是第一个基于 Lucene 核心库功能完备的搜索引擎产品,诞生远早于 Elasticsearch。当单纯的对已有数据进行搜索时,Solr更快。

当实时建立索引时, Solr会产生io阻塞,查询性能较差, Elasticsearch具有明显的优势。

根据大型互联网公司,实际生产环境测试,将搜索引擎从Solr转到 Elasticsearch以后的平均查询速度有了50倍的提升。

总结:
在Elastic Stack之前我们听说过ELK,ELK分别是Elasticsearch,Logstash,Kibana这三款软件在一起的简称,在发展的过程中又有新的成员Beats的加入,就形成了Elastic Stack。

在Elastic Stack生态圈中Elasticsearch作为数据存储和搜索,是生态圈的基石,Kibana在上层提供用户一个可视化及操作的界面,Logstash和Beat可以对数据进行收集。在上图的右侧X-Pack部分则是Elastic公司提供的商业项目。

国内现在有大量的公司都在使用 Elasticsearch,包括携程、滴滴、今日头条、饿了么、360安全、小米、vivo等诸多知名公司。除了搜索之外,结合Kibana、Logstash、Beats,Elastic Stack还被广泛运用在大数据近实时分析领域,包括日志分析、指标监控、信息安全等多个领域。它可以帮助你探索海量结构化、非结构化数据,按需创建可视化报表,对监控数据设置报警阈值,甚至通过使用机器学习技术,自动识别异常状况。
可以参考es的环境文件elasticsearch-env.bat

ES的jdk环境生效的优先级配置 ES_JAVA_HOME > JAVA_HOME > ES_HOME
下载地址: 7.17.3

| 目录 | 描述 |
|---|---|
| bin | 脚本文件,包括启动elasticsearch,安装插件,运行统计数据等 |
| config | 配置文件目录,如elasticsearch配置、角色配置、jvm配置等。 |
| jdk | java运行环境 |
| data | 默认的数据存放目录,包含节点、分片、索引、文档的所有数据,生产环境需要修改。 |
| lib | elasticsearch依赖的Java类库 |
| logs | 默认的日志文件存储路径,生产环境需要修改。 |
| modules | 包含所有的Elasticsearch模块,如Cluster、Discovery、Indices等。 |
| plugins | 已安装插件目录 |

| 配置项 | 说明 |
|---|---|
| cluster.name | 当前节点所属集群名称,多个节点如果要组成同一个集群,那么集群名称一定要配置成相同。默认值elasticsearch,生产环境建议根据ES集群的使用目的修改成合适的名字。 |
| node.name | 当前节点名称,默认值当前节点部署所在机器的主机名,所以如果一台机器上要起多个ES节点的话,需要通过配置该属性明确指定不同的节点名称。 |
| path.data | 配置数据存储目录,比如索引数据等,默认值 $ES_HOME/data,生产环境下强烈建议部署到另外的安全目录,防止ES升级导致数据被误删除。 |
| path.logs | 配置日志存储目录,比如运行日志和集群健康信息等,默认值 $ES_HOME/logs,生产环境下强烈建议部署到另外的安全目录,防止ES升级导致数据被误删除。 |
| bootstrap.memory_lock | 配置ES启动时是否进行内存锁定检查,默认值true。ES对于内存的需求比较大,一般生产环境建议配置大内存,如果内存不足,容易导致内存交换到磁盘,严重影响ES的性能。所以默认启动时进行相应大小内存的锁定,如果无法锁定则会启动失败。非生产环境可能机器内存本身就很小,能够供给ES使用的就更小,如果该参数配置为true的话很可能导致无法锁定内存以致ES无法成功启动,此时可以修改为false。 |
| network.host | 配置能够访问当前节点的主机,默认值为当前节点所在机器的本机回环地址127.0.0.1 和[::1],这就导致默认情况下只能通过当前节点所在主机访问当前节点。可以配置为 0.0.0.0 ,表示所有主机均可访问。 |
| http.port | 配置当前ES节点对外提供服务的http端口,默认值 9200 |
| discovery.seed_hosts | 配置参与集群节点发现过程的主机列表,说白一点就是集群中所有节点所在的主机列表,可以是具体的IP地址,也可以是可解析的域名。 |
| cluster.initial_master_nodes | 配置ES集群初始化时参与master选举的节点名称列表,必须与node.name配置的一致。ES集群首次构建完成后,应该将集群中所有节点的配置文件中的cluster.initial_master_nodes配置项移除,重启集群或者将新节点加入某个已存在的集群时切记不要设置该配置项。 |
JVM配置修改
修改 $ES_HOME/config/jvm.option 配置文件,调整jvm堆内存大小:
- ################################################################
- ## IMPORTANT: JVM heap size
- ################################################################
- ##
- ## The heap size is automatically configured by Elasticsearch
- ## based on the available memory in your system and the roles
- ## each node is configured to fulfill. If specifying heap is
- ## required, it should be done through a file in jvm.options.d,
- ## and the min and max should be set to the same value. For
- ## example, to set the heap to 4 GB, create a new file in the
- ## jvm.options.d directory containing these lines:
- ##
- -Xms4g
- -Xmx4g
配置的建议:
Windows
直接运行 elasticsearch.bat
Linux(centos7)
ES不允许使用root账号启动服务,如果你当前账号是root,则需要创建一个专有账户
- #非root用户
- bin/elasticsearch
- # -d 后台启动
- bin/elasticsearch -d

注意:es默认不能用root用户启动,生产环境建议为elasticsearch创建用户。
- #为elaticsearch创建用户并赋予相应权限
- adduser es
- passwd es
- chown -R es:es .
- elasticsearch-17.3
运行http://localhost:9200/

如果ES服务启动异常,会有提示:

max file descriptors [4096] for elasticsearch process is too low, increase to at least [65536]
ES因为需要大量的创建索引文件,需要大量的打开系统的文件,所以我们需要解除linux系统当中打开文件最大数目的限制,不然ES启动就会抛错:
- \#切换到root用户
- vim /etc/security/limits.conf
- 末尾添加如下配置:
- * soft nofile 65536
- * hard nofile 65536
- * soft nproc 4096
- * hard nproc 4096
max number of threads [1024] for user [es] is too low, increase to at least [4096]
无法创建本地线程问题,用户最大可创建线程数太小
- vim /etc/security/limits.d/20-nproc.conf
- 改为如下配置:
- * soft nproc 4096
max virtual memory areas vm.max_map_count [65530] is too low, increase to at least [262144]
最大虚拟内存太小,调大系统的虚拟内存
- vim /etc/sysctl.conf
- 追加以下内容:
- vm.max_map_count=262144
- 保存退出之后执行如下命令:
- sysctl -p
- the default discovery settings are unsuitable for production use; at least one of [discovery.seed_hosts, discovery.seed_providers, cluster.initial_master_nodes] must be
- configured
缺少默认配置,至少需要配置:discovery.seed_hosts / discovery.seed_providers / cluster.initial_master_nodes中的一个参数。
- vim config/elasticsearch.yml
- #添加配置
- discovery.seed_hosts: ["127.0.0.1"]
- cluster.initial_master_nodes: ["node-1"]

- vim config/kibana.yml
- server.port: 5601
- #服务器ip
- server.host: "localhost"
- #elasticsearch的访问地址
- elasticsearch.hosts: ["http://localhost:9200"]
- #Kibana汉化
- i18n.locale: "zh-CN"
- bin/kibana #后台启动
- nohup bin/kibana &
访问Kibana: http://localhost:5601/

| API | 用途 |
|---|---|
| /_cat/allocation | 查看单节点的shard分配整体情况 |
| /_cat/shards | #查看各shard的详细情况 |
| /_cat/shards/{index} | #查看指定分片的详细情况 |
| /_cat/master | #查看master节点信息 |
| /_cat/nodes | #查看所有节点信息 |
| /_cat/indices | #查看集群中所有index的详细信息 |
| /_cat/indices/{index} | #查看集群中指定index的详细信息 |
| /_cat/segments | #查看各index的segment详细信息,包括segment名, 所属shard, 内存(磁盘)占用大小, 是否刷盘 |
| /_cat/segments/{index} | #查看指定index的segment详细信息 |
| /_cat/count | #查看当前集群的doc数量 |
| /_cat/count/{index} | #查看指定索引的doc数量 |
| /_cat/recovery | #查看集群内每个shard的recovery过程.调整replica。 |
| /_cat/recovery/{index} | #查看指定索引shard的recovery过程 |
| /_cat/health | #查看集群当前状态:红、黄、绿 |
| /_cat/pending_tasks | #查看当前集群的pending task |
| /_cat/aliases | #查看集群中所有alias信息,路由配置等 |
| /_cat/aliases/{alias} | #查看指定索引的alias信息 |
| /_cat/thread_pool | #查看集群各节点内部不同类型的threadpool的统计信息 |
| /_cat/plugins | #查看集群各个节点上的plugin信息 |
| /_cat/fielddata | #查看当前集群各个节点的fielddata内存使用情况 |
| /_cat/fielddata/{fields} | #查看指定field的内存使用情况,里面传field属性对应的值 |
| /_cat/nodeattrs | #查看单节点的自定义属性 |
| /_cat/repositories | #输出集群中注册快照存储库 |
| /_cat/templates | #输出当前正在存在的模板信息 |
Elasticsearch提供插件机制对系统进行扩展,以安装analysis-icu这个分词插件为例。
- #查看已安装插件
- bin/elasticsearch-plugin list
- #安装插件
- bin/elasticsearch-plugin install analysis-icu
- #删除插件
- bin/elasticsearch-plugin remove analysis-icu

注意:安装和删除完插件后,需要重启ES服务才能生效。另外执行安装命令的时候,需要保证 es 服务是在运行状态。
本地下载相应的插件,解压,然后手动上传到 elasticsearch的 plugins 目录,然后重启ES实例就可以了。比如 ik 中文分词插件:ik分词器
- unzip elasticsearch-analysis-ik-7.17.3.zip -d analysis-ik
- rm elasticsearch-analysis-ik-7.17.3.zip
测试分词效果,ES 的默认分词设置是standard,会单字拆分:
- POST _analyze
- {
- "analyzer":"standard",
- "text":"中华人民共和国"
- }
-
- #ik_smart:会做最粗粒度的拆
- POST _analyze
- {
- "analyzer": "ik_smart",
- "text": "中华人民共和国"
- }
-
- #ik_max_word:会将文本做最细粒度的拆分
- POST _analyze
- {
- "analyzer":"ik_max_word",
- "text":"中华人民共和国"
- }

创建索引时可以指定IK分词器作为默认分词器:
- PUT /es_db
- {
- "settings" : {
- "index" : {
- "analysis.analyzer.default.type": "ik_max_word"
- }
- }
- }
在7.0之前,一个 Index 可以设置多个Types,目前Type已经被Deprecated,7.0开始,一个索引只能创建一个Type - “_doc”。传统关系型数据库和Elasticsearch的区别:

一个索引就是一个拥有几分相似特征的文档的集合。比如说,可以有一个客户数据的索引,另一个产品目录的索引,还有一个订单数据的索引。一个索引由一个名字来标识(必须全部是小写字母的),并且当我们要对对应于这个索引中的文档进行索引、搜索、更新和删除的时候,都要使用到这个名字。

文档元数据

元数据,用于标注文档的相关信息:
并发场景下修改文档
seq_no和primary_term是对version的优化,7.X版本的ES默认使用这种方式控制版本,所以当在高并发环境下使用乐观锁机制修改文档时,要带上当前文档的seq_no和_primary_term进行更新:
- POST /es_db/_doc/2?if_seq_no=21&if_primary_term=6
- {
- "name": "李四xxx"
- }
如果版本号不对,会抛出版本冲突异常,如下图:

创建索引:
索引命名必须小写,不能以下划线开头。格式: PUT /索引名称
- #创建索引
- PUT /es_db
-
- #创建索引时可以设置分片数和副本数
- PUT /es_db
- {
- "settings" : {
- "number_of_shards" : 3,
- "number_of_replicas" : 2
- }
- }
-
- #修改索引配置
- PUT /es_db/_settings
- {
- "index" : {
- "number_of_replicas" : 1
- }
- }

查询索引:
格式: GET /索引名称
- #查询索引
- GET /es_db
-
- #es_db是否存在
- HEAD /es_db

删除索引:
格式: DELETE /索引名称
DELETE /es_db
准备工作:
- PUT /es_db
- {
- "settings" : {
- "index" : {
- "analysis.analyzer.default.type": "ik_max_word"
- }
- }
- }
-
-
- PUT /es_db/_doc/1
- {
- "name": "张三",
- "sex": 1,
- "age": 25,
- "address": "广州天河公园",
- "remark": "java developer"
- }
-
-
- PUT /es_db/_doc/2
- {
- "name": "李四",
- "sex": 1,
- "age": 28,
- "address": "广州荔湾大厦",
- "remark": "java assistant"
- }
-
-
- PUT /es_db/_doc/3
- {
- "name": "王五",
- "sex": 0,
- "age": 26,
- "address": "广州白云山公园",
- "remark": "php developer"
- }
-
-
- PUT /es_db/_doc/4
- {
- "name": "赵六",
- "sex": 0,
- "age": 22,
- "address": "长沙橘子洲",
- "remark": "python assistant"
- }
-
-
- PUT /es_db/_doc/5
- {
- "name": "张龙",
- "sex": 0,
- "age": 19,
- "address": "长沙麓谷企业广场",
- "remark": "java architect assistant"
- }
-
-
-
- PUT /es_db/_doc/6
- {
- "name": "赵虎",
- "sex": 1,
- "age": 32,
- "address": "长沙麓谷兴工国际产业园",
- "remark": "java architect"
- }
格式: [PUT | POST] /索引名称/[_doc | _create ]/id
- # 创建文档,指定id
- # 如果id不存在,创建新的文档,否则先删除现有文档,再创建新的文档,版本会增加
- PUT /es_db/_doc/1
- {
- "name": "张三",
- "sex": 1,
- "age": 25,
- "address": "广州天河公园",
- "remark": "java developer"
- }
-
-
- #创建文档,ES生成id
- POST /es_db/_doc
- {
- "name": "张三",
- "sex": 1,
- "age": 25,
- "address": "广州天河公园",
- "remark": "java developer"
- }

注意:POST和PUT都能起到创建/更新的作用,PUT需要对一个具体的资源进行操作也就是要确定id才能进行更新/创建,而POST是可以针对整个资源集合进行操作的,如果不写id就由ES生成一个唯一id进行创建新文档,如果填了id那就针对这个id的文档进行创建/更新

Create -如果ID已经存在,会失败

全量更新
全量更新,整个json都会替换,格式: [PUT | POST] /索引名称/_doc/id。如果文档存在,现有文档会被删除,新的文档会被索引
- # 全量更新,替换整个json
- PUT /es_db/_doc/1/
- {
- "name": "张三",
- "sex": 1,
- "age": 25
- }
-
-
- #查询文档
- GET /es_db/_doc/1

部分更新
使用 update 部分更新,格式: POST /索引名称/update/id。update不会删除原来的文档,而是实现真正的数据更新
- # 部分更新:在原有文档上更新
- # Update -文档必须已经存在,更新只会对相应字段做增量修改
- POST /es_db/_update/1
- {
- "doc": {
- "age": 28
- }
- }
-
- #查询文档
- GET /es_db/_doc/1

查询并更新:
使用 _update_by_query 更新文档:
- POST /es_db/_update_by_query
- {
- "query": {
- "match": {
- "_id": 1
- }
- },
- "script": {
- "source": "ctx._source.age = 30"
- }
- }

根据id查询文档,格式: GET /索引名称/_doc/id
GET /es_db/_doc/1

条件查询 search,格式: /索引名称/doc/_search
- # 查询前10条文档
- GET /es_db/_doc/_search

ES Search API提供了两种条件查询搜索方式:
- #通过URI搜索,使用“q”指定查询字符串,“query string syntax” KV键值对
- #条件查询, 如要查询age等于28岁的 _search?q=*:***
- GET /es_db/_doc/_search?q=age:28
-
- #范围查询, 如要查询age在25至26岁之间的 _search?q=***[** TO **] 注意: TO 必须为大写
- GET /es_db/_doc/_search?q=age[25 TO 26]
-
- #查询年龄小于等于28岁的 :<=
- GET /es_db/_doc/_search?q=age:<=28
-
- #查询年龄大于28前的 :>
- GET /es_db/_doc/_search?q=age:>28
-
- #分页查询 from=*&size=*
- GET /es_db/_doc/_search?q=age[25 TO 26]&from=0&size=1
-
- #对查询结果只输出某些字段 _source=字段,字段
- GET /es_db/_doc/_search?_source=name,age
-
- #对查询结果排序 sort=字段:desc/asc
- GET /es_db/_doc/_search?sort=age:desc
格式: DELETE /索引名称/_doc/id
DELETE /es_db/_doc/1
批量对文档进行写操作是通过_bulk的API来实现的
- {"actionName":{"_index":"indexName", "_type":"typeName","_id":"id"}}
- {"field1":"value1", "field2":"value2"}
- {"actionName":{"_index":"indexName", "_type":"typeName","_id":"id"}}
- {"field1":"value1", "field2":"value2"}
- POST _bulk
- {"create":{"_index":"article", "_type":"_doc", "_id":3}}
- {"id":3,"title":"fox老师","content":"fox老师666","tags":["java", "面向对
- 象"],"create_time":1554015482530}
- {"create":{"_index":"article", "_type":"_doc", "_id":4}}
- {"id":4,"title":"mark老师","content":"mark老师NB","tags":["java", "面向对
- 象"],"create_time":1554015482530}

如果是已经存在的文档则会产生冲突:

- POST _bulk
- {"index":{"_index":"article", "_type":"_doc", "_id":3}}
- {"id":3,"title":"图灵徐庶老师","content":"图灵学院徐庶老师666","tags":["java", "面向对象"],"create_time":1554015482530}
- {"index":{"_index":"article", "_type":"_doc", "_id":4}}
- {"id":4,"title":"图灵诸葛老师","content":"图灵学院诸葛老师NB","tags":["java", "面向对象"],"create_time":1554015482530}
-
- GET /article/_search

- POST _bulk
- {"update":{"_index":"article", "_type":"_doc", "_id":3}}
- {"doc":{"title":"ES大法必修内功"}}
- {"update":{"_index":"article", "_type":"_doc", "_id":4}}
- {"doc":{"create_time":1554018421008}}
-
- GET /article/_search

- POST _bulk
- {"delete":{"_index":"article", "_type":"_doc", "_id":3}}
- {"delete":{"_index":"article", "_type":"_doc", "_id":4}}
-
- GET /article/_search
- POST _bulk
- {"delete":{"_index":"article", "_type":"_doc", "_id":3}}
- {"create":{"_index":"article", "_type":"_doc", "_id":3}}
- {"title":"fox老师","content":"fox老师666","tags":["java", "面向对象"],"create_time":1554015482530}
- {"update":{"_index":"article", "_type":"_doc", "_id":4}}
- {"doc":{"create_time":1554018421008}}
-
- GET /article/_search
es的批量查询可以使用mget和msearch两种。其中mget是需要我们知道它的id,可以指定不同的 index,也可以指定返回值source。msearch可以通过字段查询来进行一个批量的查找。
- #可以通过ID批量获取不同index和type的数据
- GET _mget
- {
- "docs": [
- {
- "_index": "es_db",
- "_id": 1
- },
- {
- "_index": "article",
- "_id": 4
- }
- ]
- }
-
- #可以通过ID批量获取es_db的数据
- GET /es_db/_mget
- {
- "docs": [
- {
- "_id": 1
- },
- {
- "_id": 4
- }
- ]
- }
-
- #简化后
- GET /es_db/_mget
- {
- "ids":["1","2"]
- }

在_msearch中,请求格式和bulk类似。查询一条数据需要两个对象,第一个设置index和type,第二个设置查询语句。查询语句和search相同。如果只是查询一个index,我们可以在url中带上index,这样,如果查该index可以直接用空对象表示。

索引是加速数据查询的重要手段,其核心原理是通过不断的缩小想要获取数据的范围来筛选出最终想要的结果,同时把随机的事件变成顺序的事件。
磁盘IO是程序设计中非常高昂的操作,也是影响程序性能的重要因素,因此应当尽量避免过多的磁盘IO,有效的利用内存可以大大的提升程序的性能。在操作系统层面,发生一次IO时,不光把当前磁盘地址的数据,而是把相邻的数据也都读取到内存缓冲区内,局部预读性原理告诉我们,当计算机访问一个地址的数据的时候,与其相邻的数据也会很快被访问到。每一次IO读取的数据我们称之为一页(page)。具体一页有多大数据跟操作系统有关,一般为4k或8k,也就是我们读取一页内的数据时候,实际上才发生了一次IO,这个理论对于索引的数据结构设计非常有帮助。
当数据写入 ES 时,数据将会通过 分词 被切分为不同的 term,ES 将 term 与其对应的文档列表建立一种映射关系,这种结构就是 倒排索引。如下图所示:

为了进一步提升索引的效率,ES 在 term 的基础上利用 term 的前缀或者后缀构建了 term index, 用于对 term 本身进行索引,ES 实际的索引结构如下图所示:

这样当我们去搜索某个关键词时,ES 首先根据它的前缀或者后缀迅速缩小关键词的在 term dictionary 中的范围,大大减少了磁盘IO的次数

ES中提供了一种强大的检索数据方式,这种检索方式称之为Query DSL(Domain Specified Language), Query DSL是利用Rest API传递JSON格式的请求体(RequestBody)数据与ES进行交互,这种方式的丰富查询语法让ES检索变得更强大,更简洁。
- GET /es_db/_doc/_search {json请求体数据}
-
- #可以简化为下面写法
- GET /es_db/_search {json请求体数据}
- #指定ik分词器
- PUT /es_db
- {
- "settings" : {
- "index" : {
- "analysis.analyzer.default.type": "ik_max_word"
- }
- }
- }
-
- # 创建文档,指定id
- PUT /es_db/_doc/1
- {
- "name": "张三",
- "sex": 1,
- "age": 25,
- "address": "广州天河公园",
- "remark": "java developer"
- }
-
- PUT /es_db/_doc/2
- {
- "name": "李四",
- "sex": 1,
- "age": 28,
- "address": "广州荔湾大厦",
- "remark": "java assistant"
- }
-
- PUT /es_db/_doc/3
- {
- "name": "王五",
- "sex": 0,
- "age": 26,
- "address": "广州白云山公园",
- "remark": "php developer"
- }
-
- PUT /es_db/_doc/4
- {
- "name": "赵六",
- "sex": 0,
- "age": 22,
- "address": "长沙橘子洲",
- "remark": "python assistant"
- }
-
-
- PUT /es_db/_doc/5
- {
- "name": "张龙",
- "sex": 0,
- "age": 19,
- "address": "长沙麓谷企业广场",
- "remark": "java architect assistant"
- }
-
-
- PUT /es_db/_doc/6
- {
- "name": "赵虎",
- "sex": 1,
- "age": 32,
- "address": "长沙麓谷兴工国际产业园",
- "remark": "java architect"
- }
使用match_all,默认只会返回10条数据。原因:_search查询默认采用的是分页查询,每页记录数size的默认值为10。如果想显示更多数据,指定 size
- GET /es_db/_search
-
- #等同于
- GET /es_db/_search
- {
- "query":{
- "match_all":{}
- }
- }
- GET /es_db/_search
- {
- "size": 3
- }

from 关键字: 用来指定起始返回位置,和size关键字连用可实现分页效果
- GET /es_db/_search
- {
- "query": {
- "match_all": {}
- },
- "size": 5,
- "from": 0
- }
思考: size可以无限增加吗?
- GET /es_db/_search
- {
- "size": 10001
- }

原因:
- PUT /es_db/_settings
- {
- "index.max_result_window" :"20000"
- }
-
- #修改现有所有的索引,但新增的索引,还是默认的10000
- PUT /_all/_settings
- {
- "index.max_result_window" :"20000"
- }
-
- #查看所有索引中的index.max_result_window值
- GET /_all/_settings/index.max_result_window

参数index.max_result_window主要用来限制单次查询满足查询条件的结果窗口的大小,窗口大小由from + size共同决定。不能简单理解成查询返回给调用方的数据量。这样做主要是为了限制内存的消耗。
比如:from为1000000,size为10,逻辑意义是从满足条件的数据中取1000000到(1000000 + 10)的记录。这时ES一定要先将(1000000 + 10)的记录(即result_window)加载到内存中,再进行分页取值的操作。尽管最后我们只取了10条数据返回给客户端,但ES进程执行查询操作的过程中确需要将(1000000 + 10)的记录都加载到内存中,可想而知对内存的消耗有多大。这也是ES中不推荐采用(from + size)方式进行深度分页的原因。
同理,from为0,size为1000000时,ES进程执行查询操作的过程中确需要将1000000 条记录都加载到内存中再返回给调用方,也会对ES内存造成很大压力。
改动index.max_result_window参数值的大小,只能解决一时的问题,当索引的数据量持续增长时,在查询全量数据时还是会出现问题。而且会增加ES服务器内存大结果集消耗完的风险。最佳实践还是根据异常提示中的采用scroll api更高效的请求大量数据集。
- #查询命令中新增scroll=1m,说明采用游标查询,保持游标查询窗口一分钟。
- #这里由于测试数据量不够,所以size值设置为2。
- #实际使用中为了减少游标查询的次数,可以将值适当增大,比如设置为1000。
- GET /es_db/_search?scroll=1m
- {
- "query": {
- "match_all": {}
- },
- "size": 2
- }

除了返回前2条记录,还返回了一个游标ID值_scroll_id。然后采用游标id查询:
- # scroll_id 的值就是上一个请求中返回的 _scroll_id 的值
- GET /_search/scroll
- {
- "scroll": "1m",
- "scroll_id" :"FGluY2x1ZGVfY29udGV4dF91dWlkDXF1ZXJ5QW5kRmV0Y2gBFmVkNS1GTmZJVFh5d0dkQVhrLW9TZncAAAAAAAAMxxZaZ0JWdTZyVFRTeVQtdGVXMzVBZEF3"
- }

多次根据scroll_id游标查询,直到没有数据返回则结束查询。采用游标查询索引全量数据,更安全高效,限制了单次对内存的消耗。
注意:会让得分失效
- GET /es_db/_search
- {
- "query": {
- "match_all": {}
- },
- "sort": [
- {
- "age": "desc"
- }
- ]
- }
-
-
- #排序,分页
- GET /es_db/_search
- {
- "query": {
- "match_all": {}
- },
- "sort": [
- {
- "age": "desc"
- }
- ],
- "from": 0,
- "size": 5
- }

_source 关键字: 是一个数组,在数组中用来指定展示那些字段
- GET /es_db/_search
- {
- "query": {
- "match_all": {}
- },
- "_source": [
- "name",
- "address"
- ]
- }
match在匹配时会对所查找的关键词进行分词,然后按分词匹配查找。match支持以下参数:
- #模糊匹配 match 分词后or的效果
- GET /es_db/_search
- {
- "query": {
- "match": {
- "address": "广州白云山公园"
- }
- }
- }
-
- POST _analyze
- {
- "analyzer":"standard",
- "text":"广州白云山公园"
- }
-
- POST _analyze
- {
- "analyzer": "ik_smart",
- "text": "广州白云山公园"
- }

- #模糊匹配 match 分词后 and的效果
- GET /es_db/_search
- {
- "query": {
- "match": {
- "address": {
- "query": "广州白云山公园",
- "operator": "and"
- }
- }
- }
- }

在match中的应用: 当operator参数设置为or时,minnum_should_match参数用来控制匹配的分词的最少数量。
- # 最少匹配广州,公园两个词
- GET /es_db/_search
- {
- "query": {
- "match": {
- "address": {
- "query": "广州公园",
- "minimum_should_match": 2
- }
- }
- }
- }

match_phrase 查询分析文本并根据分析的文本创建一个短语查询。match_phrase 会将检索关键词分词。match_phrase的分词结果必须在被检索字段的分词中都包含,而且顺序必须相同,而且默认必须都是连续的。
- GET /es_db/_search
- {
- "query": {
- "match_phrase": {
- "address": "广州白云山"
- }
- }
- }
-
- GET /es_db/_search
- {
- "query": {
- "match_phrase": {
- "address": "广州白云"
- }
- }
- }
思考:为什么查询广州白云山有数据,广州白云没有数据?

分析原因:
先查看广州白云山公园分词结果,可以知道广州和白云不是相邻的词条,中间会隔一个白云山,而match_phrase匹配的是相邻的词条,所以查询广州白云山有结果,但查询广州白云没有结果。
- POST _analyze
- {
- "analyzer": "ik_max_word",
- "text": "广州白云山"
- }
-
- #分析结果
- {
- "tokens" : [
- {
- "token" : "广州",
- "start_offset" : 0,
- "end_offset" : 2,
- "type" : "CN_WORD",
- "position" : 0
- },
- {
- "token" : "白云山",
- "start_offset" : 2,
- "end_offset" : 5,
- "type" : "CN_WORD",
- "position" : 1
- },
- {
- "token" : "白云",
- "start_offset" : 2,
- "end_offset" : 4,
- "type" : "CN_WORD",
- "position" : 2
- },
- {
- "token" : "云山",
- "start_offset" : 3,
- "end_offset" : 5,
- "type" : "CN_WORD",
- "position" : 3
- }
- ]
- }
解决办法:
如何解决词条间隔的问题?可以借助slop参数,slop参数告诉match_phrase查询词条能够相隔多远时仍然将文档视为匹配。
- #广州云山分词后相隔为2,可以匹配到结果
- GET /es_db/_search
- {
- "query": {
- "match_phrase": {
- "address": {
- "query": "广州云山",
- "slop": 2
- }
- }
- }
- }

可以根据字段类型,决定是否使用分词查询,得分最高的在前面
- GET /es_db/_search
- {
- "query": {
- "multi_match": {
- "query": "长沙张龙",
- "fields": [
- "address",
- "name"
- ]
- }
- }
- }

注意:字段类型分词,将查询条件分词之后进行查询,如果该字段不分词就会将查询条件作为整体进行查询
允许我们在单个查询字符串中指定AND | OR | NOT条件,同时也和 multi_match query 一样,支持多字段搜索。和match类似,但是match需要指定字段名,query_string是在所有字段中搜索,范围更广泛。
- GET /es_db/_search
- {
- "query": {
- "query_string": {
- "query": "张三 OR 橘子洲"
- }
- }
- }

- GET /es_db/_search
- {
- "query": {
- "query_string": {
- "default_field": "address",
- "query": "白云山 OR 橘子洲"
- }
- }
- }

- GET /es_db/_search
- {
- "query": {
- "query_string": {
- "fields": [
- "name",
- "address"
- ],
- "query": "张三 OR (广州 AND 王五)"
- }
- }
- }

类似Query String,但是会忽略错误的语法,同时只支持部分查询语法,不支持AND OR NOT,会当作字符串处理。支持部分逻辑:
- #simple_query_string 默认的operator是OR
- GET /es_db/_search
- {
- "query": {
- "simple_query_string": {
- "fields": [
- "name",
- "address"
- ],
- "query": "广州公园",
- "default_operator": "AND"
- }
- }
- }
-
- GET /es_db/_search
- {
- "query": {
- "simple_query_string": {
- "fields": [
- "name",
- "address"
- ],
- "query": "广州+公园"
- }
- }
- }

Term用来使用关键词查询(精确匹配),还可以用来查询没有被进行分词的数据类型。Term是表达语意的最小单位,搜索和利用统计语言模型进行自然语言处理都需要处理Term。match在匹配时会对所查找的关键词进行分词,然后按分词匹配查找,而term会直接对关键词进行查找。一般模糊查找的时候,多用match,而精确查找时可以使用term。
- #关键字查询 term
- # 思考: 查询广州白云是否有数据,为什么?
- GET /es_db/_search
- {
- "query": {
- "term": {
- "address": {
- "value": "广州白云"
- }
- }
- }
- }
-
-
- # 采用term精确查询, 查询字段映射类型为keyword
- GET /es_db/_search
- {
- "query": {
- "term": {
- "address.keyword": {
- "value": "广州白云山公园"
- }
- }
- }
- }

在ES中,Term查询,对输入不做分词。会将输入作为一个整体,在倒排索引中查找准确的词项,并且使用相关度算分公式为每个包含该词项的文档进行相关度算分。
- PUT /product/_bulk
- {"index":{"_id":1}}
- {"productId":"xxx123","productName":"iPhone"}
- {"index":{"_id":2}}
- {"productId":"xxx111","productName":"iPad"}
-
- # 思考: 查询iPhone可以查到数据吗?
- GET /product/_search
- {
- "query": {
- "term": {
- "productName": {
- "value": "iPhone"
- }
- }
- }
- }
-
-
- GET /product/_analyze
- {
- "analyzer": "standard",
- "text": "iPhone"
- }
其实查询 iPhone 没有数据的原因很简单,根据 分词 可以知道 es 将【iPhone】分词成了【iphone】了,而使用 term 查询是精确查询,当然也包括大小写问题了 。
此外,可以通过 Constant Score 将查询转换成一个 Filtering,避免算分,并利用缓存,提高性能。
- GET /es_db/_search
- {
- "query": {
- "constant_score": {
- "filter": {
- "term": {
- "address.keyword": "广州白云山公园"
- }
- }
- }
- }
- }

需要注意的是 term 查询 是包含,而不是 等于。
- POST /employee/_bulk
- {"index":{"_id":1}}
- {"name":"小明","interest":["跑步","篮球"]}
- {"index":{"_id":2}}
- {"name":"小红","interest":["跳舞","画画"]}
- {"index":{"_id":3}}
- {"name":"小丽","interest":["跳舞","唱歌","跑步"]}
-
- POST /employee/_search
- {
- "query": {
- "term": {
- "interest.keyword": {
- "value": "跑步"
- }
- }
- }
- }

它会对分词后的 term 进行前缀搜索。
prefix的原理:需要遍历所有倒排索引,并比较每个term是否已有所指定的前缀开头。
- GET /es_db/_search
- {
- "query": {
- "prefix": {
- "address": {
- "value": "广州白云"
- }
- }
- }
- }
通配符查询:工作原理和prefix相同,只不过它不是只比较开头,它能支持更为复杂的匹配模式。
- GET /es_db/_search
- {
- "query": {
- "wildcard": {
- "address": {
- "value": "*白*"
- }
- }
- }
- }

- POST /es_db/_search
- {
- "query": {
- "range": {
- "age": {
- "gte": 25,
- "lte": 28
- }
- }
- }
- }
-
- POST /es_db/_search
- {
- "query": {
- "range": {
- "age": {
- "gte": 25,
- "lte": 28
- }
- }
- },
- "from": 0,
- "size": 2,
- "_source": [
- "name",
- "age",
- "book"
- ],
- "sort": {
- "age": "desc"
- }
- }
-
-
- #日期范围查询
- DELETE /product
-
- POST /product/_bulk
- {"index":{"_id":1}}
- {"price":100,"date":"2021-01-01","productId":"XHDK-1293"}
- {"index":{"_id":2}}
- {"price":200,"date":"2022-01-01","productId":"KDKE-5421"}
-
- GET /product/_mapping
-
- GET /product/_search
- {
- "query": {
- "range": {
- "date": {
- "gte": "now-2y"
- }
- }
- }
- }
常用时间范围查询主要有以下几种方式:
1、使用内置的时间数学表达式
Elasticsearch 中时间可以表示为 now,也就是系统当前时间;也可以是以 || 结尾的日期字符串表示。在日期之后,可以选择一个或多个数学表达式。
- +1h:表示加1小时
- -1d:表示减1天
- /d:四舍五入到最近的一天
表达式 含义 表达式 含义 y 年 M 月 w 星期 d 天 h 小时 H 小时 m 分钟 s 秒 假设系统当前时间
now = 2018-10-01 12:00:00
now+1h: now的毫秒值 + 1小时, 结果是: 2018-10-01 13:00:00.now-1h: now的毫秒值 - 1小时, 结果是: 2018-10-01 11:00:00.now-1h/d: now的毫秒值 - 1小时, 然后四舍五入到最近的一天的起始, 结果是: 2018-10-01 00:00:00.2018.10.01||+1M/d: 2018-10-01的毫秒值 + 1月, 再四舍五入到最近一天的起始, 结果是: 2018-11-01 00:00:00关于时间的四舍五入:
- “gt”: " 2014-11-18||/M" —— 大于日期, 需要向上舍入, 结果是
2014-12-01T00:00:00.000, 也就是不包含整个11月.- “gte”: “2014-11-18||/M” —— 大于或等于日期, 需要向下舍入, 结果是
2014-11-01T00:00:00.000, 也就是包含整个11月.- “lt”: “2014-11-18||/M” —— 小于日期, 需要向上舍入, 结果是
2014-10-31T23:59:59.999, 也就是不包含整个11月.- “lte”: “2014-11-18||/M” —— 小于或等于日期, 需要向下舍入, 结果是
2014-11-30T23:59:59.999, 也就是包含整个11月.2、日期格式化范围查询
格式化日期查询时, 将默认使用日期field中指定的格式进行解析, 当然也可以通过format参数来覆盖默认配置.示例如下:
GET website/_search { "query": { "range": { "post_date": { "gte": "2/1/2018", "lte": "2019", "format": "dd/MM/yyyy||yyyy" \\这里的||表示或 } } } }注意: 如果日期中缺失了部分年、月、日, 缺失的部分将被填充为如下默认值:
MONTH_OF_YEAR: 01 DAY_OF_MONTH: 01 HOUR_OF_DAY: 23 MINUTE_OF_HOUR: 59 SECOND_OF_MINUTE: 59 NANO_OF_SECOND: 999_999_9993、QueryString 查询
QueryString查询也可以实现时间范围查询。这种方式比较简单,直接在查询语句中指定属性和时间范围即可。
{ "query": { "query_string": { "query": "created_at:[2022-06-01T00:00:00.000Z TO 2022-06-01T23:59:59.999Z]" } } }4、Date Range 聚合查询
如果需要在ES中进行时间范围的聚合分析,可以使用Date Range聚合。
{ "aggs": { "name": { "date_range": { "field": "created_at", "ranges": [ { "from": "2022-06-01T00:00:00.000Z", "to": "2022-06-01T23:59:59.999Z" }, { "from": "2022-06-02T00:00:00.000Z", "to": "2022-06-02T23:59:59.999Z" } ] } } } }
ids 关键字 : 值为数组类型,用来根据一组id获取多个对应的文档
- GET /es_db/_search
- {
- "query": {
- "ids": {
- "values": [
- 1,
- 2
- ]
- }
- }
- }

在实际的搜索中,我们有时候会打错字,从而导致搜索不到。在Elasticsearch中,我们可以使用 fuzziness 属性来进行模糊查询,从而达到搜索有错别字的情形。fuzzy 查询会用到两个很重要的参数,fuzziness,prefix_length
- GET /es_db/_search
- {
- "query": {
- "fuzzy": {
- "address": {
- "value": "白运山",
- "fuzziness": 1,
- "prefix_length": 0
- }
- }
- }
- }
注意: fuzzy 模糊查询 最大模糊错误 必须在0-2之间
highlight 关键字: 可以让符合条件的文档中的关键词高亮。主要有以下这些属性:
- #指定ik分词器
- PUT /products
- {
- "settings": {
- "index": {
- "analysis.analyzer.default.type": "ik_max_word"
- }
- }
- }
-
- PUT /products/_doc/1
- {
- "proId": "2",
- "name": "牛仔男外套",
- "desc": "牛仔外套男装春季衣服男春装夹克修身休闲男生潮牌工装潮流头号青年春秋棒球服男 7705浅蓝常规 XL",
- "timestamp": 1576313264451,
- "createTime": "2019-12-13 12:56:56"
- }
-
- PUT /products/_doc/1
- {
- "proId": "6",
- "name": "HLA海澜之家牛仔裤男",
- "desc": "HLA海澜之家牛仔裤男2019时尚有型舒适HKNAD3E109A 牛仔蓝(A9)175/82A(32)",
- "timestamp": 1576314265571,
- "createTime": "2019-12-18 15:56:56"
- }
-

可以在highlight中使用pre_tags和post_tags
- GET /products/_search
- {
- "query": {
- "term": {
- "name": {
- "value": "牛仔"
- }
- }
- },
- "highlight": {
- "post_tags": [
- ""
- ],
- "pre_tags": [
- ""
- ],
- "fields": {
- "*": {}
- }
- }
- }

- GET /products/_search
- {
- "query": {
- "term": {
- "name": {
- "value": "牛仔"
- }
- }
- },
- "highlight": {
- "pre_tags": [
- ""
- ],
- "post_tags": [
- ""
- ],
- "require_field_match": "false",
- "fields": {
- "name": {},
- "desc": {}
- }
- }
- }

搜索是用户和搜索引擎的对话,用户关心的是搜索结果的相关性
如何衡量相关性:
搜索的相关性算分,描述了一个文档和查询语句匹配的程度。ES 会对每个匹配查询条件的结果进行算分_score。
打分的本质是排序,需要把最符合用户需求的文档排在前面。ES 5之前,默认的相关性算分采用TFIDF,现在采用BM 25。
| 关键词 | 文档ID |
| JAVA | 1,2,3 |
| 设计模式 | 1,2,3,4,5,6 |
| 多线程 | 2,3,7,9 |
TF-IDF(term frequency–inverse document frequency)是一种用于信息检索与数据挖掘的常用加权技术。
以上三个因素——词频(term frequency)、逆向文档频率(inverse document frequency)和字段长度归一值(field-length norm)——是在索引时计算并存储的,最后将它们结合在一起计算单个词在特定文档中的权重。

- PUT /test_score/_bulk
- {"index":{"_id":1}}
- {"content":"we use Elasticsearch to power the search"}
- {"index":{"_id":2}}
- {"content":"we like elasticsearch"}
- {"index":{"_id":3}}
- {"content":"Thre scoring of documents is caculated by the scoring formula"}
- {"index":{"_id":4}}
- {"content":"you know,for search"}
-
- GET /test_score/_search
- {
- "explain": true,
- "query": {
- "match": {
- "content": "elasticsearch"
- }
- }
- }

Boosting是控制相关度的一种手段。参数boost的含义:
返回匹配 positive 查询的文档并降低匹配 negative 查询的文档相似度分。这样就可以在不排除某些文档的前提下对文档进行查询,搜索结果中存在只不过相似度分数相比正常匹配的要低;
- GET /test_score/_search
- {
- "query": {
- "boosting": {
- "positive": {
- "term": {
- "content": "elasticsearch"
- }
- },
- "negative": {
- "term": {
- "content": "like"
- }
- },
- "negative_boost": 0.2
- }
- }
- }

一个bool查询,是一个或者多个查询子句的组合,总共包括4种子句,其中2种会影响算分,2种不影响算分。
在Elasticsearch中,有Query和 Filter两种不同的Context
相关性并不只是全文本检索的专利,也适用于yes | no 的子句,匹配的子句越多,相关性评分
越高。如果多条查询子句被合并为一条复合查询语句,比如 bool查询,则每个查询子句计算得出的评分会被合并到总的相关性评分中。
bool查询语法
- GET /es_db/_search
- {
- "query": {
- "bool": {
- "must": {
- "match": {
- "remark": "java developer"
- }
- },
- "filter": {
- "term": {
- "sex": "1"
- }
- },
- "must_not": {
- "range": {
- "age": {
- "gte": 30
- }
- }
- },
- "should": [
- {
- "term": {
- "address.keyword": {
- "value": "广州天河公园"
- }
- }
- },
- {
- "term": {
- "address.keyword": {
- "value": "广州白云山公园"
- }
- }
- }
- ],
- "minimum_should_match": 1
- }
- }
- }
数据准备
- POST /employee/_bulk
- {"index":{"_id":1}}
- {"name":"小明","interest":["跑步","篮球"]}
- {"index":{"_id":2}}
- {"name":"小红","interest":["跑步"]}
- {"index":{"_id":3}}
- {"name":"小丽","interest":["跳舞","唱歌","跑步"]}
-
- POST /employee/_search
- {
- "query": {
- "term": {
- "interest.keyword": {
- "value": "跑步"
- }
- }
- }
- }
解决方案:增加count字段,使用bool查询解决
- POST /employee/_bulk
- {"index":{"_id":1}}
- {"name":"小明","interest":["跑步","篮球"],"interest_count":2}
- {"index":{"_id":2}}
- {"name":"小红","interest":["跑步"],"interest_count":1}
- {"index":{"_id":3}}
- {"name":"小丽","interest":["跳舞","唱歌","跑步"],"interest_count":3}
- # must 算分
- POST /employee/_search
- {
- "query": {
- "bool": {
- "must": [
- {
- "term": {
- "interest.keyword": {
- "value": "跑步"
- }
- }
- },
- {
- "term": {
- "interest_count": {
- "value": 1
- }
- }
- }
- ]
- }
- }
- }
-
- # filter不算分
- POST /employee/_search
- {
- "query": {
- "bool": {
- "filter": [
- {
- "term": {
- "interest.keyword": {
- "value": "跑步"
- }
- }
- },
- {
- "term": {
- "interest_count": {
- "value": 1
- }
- }
- }
- ]
- }
- }
- }
- GET /es_db/_search
- {
- "query": {
- "bool": {
- "must": {
- "match": {
- "remark": "java developer"
- }
- },
- "should": [
- {
- "bool": {
- "must_not": [
- {
- "term": {
- "sex": 1
- }
- }
- ]
- }
- }
- ],
- "minimum_should_match": 1
- }
- }
- }

Boosting是控制相关度的一种手段。可以通过指定字段的boost值影响查询结果。参数boost的含义:
- POST /blogs/_bulk
- {"index":{"_id":1}}
- {"title":"Apple iPad","content":"Apple iPad,Apple iPad"}
- {"index":{"_id":2}}
- {"title":"Apple iPad,Apple iPad","content":"Apple iPad"}
-
- GET /blogs/_search
- {
- "query": {
- "bool": {
- "should": [
- {
- "match": {
- "title": {
- "query": "apple,ipad",
- "boost": 1
- }
- }
- },
- {
- "match": {
- "content": {
- "query": "apple,ipad",
- "boost": 4
- }
- }
- }
- ]
- }
- }
- }

案例:要求苹果公司的产品信息优先展示
- POST /news/_bulk
- {"index":{"_id":1}}
- {"content":"Apple Mac"}
- {"index":{"_id":2}}
- {"content":"Apple iPad"}
- {"index":{"_id":3}}
- {"content":"Apple employee like Apple Pie and Apple Juice"}
-
-
- GET /news/_search
- {
- "query": {
- "bool": {
- "must": {
- "match": {
- "content": "apple"
- }
- }
- }
- }
- }

利用must not 排除不是苹果公司产品的文档
- GET /news/_search
- {
- "query": {
- "bool": {
- "must": {
- "match": {
- "content": "apple"
- }
- },
- "must_not": {
- "match": {
- "content": "pie"
- }
- }
- }
- }
- }
利用 negative boost 降低相关性
对某些返回结果不满意,但又不想排除掉( must_not),可以考虑boosting query的negative_boost。
- GET /news/_search
- {
- "query": {
- "boosting": {
- "positive": {
- "match": {
- "content": "apple"
- }
- },
- "negative": {
- "match": {
- "content": "pie"
- }
- },
- "negative_boost": 0.2
- }
- }
- }

Mapping类似数据库中的schema的定义,作用如下:
ES中Mapping映射可以分为动态映射和静态映射。
动态映射:
在关系数据库中,需要事先创建数据库,然后在该数据库下创建数据表,并创建表字段、类型、长度、主键等,最后才能基于表插入数据。而Elasticsearch中不需要定义Mapping映射(即关系型数据库的表、字段等),在文档写入Elasticsearch时,会根据文档字段自动识别类型,这种机制称之为动态映射。
静态映射:
静态映射是在Elasticsearch中也可以事先定义好映射,包含文档的各字段类型、分词器等,这种方式称之为静态映射。
动态映射(Dynamic Mapping)的机制,使得我们无需手动定义Mappings,Elasticsearch会自动根据文档信息,推算出字段的类型。但是有时候会推算的不对,例如地理位置信息。当类型如果设置不对时,会导致一些功能无法正常运行,例如Range查询
Dynamic Mapping类型自动识别:
| JSON类型 | Elasticsearch类型 |
|---|---|
| 字符串 |
|
| 布尔值 | Boolean |
| 浮点数 | float |
| 整数 | long |
| 对象 | object |
| 数组 | 由第一个非空数值的类型所决定 |
| 空值 | 忽略 |
动态映射示例
- #删除原索引
- DELETE /user
-
- #创建文档(ES根据数据类型, 会自动创建映射)
- PUT /user/_doc/1
- {
- "name": "fox",
- "age": 32,
- "address": "长沙麓谷"
- }
-
- #获取文档映射
- GET /user/_mapping

思考:能否后期更改Mapping的字段类型?
主要有两种情况:
| 描述 | true | false | strict |
|---|---|---|---|
| 文档可索引 | yes | yes | no |
| 字段可索引 | yes | no | no |
| Mapping被更新 | yes | no | no |
原因如下:
- DELETE /user
-
- PUT /user
- {
- "mappings": {
- "dynamic": "strict",
- "properties": {
- "name": {
- "type": "text"
- },
- "address": {
- "type": "object",
- "dynamic": "true"
- }
- }
- }
- }
-
- # 插入文档报错,原因为age为新增字段,会抛出异常
- PUT /user/_doc/1
- {
- "name": "fox",
- "age": 32,
- "address": {
- "province": "湖南",
- "city": "长沙"
- }
- }

修改dynamic后再次插入文档成功:
- #修改daynamic
- PUT /user/_mapping
- {
- "dynamic": true
- }
- POST _reindex
- {
- "source": {
- "index": "user"
- },
- "dest": {
- "index": "user2"
- }
- }
-
- DELETE /user
-
-
- PUT /user2/_alias/user
-
-
- GET /user
注意: 通过这几个步骤就实现了索引的平滑过渡,并且是零停机
- DELETE /user
-
- PUT /user
- {
- "mappings": {
- "properties": {
- "address": {
- "type": "text",
- "index": false
- },
- "age": {
- "type": "long"
- },
- "name": {
- "type": "text"
- }
- }
- }
- }
-
- PUT /user/_doc/1
- {
- "name": "fox",
- "address": "广州白云山公园",
- "age": 30
- }
-
- GET /user
-
- GET /user/_search
- {
- "query": {
- "match": {
- "address": "广州"
- }
- }
- }
text类型默认记录postions,其他默认为 docs。记录内容越多,占用存储空间越大
- DELETE /user
-
- PUT /user
- {
- "mappings": {
- "properties": {
- "address": {
- "type": "text",
- "index_options": "offsets"
- },
- "age": {
- "type": "long"
- },
- "name": {
- "type": "text"
- }
- }
- }
- }
-
- GET /user
- DELETE /user
-
- PUT /user
- {
- "mappings": {
- "properties": {
- "address": {
- "type": "keyword",
- "null_value": "NULL"
- },
- "age": {
- "type": "long"
- },
- "name": {
- "type": "text"
- }
- }
- }
- }
-
- PUT /user/_doc/1
- {
- "name": "fox",
- "address": null,
- "age": 30
- }
-
- GET /user/_search
- {
- "query": {
- "term": {
- "address": "NULL"
- }
- }
- }

- # 设置copy_to
- PUT /address
- {
- "mappings": {
- "properties": {
- "province": {
- "type": "keyword",
- "copy_to": "full_address"
- },
- "city": {
- "type": "text",
- "copy_to": "full_address"
- }
- }
- }
- }
-
- PUT /address/_doc/1
- {
- "province": "湖南",
- "city": "长沙"
- }
-
- PUT /address/_doc/2
- {
- "province": "湖南",
- "city": "常德"
- }
-
- GET /address/_search
- {
- "query": {
- "match": {
- "full_address": {
- "query": "湖南常德",
- "operator": "and"
- }
- }
- }
- }

Index Templates可以帮助你设定 Mappings 和 Settings,并按照一定的规则,自动匹配到新创建的索引之上:
- PUT /_template/template_default
- {
- "index_patterns": [
- "*"
- ],
- "order": 0,
- "version": 1,
- "settings": {
- "number_of_shards": 1,
- "number_of_replicas": 1
- }
- }
-
- #"date_detection": false 关闭日期探测
- PUT /_template/template_test
- {
- "index_patterns": [
- "test*"
- ],
- "order": 1,
- "settings": {
- "number_of_shards": 1,
- "number_of_replicas": 1
- },
- "mappings": {
- "date_detection": false,
- "numeric_detection": true
- }
- }
lndex Template的工作方式
当一个索引被新创建时:
- #查看template信息
- GET /_template/template_default
-
- GET /_template/temp*
-
- # 关闭日期探测,createDate会推断为text类型
- PUT /testtemplate/_doc/1
- {
- "orderNo": 1,
- "createDate": "2022/01/01"
- }
-
- GET /testtemplate/_mapping
-
- GET /testtemplate/_settings
-
- GET /testtemplate
-
- # 开启日期探测:将会自动识别检测日期格式的字符串数值
- PUT /testmy
- {
- "mappings": {
- "date_detection": true
- }
- }
-
- PUT /testmy/_doc/1
- {
- "orderNo": 1,
- "createDate": "2022/01/01"
- }
-
- GET /testmy/_mapping
-
- GET /testmy
根据Elasticsearch识别的数据类型,结合字段名称,来动态设定字段类型
- PUT /my_test_index
- {
- "mappings": {
- "dynamic_templates": [
- {
- "full_name": {
- "path_match": "name.*",
- "path_unmatch": "*.middle",
- "mapping": {
- "type": "text",
- "copy_to": "full_name"
- }
- }
- }
- ]
- }
- }
-
- PUT /my_test_index/_doc/1
- {
- "name": {
- "first": "John",
- "middle": "Winston",
- "last": "Lennon"
- }
- }
-
- GET /my_test_index/_search
- {
- "query": {
- "match": {
- "full_name": "John"
- }
- }
- }
