主页
关于
Stay before every beautiful thoughts
在每一个美好的思想前停留
文章
>
学习笔记
>
正文
Elasticsearch 学习笔记
Elasticsearch
学习笔记
Created at 2022-11-03 18:28
# Elasticsearch 学习笔记 # 简介 ElasticSearch 是一个基于 Lucene 的搜索服务器。 它提供了一个分布式多用户能力的全文搜索引擎,基于 RESTful web 接口。 Elasticsearch 是用 Java 开发的,并作为 Apache 许可条款下的开放源码发布,是当前流行的企业级搜索引擎。 设计用于云计算中,能够达到实时搜索,稳定,可靠,快速,安装使用方便。 我们建立一个网站或应用程序,并要添加搜索功能,但是想要完成搜索工作的创建是非常困难的。 我们希望搜索解决方案要运行速度快, 我们希望能有一个零配置和一个完全免费的搜索模式, 我们希望能够简单地使用 JSON 通过 HTTP 来索引数据, 我们希望我们的搜索服务器始终可用, 我们希望能够从一台开始并扩展到数百台, 我们要实时搜索,我们要简单的多租户,我们希望建立一个云的解决方案。 因此我们利用 Elasticsearch 来解决所有这些问题及可能出现的更多其它问题。 # 安装 ## 使用 docker 安装 Elasticsearch **Docker-compose ** ``` # ELK_VERSION=7.9.1 # ELASTICSEARCH_HOST_HTTP_PORT=9200 # ELASTICSEARCH_HOST_TRANSPORT_PORT=9300 elasticsearch: build: context: ./elasticsearch args: - ELK_VERSION=${ELK_VERSION} volumes: - elasticsearch:/usr/share/elasticsearch/data environment: - cluster.name=laradock-cluster - node.name=laradock-node - bootstrap.memory_lock=true - "ES_JAVA_OPTS=-Xms512m -Xmx512m" - cluster.initial_master_nodes=laradock-node ulimits: memlock: soft: -1 hard: -1 nofile: soft: 65536 hard: 65536 ports: - "${ELASTICSEARCH_HOST_HTTP_PORT}:9200" - "${ELASTICSEARCH_HOST_TRANSPORT_PORT}:9300" depends_on: - php-fpm networks: - frontend - backend ``` **Dockerfile** ``` # ELK_VERSION=7.9.1 ARG ELK_VERSION FROM elasticsearch:${ELK_VERSION} EXPOSE 9200 9300 ``` **Docker 容器中安装的目录:**/usr/share/elasticsearch ## 使用 docker 安装 Kibana **Docker-compose ** ``` # KIBANA_HTTP_PORT=5601 # ELK_VERSION=7.9.1 kibana: build: context: ./kibana args: - ELK_VERSION=${ELK_VERSION} ports: - "${KIBANA_HTTP_PORT}:5601" depends_on: - elasticsearch networks: - frontend - backend ``` **Dockerfile** ``` # ELK_VERSION=7.9.1 ARG ELK_VERSION FROM kibana:${ELK_VERSION} EXPOSE 5601 ``` ## 安装或者启动报错总结 [报错总结](https://y7fgg0syih.feishu.cn/docs/doccnLg4soPxzUx4dhmMJGnwtif) # 插件安装 注意:安装完插件后需要重启 elasticsearch 服务 ES 提供一个插件安装工具 elasticsearch-plugin 有的包官方提供在线安装,有的包只能线下通过安装包形式安装 ## **直接安装** analysis-icu 分词器 ``` elasticsearch-plugin list elasticsearch-plugin install analysis-icu elasticsearch-plugin remove analysis-icu ``` ## **通过安装包安装** Ik 分词器 GitHub 地址: [https://github.com/medcl/elasticsearch-analysis-ik](https://github.com/medcl/elasticsearch-analysis-ik) ``` elasticsearch-plugin install https://github.com/medcl/elasticsearch-analysis-ik/releases/download/v7.9.1/elasticsearch-analysis-ik-7.9.1.zip ``` 其中 7.9.1 是 es 的版本号 ## **通过压缩包直接解压到 plugin 目录安装** 略 # 集群的健康值检查 @cat  - Green :所有的主分片和复制分片均为正常 集群健康 - Yellow: 至少又一个复制分片不可用,但是所有的主分片均为可用,数据仍然是可以保证完整性的 - Red : 至少又一个主分片为不可用状态,数据不完整,集群不可用 ``` GET _cat/health?v epoch timestamp cluster status node.total node.data shards pri relo init unassign pending_tasks max_task_wait_time active_shards_percent 1663067039 11:03:59 laradock-cluster green 1 1 6 6 0 0 0 0 - 100.0% ``` epoch timestamp 时间戳、格林威治时间、加 8 个小时为中国上海时间、时区问题 cluster 当前集群的集群名称 status 健康值状态 node.total 集群包含的所有的节点数 node.data 数据节点数 shards 当前又多少个分片 ``` GET _cluster/health { "cluster_name" : "laradock-cluster", "status" : "green", "timed_out" : false, "number_of_nodes" : 1, "number_of_data_nodes" : 1, "active_primary_shards" : 6, "active_shards" : 6, "relocating_shards" : 0, # 对应 上面的 relo ,代表目前正在迁移的分片数量 "initializing_shards" : 0, # 对应上面的 init,代表正在初始化的分片数量 "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 # 当前活动的分片百分比 } ``` # 什么是搜索引擎 ## **全文搜索引擎** 自然语言处理(NLP)、爬虫、网页处理、大数据处理 如谷歌、百度、搜狗、必应等等 一个词语可能代表多个意思,或者多个应用场景,需要多种维度的来展示内容 数据来源于站外采集或爬虫 ## **垂直搜索引擎** 有明确搜索目的的搜索行为 各大电商网站、OA、站内搜索、视频网站等 例如在淘宝检索商品,那几乎就是搜索商品名称、分类、或者品类,数据源于站内数据 ## **搜索引擎应该具备哪些要求** - 查询速度快 - 高效的压缩算法 - 快速的编码和解码速度 - 结构准确度 - 评分算法,评分越高排名靠前 - BM25 - TF-IDF - 检索结果丰富 - 召回率 ## **面向海量数据,如果达到搜索引擎级别的查询效率** - 索引 - 帮助快速检索 - 以数据结构为载体 - 以文件形式落地 ## **mysql 的索引原理** B 树节点存储数据、B+ 树只有叶子节点存储数据,所以 B+ 树单个页存储的节点数比 B 树多,层级会比 B 树浅,因为不存储数据所以 IO 比 B 树少 ## **Mysql 索引能解决大数据搜索的问题吗?** - 索引往往字段很长,如果使用 B+trees,树可能很深,IO 很可怕 - 索引可能会失效(全表扫描) - 准确度精度差 # **全文检索**   全文检索全文检索:索引系统通过扫描文章中的每一个词,对其创建索引,指明在文章中出现的次数和位置,当用户查询时,索引系统过就会根据事先简历的索引进行查找,并将査找的结果反馈给用户的检索方式 # 倒排索引的原理  # 倒排索引的数据结构  # Elasticsearch 中涉及到的重要概念 Elasticsearch 有几个核心概念。从一开始理解这些概念会对整个学习过程有莫大的帮助。  ### **1) 接近实时(NRT)** Elasticsearch 是一个接近实时的搜索平台。这意味着,从索引一个文档直到这个文档能够被搜索到有一个轻微的延迟(通常是 1 秒)。 ### **2) 集群(cluster) ** 一个集群就是由一个或多个节点组织在一起,它们共同持有你整个的数据,并一起提供索引和搜索功能。一个集群由一个唯一的名字标识,这个名字默认就是“elasticsearch”。这个名字是重要的,因为一个节点只能通过指定某个集群的名字,来加入这个集群。在产品环境中显式地设定这个名字是一个好习惯,但是使用默认值来进行测试/开发也是不错的。 ### **3) 节点(node) ** 一个节点是你集群中的一个服务器,作为集群的一部分,它存储你的数据,参与集群的索引和搜索功能。 和集群类似,一个节点也是由一个名字来标识的,默认情况下,这个名字是一个随机的漫威漫画角色的名字,这个名字会在启动的时候赋予节点。这个名字对于管理工作来说挺重要的,因为在这个管理过程中,你会去确定网络中的哪些服务器对应于 Elasticsearch 集群中的哪些节点。 一个节点可以通过配置集群名称的方式来加入一个指定的集群。默认情况下,每个节点都会被安排加入到一个叫做“elasticsearch”的集群中,这意味着,如果你在你的网络中启动了若干个节点,并假定它们能够相互发现彼此,它们将会自动地形成并加入到一个叫做“elasticsearch”的集群中。 在一个集群里,只要你想,可以拥有任意多个节点。而且,如果当前你的网络中没有运行任何 Elasticsearch 节点,这时启动一个节点,会默认创建并加入一个叫做“elasticsearch”的集群。 ### **4) 索引(index) ** 一个索引就是一个拥有几分相似特征的文档的集合。 比如说,你可以有一个客户数据的索引,另一个产品目录的索引,还有一个订单数据的索引。 一个索引由一个名字来标识(必须全部是小写字母的),并且当我们要对对应于这个索引中的文档进行索引、搜索、更新和删除的时候,都要使用到这个名字。 **索引类似于关系型数据库中 Database 的概念。** 在一个集群中,如果你想,可以定义任意多的索引。 ### **5) 类型(type) ** 在一个索引中,你可以定义一种或多种类型。 一个类型是你的索引的一个逻辑上的分类/分区,其语义完全由你来定。 通常,会为具有一组共同字段的文档定义一个类型。比如说,我们假设你运营一个博客平台并且将你所有的数据存储到一个索引中。 在这个索引中,你可以为用户数据定义一个类型,为博客数据定义另一个类型,当然,也可以为评论数据定义另一个类型。 **类型类似于关系型数据库中 Table 的概念。** **7.X 弱化 TYPE 概念使用 _doc 表示、 8.X 完全删除** ### **6)文档(document) ** 一个文档是一个可被索引的基础信息单元。 比如,你可以拥有某一个客户的文档,某一个产品的一个文档,当然,也可以拥有某个订单的一个文档。 文档以 JSON(Javascript Object Notation)格式来表示,而 JSON 是一个到处存在的互联网数据交互格式。 在一个 index/type 里面,只要你想,你可以存储任意多的文档。 注意,尽管一个文档,物理上存在于一个索引之中,文档必须被索引/赋予一个索引的 type。 **文档类似于关系型数据库中 Record 的概念。** 实际上一个文档除了用户定义的数据外,还包括 `_index`、`_type` 、_id 、_version 字段。 - _index:是索引名; - _type:文档类型(以后会弃用); - _id:文档唯一标识,操作文档时就需要使用此唯一标识; - _version:文档版本,每更新一次会加一; - seq_no:和 _versionー 样,一旦数据发生更改,数据也一直是累计的。 Shard 级别严格递增,保证后写入的 Doc 的 seq_ no 大于先写入的_docseq_no。 - _primary_term: primary_term 主要是明来复数据的处理当多个文档的_seq_no 一样时的冲突,避免 Primary Shard 上的写入被覆盖。每当 Primary Shard 发生重新分配时,比如重启, Primary 选举等,_ primary_term 会递增 1。 **并发场景下修改文档** seq_no,和 _primary_term 是对 version 的优化,7X 版本的 ES 默以认使用这种方式控制版本,所以当在高并发环境下使用乐观锁机制修改文档时,要带上当前文档的 seq_no 和 primary_term 进行更新 ``` POST /product/_doc/1?if_seq_no=0&if_primary_term=1 { "name": "小米 nfc phone aa" } ``` 如果版本号不对,会抛出版本异常,如下 ``` { "error": { "root_cause": [ { "type": "version_conflict_engine_exception", "reason": "[1]: version conflict, required seqNo [0], primary term [1]. current document has seqNo [2] and primary term [1]", "index_uuid": "ZJ79cTY-R0WAVP8Q7_wm7Q", "shard": "2", "index": "product" } ], "type": "version_conflict_engine_exception", "reason": "[1]: version conflict, required seqNo [0], primary term [1]. current document has seqNo [2] and primary term [1]", "index_uuid": "ZJ79cTY-R0WAVP8Q7_wm7Q", "shard": "2", "index": "product" }, "status": 409 } ``` ### 7) 分片和复制(shards & replicas) 一个索引可以存储超出单个结点硬件限制的大量数据。 比如,一个具有 10 亿文档的索引占据 1TB 的磁盘空间,而任一节点都没有这样大的磁盘空间;或者单个节点处理搜索请求,响应太慢。 为了解决这个问题,Elasticsearch 提供了将索引划分成多份的能力,这些份就叫做分片。 当你创建一个索引的时候,你可以指定你想要的分片的数量。 每个分片本身也是一个功能完善并且独立的“索引”,这个“索引”可以被放置到集群中的任何节点上。 **分片之所以重要,主要有两方面的原因:** - 允许你水平分割/扩展你的内容容量 - 允许你在分片(潜在地,位于多个节点上)之上进行分布式的、并行的操作,进而提高性能/吞吐量 至于一个分片怎样分布,它的文档怎样聚合回搜索请求,是完全由 Elasticsearch 管理的,对于作为用户的你来说,这些都是透明的。 在一个网络/云的环境里,失败随时都可能发生,在某个分片/节点不知怎么的就处于离线状态,或者由于任何原因消失了。这种情况下,有一个故障转移机制是非常有用并且是强烈推荐的。 为此目的,Elasticsearch 允许你创建分片的一份或多份拷贝,这些拷贝叫做复制分片,或者直接叫复制。 **复制之所以重要,主要有两方面的原因:** - 在分片/节点失败的情况下,提供了高可用性。因为这个原因,注意到复制分片从不与原/主要(original/primary)分片置于同一节点上是非常重要的。 - 扩展你的搜索量/吞吐量,因为搜索可以在所有的复制上并行运行 **总之,每个索引可以被分成多个分片。一个索引也可以被复制 0 次(意思是没有复制)或多次。** 一旦复制了,每个索引就有了主分片(作为复制源的原来的分片)和复制分片(主分片的拷贝)之别。分片和复制的数量可以在索引创建的时候指定。 **在索引创建之后,你可以在任何时候动态地改变复制数量,但是不能改变分片的数量。** 默认情况下,Elasticsearch 中的每个索引被分片 5 个主分片和 1 个复制,这意味着,如果你的集群中至少有两个节点,你的索引将会有 5 个主分片和另外 5 个复制分片(1 个完全拷贝),这样的话每个索引总共就有 10 个分片。 一个索引的多个分片可以存放在集群中的一台主机上,也可以存放在多台主机上,这取决于你的集群机器数量。主分片和复制分片的具体位置是由 ES 内在的策略所决定的。 # ElasticSearch 索引操作 官方文档: 索引可以对应关系数据库中数据库的概念 ## 创建索引 索引名称必须是小写字母 格式:PUT/索引名称 ``` # 创建索引 PUT /product # 创建索引时可以设置分片数量和副本数量 PUT /product { "settings":{ "number_of_shards": 3, "number_of_replicas": 2 } } **# 修改索引配置** PUT /product/_settings { "index":{ "number_of_replicas":1 } } ``` ## 删除索引 格式:delete /索引名称 ``` # 删除索引 delete /product ``` # ElasticSearch 文档操作 ## 文档的操作类型 - Create 不存在则创建,存在则报错 - Delete 删除文档 - Update 全量更新或部分更新 - Index 索引(动词)(创建 或者 全量替换) ``` # 创建索引 PUT /product { "settings":{ "number_of_shards": 3, "number_of_replicas": 2, "index":{ "analysis.analyzer.default.type": "ik_max_word" } } } ``` ## 添加文档 **格式:** [ PUT | POST ] / 索引名称 / [_doc | _create ] / id **注意:** POST 和 PUT 都能起到创建/更新的作用 PUT 需要对一个具体的资源进行操作,也就是要确定 ID 才能进行更新/创建 而 POST 是可以对整个资源集合进行操作的,如果不写 ID 就由 ES 生成一个唯一 ID 进行创建新文档,如果填了 ID 那就针对这个 ID 的文档进行创建/更新 **_create/ID 不存在会创建,如果存在则会报错** ``` # 创建文档指定ID # PUT 方式创建文档 # 如果ID 不存在,创建新文档 # 如果ID 存在(覆盖),则删除现有文档,在新增,版本号增加 PUT /product/_doc/1 { "name": "小米 nfc phone", "desc": "zhichi quanguoneng nfc,shouji zhong de jianjiji", "price": 4999, "tags": [ "xingjiabi", "fashao", "gonjiaoka" ] } #创建文档 - PUT - create # 如果ID存在就报错,如果不存在则正常添加 PUT /product/_create/3 { "name": "小米 nfc phone", "desc": "zhichi quanguoneng nfc,shouji zhong de jianjiji", "price": 4999, "tags": [ "xingjiabi", "fashao", "gonjiaoka" ] } # POST 指定ID POST /product/_doc/2 { "name": "小米 nfc phone", "desc": "zhichi quanguoneng nfc,shouji zhong de jianjiji", "price": 4999, "tags": [ "xingjiabi", "fashao", "gonjiaoka" ] } # POST 不指定ID,ID自动生成 POST /product/_doc { "name": "小米 nfc phone", "desc": "zhichi quanguoneng nfc,shouji zhong de jianjiji", "price": 4999, "tags": [ "xingjiabi", "fashao", "gonjiaoka" ] } ``` ## 修改文档 ### 全量更新 整个 json 都会被替换 **格式:[ PUT | POST ] / 索引名称 / _doc ****/ ID** ``` # 修改文档 - PUT 全量更新 PUT /product/_doc/1 { "name": "小米 nfc phone aa" } # 查询修改后的信息 GET /product/_doc/1 { "_index": "product", "_type": "_doc", "_id": "1", "_version": 2, "_seq_no": 1, "_primary_term": 1, "found": true, "_source": { "name": "小米 nfc phone aa" } } # 修改文档 - POST 全量更新 POST /product/_doc/1 { "name": "小米 nfc phone", "desc": "zhichi quanguoneng nfc,shouji zhong de jianjiji", "price": 4999, "tags": [ "xingjiabi", "fashao", "gonjiaoka" ] } ``` ### 部分更新 使用 update 部分更新, 格式:POST / 索引名称 / **update **/ ID ``` # 修改文档 - POST - 部分更新 POST /product/_update/1 { "doc":{ "name": "小米 nfc phone aa" } } # 查询修改后的信息 GET /product/_doc/1 { "_index": "product", "_type": "_doc", "_id": "1", "_version": 12, "_seq_no": 11, "_primary_term": 1, "found": true, "_source": { "name": "小米 nfc phone aa", "desc": "zhichi quanguoneng nfc,shouji zhong de jianjiji", "price": 4999, "tags": [ "xingjiabi", "fashao", "gonjiaoka" ] } } ``` ### 使用_update_by_query 更新文档 根据查询结果更新文档 ``` # 修改文档 - 根据查询结果更新文档 POST /product/_update_by_query { "query":{ "match":{ "_id":1 } }, "script":{ "source":"ctx._source.price = 299" } } ``` ### ### op_type=index 创建 或者 全量替换  ## 查询文档 ### 指定 ID 查询 格式:GET / 索引名称 / _doc / id ``` GET /product/_doc/1 { "_index": "product", "_type": "_doc", "_id": "1", "_version": 13, "_seq_no": 12, "_primary_term": 1, "found": true, "_source": { "price": 299, "name": "小米 nfc phone aa", "desc": "zhichi quanguoneng nfc,shouji zhong de jianjiji", "tags": [ "xingjiabi", "fashao", "gonjiaoka" ] } } ``` ### 搜索查询 格式:GET / 索引名称 / _search ``` # 搜索索引下的数据 GET /product/_search { "took": 1, "timed_out": false, "_shards": { "total": 3, "successful": 3, "skipped": 0, "failed": 0 }, "hits": { "total": { "value": 6, "relation": "eq" }, "max_score": 1, "hits": [ { "_index": "product", "_type": "_doc", "_id": "1", "_score": 1, "_source": { "price": 299, "name": "小米 nfc phone aa", "desc": "zhichi quanguoneng nfc,shouji zhong de jianjiji", "tags": [ "xingjiabi", "fashao", "gonjiaoka" ] } } ] } } ``` ES Search Apl 提供了两种条件查询搜索方式: - REST 风格的请求 URI,直接将参数带过去 - 封装到 request body 中,这种方式可以定义更加易读的 JSON 格式 ``` # 通过URI搜索,使用“q”指定查询字符串,“ query string syntax"Kv键值对 # 条件查询,如要查询价格等于299的 _search?q=*:** GET /product/_doc/_search?q=price:299 # 范围查询,如要查询价格在 1 - 300之间的 _search?q=***[***To***],注意:To必须为大写 GET /product/_doc/_search?q=price[1 TO 300] # 查询价格小于等于300的 :<= GET /product/_doc/_search?q=price:<=300 # 查询价格大于300的 :> GET /product/_doc/_search?q=price:>300 # 分页查询 from=*&size=* GET /product/_doc/_search?q=price:>300&from=0&size=1 # 只输出某些字段 _source=字段,字段 GET /product/_doc/_search?_source=name,price # 对查询结果排序 sort=字段:desc/asc GET /product/_doc/_search?sort=price:desc ``` 以上是通过 URI 方式查询,request body 方式较为复杂,后续再试 ## 删除文档 格式:DELETE / 索引名称 / _doc / ID ``` DELETE /product/_doc/AiSFO4MB8Iy_dzpn3BNo ``` ## _bulk 批量操作 - 增删改 批量对文档进行写操作是通过 buk 的 AP 来实现的 - **请求方式:**POST - **请求地址:**_bulk - **请求参数:**通过 buk 操作文档,一般至少有两行参数(或偶数行参数) - 第一行参数为指定操作的类型及操作的对象 (index, type 和 id) - 第二行参数才是操作的数据 **参数类似于:** ``` {"actionName":{"_index":"indexName", "_type":"typeName", "_id":"id"}} {"filed1":"value1", "filed2":"value2"} {"actionName":{"_index":"indexName", "_type":"typeName", "_id":"id"}} {"filed1":"value1", "filed2":"value2"} ``` **actionName**:表示操作类型,主要有 CREATE,INDEX,DELETE,UPDATE ### **批量创建文档:create** ``` PUT _bulk { "create":{"_index":"article", "_type":"_doc", "_id":1} } { "id":1, "title":"title - 1" } { "create":{"_index":"article", "_type":"_doc", "_id":2} } { "id":2, "title":"title - 2" } { "create":{"_index":"article", "_type":"_doc", "_id":3} } { "id":3, "title":"title - 3" } ``` ### 普通创建或全量替换: index ``` POST _bulk { "index":{"_index":"article", "_type":"_doc", "_id":1} } { "id":1, "title":"title - 1" } { "index":{"_index":"article", "_type":"_doc", "_id":2} } { "id":2, "title":"title - 2" } { "index":{"_index":"article", "_type":"_doc", "_id":3} } { "id":3, "title":"title - 3" } ``` - 如果原文档不存在,则是创建 - 如果原文档存在,则是替换,全量修改原文档 ### 批量删除:delete ``` POST _bulk { "delete":{"_index":"article", "_type":"_doc", "_id":1} } { "delete":{"_index":"article", "_type":"_doc", "_id":2} } { "delete":{"_index":"article", "_type":"_doc", "_id":3} } ``` ### 批量修改:update ``` POST _bulk { "update":{"_index":"article", "_type":"_doc", "_id":1} } { "doc":{ "title":"title - 1" } } { "update":{"_index":"article", "_type":"_doc", "_id":2} } { "doc":{ "title":"title - 2" } } { "update":{"_index":"article", "_type":"_doc", "_id":3} } { "doc":{ "title":"title - 3" } } ``` ### 组合应用 ``` { "create":{"_index":"article", "_type":"_doc", "_id":1} } { "id":1, "title":"title - 1" } { "delete":{"_index":"article", "_type":"_doc", "_id":3} } { "update":{"_index":"article", "_type":"_doc", "_id":2} } { "doc":{ "title":"title - 2" } } ``` ### Filter_path=items.*.error **方便在批量处理中,捕获失败的原因和数据** 当我们在_bulk 中对数据进行大量操作,例如有 10000 条操作,如果有错误,我们只想展示错误的文档信息和具体的错误信息时,可以使用这条语句对返回结果进行后置过滤  ``` **POST /_bulk?filter_path=items.*.error** { "create":{"_index":"article", "_type":"_doc", "_id":1} } { "id":1, "title":"title - 1" } { "delete":{"_index":"article", "_type":"_doc", "_id":3} } { "update":{"_index":"article", "_type":"_doc", "_id":2} } { "doc":{ "title":"title - 2" } } ``` ## 批量操作 - 读取 es 的批量查询可以使用 mge 和 msearch 两种。 其中 mget 是需要我们知道它的 id,可以指定不同的 index,也可以指定返回值 source。 msearch 可以通过字段查询来进行一个批量的查找 ### _mget #### 可以通过 ID 批量获取不同 index 和 type 的数据 ``` # 可以通过ID批量获取不同index和type的数据 GET _mget { "docs": [ { "_index": "product", "_id": 1 }, { "_index": "article", "_id": 1 } ] } ``` #### **可以通过 ID 批量获取指定 index 的数据** ``` # 可以通过ID批量获取指定index的数据 GET /product/_mget { "docs": [ { "_id": 1 }, { "_id": 2 } ] } # 简化后的 - 可以通过ID批量获取指定index的数据 # 类似 mysql 中的IN GET /product/_mget { "ids":["1", "2"] } ``` #### 通过_source 指定返回的字段 ``` GET /product/_mget { "docs": [ { "_id": 2, ** "_source": [** ** "name",** ** "price"** ** ]** }, { "_id": 3 } ] } { "docs" : [ { "_index" : "product", "_type" : "_doc", "_id" : "2", "_version" : 8, "_seq_no" : 22, "_primary_term" : 2, "found" : true, ** "_source" : {** ** "price" : 4991,** ** "name" : "小米 NFC 手机"** ** }** }, { "_index" : "product", "_type" : "_doc", "_id" : "3", "_version" : 3, "_seq_no" : 15, "_primary_term" : 1, "found" : true, "_source" : { "id" : 3, "name" : "NFC手机", "desc" : "手机中的轰炸机", "price" : 2999, "lv" : "高端机", "type" : "手机", "createtime" : "2020-06-20", "tags" : [ "性价比", "快充", "门禁卡" ] } } ] } ``` #### 通过 include 与 exclude 指定需要与排除的字段 注意、同时指定的时候不要冲突 ``` GET /product/_mget { "docs": [ { "_id": 2, "_source": [ "name", "price" ] }, { "_id": 3, "_source":{ "include":["name", "price"], "exclude":["lv", "type"] } } ] } ``` ### _msearch 在 _msearch 中,请求格式和_buk 类似。 查询条数据需要两个对象,第一个设置 index 和 type,第二个设置查询语句。查询语句和 search 相同。如果只是查询一个 index,我们可以在 URL 中带上 index,然后在 request 中可以直接用空对象表示。 ``` GET /product/_msarch {} { "query": { "match_all": {}, "from": 0, "size": 2 } } {"index":"article"} { "query": { "match_all": {} } } ``` # ES 检索原理分析 ## 索引的原理 索引是加速数据查询的重要手段,其核心原理是通过不断的缩小想要获取数据的范围来筛选出最终想要的结果,同时把随机的事件变成顺序的事件 ## 磁盘 IO 与预读 磁盘 IO 是程序设计中非常高昂的操作,也是影响程序性能的重要因素,因此应当尽量避免过多的磁盘 IO,有效的利用内存可以大大的提升程序的性能。 在操作系统层面,发生一次 IO 时,不光把当前磁盘地址的数据,而是把相邻的数据也都读取到内存缓冲区内,局部预读性原理告诉我们,当计算机访问一个地址的数据的时候,与其相邻的数据也会很快被访问到。每一次 IO 读取的数据我们称之为页(page)。具体一页有多大数据跟操作系统有关,一般为 4k 或 8k,也就是我们读取一页内的数据时候,实际上才发生了一次 O,这个理论对于索引的数据结构设计非常有帮助 ## 倒排索引 当数据写入 ES 时,数据将会通过分词被切分为不同的 term,ES 将 term 与其对应的文档列表建立一种映射关系,这种结构就是倒排索引。如下图所示  为了进一步提升索引的效率,ES 在 term 的基础上利用 term 的前缀或者后缀构建了 term index,用于对 term 本身进行索引,ES 实际的索引结构如下图所示  这样当我们去搜索某个关键词时,ES 首先根据它的前缀或者后缀迅速缩小关键词的在 term dictionary 中的范围,大大减少了磁盘 lO 的次数 - **单词词典( Term Dictionary):**记录所有文档的单词,记录单词到倒排列表的关联关系 - **倒排列表( Posting List):**记录了单词对应的文档结合,由倒排索引项组成 - **倒排索引项( Posting)** - **文档 |D** - **词频 TF-**该单词在文档中出现的次数,用于相关性评分 - **位置( Position)**单词在文档中分词的位置。用于短语搜索( match phrase query) - **偏移( Offset)**记录单词的开始结束位置,实现高亮显示  # ES 高级查询 Query DSL ES 中提供了一种强大的检索数据方式这种检索方式称之为 Query DSL( Domain Specified Language), Query DSL 是利用 Rest API 传递 JSON 格式的请求体( Request body)数据与 ES 进行交互,这种方式的丰富查询语法让 ES 检索变得更强大,更简洁 ## **官方文档:** ## **语法:** ``` GET /product/_doc/_search {json 请求体数据} # 简化后为 GET /product/_search {json 请求体数据} ``` ## **初始化数据:** ``` # 删除索引 DELETE /product # 创建索引 PUT /product { "settings":{ "number_of_shards": 3, "number_of_replicas": 2, "index":{ "analysis.analyzer.default.type": "ik_max_word" } } } # 添加数据 POST /product/_doc/1 { "name":"张三", "sex":1, "age":20, "remark": "php code" } POST /product/_doc/2 { "name":"李四", "sex":2, "age":21, "remark": "java code" } POST /product/_doc/3 { "name":"王五", "sex":1, "age":25, "remark": "nodejs code" } POST /product/_doc/4 { "name":"赵六", "sex":2, "age":31, "remark": "golang code" } POST /product/_doc/5 { "name": "陈荣", "sex": 1, "age": 28, "remark": "北京市通州区北苑街道果园西小区" } POST /product/_doc/6 { "name": "陈佳丽", "sex": 0, "age": 25, "remark": "福建省莆田市秀屿区湄洲镇港楼村" } # 查询添加进去的数据 GET /product/_search ``` ## 查询所有:match_all 使用 match_all,默认只会返回 10 条数据。 原因: search 查询默认采用的是分页查询,每页记录数 size 的默认值为 10。 如果想显示更多数据,指定 size ### 简单查询 ``` GET /product/_search # 等同于 GET /product/_search { "query":{ "match_all":{} } } ``` ### 返回指定条数 size ``` GET /product/_search { "query":{ "match_all":{} }, "size":2 } ``` ### 分页查询 from From 关键字:用来指定其实返回位置,和 size 关键字连用可实现分页效果 ``` GET /product/_search { "query":{ "match_all":{} }, "size":2, "from":2 } ``` Size 可以无限增加吗? ``` # 测试 - GET /product/_search { "query":{ "match_all":{} }, "size":20000, "from":2 } ``` ``` { "error": { "root_cause": [ { "type": "illegal_argument_exception", "reason": "Result window is too large, from + size must be less than or equal to: [10000] but was [20002]. See the scroll api for a more efficient way to request large data sets. This limit can be set by changing the [index.max_result_window] index level setting." } ], "type": "search_phase_execution_exception", "reason": "all shards failed", "phase": "query", "grouped": true, "failed_shards": [ { "shard": 0, "index": "product", "node": "3kk5DHKQRAmlRHnpJn2GYw", "reason": { "type": "illegal_argument_exception", "reason": "Result window is too large, from + size must be less than or equal to: [10000] but was [20002]. See the scroll api for a more efficient way to request large data sets. This limit can be set by changing the [index.max_result_window] index level setting." } } ], "caused_by": { "type": "illegal_argument_exception", "reason": "Result window is too large, from + size must be less than or equal to: [10000] but was [20002]. See the scroll api for a more efficient way to request large data sets. This limit can be set by changing the [index.max_result_window] index level setting.", "caused_by": { "type": "illegal_argument_exception", "reason": "Result window is too large, from + size must be less than or equal to: [10000] but was [20002]. See the scroll api for a more efficient way to request large data sets. This limit can be set by changing the [index.max_result_window] index level setting." } } }, "status": 400 } ``` **异常原因:** 1、查询结果的窗口太大,from+size 的结果必须小于或等于 10000,而当前查询结果的窗口为 20000 2、可以采用 scroll api 更高效的请求大量数据 3、查询结果的窗口的限制可以通过参数 index.max_result_window 进行设置 ``` #修改指定索引 PUT /product/_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 进程执行查询操作的过程中却需要将(100000010)的记录都加载到内存中,可想而知对内存的消耗有多大。这也是 ES 中不推荐采用(fom+sze)方式进行深度分页的原因。 同理,from 为 0,size 为 1000000,ES 进程执行查询操作的过程中确需要将 100000 条记录都加载到内存中再返回给调用方,也会对 ES 内存造成很大压力 ### 分页查询 scroll 改动 index.max_result_window 参数值的大小,只能解决一时的问题,当索引的数据量持续增长时,在査询全量数据时还是会出现问题。而且会增加 ES 服务器内存,因大结果集消耗完的风险。最佳实践还是根据异常提示中的采用 scroll api 更高效的请求大量数据集。 ``` # 査询命令中新增scro11=1m,说明采用游标查询,保持游标查询窗口一分钟 # 这里由于测试数据量不够,所以size值设置为2 # 实际使用中为了减少游标查询的次数,可以将值适当增大,比如设置为1000 GET /product/_search?scroll=1m { "query":{"match_all":{}}, "size":2 } ``` 查询结果: 除了返回前两条数据,还返回了一个游标 ID 值_scroll_id ``` { "**_scroll_id**": "FGluY2x1ZGVfY29udGV4dF91dWlkDnF1ZXJ5VGhlbkZldGNoAxRwU1JMUElNQjhJeV9kenBuYWlZWAAAAAAAABRCFjNrazVESEtRUkFtbFJIbnBKbjJHWXcUcGlSTFBJTUI4SXlfZHpwbmFpWVgAAAAAAAAUQxYza2s1REhLUVJBbWxSSG5wSm4yR1l3FHB5UkxQSU1COEl5X2R6cG5haVlYAAAAAAAAFEQWM2trNURIS1FSQW1sUkhucEpuMkdZdw==", "took": 0, "timed_out": false, "_shards": { "total": 3, "successful": 3, "skipped": 0, "failed": 0 }, "hits": { "total": { "value": 4, "relation": "eq" }, "max_score": 1, "hits": [ { "_index": "product", "_type": "_doc", "_id": "2", "_score": 1, "_source": { "name": "李四", "sex": 2, "age": 21, "remark": "java code" } }, { "_index": "product", "_type": "_doc", "_id": "3", "_score": 1, "_source": { "name": "王五", "sex": 1, "age": 25, "remark": "nodejs code" } } ] } } ``` **采用游标 ID 查询** ``` **GET /_search/scroll** { "scroll":"1m", "scroll_id":"FGluY2x1ZGVfY29udGV4dF91dWlkDnF1ZXJ5VGhlbkZldGNoAxQ1U1JOUElNQjhJeV9kenBuMHlhTwAAAAAAABSCFjNrazVESEtRUkFtbFJIbnBKbjJHWXcUNWlSTlBJTUI4SXlfZHpwbjB5YU8AAAAAAAAUgxYza2s1REhLUVJBbWxSSG5wSm4yR1l3FDV5Uk5QSU1COEl5X2R6cG4weWFPAAAAAAAAFIQWM2trNURIS1FSQW1sUkhucEpuMkdZdw==" } ``` 多次根据 scroll_id 游标查询,直到没有数据返回则结束查询。采用游标查询索引全量数据,更安全高效,限制了单次对内存的消耗。 ### 指定字段排序 sort 注意:会让得分失效 ``` GET /product/_search { "query":{ "match_all":{} }, "sort":[ { "age":"desc" } ] } # 排序 + 分页 GET /product/_search { "query":{ "match_all":{} }, "sort":[ { "age":"desc" } ], "from":2, "size":2 } ``` ### 返回指定字段:_source _source 关键字:是一个数组,在数组中用来指定展示那些字段 ``` GET /product/_search { "query":{ "match_all":{} }, "_source":["name","age"] } ``` ## match match 在匹配时会对所查找的关键词进行分词,然后按分词匹配查找 match 支持以下参数 - query:指定匹配的值 - operator:匹配条件类型 - and:条件分词后都要匹配 - or:条件分词后有一个匹配即可(默认) - minimum_should_match:最低匹配度,即条件在倒排素引中最低的匹配度 ``` GET /product/_search { "query":{ "match":{ "remark": "北京市" } } } # 查询结果 # match 北京市分词后 会分为 北京 、 市 # 默认分词后是 or 关系 # 所以市莆田市中的市 也匹配到了 { "took": 1, "timed_out": false, "_shards": { "total": 3, "successful": 3, "skipped": 0, "failed": 0 }, "hits": { "total": { "value": 2, "relation": "eq" }, "max_score": 0.8630463, "hits": [ { "_index": "product", "_type": "_doc", "_id": "5", "_score": 0.8630463, "_source": { "name": "陈荣", "sex": 1, "age": 28, "remark": "北京市通州区北苑街道果园西小区" } }, { "_index": "product", "_type": "_doc", "_id": "6", "_score": 0.53319013, "_source": { "name": "陈佳丽", "sex": 0, "age": 25, "remark": "福建省莆田市秀屿区湄洲镇港楼村" } } ] } } ``` ### **使用 operator 指定分词后的检索关系(and)** ``` GET /product/_search { "query": { "match": { "remark": { "query": "北京市", "operator": "and" } } } } # 查询结果只有北京市 { "_index": "product", "_type": "_doc", "_id": "5", "_score": 0.8630463, "_source": { "name": "陈荣", "sex": 1, "age": 28, "remark": "北京市通州区北苑街道果园西小区" } } ``` ### **使用 minimum_should_match 指定最少匹配多少个词,用来提高精度,是 and 与 or 的中间状态** ``` GET /product/_search { "query": { "match": { "remark": { "query": "北京市通州区", "minimum_should_match": 2 } } } } ``` ### fuzziness 与 fuzzy 查询中的 fuzziness 参数,作用一致,都是模糊查询 **表示输入的关键字通过几次操作可以转变成 ES 库里面的对应 trem 词项** 注意,match 查询条件是做分词查询的,而 fuzzy 是不做分词的  ## 短语匹配 match_phrase match_phrase 查询分析文本并根据分析的文本创建一个短语查询。 match_phrase 会将检索关键词分词。 **match_phrase 的分词结果必须在被检索字段的分词中都每含,而且顺序必须相同,而且默认必须都是连续的。** ``` **# ==== 测试1** GET /product/_search { "query": { "match_phrase": { "remark": "北京市通州区" } } } # 查询结果 { "took": 0, "timed_out": false, "_shards": { "total": 3, "successful": 3, "skipped": 0, "failed": 0 }, "hits": { "total": { "value": 1, "relation": "eq" }, "max_score": 1.7260926, "hits": [ { "_index": "product", "_type": "_doc", "_id": "5", "_score": 1.7260926, "_source": { "name": "陈荣", "sex": 1, "age": 28, "remark": "北京市通州区北苑街道果园西小区" } } ] } } ``` ``` **# ==== 测试2 ** GET /product/_search { "query": { "match_phrase": { "remark": "北京市通州"** ****# 没有区** } } } **# 查询结果没有数据** { "took": 0, "timed_out": false, "_shards": { "total": 3, "successful": 3, "skipped": 0, "failed": 0 }, "hits": { "total": { "value": 0, "relation": "eq" }, "max_score": null, "hits": [] } } **# 我们使用****分词****器来分析结果** POST /_analyze { "analyzer":"ik_max_word", "text":"北京市通州区" } { "tokens": [ { "token": "北京市", "start_offset": 0, "end_offset": 3, "type": "CN_WORD", "position": 0 }, { "token": "北京", "start_offset": 0, "end_offset": 2, "type": "CN_WORD", "position": 1 }, { "token": "市", "start_offset": 2, "end_offset": 3, "type": "CN_CHAR", "position": 2 }, ** {** ** "token": "通州区",** ** "start_offset": 3,** ** "end_offset": 6,** ** "type": "CN_WORD",** ** "position": 3** ** },** ** {** ** "token": "通州",** ** "start_offset": 3,** ** "end_offset": 5,** ** "type": "CN_WORD",** ** "position": 4** ** },** { "token": "区", "start_offset": 5, "end_offset": 6, "type": "CN_CHAR", "position": 5 } ] } # 我们发现北京市分词后是满足连续匹配的,但是 通州区在通州前面,所以 通州 不满足连续匹配 ``` ### 使用 slop,允许跨词语匹配 ``` GET /product/_search { "query": { "match_phrase": { "remark": { "query": "北京市通州", "slop": 2 } } } } # 上面没有查出来的,通过slop 参数指定跨词匹配,这次就查出来了 { "took": 0, "timed_out": false, "_shards": { "total": 3, "successful": 3, "skipped": 0, "failed": 0 }, "hits": { "total": { "value": 1, "relation": "eq" }, "max_score": 0.74458885, "hits": [ { "_index": "product", "_type": "_doc", "_id": "5", "_score": 0.74458885, "_source": { "name": "陈荣", "sex": 1, "age": 28, "remark": "北京市通州区北苑街道果园西小区" } } ] } } ``` ## 短语前缀 match_phrase_prefix 大致与 match_phrase 相同, 不同的是,match_phrase 都是词语匹配,但 match_phrase_prefix 最后的一个词是做前缀查询   ## Ngram 和 edge-ngram **通过自建分词器,以达到类似 match_phrase 的功能** **优点:比 match_phrase 要快,** **缺点:因为分词较多会占用大量磁盘空间** **项目中一般使用 edge-ngram 支持前缀索引即可,** **Ngram 后缀和中缀索引如果要支持开销太大如非必要不建议使用** 在 setting 中引入分词器,ngram_tokenizer 里设置类型 type 为 edge_ngram 或者 ngram,然后 mapping 中需要作 ngram 分词的字段指定一下就可以了。 需要注意的是 es7 以后的版本 min_gram 和 max_gram 的粒度默认是不大于 1,也就是说分词是一个字符一个字符逐个分的。 如果粒度需要大于 1 需要设置一下 index.max_ngram_diff 大于等于它们的差值,否则会报错。 ### 指定切分粒度的两个参数 - min_gram 指定最小的切分单位长度 - 如果为 1 的话就是每个字符,创建一个词项 - max_gram 指定最大的切分单位长度 ### **分词粒度的效果**, 例:搜索我是中国人 **分词粒度为默认 1,**以 ngram 分词器分词,则分词效果为 我 是 中 国 人 **分词粒度为默认 3,**以 ngram 分词器分词,则分词效果为 我 我是 我是中 是 是中 是中国 中 中国 中国人 国 国人 人  ### **ngram 和 edge_ngram 类型两者的区别** #### 使用场景 - Ngram 适合中缀搜索(分词量大) - edge-ngram 适合前后缀搜索 #### **ngram 分词效果**  #### **edge_ngram 分词效果**  #### 小结: 主要区别在于 edge_ngram 会按照首字符逐字匹配,ngram 是全字符逐个匹配,比如分词粒度都是 3 的两个分词器,搜索我是中国人: **ngram 分词** 我 我是 我是中 是 是中 是中国 中 中国 中国人 国 国人 人(ngram 分词逐字开始按步长,逐字符分词) **edge_ngram 分词** 我 我是 我是中 (edge_ngram 分词必须以首字 ”我“ 开头逐个按步长,逐字符分词) ### 测试分词效果 ``` GET /_analyze { "tokenizer": "ngram", "text": "reba always loves me" } # 几乎是隔着一两个字母,分一个单词 { "tokens" : [ { "token" : "r", "start_offset" : 0, "end_offset" : 1, "type" : "word", "position" : 0 }, { "token" : "re", "start_offset" : 0, "end_offset" : 2, "type" : "word", "position" : 1 }, { "token" : "e", "start_offset" : 1, "end_offset" : 2, "type" : "word", "position" : 2 }, { "token" : "eb", "start_offset" : 1, "end_offset" : 3, "type" : "word", "position" : 3 }, ……………………………… ``` ### Token filter 词项过滤器 ``` GET /_analyze { "tokenizer": "standard", ** "filter": ["ngram"],** "text": "reba always loves me" } { "tokens" : [ { "token" : "r", "start_offset" : 0, "end_offset" : 4, "type" : "<ALPHANUM>", "position" : 0 }, { "token" : "re", "start_offset" : 0, "end_offset" : 4, "type" : "<ALPHANUM>", "position" : 0 }, { "token" : "e", "start_offset" : 0, "end_offset" : 4, "type" : "<ALPHANUM>", "position" : 0 }, { "token" : "eb", "start_offset" : 0, "end_offset" : 4, "type" : "<ALPHANUM>", "position" : 0 }, ……………………………… ``` **切分过程分析**  ### **定义 ngram 分词器** ``` PUT /test_aaa { "settings": { "index.max_ngram_diff": 10, "number_of_shards": 128, "number_of_replicas": 2, "refresh_interval": "5s", "blocks": { "read_only_allow_delete": "false" }, "analysis": { "analyzer": { ** "ngram_analyzer": {** ** "tokenizer": "ngram_tokenizer"** ** }** }, "tokenizer": { **"ngram_tokenizer": {** ** "token_chars": [** ** "letter",** ** "digit"** ** ],** ** "min_gram": "1",** ** "type": "ngram",** ** "max_gram": "10"** ** }** } } }, "mappings": { "_routing": { "required": true }, "properties": { "id": { "type": "long" }, "username": { "type": "text", "fields": { "ngram": { "type": "text", ** "analyzer": "ngram_analyzer"** } } }, "password": { "type": "long" }, "createTime": { "type": "date", "format": "yyyy-MM-dd HH:mm:ss||strict_date_optional_time||epoch_millis" } } } } ``` ## 多字段查询 multi_match 可以根据字段类型,决定是否使用分词查询,得分高的在前面 Query 也会进行分词,分为张三 |code ``` GET /product/_search { "query": { "**multi_match**": { "query": "张三code", "fields": [ "reamrk", "name" ] } } } # 返回数据 { "took": 0, "timed_out": false, "_shards": { "total": 3, "successful": 3, "skipped": 0, "failed": 0 }, "hits": { "total": { "value": 1, "relation": "eq" }, "max_score": 1.3862942, "hits": [ { "_index": "product", "_type": "_doc", "_id": "1", "_score": 1.3862942, "_source": { "name": "张三", "sex": 1, "age": 20, "remark": "php code" } } ] } } ``` ## 短语查询 query_string 允许我们在单个查询字符串中指定 AND|OR|NOT 条件,同时也和 multi_match query 一样,支持多字段搜索。 和 math 类似,但是 match 需要指定字段名,query_string 是在所有字段中搜索,范围更广泛。 **注意:** 查询的字段如果分词的话,就将查询条件做分词查询 查询的字段如果不分词,就将查询条件做不分词查询 ### 未指定字段查询 ``` GET /product/_search { "query": { "query_string":{ "query":"李四 OR PHP" } } } { "took": 1, "timed_out": false, "_shards": { "total": 3, "successful": 3, "skipped": 0, "failed": 0 }, "hits": { "total": { "value": 2, "relation": "eq" }, "max_score": 1.9616582, "hits": [ { "_index": "product", "_type": "_doc", "_id": "2", "_score": 1.9616582, "_source": { "name": "李四", "sex": 2, "age": 21, "remark": "java code" } }, { "_index": "product", "_type": "_doc", "_id": "1", "_score": 0.9902103, "_source": { "name": "张三", "sex": 1, "age": 20, "remark": "php code" } } ] } } # 使用 AND { "query": { "query_string":{ "query":"李四 AND java" } } } { "took": 1, "timed_out": false, "_shards": { "total": 3, "successful": 3, "skipped": 0, "failed": 0 }, "hits": { "total": { "value": 1, "relation": "eq" }, "max_score": 2.9424872, "hits": [ { "_index": "product", "_type": "_doc", "_id": "2", "_score": 2.9424872, "_source": { "name": "李四", "sex": 2, "age": 21, "remark": "java code" } } ] } } ``` ### 指定单个字段查询 **default_field : 字段名** ``` GET /product/_search { "query": { "query_string": { **"default_field": "remark",** "query": "李四 OR PHP" } } } { "took": 0, "timed_out": false, "_shards": { "total": 3, "successful": 3, "skipped": 0, "failed": 0 }, "hits": { "total": { "value": 1, "relation": "eq" }, "max_score": 0.9902103, "hits": [ { "_index": "product", "_type": "_doc", "_id": "1", "_score": 0.9902103, "_source": { "name": "张三", "sex": 1, "age": 20, "remark": "php code" } } ] } } ``` ### 指定多个字段查询 **fields 字段数组** ``` GET /product/_search { "query": { "query_string": { ** "fields": ["name","remark"],** ** "query": "张三 OR (李四 AND java) "** } } } { "took": 0, "timed_out": false, "_shards": { "total": 3, "successful": 3, "skipped": 0, "failed": 0 }, "hits": { "total": { "value": 2, "relation": "eq" }, "max_score": 2.9424872, "hits": [ { "_index": "product", "_type": "_doc", "_id": "2", "_score": 2.9424872, "_source": { "name": "李四", "sex": 2, "age": 21, "remark": "java code" } }, { "_index": "product", "_type": "_doc", "_id": "1", "_score": 1.3862942, "_source": { "name": "张三", "sex": 1, "age": 20, "remark": "php code" } } ] } } ``` ## 简单短语查询 simple_query_string 类似 Query String,但是会忽略错误的语法,同时只支持部分查询语法,不支持 AND OR NOT,会当做字符串处理。 **Simple_query_string 默认的 operator 是 or** **支持部分逻辑:** ``` + 替代 AND | 替代 OR - 替代 NOT ``` ``` GET /product/_search { "query": { "simple_query_string": { "fields": ["name","remark"], "query": "张三java" } } } { "took": 0, "timed_out": false, "_shards": { "total": 3, "successful": 3, "skipped": 0, "failed": 0 }, "hits": { "total": { "value": 2, "relation": "eq" }, "max_score": 1.3862942, "hits": [ { "_index": "product", "_type": "_doc", "_id": "1", "_score": 1.3862942, "_source": { "name": "张三", "sex": 1, "age": 20, "remark": "php code" } }, { "_index": "product", "_type": "_doc", "_id": "2", "_score": 0.9808291, "_source": { "name": "李四", "sex": 2, "age": 21, "remark": "java code" } } ] } } ``` ### **default_operator ** 指定分词关系 Query 中 使用 + 号或者使用 default_operator 关键字都可 ``` GET /product/_search { "query": { "simple_query_string": { "fields": ["name","remark"], ** "query": "李四 + java",** } } } # 等同上面的+ { "query": { "simple_query_string": { "fields": ["name","remark"], "query": "李四java", ** "default_operator":"and"** } } } { "took": 0, "timed_out": false, "_shards": { "total": 3, "successful": 3, "skipped": 0, "failed": 0 }, "hits": { "total": { "value": 1, "relation": "eq" }, "max_score": 2.9424872, "hits": [ { "_index": "product", "_type": "_doc", "_id": "2", "_score": 2.9424872, "_source": { "name": "李四", "sex": 2, "age": 21, "remark": "java code" } } ] } } ``` ## 关键词查询 Term Term 用来使用关键词查迿(精确匹配),还可以用来查询没有被进行分词的数据类型。 Term 是表达语意的最小单位,搜索和利用统计语言模型进行自然语言处理都需要处理 Term。 match 在匹配时会对所查找的关键词进行分词,然后按分词匹配查找,而 term 会直接对关键词进行查找。 **一般模糊查找的时候,多用 match,而精确查找时可以使用 term** - ES 中默认使用分词器为标准分词器( StandardAnalyzer),标准分词器对于英文单词分词,**对于中文单字分词** - **在 ES 的 Mapping Type 中 keyword, date, integer, long, double, boolean or ip 这些类型不分词,只有****text 类型分词。** **因为默认的分词引擎对中文分词采用的是单字分词 所以会检索不到数据** ``` GET /product/_search { "query": { "term": { "remark": { "value": "北京市通州区北苑街道果园西小区" } } } } { "took": 0, "timed_out": false, "_shards": { "total": 3, "successful": 3, "skipped": 0, "failed": 0 }, "hits": { "total": { "value": 0, "relation": "eq" }, "max_score": null, "hits": [] } } ``` **根据上面的解释,只有 TEXT 的类型进行分词** **使用 Term 是精确匹配,也就必须相等,那么我们查询一下 product 的 mapping 映射看看 remark 是什么类型的** **从结果可以看出是 Text 类型的,也就是说系统对这个字段进行了分词,从而导致我们无法使用 Term 查询到这条数据** ``` GET /product/_mapping { "product": { "mappings": { "properties": { "age": { "type": "long" }, "name": { "type": "text", "fields": { "keyword": { "type": "keyword", "ignore_above": 256 } } }, ** "remark": {** ** "type": "text",** ** "fields": {** ** "keyword": {** ** "type": "keyword",** ** "ignore_above": 256** ** }** ** }** ** },** "sex": { "type": "long" } } } } } ``` 官方为了解决这个问题,又提供了 keyword 的方式来进行查询 ### 使用 field.keyword 进行查询 ``` GET /product/_search { "query": { "term": { ** "remark.keyword": {** "value": "北京市通州区北苑街道果园西小区" } } } } { "took": 0, "timed_out": false, "_shards": { "total": 3, "successful": 3, "skipped": 0, "failed" : 0 }, "hits": { "total": { "value": 1, "relation": "eq" }, "max_score": 0.2876821, "hits": [ { "_index": "product", "_type": "_doc", "_id": "5", "_score": 0.2876821, "_source": { "name": "陈荣", "sex": 1, "age": 28, "remark": "北京市通州区北苑街道果园西小区" } } ] } } ``` ### Term 中大小写问题 在 ES 中,Term 查询,对输入不做分词。会将输入作为一个整体,在倒排索引中查找准确的词项,并且使用相关度算分公式为每个包含该词项的文档进行相关度算分。 **创建测试数据** ``` PUT /product/_doc/10 { "name": "iPhone", "sex": 1, "age": 21, "remark": "this is iPhone" } PUT /product/_doc/11 { "name": "iPad", "sex": 1, "age": 21, "remark": "this is iPad" } ``` **使用 Term 查询数据** ``` GET /product/_search { "query":{ "term":{ "name":{ "value":"iPhone" } } } } { "took": 0, "timed_out": false, "_shards": { "total": 3, "successful": 3, "skipped": 0, "failed": 0 }, "hits": { "total": { "value": 0, "relation": "eq" }, "max_score": null, "hits": [] } } ``` 通过以上的例子会发现问题,明明有 iphone 的数据,为什么查不出来呢? 我们通过分词器来分析 iphone 分词后长什么样 ``` POST /_analyze { "analyzer":"standard", "text":"iPhone" } { "tokens": [ { "token": "iphone", "start_offset": 0, "end_offset": 6, "type": "<ALPHANUM>", "position": 0 } ] } ``` 此时会发现分词器把字母 P 转为小写了,这就是为什么明明有数据却匹配不上的原因 **解决方案** ``` # 使用小写查询 GET /product/_search { "query":{ "term":{ "name":{ "value":"iphone" } } } } # 使用Keyword 查询 GET /product/_search { "query":{ "term":{ "name.keyword":{ "value":"iPhone" } } } } { "took": 0, "timed_out": false, "_shards": { "total": 3, "successful": 3, "skipped": 0, "failed": 0 }, "hits": { "total": { "value": 1, "relation": "eq" }, ** "max_score": 1.2039728,** "hits": [ { "_index": "product", "_type": "_doc", "_id": "10", "_score": 1.2039728, "_source": { "name": "iPhone", "sex": 2, "age": 21, "remark": "this is iPhone" } } ] } } ``` ### Constant score 算分字段 **max_score** 我们使用 Term 基本上就是为了精确匹配,而精确匹配往往是不需要算分的, 对大量数据的精确检索完全可以避免算分带来的不必要的性能损耗 可以通过 Constant score 将查询转换成一个 Filtering,避免算分,并利用缓存,提高性能 - Query 转成 Filter,忽略 TF-IDF 计算,避免相关性算分的开销· - Filter 可以有效利用缓存 ``` GET /product/_search { "query":{ "constant_score":{ "filter":{ "term":{ "name.keyword":"iPhone" } } } } } ``` **应用场景:对 bool,日期,数字,结构化的文本可以使用 term 做精确匹配,****基础条件不算分场景** ### Term 处理多值字段,Term 查询是包含不是等于 **测试数据** ``` PUT /product/_doc/20 { "name":"小明", "tags":[ "跑步", "篮球" ] } PUT /product/_doc/21 { "name":"小红", "tags":[ "跳舞", "画画" ] } PUT /product/_doc/22 { "name":"小丽", "tags":[ "跳舞", "唱歌", "跑步" ] } ``` **数据查询** ``` GET /product/_search { "query": { "term": { "tags.keyword": { "value": "跑步" } } } } { "took": 952, "timed_out": false, "_shards": { "total": 3, "successful": 3, "skipped": 0, "failed": 0 }, "hits": { "total": { "value": 2, "relation": "eq" }, "max_score": 0.8713851, "hits": [ { "_index": "product", "_type": "_doc", "_id": "20", "_score": 0.8713851, "_source": { "name": "小明", "tags": [ "跑步", "篮球" ] } }, { "_index": "product", "_type": "_doc", "_id": "22", "_score": 0.39556286, "_source": { "name": "小丽", "tags": [ "跳舞", "唱歌", "跑步" ] } } ] } } ``` ## 前缀检索 prefix - **它会对分词后的****term****进行前缀搜索。** - 它不会分析要搜索字符串,传入的前缀就是想要查找的前缀 - 默认状态下,前缀查询不做相关度分数计算,它只是将所有匹配的文档返回,然后赋予所有相关分数值为 1。 - 它的行为更像是一个过滤器而不是查询。两者实际的区别就是过滤器是可以被缓存的,而前缀查询不行。 - **prefix 的原理:需要遍历所有倒排索引,并比较每个 term 是否已所指定的前缀开头。(不建议使用)** - **这里的前缀是指分词后词的前缀,而不是整个文本的前缀** ``` GET /product/_search { "query":{ "prefix":{ "remark":"通州" } } } { "took": 0, "timed_out": false, "_shards": { "total": 3, "successful": 3, "skipped": 0, "failed": 0 }, "hits": { "total": { "value": 1, "relation": "eq" }, "max_score": 1, "hits": [ { "_index": "product", "_type": "_doc", "_id": "5", "_score": 1, "_source": { "name": "陈荣", "sex": 1, "age": 28, "remark": "北京市通州区北苑街道果园西小区" } } ] } } **# 非字段值前缀** { "query":{ "prefix":{ "remark":"北京市通州区" } } } **# 没有数据** { "took": 0, "timed_out": false, "_shards": { "total": 3, "successful": 3, "skipped": 0, "failed": 0 }, "hits": { "total": { "value": 0, "relation": "eq" }, "max_score": null, "hits": [] } } ``` ## 通配符查询 wildcard 通配符查询:工作原理和 prefix 相同,只不过它只是比较开头,它能支持更为复杂的匹配模式。 **匹配的也是 term,而不是 field** **注意这里的通配符查询同样也是针对分词之后的词语的,并不是针对字段值** ``` GET /product/_search { "query":{ "wildcard":{ "remark":{ "value":"*通*" } } } } { "took": 1, "timed_out": false, "_shards": { "total": 3, "successful": 3, "skipped": 0, "failed": 0 }, "hits": { "total": { "value": 1, "relation": "eq" }, "max_score": 1, "hits": [ { "_index": "product", "_type": "_doc", "_id": "5", "_score": 1, "_source": { "name": "陈荣", "sex": 1, "age": 28, "remark": "北京市通州区北苑街道果园西小区" } } ] } } ``` **如果字段值为数组时,匹配的时候就需要加.keyword** **tags:["a", "b", "c"]** **tags.keyword:"a"** ## 正则匹配 regexp **正则在实际项目中使用较少**  ## 范围查询 range Range:范围关键字 - Gte 大于等于 - Let 小于等于 - GT 大于 - LT 小于 - NOW 当前时间 ``` GET /product/_search { "query":{ "range":{ "age":{ "gte":28, "lte":30 } } } } ``` ### 日期 range **测试数据** ``` PUT /product/_doc/30 { "name":"range-30", "date":"2022-09-15" } PUT /product/_doc/31 { "name":"range-31", "date":"2021-09-15" } PUT /product/_doc/32 { "name":"range-32", "date":"2020-09-15" } ``` **测试查询** ``` GET /product/_search { "query":{ "range":{ "date":{ "gte":"now-2y" } } } } { "took": 225, "timed_out": false, "_shards": { "total": 3, "successful": 3, "skipped": 0, "failed": 0 }, "hits": { "total": { "value": 2, "relation": "eq" }, "max_score": 1, "hits": [ { "_index": "product", "_type": "_doc", "_id": "30", "_score": 1, "_source": { "name": "range-30", "date": "2022-09-15" } }, { "_index": "product", "_type": "_doc", "_id": "31", "_score": 1, "_source": { "name": "range-31", "date": "2021-09-15" } } ] } } ``` ## 多 id 查询 ``` GET /product/_search { "query":{ **"ids":{** ** "values":[1,11,21,31]** ** }** } } ``` ## 模糊查询 fuzzy (错别字,容错) 在实际的搜索中,我们有时候会打错字,从而导致搜索。在 Elasticsearch 中,我们可以使用 Fuzziness 属性来进行模糊查询,从而达到搜索有错别字的情形。 Fuzzy 查询会用到两个很重要的参数,fuzziness,prefix_length - **Fuzziness:表示输入的关键字通过几次操作可以转变成 ES 库里面的对应 field 的字段** - 操作是指:新增一个字符,删除一个字符,修改一个字符,每次操作可以记做编辑距离为 1 - 如中文集团到中威集团编辑距离就是 1,只需要修改一个字符 - 该参数默认值为 0,既不开启模糊查询。 - 如果 fuzziness 值在这里设置成 2,会把编辑距离为 2 的京东集团也查出来, - **prefix_length:表示限制输入关键字和 ES 对应查询 field 的内容开头的第 n 个字符必须完全匹配,不允许错别字匹配** - 这里等于 1,则表示开头的字必须匹配则不返回 - 默认值也是 0 - 加大 prefix_length 的值可以提高效率和准确率 - **Fuzzy 与 macth 都可以使用 fuzziness 参数** - **她们的区别在于,fuzzy 对查询的条件是不分词的,而 match 是分词的** ``` GET /product/_search { "query":{ "fuzzy":{ "remark":{ "value":"北静市", "fuzziness":1 } } } } { "took": 10, "timed_out": false, "_shards": { "total": 3, "successful": 3, "skipped": 0, "failed": 0 }, "hits": { "total": { "value": 1, "relation": "eq" }, "max_score": 0.19178805, "hits": [ { "_index": "product", "_type": "_doc", "_id": "5", "_score": 0.19178805, "_source": { "name": "陈荣", "sex": 1, "age": 28, "remark": "北京市通州区北苑街道果园西小区" } } ] } } ``` ``` GET /product/_search { "query":{ "fuzzy":{ "remark":{ "value":"北静市", "fuzziness":1, ** "prefix_length":2** } } } } { "took": 0, "timed_out": false, "_shards": { "total": 3, "successful": 3, "skipped": 0, "failed": 0 }, "hits": { "total": { "value": 0, "relation": "eq" }, "max_score": null, "hits": [] } } ``` **注意:fuzzy 模糊查询,最大模糊错误必须在 0-2 之间** - **搜索关键词长度为 2,不允许存在模糊** - **搜索关键词长度为 3-5,允许 1 次模糊** - **搜索关键词长度大于 5,允许最大 2 次模糊** ## 搜索推荐:suggester ### 概述 搜索一般都会要求具有“搜索推荐”或者叫“搜索补全”的功能,即在用户输入搜索的过程中,进行自动补全或者纠错。以此来提高搜索文档的匹配精准度,进而提升用户的搜索体验,这就是 Suggest。  ### **ES 针对不同场景,把 Suggeter 主要分为以下四种** - Term Suggester - Phrase Suggester - Completion Suggester 主要学习 - Context Suggester ### 测试数据准备 ``` DELETE /blogs PUT /blogs/ { "mappings": { "properties": { "body": { "type": "text" } } } } POST _bulk/?refresh=true {"index":{"_index":"blogs","_type":"_doc"}} {"body":"Lucene is cool"} {"index":{"_index":"blogs","_type":"_doc"}} {"body":"Elasticsearch builds on top of lucene"} {"index":{"_index":"blogs","_type":"_doc"}} {"body":"Elasticsearch rocks"} {"index":{"_index":"blogs","_type":"_doc"}} {"body":"Elastic is the company behind ELK stack"} {"index":{"_index":"blogs","_type":"_doc"}} {"body":"elk rocks"} {"index":{"_index":"blogs","_type":"_doc"}} {"body":"elasticsearch is rock solid"} GET /blogs/_search ``` ### Term Suggester term suggester 正如其名,只基于 tokenizer 之后的单个 term 去匹配建议词,并不会考虑多个 term 之间的关系 ``` POST <index>/_search { "suggest": { "<suggest_name>": { "text": "<search_content>", "term": { "suggest_mode": "<suggest_mode>", "field": "<field_name>" } } } } ``` #### 可选参数 - text:用户搜索的文本 - field:要从哪个字段选取推荐数据 - analyzer:使用哪种分词器 - size:每个建议返回的最大结果数 - sort:如何按照提示词项排序,参数值只可以是以下两个枚举: - score:分数 > 词频 > 词项本身 - frequency:词频 > 分数 > 词项本身 - suggest_mode:搜索推荐的推荐模式,参数值亦是枚举: - missing:默认值,仅为不在索引中的词项生成建议词 - popular:仅返回与搜索词文档词频或文档词频更高的建议词 - always:根据 建议文本中的词项 推荐 任何匹配的建议词 - max_edits:可以具有最大偏移距离候选建议以便被认为是建议。只能是 1 到 2 之间的值。任何其他值都将导致引发错误的请求错误。默认为 2 - prefix_length:前缀匹配的时候,必须满足的最少字符 - min_word_length:最少包含的单词数量 - **min_doc_freq**:最少的文档频率 - max_term_freq:最大的词频 #### 使用 Term Suggester 搜索:lucne , rock 返回:**lucene, [ ]** **"freq" : 2 指的是文档的个数** **Options: 推荐结果** ``` GET /blogs/_search { ** "suggest": {** "my_test": { "text": "lucne rock", ** "term": {** ** "suggest_mode":"missing",** ** "field": "body"** ** }** } } } { "took" : 0, "timed_out" : false, "_shards" : { "total" : 1, "successful" : 1, "skipped" : 0, "failed" : 0 }, "hits" : { "total" : { "value" : 0, "relation" : "eq" }, "max_score" : null, "hits" : [ ] }, "suggest" : { "my_test" : [ { "text" : "lucne", "offset" : 0, "length" : 5, ** "options" : [** ** {** ** "text" : "lucene",** ** "score" : 0.8,** ** "freq" : 2** ** }** ** ]** }, ** {** ** "text" : "rock",** ** "offset" : 6,** ** "length" : 4,** ** "options" : [ ]** ** }** ] } } ``` ### phrase suggester: phrase suggester 和 term suggester 相比,对建议的文本会参考上下文,也就是一个句子的其他 token,不只是单纯的 token 距离匹配,它可以基于共生和频率选出更好的建议。 **注意:匹配出来的 options 中的参考值,并不一定实际存在于文档中,这些数据只是系统自动推荐给我们的短语** #### 可选参数 - real_word_error_likelihood: 此选项的默认值为 0.95。此选项告诉 Elasticsearch 索引中 5% 的术语拼写错误。这意味着随着这个参数的值越来越低,Elasticsearch 会将越来越多存在于索引中的术语视为拼写错误,即使它们是正确的 - max_errors:为了形成更正,最多被认为是拼写错误的术语的最大百分比。默认值为 1 - confidence:默认值为 1.0,最大值也是。该值充当与建议分数相关的阈值。只有得分超过此值的建议才会显示。例如,置信度为 1.0 只会返回得分高于输入短语的建议 - collate:告诉 Elasticsearch 根据指定的查询检查每个建议,以修剪索引中不存在匹配文档的建议。在这种情况下,它是一个匹配查询。由于此查询是模板查询,因此搜索查询是当前建议,位于查询中的参数下。可以在查询下的“params”对象中添加更多字段。同样,当参数“prune”设置为 true 时,我们将在响应中增加一个字段“collate_match”,指示建议结果中是否存在所有更正关键字的匹配 - direct_generator:phrase suggester 使用候选生成器生成给定文本中每个项可能的项的列表。单个候选生成器类似于为文本中的每个单独的调用 term suggester。生成器的输出随后与建议候选项中的候选项结合打分。目前只支持一种候选生成器,即 direct_generator。建议 API 接受密钥直接生成器下的生成器列表;列表中的每个生成器都按原始文本中的每个项调用。 #### 官方文档 ### Completion Suggester - 基于内存而非索引,性能强悍 - 需要结合特定的 Completion 类型 - 只适合前缀推荐 自动补全,自动完成,支持三种查询【前缀查询(prefix)模糊查询(fuzzy)正则表达式查询(regex)】 ,主要针对的应用场景就是"Auto Completion"。 此场景下用户每输入一个字符的时候,就需要即时发送一次查询请求到后端查找匹配项,在用户输入速度较高的情况下对后端响应速度要求比较苛刻。 因此实现上它和前面两个 Suggester 采用了不同的数据结构,索引并非通过倒排来完成,而是将 analyze 过的数据编码成 FST 和索引一起存放。 对于一个 open 状态的索引,FST 会被 ES 整个装载到内存里的,进行前缀查找速度极快。 但是 FST 只能用于前缀查找,这也是 Completion Suggester 的局限所在。 - completion:es 的一种特有类型,专门为 suggest 提供,基于内存,性能很高。 - prefix query:基于前缀查询的搜索提示,是最常用的一种搜索推荐查询。 - prefix:客户端搜索词 - field:建议词字段 - size:需要返回的建议词数量(默认 5) - skip_duplicates:是否过滤掉重复建议,默认 false - fuzzy query - fuzziness:允许的偏移量,默认 auto - transpositions:如果设置为 true,则换位计为一次更改而不是两次更改,默认为 true。 - min_length:返回模糊建议之前的最小输入长度,默认 3 - prefix_length:输入的最小长度(不检查模糊替代项)默认为 1 - unicode_aware:如果为 true,则所有度量(如模糊编辑距离,换位和长度)均以 Unicode 代码点而不是以字节为单位。这比原始字节略慢,因此默认情况下将其设置为 false。 - regex query:可以用正则表示前缀,不建议使用 #### 初始化索引与数据 ``` DELETE suggest_carinfo PUT suggest_carinfo { "mappings": { "properties": { "title": { "type": "text", "analyzer": "ik_max_word", "fields": { "suggest": { "type": "completion", "analyzer": "ik_max_word" } } }, "content": { "type": "text", "analyzer": "ik_max_word" } } } } POST _bulk {"index":{"_index":"suggest_carinfo","_id":1}} {"title":"宝马X5 两万公里准新车","content":"这里是宝马X5图文描述"} {"index":{"_index":"suggest_carinfo","_id":2}} {"title":"宝马5系","content":"这里是奥迪A6图文描述"} {"index":{"_index":"suggest_carinfo","_id":3}} {"title":"宝马3系","content":"这里是奔驰图文描述"} {"index":{"_index":"suggest_carinfo","_id":4}} {"title":"奥迪Q5 两万公里准新车","content":"这里是宝马X5图文描述"} {"index":{"_index":"suggest_carinfo","_id":5}} {"title":"奥迪A6 无敌车况","content":"这里是奥迪A6图文描述"} {"index":{"_index":"suggest_carinfo","_id":6}} {"title":"奥迪双钻","content":"这里是奔驰图文描述"} {"index":{"_index":"suggest_carinfo","_id":7}} {"title":"奔驰AMG 两万公里准新车","content":"这里是宝马X5图文描述"} {"index":{"_index":"suggest_carinfo","_id":8}} {"title":"奔驰大G 无敌车况","content":"这里是奥迪A6图文描述"} {"index":{"_index":"suggest_carinfo","_id":9}} {"title":"奔驰C260","content":"这里是奔驰图文描述"} GET /suggest_carinfo/_search ``` #### 使用示例 ``` GET suggest_carinfo/_search?pretty { "suggest": { "car_suggest" : { "prefix" : "宝马X5", "completion" : { "field" : "title.suggest" } } } } ``` #### 缺点 **1:内存代价太大,原话是:性能高是通过大量的内存换来的** **2:只能前缀搜索,假如用户输入的不是前缀 召回率可能很低** ``` # **skip_duplicates 是否过滤掉重复建议** **# fuzziness:允许的偏移量,默认auto** POST suggest_carinfo/_search { "suggest": { "car_suggest": { "prefix": "宝马5系", "completion": { "field": "title.suggest", ** "skip_duplicates":true,** ** "fuzzy": {** ** "fuzziness": 2** ** }** } } } } GET suggest_carinfo/_doc/10 GET _analyze { "analyzer": "ik_max_word", "text": ["奔驰AMG 两万公里准新车"] } POST suggest_carinfo/_search { "suggest": { "car_suggest": { "regex": "[\\s\\S]*", "completion": { "field": "title.suggest", "size": 10 } } } } ``` ### context suggester **官方文档** 完成建议者会考虑索引中的所有文档,但是通常来说,我们在进行智能推荐的时候最好通过某些条件过滤,并且有可能会针对某些特性提升权重。 - contexts:上下文对象,可以定义多个 - name:context 的名字,用于区分同一个索引中不同的 context 对象。需要在查询的时候指定当前 name - type: - context 对象的类型,目前支持两种:category 和 geo,分别用于对 suggest item 分类和指定地理位置。 - boost:权重值,用于提升排名 - Precision :对地理位置的精确度的描述,1~12,具体看文档 - path:如果没有 path,相当于在 PUT 数据的时候需要指定 context.name 字段,如果在 Mapping 中指定了 path,在 PUT 数据的时候就不需要了,因为 Mapping 是一次性的,而 PUT 数据是频繁操作,这样就简化了代码。 **Context 可以理解在 completion 之上的,completion 推荐的数据可能在文档中并不实际存在,而 context 可以指定索引的数据来源,更能提高推荐数据的准确性** #### **创建索引及数据** ``` PUT place { "mappings": { "properties": { "suggest": { ** "type": "completion",** "contexts": [ { ** "name": "place_type",** ** "type": "category"** }, { ** "name": "location",** ** "type": "geo",** ** "precision": 4** } ] } } } } ``` #### 添加数据 ``` PUT place/_doc/1 { "suggest": { "input": [ "timmy's", "starbucks", "dunkin donuts" ], "contexts": { "place_type": [ "cafe", "food" ] } } } # 对suggest这个字段新增了input【 推荐值】 # 同时对这些推荐值设置了两个分类【"place_type": [ "cafe", "food" ] 】 # 注意 place_type 是来源于上面mapping中定义好的, **# 这样做的好处是,当我们在检索时,在条件添加上分类,可以跟垂直的搜索到推荐数据** PUT place/_doc/2 { "suggest": { "input": [ "monkey", "timmy's", "lamborghini" ], "contexts": { "place_type": [ "money" ] } } } 注意,timmy's 添加了两次,但是她们所属不同分类,后面测试要用 ``` ``` GET /place/_search { "took" : 250, "timed_out" : false, "_shards" : { "total" : 1, "successful" : 1, "skipped" : 0, "failed" : 0 }, "hits" : { "total" : { "value" : 2, "relation" : "eq" }, "max_score" : 1.0, "hits" : [ { "_index" : "place", "_type" : "_doc", "_id" : "2", "_score" : 1.0, "_source" : { "suggest" : { ** "input" : [** ** "monkey",** ** "timmy's",** ** "lamborghini"** ** ],** ** "contexts" : {** ** "place_type" : [** ** "money"** ** ]** ** }** } } }, { "_index" : "place", "_type" : "_doc", "_id" : "1", "_score" : 1.0, "_source" : { "suggest" : { ** "input" : [** ** "timmy's",** ** "starbucks",** ** "dunkin donuts"** ** ],** ** "contexts" : {** ** "place_type" : [** ** "cafe",** ** "food"** ** ]** ** }** } } } ] } } ``` #### 使用检索 ``` GET /place/_search { "suggest": { "place_suggestion": { "prefix":"sta", ** "completion":{** ** "field":"suggest",** ** "size":10,** ** "contexts":{** ** "place_type":["cafe", "restaurants"]** ** }** } } } } # 使用suggest的前缀检索 # **completion 使用completion检索** # **completion.field 检索的字段** # **contexts 指定检索方式** # **contexts.place_type":["cafe", "restaurants"] ** **# 通过place_type自定义方式检索,检索的分类是["cafe", "restaurants"]** { "took" : 10, "timed_out" : false, "_shards" : { "total" : 1, "successful" : 1, "skipped" : 0, "failed" : 0 }, "hits" : { "total" : { "value" : 0, "relation" : "eq" }, "max_score" : null, "hits" : [ ] }, "suggest" : { "place_suggestion" : [ { "text" : "sta", "offset" : 0, "length" : 3, "options" : [ { "text" : "starbucks", "_index" : "place", "_type" : "_doc", "_id" : "1", "_score" : 1.0, "_source" : { "suggest" : { "input" : [ "timmy s", "starbucks", "dunkin donuts" ], "contexts" : { "place_type" : [ "cafe", "food" ] } } }, "contexts" : { "place_type" : [ "cafe" ] } } ] } ] } } ``` #### 指定多个分类,并且使用 boost ``` POST /place/_search { "suggest": { "place_suggesting": { "prefix": "tim", "completion": { "field": "suggest", "contexts": { "place_type": [ { "context": "cafe" }, { "context": "money", "boost": 2 } ] } } } } } **# 使用两个分类,指定boost参数用于提高权限** ``` ``` { "took" : 0, "timed_out" : false, "_shards" : { "total" : 1, "successful" : 1, "skipped" : 0, "failed" : 0 }, "hits" : { "total" : { "value" : 0, "relation" : "eq" }, "max_score" : null, "hits" : [ ] }, "suggest" : { "place_suggesting" : [ { "text" : "tim", "offset" : 0, "length" : 3, "options" : [ { ** "text" : "timmy's",** "_index" : "place", "_type" : "_doc", "_id" : "2", "_score" : 2.0, "_source" : { ** "suggest" : {** ** "input" : [** ** "monkey",** ** "timmy's",** ** "lamborghini"** ** ],** ** "contexts" : {** ** "place_type" : [** ** "money"** ** ]** ** }** ** }** }, "contexts" : { "place_type" : [ "money" ] } }, { "text" : "timmy's", "_index" : "place", "_type" : "_doc", "_id" : "1", "_score" : 1.0, "_source" : { "suggest" : { "input" : [ "timmy's", "starbucks", "dunkin donuts" ], "contexts" : { "place_type" : [ "cafe", "food" ] } } }, "contexts" : { "place_type" : [ "cafe" ] } } ] } ] } } ``` #### **地理位置筛选器** 通过 location 匹配推荐词 ``` PUT place/_doc/3 { "suggest": { "input": "timmy's", "contexts": { "location": [ { "lat": 43.6624803, "lon": -79.3863353 }, { "lat": 43.6624718, "lon": -79.3873227 } ] } } } ``` ``` POST place/_search { "suggest": { "place_suggestion": { ** "prefix": "tim",** "completion": { "field": "suggest", "size": 10, "contexts": { "**location**": { "lat": 43.662, "lon": -79.380 } } } } } } # 搜索时指定了location 检索 { "took" : 270, "timed_out" : false, "_shards" : { "total" : 1, "successful" : 1, "skipped" : 0, "failed" : 0 }, "hits" : { "total" : { "value" : 0, "relation" : "eq" }, "max_score" : null, "hits" : [ ] }, "suggest" : { "place_suggestion" : [ { "text" : "tim", "offset" : 0, "length" : 3, "options" : [ { "text" : "timmy's", "_index" : "place", "_type" : "_doc", "_id" : "3", "_score" : 1.0, "_source" : { "suggest" : { ** "input" : "timmy's",** "contexts" : { "location" : [ { ** "lat" : 43.6624803,** ** "lon" : -79.3863353** }, { "lat" : 43.6624718, "lon" : -79.3873227 } ] } } }, "contexts" : { ** "location" : [** ** "dpz8"** ** ]** } } ] } ] } } ``` #### 使用 path 简化 创建与搜索 ##### 创建索引 ``` DELETE /place_path_category PUT /place_path_category { "mappings": { "properties": { "suggest": { "type": "completion", "contexts": [ { "name": "place_type", "type": "category", ** "path": "cat"** }, { "name": "location", "type": "geo", "precision": 4, ** "path": "loc"** } ] }, "loc": { "type": "geo_point" } } } } ```  ##### 添加数据时可以通过 path 路径进行添加 ``` PUT /place_path_category/_doc/1 { "suggest": [ "timmy's", "starbucks", "dunkin donuts" ], ** "cat": [** "cafe", "food" ] } ```  ##### 使用检索 与 没有添加 path 时一样 ``` GET /place_path_category/_search { "suggest": { "place_path_search": { "prefix": "tim", "completion": { "field": "suggest", "contexts": { "place_type": [ { "context": "cafe" } ] } } } } } ``` ## **高亮查询 highlight** highlight 关键字:可以让符合条件的文档中的关键词高亮 highlight 相关属性 - pre_tags 前缀标签 - Post_tags 后缀标签 - tags_schema 设置为 styled 可以使用内置高亮样式 - require_field_match 多字段高亮需要设置为 false **示例数据** ``` # 指定IK分词器 PUT /product { "settings":{ "number_of_shards": 3, "number_of_replicas": 2, "index":{ "analysis.analyzer.default.type": "ik_max_word" } } } # 准备测试数据 PUT /product/_doc/41 { "id":"41", "name":"牛仔裤男", "desc":"牛仔裤男春秋款直筒宽松2022年新款大码秋季男士秋冬款休闲长裤子", "timestamp": 1663244286, "createTime":"2022-09-15 20:18:05" } PUT /product/_doc/42 { "id":"42", "name":"牛仔外套", "desc":"英爵伦牛仔外套男春秋款潮牌宽松韩版黑色工装牛仔衣秋季夹克上衣", "timestamp": 1663244286, "createTime":"2022-09-15 20:18:05" } ``` ### 单字段高亮 ``` GET /product/_search { "query":{ "term":{ "name":{ "value":"牛仔" } } }, "highlight":{ "fields":{ "*":{} } } } { "took": 29, "timed_out": false, "_shards": { "total": 3, "successful": 3, "skipped": 0, "failed": 0 }, "hits": { "total": { "value": 2, "relation": "eq" }, "max_score": 0.37680492, "hits": [ { "_index": "product", "_type": "_doc", "_id": "42", "_score": 0.37680492, "_source": { "id": "42", "name": "牛仔外套", "desc": "英爵伦牛仔外套男春秋款潮牌宽松韩版黑色工装牛仔衣秋季夹克上衣", "timestamp": 1653214286, "createTime": "2022-09-15 20:18:05" }, "highlight": { ** "name": [** ** "<em>牛仔</em>外套"** ** ]** } }, { "_index": "product", "_type": "_doc", "_id": "41", "_score": 0.28637174, "_source": { "id": "41", "name": "牛仔裤男", "desc": "牛仔裤男春秋款直筒宽松2022年新款大码秋季男士秋冬款休闲长裤子", "timestamp": 1663244286, "createTime": "2022-09-15 20:18:05" }, ** "highlight": {** ** "name": [** ** "<em>牛仔</em>裤男"** ** ]** ** }** } ] } } ``` ### 多字段高亮 **require_field_match **是否单字段匹配 **fields**:匹配字段 ``` GET /product/_search { "query":{ "term":{ "name":{ "value":"牛仔" } } }, "highlight":{ "pre_tags":["<font color='red'>"], "post_tags":["</font>"], "require_field_match": false, "fields":{ "name":{}, "desc":{} } } } { "took": 1, "timed_out": false, "_shards": { "total": 3, "successful": 3, "skipped": 0, "failed": 0 }, "hits": { "total": { "value": 2, "relation": "eq" }, "max_score": 0.7549127, "hits": [ { "_index": "product", "_type": "_doc", "_id": "42", "_score": 0.7549127, "_source": { "id": "42", "name": "牛仔外套", "desc": "英爵伦牛仔外套男春秋款潮牌宽松韩版黑色工装牛仔衣秋季夹克上衣", "timestamp": 1653214286, "createTime": "2022-09-15 20:18:05" }, ** "highlight": {** ** "name": [** ** "<font color='red'>牛仔</font>外套"** ** ],** ** "desc": [** ** "英爵伦<font color='red'>牛仔</font>外套男春秋款潮牌宽松韩版黑色工装<font color='red'>牛仔</font>衣秋季夹克上衣"** ** ]** ** }** }, { "_index": "product", "_type": "_doc", "_id": "41", "_score": 0.55654144, "_source": { "id": "41", "name": "牛仔裤男", "desc": "牛仔裤男春秋款直筒宽松2022年新款大码秋季男士秋冬款休闲长裤子", "timestamp": 1663244286, "createTime": "2022-09-15 20:18:05" }, ** "highlight": {** ** "name": [** ** "<font color='red'>牛仔</font>裤男"** ** ],** ** "desc": [** ** "<font color='red'>牛仔</font>裤男春秋款直筒宽松2022年新款大码秋季男士秋冬款休闲长裤子"** ** ]** ** }** } ] } } ``` ## 相关性和相关性算分 搜索是用户和搜索引擎的对话,用户关心的是搜索结果的相关性 - 是否可以找到所有相关的内容 - 有多少不相关的内容被返回了 - 文档的打分是否合理 - 结合业务需求,平衡结果排名 **如何衡量相关性:** - Precision(查准率)尽可能返回较少的无关文档 - Recall(查全率) 尽量返回较多的相关文档 - Ranking 是否能够按照相关度进行排序 ### 相关性( Relevance) 搜索的相关性算分,描述了一个文档和查询语句匹配的程度。ES 会对每个匹配查询条件的结果进行算分_score。 打分的本质是排序,需要把最符合用户需求的文档排在前面。ES5 之前,默认的相关性算分采用 TF-IDF,现在采用 BM 25。  - "[区块链](https://so.csdn.net/so/search?q=%E5%8C%BA%E5%9D%97%E9%93%BE&spm=1001.2101.3001.7020)"在文档 1,2,3 中出现 - "的"在文档 2,3,4,5..... - "应用"在文档 2,3,4...... ### 什么是 TF-IDF TF-IDF( term frequency-inverse document frequency)是一种用于信息检索与数据挖掘的常用加权技术。 TF-IDF 被公认为是信息检索领域最重要的发明,除了在信息检索,在文献分类和其他相关领域有着非常广泛的应用 TF-IDF 的概念,最早是剑桥大学的斯巴克琼斯"提出 - 1972 年一一“关键词恃殊性的统计解释和它在文献检索中的应用”但是没有从理论上解释 TF-IDF 应该是用 log 全部文档数/检素词现过的文档总数),而不是其他函数,也没有做进一步的研究 - 1970,1980 年代萨尔顿和罗宾逊,进行了进一步的证明和研究,并用香农信息论做了证明 [https://www.staff.city.ac.uk/~sb317/papers/foundatins_bm25_review.pdf](https://links.jianshu.com/go?to=https%3A%2F%2Fwww.staff.city.ac.uk%2F%7Esb317%2Fpapers%2Ffoundatins_bm25_review.pdf) - 现代搜索引擎,对 TFDF 进行了大量细微的优化 ### Lucene 中的 TF-IDF 评分公式  - **TF 表示词频(Term Frequency )** - 检索词在文档中出现的频率越高,相关性也就越高 - Term Frequency:检索词在一篇文章中出现的概率 - 检索词出现的次数除以文档的总字数(文档的字数长短有关) - 度量一条查询和结果文档相关性的简单方法:简单将搜索中的每一个词的 TF 进行相加 - TF(区块链) + TF(的)+ TF(应用) - Stop Word - "的"在文档中出现了很多次,但是对于贡献相关度几乎没有什么用处,不应该考虑他们的 TF - **IDF 是逆向文本频率(Inverse Document Frequency)** - 每个检索词在索引中出现的频率,频率越高,相关性越低 - IDF:检索词在所有文档中出现的概率 - "区块链"在相对比较少的文档中出现 - "应用"在相对比较多的文档中出现 - "Stop Word"在大量的文档中出现 - IDF:简单说=log(全部文档数/检索词出现过的文档总数) - TF-IDF 本质上就是**将 TF 求和变成了加权求和**  - **字段长度归一值( field - length norm)** - 字段的长度是多少,字段越短,字段的权重越高。检索词出现在一个内容短的 title 要比同样的词出现在一个内容长的 content 字段权重更大。 以上三个因素——词频(Term Frequency )、逆向文档频率 (Inverse Document Frequency)和字段长度归一值( field - length norm) ——是在索引计算时并存储的,最后将它们结合在一起计算单个词在特定文档中的权重。 ### 什么是 BM 25 - 从 ES 5 开始,默认算法改为 BM 25 - 和经典的 TF-IDF 相比,当 TF 无限增加时,BM 25 算分会趋于一个数值  **BM 25 公式**  ### 通过 Explain API 查看 TF-IDF 示例: ``` # 添加测试数据 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":"The scoring of documents is caculated by the scoring formuld"} {"index":{"_id":4}} {"content":"you know, for search"} # 条件查询 GET /test_score/_search { "query":{ "match":{ "content": "elasticsearch" } } } # 查看评分过程 GET /test_score/_search { ** "explain": true, ** "query":{ "match":{ "content": "elasticsearch" } } } ``` ## Boosting Relevance **Boosting 是控制相关度的一种手段** - 在索引,字段或者查询子条件中都可以设置 **参数 boost 的含义** - 当 boost >1 时,打分的相关度算分相对性提升 - 0< 当 boost <1 时,打分的相关度算分相对性降低 - 当 boost<0 时,贡献负分 ### 应用实例 **需求:**搜索标题中包含 java 的帖子,如果标题中包含 hadoop 或者 elasticsearch 就优先搜索出来,同时呢,如果一个帖子包含 java hadoop,一个帖子包含 java ealsticsearch,包含 hadoop 的帖子要比 elasticsearch 优先搜索出来。 **知识点:**搜索条件的权重,boost 可以将某个搜索条件的权重加大,此时如果当匹配这个搜索条件和另一个搜索条件的 document,匹配权重更大的搜索条件的 document,ralevance score 会更高,当然就会优先搜索返回出来。 默认情况下,搜索条件的权重都是一样的,都是 1 ``` GET /blogs/_search { "query": { "bool": { "must": [ { "match": { "content": "java" } } ], "should": [ { ** "****boosting****": {** ** "****positive****": {** ** "match": {** ** "content": "hadoop"** ** }** ** },** ** "****negative****": {** ** "match": {** ** "content": "elasticsearch"** ** }** ** },** ** "****negative_boost****": 0.2** ** }** } ] } } } ``` **positive 积极的 加权重条件** **negative 消极的 减权重条件** **negative_boost 消极打分的百分比(例如原来是 1 因为符合这个条件,分数为 0.2)** ## 布尔查询 Bool Query 一个 bool 查询,是一个或者多个查询子句的组合,总共包括 4 种子句,其中 2 种会影响算分,2 种不影响算分 - must 相当于 &&,必须匹配,贡献算分 - should 相当于 ||,选择性匹配,贡献算分 - must_not 相当于 !,必须不能匹配,不贡献算分 - filter 必须匹配,不贡献算分 - **minimum_should_match ** - **参数指定 should 返回的文档必须匹配的子句数量或百分比,如果 Bool 查询包含至少一个 should 子句,而没有 must 或 filter 子句,则默认值为 1,否则默认值为 0** - 一般来说,只有在查询中出现 **must 或 filter 的时候才有意义** 在 Elasticsearch 中,有 Query 和 Filter 两种不同的 Context - Query Context:相关性算分 - Filter Context:不需要算分(Yes Or No),可以利用 Cache,获得更好的性能 相关性并不只是全文本检索的专利,也适用于 yes I no 的子句,匹配的子句越多,相关性评分越高。如果多条查询子句被合并为条复合查询语句,比如 bool 查询,则每个查询子句计算得出的评分会被合并到总的相关性评分中。 **在复杂查询中,我们可以使用 must_not | filter 过滤基础条件,权重较高需要评分的 再用 must | should 这样可以提高查询性能,避免评分消耗** ### Bool 查询语法 - **子查询可以任意顺序出现** - **可以嵌套多个查询** - **如果你的 bool 查询中,没有 must 条件,should 中必须至少满足一条查询** ``` #使用must 必须匹配 GET /product/_search { "query": { "bool": { "must": [ { "match": { "name": "牛仔" } }, { "term": { "desc": { "value": "夹克" } } } ] } } } # 使用 should GET /product/_search { "query": { "bool": { "should": [ { "match": { "name": "牛仔" } }, { "term": { "desc": { "value": "裤子" } } } ] } } } # 使用 must_not GET /product/_search { "query": { "bool": { "must_not": [ { "match": { "name": "牛仔" } }, { "term": { "desc": { "value": "裤子" } } } ] } } } # 使用 filter GET /product/_search { "query": { "bool": { "filter": [ { "match": { "name": "牛仔" } }, { "term": { "desc": { "value": "裤子" } } } ] } } } # 使用 must_not + minimum_should_match GET /product/_search { "query": { "bool": { "must_not": [ { "match": { "name": "牛仔" } }, { "term": { "desc": { "value": "裤子" } } } ], "minimum_should_match":1 } } } ``` ### 如何解决结构化查询 “包含而不是相等”的问题 上面有关于 term 的查询实例,我们会发现如果字段值是个数组,我们查询的条件是字符串,它会判断这个字符串条件是否在这个数组内。 **如果遇到了,不是包含而是精确相等的场景,我们应该怎么解决呢?** **解决方案:**增加 count 字段,使用 bool 查询解决 - 从业务角度,按需改进 Elasticsearch 数据模型 ``` PUT /product/_doc/50 { "name":"小明", "tags":[ "跑步", "篮球" ], "tag_count":2 } PUT /product/_doc/51 { "name":"小红", "tags":[ "画画" ], "tag_count":1 } PUT /product/_doc/52 { "name":"小丽", "tags":[ "画画", "唱歌", "跑步" ], "tag_count":3 } # 有两个人都有画画这个标签,假如我想查询只有画画这个爱好的人 GET /product/_search { "query":{ "bool": { "filter": [ { "term": { "tags": "画画" } }, { "term": { "tag_count": "1" } } ] } } } ``` ### 利用 Bool 嵌套实现 should not 逻辑 查询 remark 存在 code,但 sex != 1 ``` { "query": { "bool": { "must": { "match": { "remark": "code" } }, "should": [ { "bool": { "must_not": { "term": { "sex": 1 } } } } ], ** "minimum_should_match":1** } } } ``` ### 控制字段的 Boosting Boosting 是控制相关度的一种手段,可以通过指定字段的 boost 值影响查询结果 **参数 boost 的含义** - 当 boost > 1 时,打分的相关度相对提升 - 当 0 < boost < 1 时,打分的相关度相对降低 - 当 boost < 0 时,贡献负分 ``` POST /product/_doc/ { "name" : "Apple iPad", "content" : "Apple iPad, Apple iPad" } POST /product/_doc/ { "name" : "Apple iPad, Apple iPad", "content" : "Apple iPad" } ``` **正常查询** ``` GET /product/_search { "query": { "bool": { "should": [ { "match": { "name": { "query": "Apple iPad", ** "boost": 1** } } }, { "match": { "content": { "query": "Apple iPad", ** "boost": 1** } } } ] } } } { "took" : 0, "timed_out" : false, "_shards" : { "total" : 3, "successful" : 3, "skipped" : 0, "failed" : 0 }, "hits" : { "total" : { "value" : 3, "relation" : "eq" }, "max_score" : 4.4630055, "hits" : [ ** {** ** "_index" : "product",** ** "_type" : "_doc",** ** "_id" : "Huk-RIMBTFk5qxxyl4PP",** ** "_score" : 4.4630055,** ** "_source" : {** ** "name" : "Apple iPad",** ** "content" : "Apple iPad, Apple iPad"** ** }** ** },** ** {** ** "_index" : "product",** ** "_type" : "_doc",** ** "_id" : "H-k-RIMBTFk5qxxymINd",** ** "_score" : 4.4210916,** ** "_source" : {** ** "name" : "Apple iPad, Apple iPad",** ** "content" : "Apple iPad"** ** }** ** },** { "_index" : "product", "_type" : "_doc", "_id" : "11", "_score" : 1.9365597, "_source" : { "name" : "iPad", "sex" : 1, "age" : 21, "remark" : "this is iPad" } } ] } } ``` **通过 boost 调整算分** ``` GET /product/_search { "query": { "bool": { "should": [ { "match": { "name": { "query": "Apple iPad", "boost": 1 } } }, { "match": { "content": { "query": "Apple iPad", "boost": 0.2 } } } ] } } } { "took" : 0, "timed_out" : false, "_shards" : { "total" : 3, "successful" : 3, "skipped" : 0, "failed" : 0 }, "hits" : { "total" : { "value" : 3, "relation" : "eq" }, "max_score" : 3.9608002, "hits" : [ { "_index" : "product", "_type" : "_doc", "_id" : "H-k-RIMBTFk5qxxymINd", "_score" : 3.9608002, "_source" : { "name" : "Apple iPad, Apple iPad", "content" : "Apple iPad" } }, { "_index" : "product", "_type" : "_doc", "_id" : "Huk-RIMBTFk5qxxyl4PP", "_score" : 3.8301048, "_source" : { "name" : "Apple iPad", "content" : "Apple iPad, Apple iPad" } }, { "_index" : "product", "_type" : "_doc", "_id" : "11", "_score" : 1.9365597, "_source" : { "name" : "iPad", "sex" : 1, "age" : 21, "remark" : "this is iPad" } } ] } } ``` ### 案例:要求苹果公司的优先展示 ``` POST /news/_bulk {"index":{"_id":70}} {"content":"Apple Mac"} {"index":{"_id":71}} {"content":"Apple iPad"} {"index":{"_id":72}} {"content":"Apple employee like Apple Pie and Apple Juice"} GET /news/_search { "query": { "bool": { "must": [ { "match": { "content": "Apple" } } ] } } } { "took" : 0, "timed_out" : false, "_shards" : { "total" : 1, "successful" : 1, "skipped" : 0, "failed" : 0 }, "hits" : { "total" : { "value" : 3, "relation" : "eq" }, "max_score" : 0.17280531, "hits" : [ { "_index" : "news", "_type" : "_doc", "_id" : "72", "_score" : 0.17280531, "_source" : { ** "content" : "Apple employee like Apple Pie and Apple Juice"** } }, { "_index" : "news", "_type" : "_doc", "_id" : "70", "_score" : 0.16786805, "_source" : { "content" : "Apple Mac" } }, { "_index" : "news", "_type" : "_doc", "_id" : "71", "_score" : 0.16786805, "_source" : { "content" : "Apple iPad" } } ] } } ``` 以上查询发现,苹果的水果果汁介绍比苹果手机内容排名靠前,主要是因为内容中的词频比较高 ### 利用 must not 排除不相关文档 ``` GET /news/_search { "query": { "bool": { "must": [ { "match": { "content": "Apple" } } ], "must_not": [ { "match": { "content": "Pie" } } ] } } } { "took" : 0, "timed_out" : false, "_shards" : { "total" : 1, "successful" : 1, "skipped" : 0, "failed" : 0 }, "hits" : { "total" : { "value" : 2, "relation" : "eq" }, "max_score" : 0.16786805, "hits" : [ { "_index" : "news", "_type" : "_doc", "_id" : "70", "_score" : 0.16786805, "_source" : { "content" : "Apple Mac" } }, { "_index" : "news", "_type" : "_doc", "_id" : "71", "_score" : 0.16786805, "_source" : { "content" : "Apple iPad" } } ] } } ``` 一般我们很少排除内容,而是希望降低这些信息的权重 ### 利用 negative_boost 降低相关性 希望包含了某项内容的结果不是不出现,而是排序靠后 negative_boost 对 negative 部分 query 生效 计算评分时,boosting 部分评分不修改,negative 部分 query 乘以 negative_boost 值 negative_boost 取值:0-1.0,举例:0.3 对某些返回结果不满意,但又不想排除掉( must_not),可以考虑 boosting query 的 negative_boost。 **返回匹配 positive 查询的文档,并降低匹配 negative 查询的文档相似度分** **应用场景:希望包含了某项内容的结果不是不出现,而是排序靠后** ``` GET /news/_search { "query":{ "boosting": { "positive": { "match": { "content": "Apple" } }, "negative": { "match": { "content": "Pie" } }, "negative_boost": 0.2 } } } { "took" : 7, "timed_out" : false, "_shards" : { "total" : 1, "successful" : 1, "skipped" : 0, "failed" : 0 }, "hits" : { "total" : { "value" : 3, "relation" : "eq" }, "max_score" : 0.16786805, "hits" : [ { "_index" : "news", "_type" : "_doc", "_id" : "70", "_score" : 0.16786805, "_source" : { "content" : "Apple Mac" } }, { "_index" : "news", "_type" : "_doc", "_id" : "71", "_score" : 0.16786805, "_source" : { "content" : "Apple iPad" } }, { "_index" : "news", "_type" : "_doc", "_id" : "72", "_score" : 0.034561064, "_source" : { "content" : "Apple employee like Apple Pie and Apple Juice" } } ] } } ``` # Elasticsearch 数据类型和映射 # 文档映射 Mapping Mapping 类似数据库中的 schema 的定义,作用如下: - 定义索引中的字段的名称 - 定义字段的数据类型,例如字符串,数字,布尔等 - 字段,倒排索引的相关配置(Analyzer or Not Analyzed,Analyzer) ES 中 Mapping 映射可以分为动态映射和静态映射 ## 动态映射 在关系数据库中,需要事先创建数据库,然后在该数据库下创建数据表,并创建表字段、类型、长度、主键等,最后才能基于表插入数据。而 Elasticsearcl 中不需要定义 Mapping 映射(即关系型数据库的表、字段等),在文档写入 Elasticsearch 时,会根据文档字段自动识别类型,这种机制称之为动态映射。 ## 静态映射 静态映射是在 Elasticsearch 中也可以事先定义好映射,包含文档的各字段类型、分词器等,这种方式称之为静态映射。 动态映射( Dynamic Mapping)的机制,使得我们无需手动定义 Mappings, Elasticsearch 会自动根据文档信息,推算出字段的类型。但是有时候会推算的不对,例如地理位置信息。当类型如果设置不对时,会导致一些功能无法正常运行,例如 Range 查询 **Dynamic Mapping 类型自动识别**  **测试动态映射** ``` DELETE /user PUT /user/_doc/1 { "name":"张三", "sex": 1, "age": 28, "address":"北京通州" } GET /user/_mapping ```  **思考:能否后期更改 Mapping 的字段类型?** ## dynamic - 新增加字段 - dynamic 设为 true 时,一旦有新增字段的文档写入,Mapping 也同时被更新 - dynamic 设为 false,Mapping 不会被更新,新增字段的数据无法被索引,但是信息会出现在_source 中 - dynamic 设置成 strict(严格控制策略),文档写入失败,抛出异常  - 对已有字段,一旦已经有数据写入,就不再支持修改字段定义 - Lucene 实现的倒排索引,一旦生成后,就不允许修改 - 如果希望改变字段类型,可以利用 Reindex API,重建索引 - 具体方法: - 1)如果要推倒现有的映射, 你得重新建立一个静态索引 - 2)然后把之前索引里的数据导入到新的索引里 - 3)删除原创建的索引 - 4)为新索引起个别名, 为原索引名 - 原因: - 如果修改了字段的数据类型,会导致已被索引的数据无法被搜索 - 但是如果是增加新的字段,就不会有这样的影响 **测试 dynamic 为 strict:** ``` DELETE /user PUT /user { "mappings": { "dynamic": "strict", "properties": { "name": { "type": "text" }, "address": { "type": "object", "dynamic": true } } } } ``` **插入报错** 因为 sex 与 age 是新增字段 ``` POST /user/_doc/1 { "name": "张三", "sex": 1, "age": 28, "address": { "province": "北京市", "city": "通州区" } } { "error" : { "root_cause" : [ { "type" : "strict_dynamic_mapping_exception", "reason" : "mapping set to strict, dynamic introduction of [sex] within [_doc] is not allowed" } ], "type" : "strict_dynamic_mapping_exception", "reason" : "mapping set to strict, dynamic introduction of [sex] within [_doc] is not allowed" }, "status" : 400 } ``` 因为字段 address 设置了 dynamic = true 所以是可以自动更新 mapping 的 ``` POST /user/_doc/1 { "name": "张三", "address": { "province": "北京市", "city": "通州区" } } { "_index" : "user", "_type" : "_doc", "_id" : "2", "_version" : 1, "result" : "created", "_shards" : { "total" : 2, "successful" : 1, "failed" : 0 }, "_seq_no" : 2, "_primary_term" : 1 } ``` ### 修改 dynamic ``` PUT /user/_mapping { "dynamic":true } ``` ### 对已经存在的 mapping 映射进行修改 **具体方法:** - 1)如果要推倒现有的映射, 你得重新建立一个静态索引 - 2)然后把之前索引里的数据导入到新的索引里 - 3)删除原创建的索引 - 4)为新索引起个别名,为原索引名 ``` # 复制一个索引,数据来源于user,命名为user2 POST _reindex { "source": { "index": "user" }, "dest": { "index": "user2" } } # 删除原来的user索引 DELETE /user # 给刚刚从user 复制出来的user2 起一个别名,user PUT /user2/_alias/user # 其实这里的user是user2的别名 GET /user ``` 注意:通过这几个步骤就实现了索引的平滑过渡,并且是零停机 ## 常用 Mapping 参数配置 ### index: 控制当前字段是否被索引,默认为 true。如果设置为 false,该字段不可被搜索 ``` DELETE /user2 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": "广州" } } } ```  ### 有四种不同基本的 index options 配置,控制倒排索引记录的内容 - docs : 记录 doc id - freqs:记录 doc id 和 term frequencies(词频) - positions: 记录 doc id / term frequencies / term position - offsets: doc id / term frequencies / term position / character offsets text 类型默认记录 postions,其他默认为 docs。记录内容越多,占用存储空间越大 ``` DELETE /user PUT /user { "mappings" : { "properties" : { "address" : { "type" : "text", ** "index_options": "offsets"** }, "age" : { "type" : "long" }, "name" : { "type" : "text" } } } } ``` ### null_value: 需要对 Null 值进行搜索,只有 keyword 类型支持设计 Null_Value ``` DELETE /user PUT /user { "mappings" : { "properties" : { "address" : { "type" : "keyword", ** "null_value": "NULL"** }, "age" : { "type" : "long" }, "name" : { "type" : "text" } } } } PUT /user/_doc/1 { "name":"fox", "age":32, "address":null } GET /user/_search { "query": { "match": { "address": "NULL" } } } ``` ### copy_to 设置: 将字段的数值拷贝到目标字段,满足一些特定的搜索需求。 copy_to 的目标字段不出现在_source 中 ``` # 设置copy_to DELETE /address PUT /address { "mappings" : { "properties" : { "province" : { "type" : "keyword", ** "copy_to": "full_address"** }, "city" : { "type" : "text", ** "copy_to": "full_address"** } } }, "settings" : { "index" : { "analysis.analyzer.default.type": "ik_max_word" } } } PUT /address/_bulk { "index": { "_id": "1"} } {"province": "湖南","city": "长沙"} { "index": { "_id": "2"} } {"province": "湖南","city": "常德"} { "index": { "_id": "3"} } {"province": "广东","city": "广州"} { "index": { "_id": "4"} } {"province": "湖南","city": "邵阳"} GET /address/_search { "query": { "match": { ** "full_address": {** ** "query": "湖南常德",** ** "operator": "and"** ** }** } } } ``` ## Index Template - Index Templates 可以帮助你设定 Mappings 和 Settings,并按照一定的规则,自动匹配到新创建的索引之上 - 模版仅在一个索引被新创建时,才会产生作用。**修改模版不会影响已创建的索引** - 你可以设定多个索引模版,这些设置会被“merge”在一起 - 你可以指定“order”的数值,控制“merging”的过程 - index_patterns 索引名称的匹配模式 * 全部,test* 名称以 test 开头的索引 - order:数值越大,权重越大 ``` PUT /_template/template_default { "index_patterns": ["*"], "order": 0, "version": 1, "settings": { "number_of_shards": 1, "number_of_replicas": 1 } } PUT /_template/template_test { "index_patterns": ["test*"], "order": 1, "settings": { "number_of_shards": 2, "number_of_replicas": 1 }, "mappings": { "date_detection": false, "numeric_detection": true } } ``` ** "date_detection": false, # 关闭日期探测** ### lndex Template 的工作方式 当一个索引被新创建时: - 应用 Elasticsearch 默认的 settings 和 mappings - 应用 order 数值低的 lndex Template 中的设定 - 应用 order 高的 Index Template 中的设定,之前的设定会被覆盖 - 应用创建索引时,用户所指定的 Settings 和 Mappings,并覆盖之前模版中的设定 ``` #查看template信息 GET /_template/template_default GET /_template/temp* # 新增一个索引,并添加一个文档(测试 createDate) PUT /testtemplate/_doc/1 { "orderNo": 1, "createDate": "2022/01/01" } # 获取索引信息 GET /testtemplate/_mapping GET /testtemplate/_settings # 开启日期探测 PUT /testmy { "mappings": { "date_detection": true } } # 添加测试数据 PUT /testmy/_doc/1 { "orderNo": 1, "createDate": "2022/01/01" } # 获取索引信息 GET /testmy/_mapping ``` ## Dynamic Template 根据 Elasticsearch 识别的数据类型,结合字段名称,来动态设定字段类型 一般只针对一个索引里的字段进行设置 - 所有的字符串类型都设定成 Keyword,或者关闭 keyword 字段 - is 开头的字段都设置成 boolean - long_开头的都设置成 long 类型 ``` DELETE my_index PUT my_index/_doc/1 { "firstName":"Ruan", "isVIP":"true" } GET my_index/_mapping DELETE my_index PUT my_index { "mappings": { ** "dynamic_templates": [** { ** "strings_as_boolean": {** ** "match_mapping_type": "string",** ** "match": "is*",** ** "mapping": {** ** "type": "boolean"** ** }** ** }** }, { "strings_as_keywords": { "match_mapping_type": "string", "mapping": { "type": "keyword" } } } ] } } ``` ### 测试 Dynamic ``` 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" } } } ``` # 分词器 ## 规范化:normalization  规范化就是将 输入的字符串规范化为一种标准,方便匹配和查询   去掉了单复数,语气词,大小写等 ### 程序测试 ### 使用 pattern 基础分词器 几乎没有过滤单词,但是把大写字母修改成小写的了 ``` GET _analyze { "text": "Mr. Ma is an excellent teacher ", "analyzer":"pattern" } ```  ### 使用 **english**** **分词器分词 可以发现 is、an 被干掉了,excellent 被简化成 excel 了 ``` GET _analyze { "text": "Mr. Ma is an excellent teacher ", ** "analyzer": "english"** } ```  ### 使用系统默认的 **standard** 分词器 ``` GET _analyze { "text": "Mr. Ma is an excellent teacher ", ** "analyzer": "standard"** } ```  ## 字符过滤器:character filter 分词之前的预处理,过滤无用字符 - Html strip :过滤 html 标签 - Type : html_strip - 参数:escaped_tags 需要保留的 html 标签 - Mapping : 自定义规则,用来替换指定字符 - Type : mapping - Pattern replace:通过正则表达式来替换字符 - Type: pattern_replace ### HTML strip 创建索引并设定过滤规则 **char_filter 定义过滤规则器规则** **analyzer 定义分词器** **escaped_tags:保留标签** ``` DELETE /my_index PUT /my_index { "settings": { "analysis": { ** "char_filter": {** ** "my_char_filter":{** ** "type":"html_strip",** ** "escaped_tags":["a"]** ** }** }, ** "analyzer": {** ** "my_analyzer":{** ** "tokenizer":"keyword",** ** "char_filter":"my_char_filter"** ** }** ** }** } } } ``` **测试** ``` GET /my_index/_analyze { "analyzer": "my_analyzer", "text": "<p>test string <a href='http://www.baidu.com'>link</a></p>" } { "tokens" : [ { "token" : """ test string <a href='http://www.baidu.com'>link</a> """, "start_offset" : 0, "end_offset" : 58, "type" : "word", "position" : 0 } ] } ``` ### Mapping 自定义规则,用来替换指定字符 ``` PUT /my_index { "settings": { "analysis": { "char_filter": { "my_char_filter": { **"type": "mapping",** ** "mappings": [** ** "滚 => *",** ** "垃圾 => *",** ** "sb => *"** ] } }, "analyzer": { "my_analyzer": { "tokenizer": "keyword", "char_filter": "my_char_filter" } } } } } ``` **测试** ``` GET /my_index/_analyze { "analyzer": "my_analyzer", "text": "你就是个垃圾,滚!" } { "tokens" : [ { "token" : "你就是个*,*!", "start_offset" : 0, "end_offset" : 9, "type" : "word", "position" : 0 } ] } ``` ### Pattern replace 通过正则表达式来替换字符 ``` DELETE /my_index PUT /my_index { "settings": { "analysis": { "char_filter": { "my_char_filter": { ** "type": "pattern_replace",** ** "pattern":"(\\d{3})\\d{4}(\\d{4})",** ** "replacement":"$1****$2"** } }, "analyzer": { "my_analyzer": { "tokenizer": "keyword", "char_filter": "my_char_filter" } } } } } GET /my_index/_analyze { "analyzer": "my_analyzer", "text": "您的手机号是:17612345678" } { "tokens" : [ { "token" : "您的手机号是:176****5678", "start_offset" : 0, "end_offset" : 18, "type" : "word", "position" : 0 } ] } ``` ## 令牌过滤器(token filter ) 停用词,时态转换,大小写,同义词,语气词 比如 has=>have, him => he, apples => apple, the/oh/a => 删除 参数: - Type - synonym_graph 根据文件路径替换 - 参数 - synonyms_path - 文件路径 analysis/synonym.txt - Synonym 数组模式替换 - 参数 - Synonyms:["赵,钱,孙,李 => 周"] 官方文档:同义词过滤 ### **synonym_graph 同义词替换 文件形式** ``` 创建文件 /usr/share/elasticsearch/config/analysis/synonym.txt 文件内容 一百 => 100 两百 => 200 三百 => 300 ``` ``` DELETE /test_index PUT /test_index { "settings": { "analysis": { "filter": { ** "my_synonym":{** ** "type":"synonym_graph",** ** "synonyms_path":"analysis/synonym.txt"** ** }** }, "analyzer":{ ** "my_analyzer":{** ** "tokenizer":"ik_max_word",** ** "filter":["my_synonym"]** ** }** } } } } GET /test_index/_analyze { "analyzer": "my_analyzer", "text": "上衣一百,裤子两百,皮鞋三百" } { "tokens" : [ { "token" : "上衣", "start_offset" : 0, "end_offset" : 2, "type" : "CN_WORD", "position" : 0 }, { "token" : "100", "start_offset" : 2, "end_offset" : 4, "type" : "SYNONYM", "position" : 1 }, { "token" : "裤子", "start_offset" : 5, "end_offset" : 7, "type" : "CN_WORD", "position" : 2 }, { "token" : "200", "start_offset" : 7, "end_offset" : 9, "type" : "SYNONYM", "position" : 3 }, { "token" : "皮鞋", "start_offset" : 10, "end_offset" : 12, "type" : "CN_WORD", "position" : 4 }, { "token" : "300", "start_offset" : 12, "end_offset" : 14, "type" : "SYNONYM", "position" : 5 } ] } ``` ### **synonym 同义词替换 数组形式** ``` PUT /test_index { "settings": { "analysis": { "filter": { ** "my_synonym": {** ** "type": "synonym",** ** "synonyms":["赵,钱,孙,李=>周"]** ** }** }, "analyzer": { "my_analyzer": { ** "tokenizer": "standard",** ** "filter": [** ** "my_synonym"** ** ]** } } } } } GET /test_index/_analyze { "analyzer": "my_analyzer", "text": "赵钱孙李周吴郑王" } { "tokens" : [ { "token" : "周", "start_offset" : 0, "end_offset" : 1, "type" : "SYNONYM", "position" : 0 }, { "token" : "周", "start_offset" : 1, "end_offset" : 2, "type" : "SYNONYM", "position" : 1 }, { "token" : "周", "start_offset" : 2, "end_offset" : 3, "type" : "SYNONYM", "position" : 2 }, { "token" : "周", "start_offset" : 3, "end_offset" : 4, "type" : "SYNONYM", "position" : 3 }, { "token" : "周", "start_offset" : 4, "end_offset" : 5, "type" : "<IDEOGRAPHIC>", "position" : 4 }, { "token" : "吴", "start_offset" : 5, "end_offset" : 6, "type" : "<IDEOGRAPHIC>", "position" : 5 }, { "token" : "郑", "start_offset" : 6, "end_offset" : 7, "type" : "<IDEOGRAPHIC>", "position" : 6 }, { "token" : "王", "start_offset" : 7, "end_offset" : 8, "type" : "<IDEOGRAPHIC>", "position" : 7 } ] } ``` 其它的请查看官网示例 ``` # 小写 GET /test_index/_analyze { "tokenizer": "standard", "filter": ["lowercase"], "text": [ "SDF SDFASDF SDFA SDF ASDF D"] } # 大写 GET /test_index/_analyze { "tokenizer": "standard", "filter": ["uppercase"], "text": [ "sesf seaf sef sewe vsfsdfa sfwe"] } ``` ### Condition 条件判断 字符数量小于 5 个字的才转换为大写 ``` GET /test_index/_analyze { "tokenizer": "standard", "filter": { ** "type":"condition",** ** "filter":"uppercase",** ** "script": {** ** "source": "token.getTerm().length() < 5"** ** }** }, "text": [ "a ab abc abcd abcde abcdef " ] } { "tokens" : [ { "token" : "A", "start_offset" : 0, "end_offset" : 1, "type" : "<ALPHANUM>", "position" : 0 }, { "token" : "AB", "start_offset" : 2, "end_offset" : 4, "type" : "<ALPHANUM>", "position" : 1 }, { "token" : "abc", "start_offset" : 5, "end_offset" : 8, "type" : "<ALPHANUM>", "position" : 2 }, { "token" : "abcd", "start_offset" : 9, "end_offset" : 13, "type" : "<ALPHANUM>", "position" : 3 }, { "token" : "abcde", "start_offset" : 14, "end_offset" : 19, "type" : "<ALPHANUM>", "position" : 4 }, { "token" : "abcdef", "start_offset" : 20, "end_offset" : 26, "type" : "<ALPHANUM>", "position" : 5 } ] } ``` ### Stopwords 停用词 #### 数组形式: ``` PUT /test_index { "settings": { "analysis": { "analyzer": { "my_analyzer":{ "type":"standard", "stopwords": [ "and", "is", "the" ] } } } } } GET /test_index/_analyze { "analyzer": "my_analyzer", "text": "you and me " } { "tokens" : [ { "token" : "you", "start_offset" : 0, "end_offset" : 3, "type" : "<ALPHANUM>", "position" : 0 }, { "token" : "me", "start_offset" : 8, "end_offset" : 10, "type" : "<ALPHANUM>", "position" : 2 } ] } ``` #### 文件形式: ``` PUT /test_index { "settings": { "analysis": { "analyzer": { "my_analyzer":{ "type":"stopwords", "stopwords_path": "analysis/stop.txt" } } } } } ``` ## 分词器:tokenizer 自定义分词器 默认的分词器为:standard **standard 分词器分词效果** 中文单子分词,英文按空格分词 ``` GET /test_index/_analyze { ** "analyzer": "standard",** "text":["我爱北京天安门", "天安门上太阳升"] } { "tokens" : [ { "token" : "我", "start_offset" : 0, "end_offset" : 1, "type" : "<IDEOGRAPHIC>", "position" : 0 }, { "token" : "爱", "start_offset" : 1, "end_offset" : 2, "type" : "<IDEOGRAPHIC>", "position" : 1 }, ......... ] } ``` 指定安装的其他分词器 ``` GET /test_index/_analyze { ** "analyzer": "ik_max_word",** "text":["我爱北京天安门", "天安门上太阳升"] } { "tokens" : [ { "token" : "我", "start_offset" : 0, "end_offset" : 1, "type" : "CN_CHAR", "position" : 0 }, { "token" : "爱", "start_offset" : 1, "end_offset" : 2, "type" : "CN_CHAR", "position" : 1 }, ] } ``` ## 常见的分词器: - Standard analyzer 默认分词器,中文支持不理想,会逐字拆分 - Pattern tokenizer: 以正则匹配分割符,把文本拆分成若干词项。 - Simple pattern tokenizer 以正则匹配词项,速度比 pattern tokenzier 速度略快 - whitespace analyzer 以空白符分割 tim_cookie ## (综合应用)自定义分词器:custom analyzer - char_filter 内置或自定义字符过滤器 - Token filter 内置或自定义 token filter - Tokenizer 内置或自定义分词器 **创建分词器** - char_filter 定义字符串过滤,这里做转换 - filter 定义过滤器,这里做过滤 - tokenizer 定义拆分规则,将字符串拆分成词 - analyzer 定义分词器,定义分词器规则 ``` DELETE custom_analysis PUT custom_analysis { "settings": { "analysis": { "char_filter": { "my_char_filter": { "type": "mapping", "mappings": [ "&=>and", "|=>or" ] }, "my_html_strip": { "type": "html_strip", "escaped_tags": [ "a" ] } }, "filter": { "my_stopword": { "type": "stop", "stopwords": [ "is", "in", "the", "a", "at", "for" ] } }, "tokenizer": { "my_tokenizer": { "type": "pattern", "pattern": "[ ,.!?]" } }, "analyzer": { "my_analyzer": { "type": "custom", "char_filter": [ "my_char_filter", "my_html_strip" ], "filter": "my_stopword", "tokenizer": "my_tokenizer" } } } } } ``` **测试分词器** ``` GET custom_analysis/_analyze { "analyzer": "my_analyzer", "text": ["what is ,asdf . ss in ? & | is ! in the a at for <a>href<span>xxx</span></a> "] } { "tokens" : [ { "token" : "what", "start_offset" : 0, "end_offset" : 4, "type" : "word", "position" : 0 }, { "token" : "asdf", "start_offset" : 9, "end_offset" : 13, "type" : "word", "position" : 2 }, { "token" : "ss", "start_offset" : 16, "end_offset" : 18, "type" : "word", "position" : 3 }, { "token" : "and", "start_offset" : 24, "end_offset" : 25, "type" : "word", "position" : 5 }, { "token" : "or", "start_offset" : 26, "end_offset" : 27, "type" : "word", "position" : 6 }, { "token" : "<a>hrefxxx</a>", "start_offset" : 50, "end_offset" : 77, "type" : "word", "position" : 13 } ] } ``` ## 中文分词器:IK 分词 ### **GitHub 地址:** [https://github.com/medcl/elasticsearch-analysis-ik](https://github.com/medcl/elasticsearch-analysis-ik) ### **通过 elasticsearch-plugin 方式安装** ``` elasticsearch-plugin install https://github.com/medcl/elasticsearch-analysis-ik/releases/download/v7.9.1/elasticsearch-analysis-ik-7.9.1.zip ``` ### **IK 文件描述** - IKAnalyzer.cfg.xml:IK 分词配置文件 - 主词库:main.dic - 英文停用词:stopword.dic,不会建立在倒排索引中 - 特殊词库: - quantifier.dic:特殊词库,计量单位等 - suffix.dic:特殊词库,后绶名 - surname.dic:特殊词库:百家姓 - preposition:特殊词库:语气词 - 自定义词库:网络词汇、流行词、自造词等 ### **分词类型** Analyzer: `ik_smart` , `ik_max_word` , Tokenizer: `ik_smart` , `ik_max_word` `ik_smart` 最小化分词, `ik_max_word` 最大化分词 ### **修改配置文件可以设置不同拓展词库** IKAnalyzer.cfg.xml  ### **配置远程拓展词典 - 基于 api 接口** 只要远程地址可以输出词典即可,换行一行一个词 其中 `location` 是指一个 url,比如 `http://yoursite.com/getCustomDict`,该请求只需满足以下两点即可完成分词热更新。 1. 该 http 请求需要返回两个头部(header),一个是 `Last-Modified`,一个是 `ETag`,这两者都是字符串类型,只要有一个发生变化,该插件就会去抓取新的分词进而更新词库。 2. 该 http 请求返回的内容格式是一行一个分词,换行符用 `\n` 即可。 满足上面两点要求就可以实现热更新分词了,不需要重启 ES 实例。 可以将需自动更新的热词放在一个 UTF-8 编码的 .txt 文件里,放在 nginx 或其他简易 http server 下,当 .txt 文件修改时,http server 会在客户端请求该文件时自动返回相应的 Last-Modified 和 ETag。可以另外做一个工具来从业务系统提取相关词汇,并更新这个 .txt 文件。 ### **配置远程拓展词典 - 基于 mysql** 需要修改源码,主要修改 wltea.analyzer / dic / dictionary 这个文件 修改 loadMainDict 方法,新增 loadMySQLExtDict() 方法  略 **PS:** 我感觉还是直接使用 API 的方式比较好,实际项目中我们通常有前后台管理项目,直接部署到内网中的一台机子上就行很简单,并且我们通常也会在后台对这个字典进行管理,刚好使用后台一套搞定。 # 聚合查询 Elasticsearch 是一个分布式的检索分析存储引擎,DSL 是查询,Aggregations 则是分析,如果 DSL 权重是 100,那么 Aggregations 就是 90,重要性不言而喻 - 学习方法 - 使用场景 - 聚合分类 - 语法用法 - 数据结构 ## 官方文档: ## 使用场景 - 某商城各个品牌手机的月销量是多少 - 中国不同地区的消费水平 - 网站的平均响应时长 - 我们产品的消费群体在不同年龄段的分布式情况 ## 聚合分类 - 分桶聚合:Bucket agregations - 指标聚合:Metrics agregations - 管道聚合:pipeline agregations ## 分桶聚合:Bucket agregations 把相同属性的数据放在一个桶里,类似 mysql group by **相同标签**  **日志分析**  **各品牌的手机销量** 例如统计小米手机,小米品牌手机就是一个桶  ## 指标聚合:Metrics agregations   ## 管道聚合:pipeline agregations - 概念:对聚合的结果进行二次聚合 - 分类:父级和兄弟级 - 语法:buckets_path  ## 实战训练 ### 新建索引 ``` DELETE /product PUT /product { "mappings": { "properties": { "createtime": { "type": "date" }, "desc": { "type": "text", "analyzer": "ik_max_word", "fields": { "keyword": { "type": "keyword", "ignore_above": 256 } } }, "lv": { "type": "text", "fields": { "keyword": { "type": "keyword", "ignore_above": 256 } } }, "name": { "type": "text", "analyzer": "ik_max_word", "fields": { "keyword": { "type": "keyword", "ignore_above": 256 } } }, "price": { "type": "long" }, "tags": { "type": "text", "fields": { "keyword": { "type": "keyword", "ignore_above": 256 } } }, "type": { "type": "text", "fields": { "keyword": { "type": "keyword", "ignore_above": 256 } } } } } } ``` ### **添加数据** ``` PUT /product/_doc/1 { "id": 1, "name": "小米手机", "desc": "手机中的战斗机", "price": 3999, "lv": "旗舰机", "type": "手机", "createtime": "2020-10-01", "tags": [ "性价比", "发烧", "不卡顿" ] } PUT /product/_doc/2 { "id": 2, "name": "小米 NFC 手机", "desc": "支持全功NFC,手机中的滑翔机", "price": 4999, "lv": "旗舰机", "type": "手机", "createtime": "2020-05-21", "tags": [ "性价比", "发烧", "公交卡" ] } PUT /product/_doc/3 { "id": 3, "name": "NFC手机", "desc": "手机中的轰炸机", "price": 2999, "lv": "高端机", "type": "手机", "createtime": "2020-06-20", "tags": [ "性价比", "快充", "门禁卡" ] } PUT /product/_doc/4 { "id": 4, "name": "小米耳机", "desc": "耳机中的黄焖鸡", "price": 999, "lv": "百元机", "type": "耳机", "createtime": "2020-06-23", "tags": [ "降噪", "防水", "蓝牙" ] } PUT /product/_doc/5 { "id": 5, "name": "红米耳机", "desc": "耳机中的肯德基", "price": 399, "lv": "百元机", "type": "耳机", "createtime": "2020-07-20", "tags": [ "防火", "低音炮", "听声辨位" ] } PUT /product/_doc/6 { "id": 6, "name": "小米手机10", "desc": "充电贼快掉电更快,超级无敌望远镜,高刷电竞屏", "price": 5999, "lv": "旗舰机", "type": "手机", "createtime": "2020-07-27", "tags": [ "120HZ刷新率", "120W快充", "120倍变焦" ] } PUT /product/_doc/7 { "id": 7, "name": "挨炮 SE2", "desc": "除了CPU,一无是处", "price": 3299, "lv": "旗舰机", "type": "手机", "createtime": "2020-07-21", "tags": [ "割韭菜", "割韭菜", "割新韭菜" ] } PUT /product/_doc/8 { "id": 8, "name": "XS Max", "desc": "听说要出新款12手机了,终于可以换掉手中的4S了", "price": 4399, "lv": "旗舰机", "type": "手机", "createtime": "2020-08-19", "tags": [ "5V1A", "4G 全网通", "大" ] } PUT /product/_doc/9 { "id": 9, "name": "小米电视", "desc": "70寸性价比只选,不要一万,要不要八千八,只要两千九百九十八", "price": 2998, "lv": "高端机", "type": "电视", "createtime": "2020-08-16", "tags": [ "巨慢", "家庭影院", "游戏" ] } PUT /product/_doc/10 { "id": 10, "name": "红米电视", "desc": "我比上面更划算,我也2998,我也70寸,但我更好看", "price": 2999, "lv": "高端机", "type": "电视", "createtime": "2020-08-28", "tags": [ "大片", "蓝光8K", "超薄" ] } PUT /product/_doc/11 { "id": 11, "name": "红米电视", "desc": "我比上面更划算,我也2998,我也70寸,但我更好看", "price": 2999, "lv": "高端机", "type": "电视", "createtime": "2020-08-28", "tags": [ "大片", "蓝光8K", "超薄" ] } # 查询数据 size 默认为10,所以指定20才能展示11条完整的数据 GET /product/_search?size=11 ``` ### 查询语法 ``` GET /product/_search { "aggs": { "AGG_NAME_A": { "AGG_TYPE": { "field": "FILED_NAME" } }, "AGG_NAME_B": { "AGG_TYPE": { "field": "FILED_NAME" } } } } ``` ## 分桶聚合 ### 例:统计不同标签的商品数量 - Siez: 如果不显示指定为 0 的话,会同步返回文档的信息,通常我们在聚合查询时不需文档数据 - aggs.size:分桶默认展示 10 个数据,可以根据实际情况指定 - Order:可指定排序规则 ``` GET /product/_search { "size":0, "aggs": { "aggs_tag": { "terms": { "field": "tags.keyword", "size": 30, ** "order": {** ** "_count": "asc"** ** }** } } } } ``` ## 指标聚合 ### **例子:查询最贵,最便宜,和平均价格三个指标** ``` GET /product/_search { "size":0, "aggs": { "max_price": { ** "max": {** ** "field": "price"** ** }** }, "min_price":{ ** "min":{** ** "field": "price"** ** }** }, "avg_price":{ ** "avg": {** ** "field": "price"** ** }** }, "sum_price":{ ** "sum": {** ** "field": "price"** ** }** }, "count_price":{ ** "value_count": {** ** "field": "price"** ** }** } } } { "took" : 0, "timed_out" : false, "_shards" : { "total" : 1, "successful" : 1, "skipped" : 0, "failed" : 0 }, "hits" : { "total" : { "value" : 11, "relation" : "eq" }, "max_score" : null, "hits" : [ ] }, "aggregations" : { "max_price" : { "value" : 5999.0 }, "min_price" : { "value" : 399.0 }, "count_price" : { "value" : 11 }, "avg_price" : { "value" : 3280.7272727272725 }, "sum_price" : { "value" : 36088.0 } } } ``` ### **例子:简单查询所有指标 ****stats** ``` GET /product/_search { "size": 0, "aggs": { "stats_price": { ** "stats": {** "field": "price" } } } } { "took" : 0, "timed_out" : false, "_shards" : { "total" : 1, "successful" : 1, "skipped" : 0, "failed" : 0 }, "hits" : { "total" : { "value" : 11, "relation" : "eq" }, "max_score" : null, "hits" : [ ] }, "aggregations" : { "stats_price" : { ** "count" : 11,** ** "min" : 399.0,** ** "max" : 5999.0,** ** "avg" : 3280.7272727272725,** ** "sum" : 36088.0** } } } ``` ### **例子:按照 name 去重的数量 ****cardinality** ``` GET /product/_search { "size": 0, "aggs": { ** "name_count": {** ** "cardinality": {** ** "field": "name.****keyword****"** ** }** } } } { "took" : 5, "timed_out" : false, "_shards" : { "total" : 1, "successful" : 1, "skipped" : 0, "failed" : 0 }, "hits" : { "total" : { "value" : 11, "relation" : "eq" }, "max_score" : null, "hits" : [ ] }, "aggregations" : { ** "name_count" : {** ** "value" : 10** ** }** } } ``` ## 管道聚合 - 二次聚合 **注意:根据结果,跟谁评级就在谁的下面做查询** ### 例子:统计平均价格最低的商品分类 - 首先分析出有多少分类 - 在分类数据的基础上分析出每个分类的平均价格 - 通过 min_bucket 查询最低的分类 - **buckets_path :分桶路径** **第一步:查询商品分类的平均价格** ``` GET /product/_search { "size": 0, "aggs": { "type_bucket": { ** "terms": {** ** "field": "type.keyword"** ** },** ** "aggs": {** ** "price_bucket": {** ** "avg": {** ** "field": "price"** ** }** ** }** ** }** } } } { "took" : 2, "timed_out" : false, "_shards" : { "total" : 1, "successful" : 1, "skipped" : 0, "failed" : 0 }, "hits" : { "total" : { "value" : 11, "relation" : "eq" }, "max_score" : null, "hits" : [ ] }, "aggregations" : { "type_bucket" : { "doc_count_error_upper_bound" : 0, "sum_other_doc_count" : 0, "buckets" : [ { "key" : "手机", "doc_count" : 6, "price_bucket" : { "value" : 4282.333333333333 } }, { "key" : "电视", "doc_count" : 3, "price_bucket" : { "value" : 2998.6666666666665 } }, { "key" : "耳机", "doc_count" : 2, "price_bucket" : { "value" : 699.0 } } ] } } } ``` **第二步:查询桶中的最小分类** ``` GET /product/_search { "size": 0, "aggs": { ** "type_bucket": {** ** "terms": {** ** "field": "type.keyword"** ** },** ** "aggs": {** ** "price_bucket": {** ** "avg": {** ** "field": "price"** ** }** ** }** ** }** }, ** "min_bucket":{** ** "****min_bucket****": {** ** "buckets_path": "type_bucket>price_bucket"** ** }** ** }** } } { "took" : 2, "timed_out" : false, "_shards" : { "total" : 1, "successful" : 1, "skipped" : 0, "failed" : 0 }, "hits" : { "total" : { "value" : 11, "relation" : "eq" }, "max_score" : null, "hits" : [ ] }, "aggregations" : { "type_bucket" : { "doc_count_error_upper_bound" : 0, "sum_other_doc_count" : 0, "buckets" : [ { "key" : "手机", "doc_count" : 6, "price_bucket" : { "value" : 4282.333333333333 } }, { "key" : "电视", "doc_count" : 3, "price_bucket" : { "value" : 2998.6666666666665 } }, { "key" : "耳机", "doc_count" : 2, "price_bucket" : { "value" : 699.0 } } ] }, "min_bucket" : { "value" : 699.0, "keys" : [ "耳机" ] } } } ``` ## 嵌套聚合 商品类型分为手机,耳机,电视, 商品级别又分为,旗舰机,高端机,百元机等 类似这种嵌套关系 ### 语法 **基于前者的聚合结果,应该放在聚合方法的同级**  ### 例子:统计不同类型商品的不同级别的数量 ``` GET /product/_search { "size": 0, "aggs": { "type_agg": { "terms" : { "field": "type.keyword" }, "aggs": { "lv_agg": { "terms": { "field":"lv.keyword" } } } } } } ``` ### 例子:按照 LV 分桶,输出每个桶的具体价格信息 ``` GET /product/_search { "size": 0, "aggs": { "type_agg": { "terms" : { "field": "type.keyword" }, "aggs": { "price_agg": { "stats": { "field":"price" } } } } } } ``` ### 例子:统计不同类型,不同档次的价格信息 ``` GET /product/_search { "size": 0, "aggs": { "type_agg": { "terms" : { "field": "type.keyword" }, "aggs": { "lv_agg": { "terms": { "field":"lv.keyword" }, "aggs": { "type_lv_price_agg": { "stats": { "field": "price" } } } } } } } } ``` ### **例子:统计不同类型,不同档次的商品, 它们的价格信息和标签信息** Aggs 是复数表达,那么 aggs 下一定可以有多个 标签,如果需要统计同级信息,放在一个层级就行 ``` GET /product/_search { "size": 0, "aggs": { "type_agg": { "terms": { "field": "type.keyword" }, "aggs": { "lv_agg": { "terms": { "field": "lv.keyword" }, ** "aggs": {** ** "type_lv_price_agg": {** ** "stats": {** ** "field": "price"** ** }** ** },** ** "type_lv_tag_agg": {** ** "terms": {** ** "field": "tags.keyword"** ** }** ** }** ** }** } } } } } ``` ### 例子:统计每个商品类型中,不同档次分类中,平均价格最低的 档次 **注意:min_bucket 对哪一层求最小,就放在那一层** ``` GET /product/_search { "size": 0, "aggs": { "type_bucket": { "terms": { "field": "type.keyword", "size": 20 }, "aggs": { "lv_bucket": { "terms": { "field": "lv.keyword", "size": 20 }, "aggs": { "price_bucket": { "avg": { "field": "price" } } } }, ** "min_bucket": {** ** "min_bucket": {** ** "buckets_path": "lv_bucket>price_bucket"** ** }** ** }** } } } } ``` ## 基于查询结果的聚合 ### 例子:查询价格大于 5000 的商品的标签信息 ``` GET /product/_search { "size": 20, ** "query":{** ** "range": {** ** "price": {** ** "gte": 5000** ** }** ** }** ** },** ** "aggs": {** ** "tag_aggs": {** ** "terms": {** ** "field": "tags.keyword"** ** }** ** }** ** }** } ``` ## 基于 filter 的 aggs ``` GET /product/_search { "size": 0, "query": { "constant_score": { "filter": { "range": { "price": { "gte": 5000 } } } } }, "aggs": { "tag_aggs": { "terms": { "field": "tags.keyword" } } } } ``` ## 基于 bool 查询 的 aggs ``` GET /product/_search { "query": { "bool": { "filter": [ { "range": { "price": { "gte": 5000 } } } ] } }, "aggs": { "tag_aggs": { "terms": { "field": "tags.keyword", "size": 10 } } } } ``` ## post_filter 后置过滤,基于聚合的查询 post_filter 后置过滤:在查询命中文档、完成聚合后,再对命中的文档进行过滤。 **例如:聚合查询所有标签,而后想知道有那些商品使用了这个标签** ``` GET /product/_search { "aggs": { "tags_bucket": { "terms": { "field": "tags.keyword", "size": 10 } } }, ** "post_filter": {** "term": { ** "tags.keyword": "性价比"** } } } { "took" : 1, "timed_out" : false, "_shards" : { "total" : 1, "successful" : 1, "skipped" : 0, "failed" : 0 }, ** "hits" : {** ** "total" : {** ** "value" : 3,** ** "relation" : "eq"** ** },** ** "max_score" : 1.0,** ** "hits" : [** ** {** ** "_index" : "product",** ** "_type" : "_doc",** ** "_id" : "1",** ** "_score" : 1.0,** ** "_source" : {** ** "id" : 1,** ** "name" : "小米手机",** ** "desc" : "手机中的战斗机",** ** "price" : 3999,** ** "lv" : "旗舰机",** ** "type" : "手机",** ** "createtime" : "2020-10-01",** ** "tags" : [** ** "性价比",** ** "发烧",** ** "不卡顿"** ** ]** ** }** ** },** ** {** ** "_index" : "product",** ** "_type" : "_doc",** ** "_id" : "2",** ** "_score" : 1.0,** ** "_source" : {** ** "id" : 2,** ** "name" : "小米 NFC 手机",** ** "desc" : "支持全功NFC,手机中的滑翔机",** ** "price" : 4999,** ** "lv" : "旗舰机",** ** "type" : "手机",** ** "createtime" : "2020-05-21",** ** "tags" : [** ** "性价比",** ** "发烧",** ** "公交卡"** ** ]** ** }** ** },** ** {** ** "_index" : "product",** ** "_type" : "_doc",** ** "_id" : "3",** ** "_score" : 1.0,** ** "_source" : {** ** "id" : 3,** ** "name" : "NFC手机",** ** "desc" : "手机中的轰炸机",** ** "price" : 2999,** ** "lv" : "高端机",** ** "type" : "手机",** ** "createtime" : "2020-06-20",** ** "tags" : [** ** "性价比",** ** "快充",** ** "门禁卡"** ** ]** ** }** ** }** ** ]** ** },** "aggregations" : { "tags_bucket" : { "doc_count_error_upper_bound" : 0, "sum_other_doc_count" : 16, "buckets" : [ { "key" : "性价比", "doc_count" : 3 }, { "key" : "发烧", "doc_count" : 2 }, { "key" : "大片", "doc_count" : 2 }, { "key" : "蓝光8K", "doc_count" : 2 }, { "key" : "超薄", "doc_count" : 2 }, { "key" : "120HZ刷新率", "doc_count" : 1 }, { "key" : "120W快充", "doc_count" : 1 }, { "key" : "120倍变焦", "doc_count" : 1 }, { "key" : "4G 全网通", "doc_count" : 1 }, { "key" : "5V1A", "doc_count" : 1 } ] } } } ``` ## **Global 指定分桶查询中,取消前置条件,再查询** **例如,同时查询所有商品的平均价格,和价格大于 4000 的商品的平均价格** ``` GET /product/_search { "query": { "range": { "price": { "gte": 4000 } } }, "aggs": { "avg_agg": { "avg": { "field": "price" } }, "all_avg_agg": { ** "global": {},** "aggs": { "all_avg_agg": { "avg": { "field": "price" } } } } } } ``` ## filter,基于基础条件的个性化查询(后置过滤) **例如,查询价格大于 4000 的商品,和价格大于 4500 的商品的平均价格** ``` GET /product/_search { "size": 0, "query": { "range": { "price": { "gte": 4000 } } }, "aggs": { "price_avg_agg": { "avg": { "field": "price" } }, ** "gte_avg_agg":{** ** "filter": {** ** "range": {** ** "price": {** ** "gte": 4500** ** }** ** }** ** },** ** "aggs": {** ** "success_avg_agg":{** ** "avg": {** ** "field": "price"** ** }** ** }** ** }** ** },** ** "lte_avg_agg":{** ** "filter": {** ** "range": {** ** "price": {** ** "lte": 3000** ** }** ** }** ** },** ** "aggs": {** ** "fail_avg_agg":{** ** "avg": {** ** "field": "price"** ** }** ** }** ** }** ** }** } } { "took" : 0, "timed_out" : false, "_shards" : { "total" : 1, "successful" : 1, "skipped" : 0, "failed" : 0 }, "hits" : { "total" : { "value" : 3, "relation" : "eq" }, "max_score" : null, "hits" : [ ] }, "aggregations" : { ** "lte_avg_agg" : {** ** "doc_count" : 0,** ** "fail_avg_agg" : {** ** "value" : null** ** }** ** },** "gte_avg_agg" : { "doc_count" : 2, "success_avg_agg" : { "value" : 5499.0 } }, "price_avg_agg" : { "value" : 5132.333333333333 } } } ``` **注意:后面加了一项 **** fail_avg_agg ****目的是用来测试当 filter 中的条件与基础条件相悖的时候查询的结果是什么,根据结果可以得出结论, **** filter ****是用来对基础条件查询后的记过进行后置过滤的,其中的条件如果相悖,则没有数据,也毫无意义。** ## 聚合排序 - _count,根据数量排序(默认)desc - _key,根据 key 的自然顺序(0-9a-z) - _term,根据 key 的自然顺序(0-9a-z) - 与 key 排序一样,建议直接使用_key 排序 - 如果是嵌套聚合,可以使用下级的排序信息进排序, ``` "order": { "price_stats_filter>price_stats.min": "asc" } ``` ### 单层聚合排序 ``` GET /product/_search { "size": 0, "aggs": { "tag_count_agg": { "terms": { "field": "tags.keyword", "size": 3, "order": { "_count": "desc" } } }, "tag_key_agg": { "terms": { "field": "tags.keyword", "size": 3, "order": { "_key": "desc" } } }, "tag_term_agg": { "terms": { "field": "tags.keyword", "size": 3, "order": { "_term": "asc" } } } } } #! Deprecation: Deprecated aggregation order key [_term] used, replaced by [_key] { "took" : 0, "timed_out" : false, "_shards" : { "total" : 1, "successful" : 1, "skipped" : 0, "failed" : 0 }, "hits" : { "total" : { "value" : 11, "relation" : "eq" }, "max_score" : null, "hits" : [ ] }, "aggregations" : { "tag_count_agg" : { "doc_count_error_upper_bound" : 0, "sum_other_doc_count" : 25, "buckets" : [ { "key" : "性价比", "doc_count" : 3 }, { "key" : "发烧", "doc_count" : 2 }, { "key" : "大片", "doc_count" : 2 } ] }, "tag_term_agg" : { "doc_count_error_upper_bound" : 0, "sum_other_doc_count" : 29, "buckets" : [ { "key" : "120HZ刷新率", "doc_count" : 1 }, { "key" : "120W快充", "doc_count" : 1 }, { "key" : "120倍变焦", "doc_count" : 1 } ] }, "tag_key_agg" : { "doc_count_error_upper_bound" : 0, "sum_other_doc_count" : 29, "buckets" : [ { "key" : "降噪", "doc_count" : 1 }, { "key" : "防火", "doc_count" : 1 }, { "key" : "防水", "doc_count" : 1 } ] } } } ``` ### 嵌套聚合排序 **例如:查询每个类型的价格信息,然后按照价格中最小值排序** ``` GET /product/_search { "size": 0, "aggs": { "type_agg": { "terms": { "field": "type.keyword", "size": 99, "order": { ** "price_stats_filter>price_stats.min": "asc"** } }, "aggs": { "price_stats_filter": { "filter": { "terms": { "type.keyword": [ "手机", "耳机", "电视" ] } }, "aggs": { "price_stats": { "stats": { "field": "price" } } } } } } } } ``` ## 常用的聚合函数 - Histogram 直方图,或者柱状图 - Percentile 百分位统计,或者 饼状图 ### 例子:统计价格区间内的数量 ``` GET /product/_search { "size": 0, "aggs": { "price_range": { "range": { "field": "price", "ranges": [ { "to": 1000 }, { "from": 1000, "to": 2000 }, { "from": 2000, "to": 3000 }, { "from": 3000, "to": 4000 }, { "from": 4000 } ] } } } } { "took" : 0, "timed_out" : false, "_shards" : { "total" : 1, "successful" : 1, "skipped" : 0, "failed" : 0 }, "hits" : { "total" : { "value" : 11, "relation" : "eq" }, "max_score" : null, "hits" : [ ] }, "aggregations" : { "price_range" : { "buckets" : [ { "key" : "*-1000.0", "to" : 1000.0, "doc_count" : 2 }, { "key" : "1000.0-2000.0", "from" : 1000.0, "to" : 2000.0, "doc_count" : 0 }, { "key" : "2000.0-3000.0", "from" : 2000.0, "to" : 3000.0, "doc_count" : 4 }, { "key" : "3000.0-4000.0", "from" : 3000.0, "to" : 4000.0, "doc_count" : 2 }, { "key" : "4000.0-*", "from" : 4000.0, "doc_count" : 3 } ] } } } ``` 以上这种固定间隔的查询可以使用 `histogram` 替代 ### **histogram ** **histogram 参数:** - interval、【 int 】、步长 - keyed、【ture | false 】、是否使用 key - min_doc_count、【 int 】、筛选用,count 值比这个大于等于才展示 - Missing、【other 】、如果筛选的字段有的数据没有值或为空,那么用这个数值填充 ``` GET /product/_search { "size": 0, "aggs": { "price_range": { "histogram": { "field": "price", "interval": 1000 } } } } ``` ``` GET /product/_search { "size": 0, "aggs": { "price_rang": { "histogram": { "field": "price", "interval": 1000, "keyed": true, "min_doc_count": 1, "missing": 999 } } } } ``` ### data-**histogram ** **fixed_interval \ calendar_interval**** ****官方文档:** `fixed_interval ` **参数 ms、s、m、h、d(毫秒,秒,时、天)** `calendar_interval` 参数:**minute****,****1m****、****hour****,****1h****、****day****,****1d****、****week****,****1w****、****month****,****1M****、****quarter****,****1q****、****year****,****1y** `extended_bounds` - 数据填充 - "min": "2020-01", - "max": "2020-12" **统计创建时间的月份与商品数量的关系** ``` GET /product/_search { "size": 0, "aggs": { "my_date_product": { "date_histogram": { "field": "createtime", "interval": "month", "format": "yyyy-MM", "min_doc_count": 1 } } } } #! Deprecation: [interval] on [date_histogram] is deprecated, use [fixed_interval] or [calendar_interval] in the future. { "took" : 0, "timed_out" : false, "_shards" : { "total" : 1, "successful" : 1, "skipped" : 0, "failed" : 0 }, "hits" : { "total" : { "value" : 11, "relation" : "eq" }, "max_score" : null, "hits" : [ ] }, "aggregations" : { "my_date_product" : { "buckets" : [ { "key_as_string" : "2020-05", "key" : 1588291200000, "doc_count" : 1 }, { "key_as_string" : "2020-06", "key" : 1590969600000, "doc_count" : 2 }, { "key_as_string" : "2020-07", "key" : 1593561600000, "doc_count" : 3 }, { "key_as_string" : "2020-08", "key" : 1596240000000, "doc_count" : 4 }, { "key_as_string" : "2020-10", "key" : 1601510400000, "doc_count" : 1 } ] } } } ``` **注意上面有一个报错,** `#! Deprecation: [interval] on [date_histogram] is deprecated, use [fixed_interval] or [calendar_interval] in the future.` 告诉我们 `interval在date_histogram` 中未来要被弃用了,可以使用 `fixed_interval` 或者 `calendar_interval` 替代 `interval` ``` GET /product/_search { "size": 0, "aggs": { "my_date_product": { "date_histogram": { "field": "createtime", "calendar_interval": "month", "extended_bounds": { "min": "2020-01", "max": "2020-12" }, "order": { "_count": "desc" } } } } } ``` ### percentile_ranks 饼图类型数据 **官方文档** # 脚本查询 ## 语法: ``` ctx._source.<field-name> { "script": { "source": "ctx._source.<field-name> = <update-value>" } } ``` **Ctx 是指 doc 中的上下文,可以简单理解为_doc 的数据** ## **例子:价格减一** ``` GET /product/_doc/2 POST /product/_update/2 { "script": { "source": "ctx._source.price-=1" } } # 减去版本号的值 POST /product/_update/2 { "script": { "source": "ctx._source.price-=ctx._version" } } # 简写 POST /product/_update/2 { "script": "ctx._source.price-=1" } ``` ## 例子:索引复制 ``` POST _reindex { "source": { "index": "product" }, "dest": { "index": "product_backup" } } ``` ## 例子:tags.add、小米 10 出了新款, 新增 无线充电 TAG ``` GET /product/_doc/2 POST /product/_update/2 { "script": { "source": "ctx._source.tags.add('无线充电')" } } GET /product/_doc/2 ``` ## 例子:删除数据 ``` GET /product/_doc/11 POST /product/_update/11 { "script": { ** "lang":"painless",** ** "source": "ctx.op='delete'"** } } GET /product/_doc/11 ``` ## 例子:upsert,即 update | insert ,既可以修改也可以新增 ``` GET /product/_doc/6 POST /product/_update/6 { "script": { "lang": "painless", "source": "ctx._source.price += 100" }, "upsert": { "name": "小米手机10", "desc": "充电速度快,掉电更快", "price": 1999 } } GET /product/_doc/6 GET /product/_doc/15 POST /product/_update/15 { "script": { "lang": "painless", "source": "ctx._source.price += 100" }, "upsert": { "name": "小米手机10", "desc": "充电速度快,掉电更快", "price": 1999 } } GET /product/_doc/15 ``` 这里的 script 与 upsert 的作用是这样的 如果数据存在,就更新 "ctx._source.price += 100" 如果数据不存在,就插入 upsert ## 例子:GET 查询 ### **script_fields 案例:打八折** ``` GET product/_search { "script_fields": { "price": { ** "script": {** ** "lang": "painless",** ** "source": "doc['price'].value"** ** }** }, "discount_price": { "script": { "lang": "painless", "source": "doc['price'].value * params.discount_8", "params": { "discount_8": 0.8, "discount_7": 0.7, "discount_6": 0.6, "discount_5": 0.5 } } } } } { "took" : 0, "timed_out" : false, "_shards" : { "total" : 1, "successful" : 1, "skipped" : 0, "failed" : 0 }, "hits" : { "total" : { "value" : 11, "relation" : "eq" }, "max_score" : 1.0, "hits" : [ { "_index" : "product", "_type" : "_doc", "_id" : "5", "_score" : 1.0, "fields" : { "price" : [ 399 ], ** "discount_price" : [** ** 319.20000000000005** ** ]** } }, { "_index" : "product", "_type" : "_doc", "_id" : "7", "_score" : 1.0, "fields" : { "price" : [ 3299 ], "discount_price" : [ 2639.2000000000003 ] } }, { "_index" : "product", "_type" : "_doc", "_id" : "8", "_score" : 1.0, "fields" : { "price" : [ 4399 ], "discount_price" : [ 3519.2000000000003 ] } } ] } } ``` ## Stored scripts scripts 模版 ### 创建模版 ``` # 语法 # POST /scripts/<模版ID> POST /_scripts/calculate_discount { "script":{ "lang":"painless", "source": "doc.price.value * params.discount" } } ``` ### 查看模版 ``` # 语法 # GET /_scripts/<模版ID> GET /_scripts/calculate_discount { "_id" : "calculate_discount", "found" : true, "script" : { "lang" : "painless", "source" : "doc.price.value * params.discount" } } ``` ### 使用模版 ``` GET /product/_search { "size": 5, "script_fields": { "price": { "script": { "lang": "painless", "source": "doc['price'].value" } }, "discount_price": { "script": { "id": "calculate_discount", "params": { "discount": 0.8 } } } } } ``` ## Script 函数式编程 ### 官方文档: ### **定义多行文本采用 """三个英文的双引号包裹** ``` GET /product/_doc/1 POST /product/_update/1 { "script": { "lang": "painless", ** "source": """** ** ctx._source.tags.add(params.tag_name);** ** ctx._source.price-=100;** ** """,** "params": { "tag_name": "无线秒充2" } } } GET /product/_doc/1 ``` ### IF 与正则匹配 如果要是用正则表达式,则需要在 elasticsearch.yml 配置 **script.painless.regex.enabled: true** **官方不建议使用正则表达式,这个耗费资源,所以官方默认没有打开这个** **正则表达式 ==~** **匹配 % 小米 %** **ctx.op = "noop" 代表什么也不执行** ``` GET /product/_doc/1 POST /product/_update/1 { "script": { "lang": "painless", "source": """ if(ctx._source.name ==~ /[\s\S]*小米[\s\S]*/){ ctx._source.name += "***|"; } else { ctx.op = "noop"; } """ } } GET /product/_doc/1 ``` ### 通过脚本,统计所有价格小于 1000 的商品的 tag 的数量,不考虑重复的情况 ``` GET /product/_search { "query": { "constant_score": { "filter": { "range": { "price": { "lte": 1000 } } } } }, "aggs": { "tag_agg": { ** "sum": {** "script": { "lang": "painless", "source": """ ** int total = 0;** ** for(int i= 0; i< doc['tags.keyword'].length; i++){** ** total ++;** ** }** ** return total;** """ } } } } } GET /product/_search { "query": { "constant_score": { "filter": { "range": { "price": { "lte": 1000 } } } } }, "aggs": { "tag_agg": { ** "sum": {** ** "script": {** ** "lang": "painless",** ** "source": "doc['tags.keyword'].length"** ** }** ** }** } } } ``` ### 脚本中获取参数的两个方式 - doc["field"].value - 会把数据加载到内存 - 有缓存 - 只支持简单类型,简单类型查询时 推荐使用 - params["_source"]["field"] - 支持复杂类型 - 需要重新加载数据 - 复杂类型查询时推荐使用 ### params 使用实例   # 批量操作 - 基于 mget 的批量查询 - 基于 bulk 的批量操作 这部分知识点在上面,已经预习过了 位置: [Elasticsearch](https://y7fgg0syih.feishu.cn/docx/doxcn6iYAXTmdOhNAmts6eb26Rg) 文档操作 -> 批量操作