docker 安装 elasticsearch、kibana
elasticsearch
创建存储目录
1 2
| mkdir dockerEs mkdir data
|
拉取镜像,启动
1 2 3 4 5 6 7 8 9 10 11
| docker pull docker.elastic.co/elasticsearch/elasticsearch:8.10.4 docker run -d \ --name elasticsearch \ -p 9200:9200 \ -p 9300:9300 \ -e "discovery.type=single-node" \ -e "ES_JAVA_OPTS=-Xms512m -Xmx512m" \ -e "xpack.security.enabled=true" \ -e "ELASTIC_PASSWORD=elastic" \ -v /Users/qichuanhan/NoIconApplications/dockerEs/data:/usr/share/elasticsearch/data \ docker.elastic.co/elasticsearch/elasticsearch:8.10.4
|
kibana
kibana启动必须要一个新用户,不能使用elastic,因为elastic权限太高,如果使用kibana启动时会有提示然后启动不成功。
1 2 3 4 5 6 7 8 9 10 11 12 13
| curl -X POST "http://localhost:9200/_security/user/kibana_user" \ -u elastic:elastic \ -H "Content-Type: application/json" \ -d '{ "password": "elastic", "roles": ["kibana_system"], "full_name": "Kibana Service User" }'
curl -X GET "http://localhost:9200/_security/user/kibana_user" \ -u elastic:elastic
|
1 2 3 4 5 6 7 8 9 10
| docker pull kibana:8.10.4
docker run -d \ --name kibana \ -p 5601:5601 \ -e "ELASTICSEARCH_HOSTS=http://192.168.64.1:9200" \ -e "ELASTICSEARCH_USERNAME=kibana_user" \ -e "ELASTICSEARCH_PASSWORD=elastic" \ -e "I18N_LOCALE=zh-CN" \ kibana:8.10.4
|
概述
https://www.elastic.co/cn/elasticsearch/
https://www.elastic.co/cn/elastic-stack/
Elastic-stack是一个开源的数据分析和可视化平台,由Elasticsearch、Logstash、Kibana和Beats组成
它在各种应用场景中被广泛用于实时数据分析、搜索和可视化。
ElasticSearch
- 一个分布式实时搜索和分析引擎,高性能和可伸缩性闻名,能够快速地存储、搜索和分析大量结构化和非结构化数据
- 基于java开发,它是Elastic Stack的核心组件,提供分布式数据存储和搜索能力。
Logstash
- 是一个用于数据收集、转换和传输的数据处理引擎,支持从各种来源(如文件、日志、数据库等)收集数据
- 基于java开发,并对数据进行结构化、过滤和转换,然后将数据发送到Elasticsearch等目标存储或分析系统。
Kibana
- 基于node.js开发,数据可视化和仪表盘工具,连接到Elasticsearch,通过简单易用的用户界面创建各种图表、图形和仪表盘
- 帮助用户快速探索和理解数据,并进行强大的数据分析和可视化
Beats
- 是轻量级的数据收集器,用于收集和发送各种类型的数据到Elasticsearch或Logstash
- Beats提供了多种插件和模块,用于收集和传输日志、指标数据、网络数据和安全数据等
- 注意
- beats是用来优化logstash的,因为logstash消耗的性能比较多
- 如果只是单纯的为了收集日志,使用logstash就有点大材小用了,另外有点浪费资源
- 而beats是轻量级的用来收集日志的, 而logstash更加专注一件事,那就是数据转换,格式化,等处理工作
总结
- Elastic Stack的架构主要是将各个组件连接起来形成数据处理和分析的流程
- 数据可以通过Logstash的输入插件进行收集和处理,然后通过输出插件将数据发送到Elasticsearch进行存储和索引
- Kibana可与Elasticsearch进行连接,提供灵活的可视化界面,将数据分析结果以图表、仪表盘等形式展示出来
- Beats作为数据收集器,可以直接收集数据并将其发送到Elasticsearch或Logstash。
- 整个Elastic Stack的架构是高度可扩展和灵活的,可以根据特定需求进行定制和扩展
- 它被广泛应用于日志分析、实时监控、安全分析、商业智能和搜索等领域,为用户提供强大的数据处理和分析能力
什么是ElasticSearch
是⼀个开源,是⼀个基于Apache Lucene库构建的Restful搜索引擎. Elasticsearch是在Solr之后⼏年推出的。
它提供了⼀个分布式,多租户能⼒的全⽂搜索引擎,具有HTTP Web界⾯(REST)和⽆架构JSON⽂档
Elasticsearch的官⽅客户端库提供Java,Groovy,PHP,Ruby,Perl,Python,.NET和Javascript。
应用场景
搜索引擎
- 可以快速而准确地搜索大量的结构化和非结构化数据,用于构建搜索引擎、电商网站搜索等。
实时日志分析
- 提供了强大的实时索引功能和聚合功能,非常适合实时日志分析。
数据分析和可视化
- Elasticsearch与Kibana(Elastic Stack的一个组件)结合使用,可以进行复杂的数据分析和可视化。
实时推荐系统
- 基于Elasticsearch的实时搜索和聚合功能,可以用于构建实时推荐系统。
- 根据用户的行为和兴趣,动态地推荐相关的内容和产品。
电商商品搜索和过滤
- Elasticsearch具有强大的搜索和过滤能力,使其成为构建电商网站的商品搜索和过滤引擎的理想选择。
- 可以快速搜索和过滤商品属性、价格范围、地理位置等。
地理空间数据分析
- Elasticsearch内置了地理空间数据类型和查询功能,可以对地理位置进行索引和搜索
- 适合于构建地理信息系统(GIS)、位置服务和地理数据分析应用。
核心概念快速上手
- 在新版Elasticsearch中,文档document就是一行记录(json),而这些记录存在于索引库(index)中, 索引名称必须是小写
Mysql数据库 |
Elastic Search |
Database |
7.X版本前有Type,对比数据库中的表,新版取消了 |
Table |
Index |
Row |
Document |
Column |
Field |
元数据
- Elasticsearch中以 “ _” 开头的属性都成为元数据,都有自己特定的意思
分片shards
- 数据量特大,没有足够大的硬盘空间来一次性存储,且一次性搜索那么多的数据,响应跟不上
- ES提供把数据进行分片存储,这样方便进行拓展和提高吞吐
|
分片数据集一(一~一千万) |
分片数据集二(一千万~两千万) |
分片数据集三(两千万~三千万) |
副本replicas
- 分片的拷贝,当主分片不可用的时候,副本就充当主分片进行使用
- 索引分片的备份,shard和replica一般存储在不同的节点上,用来提高高可靠性
- 案例
- 假如Elasticsearch中的每个索引分配3个主分片和1个副本
- 如果集群中至少有两个节点,索引将会有3个主分片和另外3个复制分片(1个完全拷贝)这样每个索引总共有6个分片
主机A |
主机B |
分片数据集一(一~一千万) |
分片数据集一副本(一~一千万) |
分片数据集二(一千万~两千万) |
分片数据集二副本(一千万~两千万) |
分片数据集三(两千万~三千万) |
分片数据集三副本(两千万~三千万) |
总结
- ES默认为一个索引创建1个主分片和1个副本,在创建索引的时候使用settings属性指定,每个分片必须有零到多个副本
- 注意:索引一旦创建成功,主分片primary shard数量不可以变(只能重建索引),副本数量可以改变
正排索引和倒排索引
什么是正排索引 (Forward Index )
- 指将文档的内容按照文档的顺序进行索引,每个文档对应一个索引条目,包含了文档的各个字段的内容
- 例子假设我们有三个文档的标题和内容
- 文档1:标题 “Apple iPhone 12”,内容 “The latest iPhone model”
- 文档2:标题 “Samsung Galaxy S21”,内容 “Powerful Android smartphone”
- 文档3:标题 “Microsoft Surface Laptop”,内容 “Thin and lightweight laptop”
- 正排索引的结构示意图如下
1 2 3 4 5
| DocumentID | Title | Content ----------------------------------------------------------- 1 | Apple iPhone 12 | The latest iPhone model 2 | Samsung Galaxy S21 | Powerful Android smartphone 3 | Microsoft Surface Laptop | Thin and lightweight laptop
|
- 正排索引的优势在于可以快速的查找某个文档里包含哪些词项。但是 正排不适用于查找包含某个词项的文档有哪些
- 在数据库系统中,将正排索引类比为表格的结构,每一行是一个记录,每一列是一个字段
学生ID |
姓名 |
年龄 |
成绩 |
1 |
lisi |
20 |
90 |
2 |
zhansan |
18 |
85 |
3 |
Carol |
19 |
92 |
4 |
David |
21 |
88 |
5 |
Ellen |
22 |
91 |
倒排索引(Inverted Index)
根据关键词构建的索引结构,记录了每个关键词出现在哪些文档或数据记录中,适用于全文搜索和关键词检索的场景
它将文档或数据记录划分成关键词的集合,并记录每个关键词所出现的位置和相关联的文档或数据记录的信息
案例一
- 例子假设 使用以下文档构建倒排索引
- 文档1:标题 “Apple iPhone 12”,内容 “The latest iPhone model”
- 文档2:标题 “Samsung Galaxy S21”,内容 “Powerful Android smartphone”
- 文档3:标题 “Microsoft Surface Laptop”,内容 “Thin and lightweight laptop Apple”
- 倒排索引的结构示意图如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| Term | Documents ----------------------------------------- Apple | 1,3 iPhone | 1 12 | 1 latest | 1 Samsung | 2 Galaxy | 2 S21 | 2 Powerful | 2 Android | 2 smartphone| 2 Microsoft | 3 Surface | 3 Laptop | 3 Thin | 3 lightweight| 3
|
案例二
- 假设我们有以下三个文档
- 文档1:我喜欢吃苹果
- 文档2:我喜欢吃橙子和苹果
- 文档3:我喜欢吃香蕉
- 倒排索引的结构示意图如下
- 关键词 “我”:文档1、文档2、文档3
- 关键词 “喜欢”:文档1、文档2、文档3
- 关键词 “吃”:文档1、文档2、文档3
- 关键词 “苹果”:文档1、文档2
- 关键词 “橙子”:文档2
- 关键词 “香蕉”:文档3
- 通过倒排索引,可以快速地找到包含特定关键词的文档或数据记录
倒排索引记录了每个词语出现在哪些文档中,通过这种方式,我们可以非常快速地得知每个关键词分别出现在哪些文档中。
当我们需要搜索包含特定关键词的文档时,可以直接查找倒排索引,找到包含该关键词的文档
总结
- 正排索引和倒排索引的结构和用途不同
- 正排索引用于快速访问和提取文档的内容
- 倒排索引用于快速定位和检索包含特定词语的文档
搜索引擎的分词
什么是搜索引擎的分词
- 在Elasticsearch 8.X中,分词(tokenization)是将文本内容拆分成独立的单词或词项(tokens)的过程
- 分词是搜索引擎在建立索引和执行查询时的关键步骤,将文本拆分成单词,并构建倒排索引,可以实现更好的搜索和检索效果。
- 案例
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| 假设我们有两个产品标题:
"Apple iPhone 12 Pro Max 256GB"
"Samsung Galaxy S21 Ultra 128GB"
使用默认的标准分词器(Standard Tokenizer),这些标题会被分割为以下令牌:
标题1:["Apple", "iPhone", "12", "Pro", "Max", "256GB"]
标题2:["Samsung", "Galaxy", "S21", "Ultra", "128GB"]
分词器根据标点符号和空格将标题拆分为独立的词语。当我们执行搜索时,可以将查询进行分词,并将其与标题中的令牌进行匹配。
例如 如果我们搜索"iPhone 12",使用默认的分词器,它会将查询分解为["iPhone", "12"],然后与令牌进行匹配。 对于标题1,令牌["iPhone", "12"]匹配,它与查询相符。 标题2中没有与查询相符的令牌
|
- 分词规则是指定义如何将文本进行拆分的规则和算法
- Elasticsearch使用一系列的分词器(analyzer)和标记器(tokenizer)来处理文本内容
- 分词器通常由一个或多个标记器组成,用于定义分词的具体规则
- 以下是分词的一般过程:
- 标记化(Tokenization):
- 分词的第一步是将文本内容拆分成单个标记(tokens),标记可以是单词、数字、特殊字符等。
- 标记化过程由标记器(tokenizer)执行,标记器根据一组规则将文本切分为标记。
- 过滤(Filtering):
- 标记化后,标记会进一步被过滤器(filters)处理。
- 过滤器执行各种转换和操作,如转换为小写、去除停用词(stop words),词干提取(stemming),同义词扩展等。
- 倒排索引(Inverted Indexing):
- 分词处理完成后,Elasticsearch使用倒排索引(inverted index)来存储分词结果。
- 倒排索引是一种数据结构,通过将标记与其所属文档进行映射,快速确定包含特定标记的文档。
- 查询匹配:
- 当执行查询时,查询的文本也会进行分词处理。
- Elasticsearch会利用倒排索引来快速查找包含查询标记的文档,并计算相关性得分。
- 常见的分词器,如Standard分词器、Simple分词器、Whitespace分词器、IK分词等,还支持自定义分词器
默认的Standard分词器的分词规则
- 标点符号切分:
- 标点符号会被删除,并将连字符分隔为两个独立的词。
- 例如,”Let’s go!” 会被切分为 “Let”, “s”, “go”。
- 小写转换:
- 所有的文本会被转换为小写形式。
- 例如,”Hello World” 会被切分为 “hello”, “world”。
- 停用词过滤:
- 停用词(stop words)是在搜索中没有实际意义的常见词,如 “a”, “an”, “the” 等。
- 停用词会被过滤掉,不会作为独立的词进行索引和搜索。
- 词干提取:
- 通过应用Porter2词干提取算法,将单词还原为其原始形式。
- 例如,running -> run、swimming -> swim、jumped -> jump
- 词分隔:
kibana 开发者工具(类似postman)
进入http://localhost:5601
使用elasticsearch的用户名密码登录,然后点击三条杠 显示出左侧抽屉菜单栏,选择management下的开发工具。
点击执行按钮,就可以执行了。
HTTP API
1 2 3 4 5 6 7 8 9 10 11
| GET /_cluster/health
GET /_cat/shards?v=true&pretty
GET /_cat/nodes?v=true&pretty
GET /_cat/indices?v=true&pretty
|
索引操作(index)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47
|
PUT /<index_name> { "settings": { "number_of_shards": 1, "number_of_replicas": 1 } }
PUT /shop { "settings": { "number_of_shards": 2, "number_of_replicas": 1 } }
HEAD /<index_name>
HEAD /shop
GET /<index_name>
GET /shop
PUT /<index_name>/_settings { "settings": { "number_of_replicas": 2 } }
PUT /shop/_settings { "settings": { "number_of_replicas": 2 } }
DELETE /<index_name>
DELETE /shop
|
文档操作(document)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58
| GET /shop/_doc/1
PUT /shop/_doc/1 { "id":5555, "title":"xxx", "pv":144 }
POST /shop/_doc { "id":123, "title":"xxxx", "pv":244 }
PUT /shop/_doc/1 { "id":999, "title":"xxv2", "pv":999, "uv":55 }
POST /shop/_doc/1 { "id":999, "title":"xxxv3", "pv":999, "uv":559 }
GET /shop/_search
DELETE /shop/_doc/1
POST /product/_bulk { "index": { "_id": "1" } } { "product_id": 1, "product_name": "Product 1", "category": "books", "price": 19.99, "availability": true } { "index": { "_id": "2" } } { "product_id": 2, "product_name": "Product 2", "category": "electronics", "price": 29.99, "availability": true }
|
mapping操作
什么是Mapping
- 类似于数据库中的表结构定义 schema,
- 定义索引中的字段的名称,字段的数据类型,比如字符串、数字、布尔等
Dynamic Mapping(动态映射)
- 用于在索引文档时自动检测和定义字段的数据类型
- 当我们向索引中添加新文档时,Elasticsearch会自动检测文档中的各个字段,并根据它们的值来尝试推断字段类型
- 常见的字段类型包括文本(text)、关键词(keyword)、日期(date)、数值(numeric)等
- 动态映射具备自动解析和创建字段的便利性,但在某些情况下,由于字段类型的不确定性,动态映射可能会带来一些问题
- 例如字段解析错误、字段类型不一致等,如果对字段类型有明确的要求,最好在索引创建前通过显式映射定义来指定字段类型
ElasticSearch常见的数据类型
- 在 ES 7.X后有两种字符串类型:Text 和 Keyword
- Text类型:用于全文搜索的字符串类型,支持分词和索引建立
- Keyword类型:用于精确匹配的字符串类型,不进行分词,适合用作过滤和聚合操作。
- Numeric类型:包括整数类型(long、integer、short、byte)和浮点数类型(double、float)。
- Date类型:用于存储日期和时间的类型。
- Boolean类型:用于存储布尔值(true或false)的类型。
- Binary类型:用于存储二进制数据的类型。
- Array类型:用于存储数组或列表数据的类型。
- Object类型:用于存储复杂结构数据的类型
最高频使用的数据类型
text字段类型
- text类型主要用于全文本搜索,适合存储需要进行全文本分词的文本内容,如文章、新闻等。
- text字段会对文本内容进行分词处理,将文本拆分成独立的词项(tokens)进行索引
- 分词的结果会建立倒排索引,使搜索更加灵活和高效。
- text字段在搜索时会根据分词结果进行匹配,并计算相关性得分,以便返回最佳匹配的结果。
keyword字段类型
- keyword类型主要用于精确匹配和聚合操作,适合存储不需要分词的精确值,如ID、标签、关键字等。
- keyword字段不会进行分词处理,而是将整个字段作为一个整体进行索引和搜索
- 这使得搜索只能从精确的值进行匹配,而不能根据词项对内容进行模糊检索。
- keyword字段适合用于过滤和精确匹配,同时可以进行快速的基于精确值的聚合操作。
总结
在选择text字段类型和keyword字段类型时,需要根据具体的需求进行权衡和选择:
如果需要进行全文本检索,并且希望根据分词结果计算相关性得分,以获得最佳的匹配结果,则选择text字段类型。
如果需要进行精确匹配、排序或聚合操作,并且不需要对内容进行分词,则选择keyword字段类型。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110
| GET /<index_name>/_mapping
GET /my_index/_mapping
PUT /my_index { "mappings": { "properties": { "id": { "type": "keyword" }, "title": { "type": "text" }, "price": { "type": "float" } } } }
PUT /my_index { "mappings": { "properties": { "title": { "type": "text" }, "tags": { "type": "keyword" }, "publish_date": { "type": "date" }, "rating": { "type": "float" }, "is_published": { "type": "boolean" }, "author": { "properties": { "name": { "type": "text" }, "age": { "type": "integer" } } }, "comments": { "type": "nested", "properties": { "user": { "type": "keyword" }, "message": { "type": "text" } } } } } }
POST /my_index/_doc/1 { "title": "xxx Elasticsearch Introduction", "tags": ["search", "big data", "distributed system", "xx"], "publish_date": "2025-01-01", "rating": 4.5, "is_published": true, "author": { "name": "John Doe", "age": 30 }, "comments": [ { "user": "Alice", "message": "Great article!" }, { "user": "Bob", "message": "Very informative." } ] }
GET /my_index/_search { "query": { "match": { "title": "Elasticsearch" } } }
GET /my_index/_search { "query": { "match": { "tags": "data" } } }
|
分词器API
分词器名称 |
主要特点描述 |
standard |
默认分词器。按词切分,处理大多数语言,会转小写并过滤标点符号。 |
simple |
遇到非字母字符(数字、空格等)就切分,并转小写。 |
whitespace |
简单地按空格进行切分,不转小写。 |
stop |
在 simple 的基础上,增加过滤停用词(如 “a”, “the”, “is”)的功能。 |
keyword |
不进行分词,将整个输入内容作为一个单独的词项输出。 |
pattern |
使用正则表达式来分割文本(默认按非字符 \W+ 分割)。 |
language |
针对特定语言(如 english , french )的分词器,遵循特定语言的规则。 |
fingerprint |
用于去重检测。它会去重、排序然后组合词项生成一个”指纹” |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46
| GET /_analyze { "analyzer": "分词器名称", "text": "待分析的文本" }
POST /my_index/_analyze { "field": "title", "text": "This is some text to analyze" }
POST /my_index/_analyze { "field": "title", "text": "今天我在课堂学习数学" }
POST /my_index/_analyze { "field": "tags", "text": "This is some text to analyze" }
POST /my_index/_analyze { "field": "tags", "text": ["This is","课堂","Spring Boot" ] }
|
Query DSL
什么是Query DSL
- Query DSL(Domain-Specific Language)是一种用于构建搜索查询的强大的领域特定查询语言
- 类似我们关系性数据库的SQL查询语法,
- ES中用JSON结构化的方式定义和执行各种查询操作,在ES中进行高级搜索和过滤
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
| GET /索引库名/_search { "query":{ "查询类型":{ } }
{ "query": { "match": { "title": "elasticsearch" } } }
{ "query": { "term": { "category": "books" } } }
|
数据准备
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36
| PUT /shop_v1 { "settings": { "number_of_shards": 1, "number_of_replicas": 1 }, "mappings": { "properties": { "id": { "type": "keyword" }, "title": { "type": "keyword" }, "summary": { "type": "text" }, "price": { "type": "float" } } } }
PUT /shop_v1/_bulk { "index": { "_index": "shop_v1" } } { "id": "1", "title": "Spring Boot","summary":"this is a summary Spring Boot video", "price": 9.99 } { "index": { "_index": "shop_v1" } } { "id": "2", "title": "java","summary":"this is a summary java video", "price": 19.99 } { "index": { "_index": "shop_v1" } } { "id": "3", "title": "Spring Cloud","summary":"this is a summary Spring Cloud video", "price": 29.99 } { "index": { "_index": "shop_v1" } } { "id": "4", "title": "Spring_Boot", "summary":"this is a summary Spring_Boot video","price": 59.99 } { "index": { "_index": "shop_v1" } } { "id": "5", "title": "SpringBoot","summary":"this is a summary SpringBoot video", "price": 0.99 }
|
match查询
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29
| GET /shop_v1/_search { "query": { "match_all": {} } }
GET /shop_v1/_search { "query": { "match": { "summary": "Spring" } } }
GET /shop_v1/_search { "query": { "match": { "summary": "Spring Java" } } }
|
term查询
1 2 3 4 5 6 7 8 9 10 11 12 13 14
|
GET /shop_v1/_search { "query": { "term": { "title": { "value": "Spring Boot" } } } }
|
获取指定字段
1 2 3 4 5 6 7 8 9 10 11 12 13
|
GET /shop_v1/_search { "_source":["price","title"], "query": { "term": { "title": { "value": "Spring Boot" } } } }
|
range查询
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
|
GET /shop_v1/_search { "query": { "range": { "price": { "gte": 5, "lte": 100 } } } }
|
分页
1 2 3 4 5 6 7 8 9 10 11
|
GET /shop_v1/_search { "size": 10, "from": 0, "query": { "match_all": {} } }
|
排序
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
|
GET /shop_v1/_search { "size": 10, "from": 0, "sort": [ { "price": "asc" } ], "query": { "match_all": {} } }
|
bool 查询
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127
|
{ "query": { "bool": { "must": [ // 必须匹配的条件 ], "must_not": [ // 必须不匹配的条件 ], "should": [ // 可选匹配的条件 ], "filter": [ // 过滤条件 ] } } }
GET /shop_v1/_search { "query": { "bool": { "must": [ { "match": { "summary": "Cloud" }}, { "range": { "price": { "gte": 5 }}} ] } } }
{ "query": { "bool": { "filter": { // 过滤条件 } } } }
PUT /product { "settings": { "number_of_shards": 2, "number_of_replicas": 0 }, "mappings": { "properties": { "product_id": { "type": "integer" }, "product_name": { "type": "text" }, "category": { "type": "keyword" }, "price": { "type": "float" }, "availability": { "type": "boolean" } } } }
POST /product/_bulk { "index": { "_id": "1" } } { "product_id": 1, "product_name": "Product 1", "category": "books", "price": 19.99, "availability": true } { "index": { "_id": "2" } } { "product_id": 2, "product_name": "Product 2", "category": "electronics", "price": 29.99, "availability": true } { "index": { "_id": "3" } } { "product_id": 3, "product_name": "Product 3", "category": "books", "price": 9.99, "availability": false } { "index": { "_id": "4" } } { "product_id": 4, "product_name": "Product 4", "category": "electronics", "price": 49.99, "availability": true } { "index": { "_id": "5" } } { "product_id": 5, "product_name": "Product 5", "category": "fashion", "price": 39.99, "availability": true }
GET /product/_search { "query": { "bool": { "filter": { "term": { "category": "books" } } } } }
GET /product/_search { "query": { "bool": { "filter": { "range": { "price": { "gte": 30, "lte": 50 } } } } } }
|
多字段搜索与短语搜索数据准备
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33
| PUT /product_v2 { "settings": { "number_of_shards": 2, "number_of_replicas": 0 }, "mappings": { "properties": { "product_name": { "type": "text" }, "description": { "type": "text" }, "category": { "type": "keyword" } } } }
POST /product_v2/_bulk { "index": { "_index": "product_v2", "_id": "1" } } { "product_name": "iPhone 12", "description": "The latest iPhone model from Apple", "category": "electronics" } { "index": { "_index": "product_v2", "_id": "2" } } { "product_name": "Samsung Galaxy S21", "description": "High-performance Android smartphone", "category": "electronics" } { "index": { "_index": "product_v2", "_id": "3" } } { "product_name": "MacBook Pro", "description": "Powerful laptop for professionals", "category": "electronics" } { "index": { "_index": "product_v2", "_id": "4" } } { "product_name": "Harry Potter and the Philosopher's Stone", "description": "Fantasy novel by J.K. Rowling", "category": "books" } { "index": { "_index": "product_v2", "_id": "5" } } { "product_name": "The Great Gatsby", "description": "Classic novel by F. Scott Fitzgerald", "category": "books" }
|
多字段搜索
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29
|
GET /index/_search { "query": { "multi_match": { "query": "要搜索的文本", "fields": ["字段1", "字段2", ...] } } }
GET /product_v2/_search { "query": { "multi_match": { "query": "iPhone", "fields": ["product_name", "description"] } } }
|
短语搜索
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39
|
GET /index/_search { "query": { "match_phrase": { "field_name": { "query": "要搜索的短语" } } } }
GET /product_v2/_search { "query": { "match_phrase": { "description": "classic novel" } } }
GET /product_v2/_search { "query": { "match": { "description": "classic novel" } } }
|
单词拼写错误-fuzzy模糊查询
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75
| 什么是fuzzy模糊匹配
* `fuzzy`查询是Elasticsearch中提供的一种模糊匹配查询类型,用在搜索时容忍一些拼写错误或近似匹配 * 使用`fuzzy`查询,可以根据指定的编辑距离(即词之间不同字符的数量)来模糊匹配查询词 * 拓展:编辑距离 * 是将一个术语转换为另一个术语所需的一个字符更改的次数。 * 比如 * 更改字符(box→fox) * 删除字符(black→lack) * 插入字符(sic→sick) * 转置两个相邻字符(dgo→dog) * fuzzy模糊查询是拼写错误的简单解决方案,但具有很高的 CPU 开销和非常低的精准度 * 用法和match基本一致,Fuzzy query的查询不分词
GET /index/_search { "query": { "fuzzy": { "field_name": { "value": "要搜索的词", "fuzziness": "模糊度" } } } }
解析: * `field_name`:要进行模糊匹配的字段名。 * `value`:要搜索的词 * `fuzziness`参数指定了模糊度,常见值如下 - `0,1,2` - 指定数字,表示允许的最大编辑距离,较低的数字表示更严格的匹配,较高的数字表示更松散的匹配 - fuziness的值,表示是针对每个词语而言的,而不是总的错误的数值 - `AUTO`:Elasticsearch根据词的长度自动选择模糊度 - 如果字符串的长度大于5,那 funziness 的值自动设置为2 - 如果字符串的长度小于2,那么 fuziness 的值自动设置为 0
# 指定模糊度2,更松散匹配 GET /shop_v1/_search { "query": { "fuzzy": { "summary": { "value": "clo", "fuzziness": "2" } } } }
# 指定模糊度1,更严格匹配 GET /shop_v1/_search { "query": { "fuzzy": { "summary": { "value": "clo", "fuzziness": "1" } } } }
# 使用自动检查,1个单词拼写错误 GET /shop_v1/_search { "query": { "fuzzy": { "summary": { "value": "Sprina", "fuzziness": "auto" } } } }
|
搜索高亮显示
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112
| Elastic Search搜索引擎如何做到高亮显示 * 在 ES 中,高亮语法用于在搜索结果中突出显示与查询匹配的关键词 * 高亮显示是通过标签包裹匹配的文本来实现的,通常是 `<em>` 或其他 HTML 标签 * 基本用法:在 highlight 里面填写要高亮显示的字段,可以填写多个
PUT /high_light_test { "mappings": { "properties": { "title": { "type": "text", "analyzer": "ik_max_word" }, "content": { "type": "text", "analyzer": "ik_max_word" } } }, "settings": { "number_of_shards": 2, "number_of_replicas": 0 } }
#插入数据 PUT /high_light_test/_doc/1 { "title": "课堂2028年最新好看的电影推荐", "content": "每年都有新电影上线,2028年最新好看的电影有不少,课堂上线了《架构大课》,《低代码平台》,《老王往事》精彩电影" }
PUT /high_light_test/_doc/2 { "title": "写下你认为好看的电影有哪些", "content": "每个人都看看很多电影,说下你近10年看过比较好的电影,比如《架构大课》,《海量数据项目大课》,《冰冰和老王的故事》" }
#单条件查询高亮显示
GET /high_light_test/_search { "query": { "match": { "content": "电影" } }, "highlight": { "fields": { "content": {} } } }
#组合多条件查询,highlight里面填写需要高亮的字段 GET /high_light_test/_search { "query": { "bool": { "should": [ { "match": { "title": "课堂" } }, { "match": { "content": "老王" } } ] } }, "highlight": { "fields": { "title": {}, "content": {} } } }
match查询,使用highlight属性,可以增加属性,修改高亮样式
* pre_tags:前置标签 * post_tags:后置标签 * fields:需要高亮的字段
GET /high_light_test/_search { "query": { "bool": { "should": [ { "match": { "title": "课堂" } }, { "match": { "content": "老王" } } ] } }, "highlight": { "pre_tags": "<font color='yellow'>", "post_tags": "</font>", "fields": [{"title":{}},{"content":{}}] } }
|
聚合数据准备
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31
| PUT /sales { "mappings": { "properties": { "product": { "type": "keyword" }, "sales": { "type": "integer" } } } }
POST /sales/_bulk {"index": {}} {"product": "iPhone", "sales": 4} {"index": {}} {"product": "Samsung", "sales": 60} {"index": {}} {"product": "iPhone", "sales": 100} {"index": {}} {"product": "Samsung", "sales": 80} {"index": {}} {"product": "手机", "sales": 50} {"index": {}} {"product": "手机", "sales": 5000} {"index": {}} {"product": "手机", "sales": 200}
|
指标聚合
对数据集求最大、最小、和、平均值等指标的聚合,称为 指标聚合 metric
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| GET /index/_search { "size": 0, "aggs": { "aggregation_name": { "aggregation_type": { "aggregation_field": "field_name" // 可选参数 } } // 可以添加更多的聚合 } }
# 解析 index:要执行聚合查询的索引名称。 size: 设置为 0 来仅返回聚合结果,而不返回实际的搜索结果,这里将hits改为0表示返回的原始数据变为0 aggs:指定聚合操作的容器。
aggregation_name:聚合名称,可以自定义。 aggregation_type:聚合操作的类型,例如 terms、avg、sum 等。 aggregation_field:聚合操作的目标字段,对哪些字段进行聚合
|
聚合指标(Aggregation Metrics):
- Avg Aggregation:计算文档字段的平均值。
- Sum Aggregation:计算文档字段的总和。
- Min Aggregation:找到文档字段的最小值。
- Max Aggregation:找到文档字段的最大值。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| max 数据准备 POST /sales_v1/_doc { "product_name": "a", "price": 1000 }
POST /sales_v1/_doc { "product_name": "b", "price": 1500 }
POST /sales_v1/_doc { "product_name": "b", "price": 4500 }
GET /sales_v1/_search { "size": 0, "aggs": { "max_price": { "max": { "field": "price" } } } }
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| min 数据准备 POST /exam_scores/_doc { "student_name": "a", "score" : 80 }
POST /exam_scores/_doc { "student_name": "b", "score" : 90 }
POST /exam_scores/_doc { "student_name": "c", "score" : 40 }
GET /exam_scores/_search { "size": 0, "aggs": { "min_score": { "min": { "field": "score" } } } }
|
1 2 3 4 5 6 7 8 9 10 11
| GET /exam_scores/_search { "size": 0, "aggs": { "avg_score": { "avg": { "field": "score" } } } }
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| sum 数据准备 POST /sales_order/_doc { "product_name": "a", "sales_count" : 100 }
POST /sales_order/_doc { "product_name": "b", "sales_count" : 50 }
POST /sales_order/_doc { "product_name": "c", "sales_count" : 999 }
GET /sales_order/_search { "size": 0, "aggs": { "total_sales": { "sum": { "field": "sales_count" } } } }
|
桶聚合
对数据集进行分组group by,然后在组上进行指标聚合,在 ES 中称为分桶,桶聚合bucketing
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33
| GET /index/_search { "size": 0, "aggs": { "aggregation_name": { "bucket_type": { "bucket_options": { "bucket_option_name": "bucket_option_value", ... }, "aggs": { "sub_aggregation_name": { "sub_aggregation_type": { "sub_aggregation_options": { "sub_aggregation_option_name": "sub_aggregation_option_value", ... } } } } } } } } #解析 index: 替换为要执行聚合查询的索引名称。 aggregation_name: 替换为自定义的聚合名称。 bucket_type: 替换为特定的桶聚合类型(如 terms、date_histogram、range 等)。 bucket_option_name 和 bucket_option_value: 替换为特定桶聚合选项的名称和值。
sub_aggregation_name: 替换为子聚合的名称。 sub_aggregation_type: 替换为特定的子聚合类型(如 sum、avg、max、min 等)。 sub_aggregation_option_name 和 sub_aggregation_option_value: 替换为特定子聚合选项的名称和值
|
聚合桶(Aggregation Buckets):
- Terms Aggregation:基于字段值将文档分组到不同的桶中。
- Date Histogram Aggregation:按日期/时间字段创建时间间隔的桶。
- Range Aggregation:根据字段值的范围创建桶。
按照商品名称(product
)进行分组:
1 2 3 4 5 6 7 8 9 10
| GET /sales/_search { "aggs":{//聚合操作 "product_group":{//名称,随意起名 "terms":{//分组 "field":"product"//分组字段 } } } }
|
计算每组的销售总量,使用了 terms
聚合和 sum
聚合来实现,查询结果将返回每个产品的名称和销售总量
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| GET /sales/_search { "size": 0, "aggs": { "product_sales": { "terms": { "field": "product" }, "aggs": { "total_sales": { "sum": { "field": "sales" } } } } } }
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62
| 数据准备 PUT /book_sales { "mappings": { "properties": { "book_title": { "type": "keyword" }, "sales_count": { "type": "integer" } } }, "settings": { "number_of_shards": 2, "number_of_replicas": 0 } }
POST /book_sales/_bulk { "index": {} } { "book_title": "Elasticsearch in Action", "sales_count" : 100 } { "index": {} } { "book_title": "a", "sales_count" : 50 } { "index": {} } { "book_title": "b", "sales_count" : 80 } { "index": {} } { "book_title": "c", "sales_count" : 120 } { "index": {} } { "book_title": "d", "sales_count" : 90 } { "index": {} } { "book_title": "e", "sales_count" : 70 } { "index": {} } { "book_title": "f", "sales_count" : 110 } { "index": {} } { "book_title": "h", "sales_count" : 200 } { "index": {} } { "book_title": "i", "sales_count" : 150 } { "index": {} } { "book_title": "g", "sales_count" : 80 }
GET /book_sales/_search { "size": 0, "aggs": { "book_buckets": { "terms": { "field": "book_title", "size": 10 }, "aggs": { "total_sales": { "sum": { "field": "sales_count" } } } } } }
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66
|
GET /index/_search { "size": 0, "aggs": { "date_histogram_name": { "date_histogram": { "field": "date_field_name", "interval": "interval_expression" }, "aggs": { "sub_aggregation": { "sub_aggregation_type": {} } } } } }
index:替换为要执行聚合查询的索引名称。 date_histogram_name:替换为自定义的 date_histogram 聚合名称。 date_field_name:替换为要聚合的日期类型字段名。 interval_expression:指定用于分桶的时间间隔。时间间隔可以是一个有效的日期格式(如 1d、1w、1M),也可以是一个数字加上一个时间单位的组合(如 7d 表示 7 天,1h 表示 1 小时)。 sub_aggregation:指定在每个日期桶内进行的子聚合操作。 sub_aggregation_type:替换单独子聚合操作的类型,可以是任何有效的子聚合类型。
POST /order_history/_bulk { "index": {} } { "order_date": "2025-01-01", "amount" : 100 ,"book_title": "a"} { "index": {} } { "order_date": "2025-02-05", "amount" : 150, "book_title": "b" } { "index": {} } { "order_date": "2025-03-02", "amount" : 500 ,"book_title": "a"} { "index": {} } { "order_date": "2025-05-02", "amount" : 250 , "book_title": "b"} { "index": {} } { "order_date": "2025-05-05", "amount" : 10 ,"book_title": "c"} { "index": {} } { "order_date": "2025-02-18", "amount" : 290 , "book_title": "c"}
GET /order_history/_search { "size": 0, "aggs": { "sales_per_month": { "date_histogram": { "field": "order_date", "calendar_interval": "month", "format": "yyyy-MM" }, "aggs": { "total_sales": { "sum": { "field": "amount" } } } } } }
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74
|
GET /index/_search { "size": 0, "aggs": { "range_name": { "range": { "field": "field_name", "ranges": [ { "key": "range_key_1", "from": from_value_1, "to": to_value_1 }, { "key": "range_key_2", "from": from_value_2, "to": to_value_2 }, ... ] }, "aggs": { "sub_aggregation": { "sub_aggregation_type": {} } } } } }
index:替换为要执行聚合查询的索引名称。 range_name:替换为自定义的 range 聚合名称。 field_name:替换为要聚合的字段名。 ranges:指定范围数组,每个范围使用 key、from 和 to 参数进行定义。 key:范围的唯一标识符。 from:范围的起始值(包含)。 to:范围的结束值(不包含)。 sub_aggregation:指定在每个范围内进行的子聚合操作。 sub_aggregation_type:替换单独子聚合操作的类型,可以是任何有效的子聚合类型。
POST /product_v4/_bulk { "index": {} } { "product_name": "a", "price" : 2000 } { "index": {} } { "product_name": "b", "price" : 200 } { "index": {} } { "product_name": "c", "price" : 300 } { "index": {} } { "product_name": "d", "price" : 1500 } { "index": {} } { "product_name": "e", "price" : 4120 } { "index": {} } { "product_name": "f", "price" : 180 } { "index": {} } { "product_name": "h", "price" : 250 } { "index": {} } { "product_name": "i", "price" : 4770 } { "index": {} } { "product_name": "g", "price" : 400 } { "index": {} } { "product_name": "k", "price" : 150 }
GET /product_v4/_search { "size": 0, "aggs": { "price_ranges": { "range": { "field": "price", "ranges": [ { "to": 100 }, { "from": 100, "to": 200 }, { "from": 200 } ] } } } }
|
深分页
1 2 3 4 5 6 7 8
| GET /shop_v1/_search { "size": 10, "from": 0, "query": { "match_all": {} } }
|
Es 分页查询的原理跟mysql是一致的,都是遍历所有匹配的文档直到达到指定的起始点(from),然后返回从这一点开始的size个文档。
但是es 不支持from + size 的和不能超过es索引的index.max_result_window设置,默认为10000。这意味着如果你设置from为9900,size为100,查询将会成功。但如果from为9900,size为101,则会失败。
Scroll api 在es中的主要目的是为了能够遍历大量的数据,它通常用于数据导出或者进行大规模的数据分析。可以用于处理大量数据的深度分页问题。
1 2 3 4 5 6 7
| GET /shop_v1/_search?scroll=1m { "size": 10, "query": { "match_all": {} } }
|
如上方式初始化一个带有scroll参数的搜索请求。这个请求返回一个scroll ID, 用于后续的滚动。scroll参数指定了scroll的有效期,例如1m表示一分钟。
接下来就可以使用返回的scroll ID 来获取下一批数据。每次请求也会更新scroll ID的有效期。
1 2 3 4 5
| GET /_search/scroll { "scroll":"1m", "scroll_id":"your_scroll_id" }
|
重复以上操作直到到达想要的页数。比如第10页,则需要执行9次滚动操作,然后第10次请求将返回第10页的数据。
1 2 3 4 5
| DELETE /_search/scroll { "scroll_id": "DnF1ZXJ5VGhlbkZldGNoBQAAAAAAAC..." }
|
特点:
1.避免重复排序:
在传统的分页方式中,每次分页请求都需要对所有匹配的数据进行排序,以确定分页的起点。scroll避免了这种重复排序,因为它保持了一个游标。
2.稳定视图:
Scroll提供了对数据的“稳定视图”。当你开始一个scroll时,es会保持搜索时刻换数据快照,这意味着即使数据随后被修改,返回的结果仍然是一致的。
3.减少资源消耗:
由于不需要重复排序,scroll减少了对CPU和内存的消耗,特别是对于大数据集。
Scroll非常适合于处理需要访问大量数据但不需要快速响应的场景,如数据导出、备份或大规模数据分析。
但是使用scroll api 进行分页并不高效,因为你需要先获取对所有前面页的数据。scroll api 主要用于遍历整个索引或大量数据,而不是用于快速访问特定页数的数据。
search_after
search_after是es中用于实现深度分页的一种机制。与传统的分页方法(from和size参数)不同,search_after允许你基于上一次查询的结果 来获取下一批数据,这在处理大量数据时特别有效。
在第一次查询时,你需要定义一个排序规则。不需要指定search_after参数:
1 2 3 4 5 6 7 8 9 10 11
| GET /shop_v1/_search { "size": 10, "query": { "match_all": {} }, "sort":[ {"timestamp":"asc"}, {"id":"asc"} ] }
|
这个查询按timestamp字段排序,并在相同timestamp的情况下按_id排序。
在后续的查询中,使用上一次查询结果中最后一条记录的排序值。
1 2 3 4 5 6 7 8 9 10 11 12
| GET /shop_v1/_search { "size": 10, "query": { "match_all": {} }, "sort":[ {"timestamp":"asc"}, {"id":"asc"} ], "search_after":[1609459200000, 10000] }
|
在这个例子中,search_after数组包含了timestamp和_id的值,对应于上一次查询结果的最后一条记录。
search_after可以有效解决深度分页问题,原因如下:
1.避免重复排序:
与传统的分页方式不同,search_after不需要处理每个分页请求中所有先前页面上的数据。这大大减少了处理数据的工作量。
2.提高查询效率:
由于不需要重复计算和跳过大量先前页面上的数据,search_after方法能显著提高查询效率,尤其是在访问数据集靠前靠后部分的数据时。
但是这个方案有一些局限,一方面需要有一个全局唯一的字段用来排序,另外虽然一次分页查询时不需要处理先前页面中的数据,但实际需要依赖上一个页面中的查询结果。
|
使用场景 |
实现方式 |
优点 |
缺点 |
传统分页 |
适用于小数据集和用户界面中的标准分页,如网站上的列表分页。 |
通过指定from(起始位置)和size(页面大小)来实现分页。 |
实现简单,适用于小数据集,易于理解和使用。 |
不适用于深度分页。当from值很大时,性能急剧下降。Elasticsearch默认限制from + size不超过10000。 |
Scroll |
适用于大规模数据的导出、备份或处理,而不是实时用户请求。 |
初始化一个scroll请求,然后使用返回的scroll id来连续地获取后续数据。 |
可以有效处理大量数据。提供了数据快照,保证了查询过程中数据的一致性。 |
不适合实时请求。初始化scroll会占用更多资源,因为它在后端维护了数据的状态。 |
Search_after |
适用于深度分页和大数据集的遍历。 |
基于上一次查询结果的排序值来获取下一批数据。 |
解决了深度分页的性能问题。更适合于处理大数据量,尤其是当需要顺序遍历整个数据集时。 |
不适用于随机页访问。需要精确的排序机制,并在每次请求中维护状态。 |
对于小型数据集和需要随机页面访问的标准分页场景,传统的分页是最简单和最直接的选择。
对于需要处理大量数据但不需要随机页面访问的场景,尤其是深度分页,search_after提供了更好的性能和更高的效率。
当需要处理非常大的数据集并且对数据一致性有要求时(如数据导出或备份),Scroll API是一个更好的选择。
IK分词器
默认的Standard分词器对中文支持不是很友好。
什么是IK分词器
IK有两种颗粒度的拆分
1 2 3 4 5 6 7 8 9 10 11 12
| GET /_analyze { "text":"今天星期一,我今天去课堂学习spring cloud", "analyzer":"ik_smart" }
GET /_analyze { "text":"今天星期一,我今天去课堂学习spring cloud", "analyzer":"ik_max_word" }
|
es(直接在主机上)安装IK
1 2 3 4 5 6 7 8 9
|
bin/elasticsearch -d
ps -ef | grep elasticsearch
|
es(docker)安装IK
方法一:容器内直接安装
1 2 3 4 5 6 7
| docker exec -it <your-es-container-name> /bin/bash
./bin/elasticsearch-plugin install https://github.com/medcl/elasticsearch-analysis-ik/releases/download/v8.10.4/elasticsearch-analysis-ik-8.10.4.zip
./bin/elasticsearch-plugin install https://get.infini.cloud/elasticsearch/analysis-ik/8.10.4
docker restart <your-es-container-name>
|
方法二:Dockerfile 构建自定义镜像
1 2 3 4 5 6 7 8 9
| FROM docker.elastic.co/elasticsearch/elasticsearch:8.10.4
RUN ./bin/elasticsearch-plugin install --batch https://get.infini.cloud/elasticsearch/analysis-ik/8.10.4
USER root RUN chown -R elasticsearch:elasticsearch /usr/share/elasticsearch/plugins/ik/ USER elasticsearch
|
1 2
| docker build -t es-8.10.4-with-ik .
|
验证
1 2 3 4 5 6 7 8 9 10 11
| GET /_analyze { "analyzer": "ik_smart", "text": "待分析的文本" }
GET /_analyze { "analyzer": "ik_max_word", "text": "待分析的文本" }
|
spring boot 集成
ES官方针对java推出多个客户端进行接入ES,也分两种
更旧版的ES会用TransportClient(7.0版本标记过期)
Java Low Level REST Client(有继续迭代维护)
- 基于低级别的 REST 客户端,通过发送原始 HTTP 请求与 Elasticsearch 进行通信。
- 自己拼接好的字符串,并且自己解析返回的结果;兼容所有的Elasticsearch版本
Java High Level REST Client(7.1版本标记过期)
- 基于低级别 REST 客户端,提供了更高级别的抽象,简化了与 Elasticsearch 的交互。
- 提供了更易用的 API,封装了底层的请求和响应处理逻辑,提供了更友好和可读性更高的代码。
- 自动处理序列化和反序列化 JSON 数据,适用于大多数常见的操作,如索引、搜索、聚合等。
- 对于较复杂的高级功能和自定义操作,可能需要使用低级别 REST 客户端或原生的 Elasticsearch REST API
Java API Client(8.X版本开始推荐使用)
- Elasticsearch在7.1版本之前使用的Java客户端是Java REST Client
- 从7.1版本开始Elastic官方将Java REST Client标记为弃用(deprecated),推荐使用新版Java客户端Java API Client
- 新版的java API Client是一个用于与Elasticsearch服务器进行通信的Java客户端库
- 封装了底层的Transport通信,并提供了同步和异步调用、流式和函数式调用等方法
- 官网文档地址
SpringBoot如何整合Elastic Search
- 方案一:使用 Elasticsearch 官方提供的高级客户端库 - Elasticsearch Api Client
1 2 3 4 5
| <dependency> <groupId>co.elastic.clients</groupId> <artifactId>elasticsearch-java</artifactId> <version>8.5.3</version> </dependency>
|
提供了丰富的 CRUD 操作和查询方法,简化了数据访问,包括自动化的索引管理和映射
Spring Data Elasticsearch 对于一些高级功能和复杂查询可能不够灵活,需要额外定制处理
1 2 3 4
| <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-elasticsearch</artifactId> </dependency>
|
1 2 3 4 5 6 7 8
| spring: application: name: springboot-es-test elasticsearch: uris: - http://localhost:9200 username: elastic password: elastic
|
什么是新版的ElasticSearch的Query接口
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45
| import lombok.Data; import org.springframework.data.annotation.Id; import org.springframework.data.elasticsearch.annotations.DateFormat; import org.springframework.data.elasticsearch.annotations.Document; import org.springframework.data.elasticsearch.annotations.Field; import org.springframework.data.elasticsearch.annotations.FieldType;
import java.time.LocalDateTime;
@Data @Document(indexName = "video") public class VideoDTO {
@Id @Field(type = FieldType.Text, index = false) private Long id;
@Field(type = FieldType.Text) private String title;
@Field(type = FieldType.Text) private String description;
@Field(type = FieldType.Keyword) private String category;
@Field(type = FieldType.Integer) private Integer duration;
@Field(type = FieldType.Date, format = DateFormat.date_hour_minute_second) private LocalDateTime createTime;
public VideoDTO(){}
public VideoDTO(Long id, String title, String description, Integer duration,String category) { this.id = id; this.title = title; this.description = description; this.duration = duration; this.createTime = LocalDateTime.now(); this.category = category; }
}
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239
| @SpringBootTest class SpringbootEsTestApplicationTests {
@Autowired private ElasticsearchTemplate restTemplate;
@Autowired private ElasticsearchOperations elasticsearchOperations;
@Test void existsIndex() { IndexOperations indexOperations = restTemplate.indexOps(VideoDTO.class); boolean exists = indexOperations.exists(); System.out.println(exists); }
@Test void createIndex() { IndexOperations indexOperations = restTemplate.indexOps(VideoDTO.class); if(indexOperations.exists()){ indexOperations.delete(); }
indexOperations.create();
restTemplate.indexOps(VideoDTO.class).putMapping(); }
@Test void deleteIndex() { IndexOperations indexOperations = restTemplate.indexOps(VideoDTO.class); boolean delete = indexOperations.delete(); System.out.println(delete); }
@Test void insert(){ VideoDTO videoDTO = new VideoDTO(); videoDTO.setId(1L); videoDTO.setTitle("ddddddd"); videoDTO.setCreateTime(LocalDateTime.now()); videoDTO.setDuration(100); videoDTO.setCategory("后端"); videoDTO.setDescription("bbbb");
VideoDTO saved = restTemplate.save(videoDTO); System.out.println(saved); }
@Test void update(){ VideoDTO videoDTO = new VideoDTO(); videoDTO.setId(1L); videoDTO.setTitle("a.png"); videoDTO.setCreateTime(LocalDateTime.now()); videoDTO.setDuration(102); videoDTO.setCategory("后端"); videoDTO.setDescription("a.png");
VideoDTO saved = restTemplate.save(videoDTO); System.out.println(saved); }
@Test void batchInsert() { List<VideoDTO> list = new ArrayList<>(); list.add(new VideoDTO(2L, "a", "aaaa", 123, "后端")); list.add(new VideoDTO(3L, "b", "bbb", 100042, "前端")); list.add(new VideoDTO(4L, "c", "cccc", 5432345, "后端")); list.add(new VideoDTO(5L, "d", "dddd", 6542, "后端")); list.add(new VideoDTO(6L, "e", "eeee", 53422, "前端")); list.add(new VideoDTO(7L, "f", "ffff", 6542, "后端"));
Iterable<VideoDTO> result = restTemplate.save(list); System.out.println(result); }
@Test void searchById(){ VideoDTO videoDTO = restTemplate.get("3", VideoDTO.class); assert videoDTO != null; System.out.println(videoDTO); }
@Test void deleteById() { String delete = restTemplate.delete("2", VideoDTO.class); System.out.println(delete); }
@Test void searchAll(){
SearchHits<VideoDTO> search = restTemplate.search(Query.findAll(), VideoDTO.class); List<SearchHit<VideoDTO>> searchHits = search.getSearchHits(); List<VideoDTO> videoDTOS = new ArrayList<>(); searchHits.forEach(hit -> { videoDTOS.add(hit.getContent()); }); System.out.println(videoDTOS); }
@Test void matchQuery(){
Query query = NativeQuery.builder().withQuery(q -> q .match(m -> m .field("description") .query("spring") )).build(); SearchHits<VideoDTO> searchHits = restTemplate.search(query, VideoDTO.class);
List<VideoDTO> videoDTOS = new ArrayList<>(); searchHits.forEach(hit -> { videoDTOS.add(hit.getContent()); }); System.out.println(videoDTOS); }
@Test void pageSearch() { System.out.println("开始-------------------");
NativeQuery query = NativeQuery.builder().withQuery(q -> q .matchPhrase(m -> m .field("description") .query("*spring*") )) .withPageable(Pageable.ofSize(3).withPage(0)).build(); System.out.println(query.getQuery());
SearchHits<VideoDTO> searchHits = restTemplate.search(query, VideoDTO.class);
List<VideoDTO> videoDTOS = new ArrayList<>(); searchHits.forEach(hit -> { videoDTOS.add(hit.getContent()); }); System.out.println(videoDTOS); System.out.println("结束-------------------"); }
@Test void sortSearch() { Query query = NativeQuery.builder().withQuery(Query.findAll()) .withPageable(Pageable.ofSize(10).withPage(0)) .withSort(Sort.by("duration").descending()).build();
SearchHits<VideoDTO> searchHits = restTemplate.search(query, VideoDTO.class); List<VideoDTO> videoDTOS = new ArrayList<>(); searchHits.forEach(hit -> { videoDTOS.add(hit.getContent()); }); System.out.println(videoDTOS); }
@Test void stringQuery() {
String dsl = """ {"bool":{"must":[{"match":{"title":"架构"}},{"match":{"description":"spring"}},{"range":{"duration":{"gte":10,"lte":6000}}}]}} """; Query query = new StringQuery(dsl);
List<SearchHit<VideoDTO>> searchHitList = restTemplate.search(query, VideoDTO.class).getSearchHits();
List<VideoDTO> videoDTOS = new ArrayList<>(); searchHitList.forEach(hit -> { videoDTOS.add(hit.getContent()); }); System.out.println(videoDTOS); }
@Test void aggQuery() { Query query = NativeQuery.builder() .withAggregation("category_group", Aggregation.of(a -> a .terms(ta -> ta.field("category").size(2)))) .build();
SearchHits<VideoDTO> searchHits = restTemplate.search(query, VideoDTO.class);
ElasticsearchAggregations aggregationsContainer = (ElasticsearchAggregations) searchHits.getAggregations(); Map<String, ElasticsearchAggregation> aggregations = Objects.requireNonNull(aggregationsContainer).aggregationsAsMap();
ElasticsearchAggregation aggregation = aggregations.get("category_group"); Buckets<StringTermsBucket> buckets = aggregation.aggregation().getAggregate().sterms().buckets();
buckets.array().forEach(bucket -> { System.out.println("组名:"+bucket.key().stringValue() + ", 值" + bucket.docCount()); });
List<VideoDTO> videoDTOS = new ArrayList<>(); searchHits.forEach(hit -> { videoDTOS.add(hit.getContent()); }); System.out.println(videoDTOS); }
}
|
日志的打印
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
|
@Test void pageSearch() { System.out.println("开始-------------------");
NativeQuery query = NativeQuery.builder().withQuery(q -> q .matchPhrase(m -> m .field("description") .query("*spring*") )) .withPageable(Pageable.ofSize(3).withPage(0)).build(); System.out.println(query.getQuery());
SearchHits<VideoDTO> searchHits = restTemplate.search(query, VideoDTO.class);
List<VideoDTO> videoDTOS = new ArrayList<>(); searchHits.forEach(hit -> { videoDTOS.add(hit.getContent()); }); System.out.println(videoDTOS); System.out.println("结束-------------------"); }
|
1 2 3 4 5 6 7
| System.out.println(query.getQuery());
System.out.println(query.getPageable());
|