ElasticSearch
- 官方文档:www.elastic.co/guide/en/el...
- 非官方中文文档:learnku.com/docs/elasti...
- 极简概括:基于Apache Lucene构建开源的分布式搜索引擎。
- 解决问题:MySQL like中文全文搜索不走索引,或大数据搜索性能低下的问题。
- 适用场景:
- 大数据检索:在大数据量的查询场景下,ES查询性能依然保持优势,常用于替代MySQL由于性能不足而做一些复杂的查询。
- 大数据开发:大数据开发几乎离不开Spark、Fink、Hadoop、ElasticSearch、MySQL、Redis、ZooKeeper这些组件。
- ELK结合:ES结合LK作为ELK(Elasticsearch(搜索), Logstash(采集转换), Kibana(分析))组合,可用于实时监控、分析和可视化大量日志和事件数据,如系统日志、应用程序日志、网络流量日志等。
- 优点:
- 跨平台:组件支持在Linux、Windows、MacOS上运行。
- 查询性能优异:在超大数据量的查询场景下,ES查询性能依然保持优势。
- 支持全文检索:替代MySQL中文全文检索不走索引的查询弱项。
- 生态繁荣:是面向开发者的主流的搜索引擎,文档,解决方案,疑难杂症,非0day漏洞,基本都有成熟的解决方案。
- 支持分布式:每个ES节点,都可以执行一部分搜索任务,然后将结果合并。累加的算力效果如虎添翼。
- 支持复杂查询:支持,模糊匹配,范围查询,布尔搜索。
- 缺点:
- ES没有事务机制,对于MySQL的合作呢,也是最终一致性,所以强一致性的搜索环境下并不适用,推荐Redis。
- json请求体父子格式反人类:果然技术厉害的程序员往往不会是一个好的产品经理。
- json响应体格式反人类,按照["成功或失败的code", "data数据", "msg补充说明"]这种格式返回就好了。
- PHP API经常性异常:APi接口,写操作失败返回false也行,非要返回异常,异常若没有处理,会中断程序执行。
- 查询方式受mapping限制:相比于MySQL,哪怕是个数字,都可以用like强制查询,但是ES不行。
- 同类组件:Apache Solr、Apache Lucene、Algolia、Sphinx、XunSearch。
正排索引和倒排索引
ES用的倒排索引算法。正倒两种索引都是用于快速检索数据的实现方案,我没有太官方的解释,所以举例说明:
- 正排索引:有一个文章表,有文章id、标题、详情3个字段,通过文章列表功能获取文章,通过id作为索引值获取文章内容,这是很普遍的业务逻辑。想要搜索包含指定关键词的文章,数据库就需要对文章的标题和内容逐一做对比,因为不走索引,数据量不大还好,数据量一大性能降低。
- 倒排索引:用于加速文本的检索,文章内容利用分词器拆分,将拆分好的关键词与文章id做关联,然后保存。类比MySQL表的两个列,一列是关键词,另一列是包含这个关键词的文章id,多个倒排索引数据集组成一个倒排表。再查询时,不需要针对数据源本身做查询,而是变成了,关键词为xxx的id为多少。
分词
分词就是把字符串拆分成有用的关键词,用于提供高质量搜索的数据源。
- 对英文:分词直接用空格就行,I love you,可直接利用空格分成3个词,对中文显然不适用。
- 对中文:例如"今天温度很高",能用的词汇可以拆分成"今天"、"温度"、"很高",可程序不知道怎么拆分,若拆分为"今天温"、"天温"、""度很"这样的关键词就显得很怪异。 所以也就诞生了语法分析+字典的解决方案,用人工干涉+词典的方式实现分词器的逻辑。 至于利用NLP语义分析,上下文预测,的AI模式,不属于ES的范畴,不展开。
- 若搜索关键词为语句或短语:需要利用TF-IDF和BM25算法(等更高级的算法),先对句子进行分词,然后根据这多个分词的再对结果集进行分词查询,然后评分,组合,最终返回结果。
安装ES 8.14.1
- 系统配置,用于开启防火墙,创建用户,和大数据情况下提升性能。
sh
Java写的组件吃内存,建议VM虚拟机内存设置大一点,系统设置为1G内存。
开两个端口,并重启防火墙
firewall-cmd --add-port=9200/tcp --zone=public --permanent
firewall-cmd --add-port=9300/tcp --zone=public --permanent
systemctl restart firewalld
新建一个es用户,以非root形式运行,否则运行es会报错,java.lang.RuntimeException: can not run elasticsearch as root
useradd -M es
passwd es 密码为123456
vim /etc/security/limits.conf
文末添加两行配置,优化文件描述符软硬限制,对提高性能非常重要,文件描述符用于标识和管理每个进程都可以打开文件的数量
es soft nofile 65536
es hard nofile 65536
vim /etc/security/limits.d/20-nproc.conf
文末添加两行配置,优化文件描述符软硬限制,对提高性能非常重要,文件描述符用于标识和管理每个进程都可以打开文件的数量
es soft nofile 65536
es hard nofile 65536
vim /etc/sysctl.conf
定义系统中可以同时打开的最大文件描述符数量。
fs.file-max=655350
定义Linux内核中进程可以拥有的最大内存映射区域数量
vm.max_map_count=262144
重启
sysctl -p
- 安装相关
sh
下载tar包并解压,这个包地址来源于官网,并非java源码包
wget https://artifacts.elastic.co/downloads/elasticsearch/elasticsearch-8.14.1-linux-x86_64.tar.gz
tar zxf elasticsearch-8.14.1-linux-x86_64.tar.gz
更改所属的用户和用户组
chown -R es:es elasticsearch-8.14.1
切换用户
su es
启动ES,如果发现报错,请清空bin目录同级的data目录
./bin/elasticsearch
启动后,直到看到如下字样,说明能成功启动,但是输入它生成的用户名密码,登不进去
然后Ctrl + C强制停止,因为启动一次之后,config/elasticsearch.yml配置文件,会发生变化,这一步��可少
Elasticsearch security features have been automatically configured!
登不进去,那就改配置
vim config/elasticsearch.yml
把91~103行的true全部改为false,如下,注意配置格式,key: value之间要留出空格,否则ES不识别对应的值。
# Enable security features
xpack.security.enabled: false
xpack.security.enrollment.enabled: false
# Enable encryption for HTTP API client connections, such as Kibana, Logstash, and Agents
xpack.security.http.ssl:
enabled: false
keystore.path: certs/http.p12
# Enable encryption and mutual authentication between cluster nodes
xpack.security.transport.ssl:
enabled: false
保存退出后,清除初始化的data数据
rm -rf elasticsearch-8.14.1/data/*
再次执行,并使其后台运行
./bin/elasticsearch -d
查看进程,确定ES是否成功执行
ps aux | grep elastic
es 49044 30.2 64.3 8291804 640416 pts/0 Sl 05:08 0:26 /test/elasticsearch-8.14.1/jdk/bin/java -Des.networkaddress.cache.ttl=60 -Des.networkaddress.cache.negative.ttl=10 -Djava.security.manager=allow -XX:+AlwaysPreTouch -Xss1m -Djava.awt.headless=true -Dfile.encoding=UTF-8 -Djna.nosys=true -XX:-OmitStackTraceInFastThrow -Dio.netty.noUnsafe=true -Dio.netty.noKeySetOptimization=true -Dio.netty.recycler.maxCapacityPerThread=0 -Dlog4j.shutdownHookEnabled=false -Dlog4j2.disable.jmx=true -Dlog4j2.formatMsgNoLookups=true -Djava.locale.providers=SPI,COMPAT --add-opens=java.base/java.io=org.elasticsearch.preallocate --add-opens=org.apache.lucene.core/org.apache.lucene.store=org.elasticsearch.vec --enable-native-access=org.elasticsearch.nativeaccess -XX:ReplayDataFile=logs/replay_pid%p.log -Djava.library.path=/test/elasticsearch-8.14.1/lib/platform/linux-x64:/usr/java/packages/lib:/usr/lib64:/lib64:/lib:/usr/lib -Djna.library.path=/test/elasticsearch-8.14.1/lib/platform/linux-x64:/usr/java/packages/lib:/usr/lib64:/lib64:/lib:/usr/lib -Des.distribution.type=tar -XX:+UnlockDiagnosticVMOptions -XX:G1NumCollectionsKeepPinned=10000000 -XX:+UseG1GC -Djava.io.tmpdir=/tmp/elasticsearch-13971958964404181235 --add-modules=jdk.incubator.vector -XX:+HeapDumpOnOutOfMemoryError -XX:+ExitOnOutOfMemoryError -XX:HeapDumpPath=data -XX:ErrorFile=logs/hs_err_pid%p.log -Xlog:gc*,gc+age=trace,safepoint:file=logs/gc.log:utctime,level,pid,tags:filecount=32,filesize=64m -Xms389m -Xmx389m -XX:MaxDirectMemorySize=204472320 -XX:G1HeapRegionSize=4m -XX:InitiatingHeapOccupancyPercent=30 -XX:G1ReservePercent=15 --module-path /test/elasticsearch-8.14.1/lib --add-modules=jdk.net --add-modules=ALL-MODULE-PATH -m org.elasticsearch.server/org.elasticsearch.bootstrap.Elasticsearch
es 49075 0.0 0.0 55180 880 pts/0 Sl 05:09 0:00 /test/elasticsearch-8.14.1/modules/x-pack-ml/platform/linux-x86_64/bin/controller
es 49230 0.0 0.0 112828 968 pts/0 R+ 05:10 0:00 grep elastic
访问:
http://IP:9200/
设置密码(推荐添加)
上文配置的是没有密码的方案,倘若服务器IP和端口对外暴露,这不是一种安全的行为。 注意,要部署集群,各个节点密码应当一致。
sh
注意配置格式,key: value之间要留出空格,否则ES不识别对应的值。
vim es根目录/config/elasticsearch.yml
修改以下配置
xpack.security.enabled: true
非root用户下启动es
./bin/elasticsearch -d
启动一个交互式命令行界面,从而设置密码,期间的几个交互,全部设置为123456
./bin/elasticsearch-setup-passwords interactive
默认用户名:elastic
密码:123456
概念辅助类比
ES中有些新的概念,可通过MySQL的概念去辅助记忆。
ES | MySQL | 备注 |
---|---|---|
Index(索引) | 库表 | / |
Type(类型) | 表 | 7及以上的版本被移除,原先是对标MySQL表的理念,后来发现这对于ES并非必须,就移除了 |
Documents(文档) | 行数据 | / |
Fields(字段) | 字段 | / |
Mapping(映射) | 表结构 | / |
Shards(分片) | 分表 | 顾名思义,当数据量太大单个节点都装不下的时候,就拆分到其它节点上 |
默认页说明
- 默认页: GET请求IP:9200/
json
{
"name": "lnmp",
"cluster_name": "elasticsearch",
"cluster_uuid": "k61PBMDqTKO31rZeV-ENGA",
"version": {
"number": "8.14.1",
"build_flavor": "default",
"build_type": "tar",
"build_hash": "93a57a1a76f556d8aee6a90d1a95b06187501310",
"build_date": "2024-06-10T23:35:17.114581191Z",
"build_snapshot": false,
"lucene_version": "9.10.0",
"minimum_wire_compatibility_version": "7.17.0",
"minimum_index_compatibility_version": "7.0.0"
},
"tagline": "You Know, for Search"
}
"name": "lnmp":系统标识
"cluster_name": "elasticsearch":Elasticsearch 集群的名称为 "elasticsearch"。
"cluster_uuid": "k61PBMDqTKO31rZeV-ENGA":Elasticsearch集群的唯一标识符。
"version":版本信息:
"number": 版本号
"build_flavor": "default":构建的类型,这里是默认的。
"build_type": "tar":构建类型为 tar 包。
"build_hash": "93a57a1a76f556d8aee6a90d1a95b06187501310":构建的哈希值,用于唯一标识这个特定的构建。
"build_date": "2024-06-10T23:35:17.114581191Z":构建的日期和时间。
"build_snapshot": false:表示这个构建不是一个快照版本。
"lucene_version": "9.10.0":基于Lucene 9.10.0的版本。
"minimum_wire_compatibility_version": "7.17.0":最低兼容的网络传输版本。
"minimum_index_compatibility_version": "7.0.0":最低兼容的索引版本。
"tagline": "You Know, for Search":Elasticsearch 的标语,说明其用途是进行搜索。
索引增删查操作
- 创建索引: PUT请求 IP:9200/索引名
json
{
"acknowledged": true,
"shards_acknowledged": true,
"index": "zs_index"
}
"acknowledged": true:指示请求是否被成功接受和处理。
"shards_acknowledged": true:指示所有分片是否已经确认请求。
"index": "zs_index":这表示操作涉及的索引名称为 "zs_index"。
- 创建索引: 重复创建,报错说明:
json
{
"error": {
"root_cause": [
{
"type": "resource_already_exists_exception",
"reason": "index [zs_index/dCMAgdlqTeaihB4JSH1gNw] already exists",
"index_uuid": "dCMAgdlqTeaihB4JSH1gNw",
"index": "zs_index"
}
],
"type": "resource_already_exists_exception",
"reason": "index [zs_index/dCMAgdlqTeaihB4JSH1gNw] already exists",
"index_uuid": "dCMAgdlqTeaihB4JSH1gNw",
"index": "zs_index"
},
"status": 400
}
"error":这个对象包含了发生的错误信息。
"root_cause":根本原因的数组,指示导致问题的具体原因。
"type": "resource_already_exists_exception":错误的类型,表示尝试创建的索引已经存在。
"reason": "index [zs_index/dCMAgdlqTeaihB4JSH1gNw] already exists":错误的详细原因,指明索引 "zs_index" 和其唯一标识符 "dCMAgdlqTeaihB4JSH1gNw" 已经存在。
"index_uuid": "dCMAgdlqTeaihB4JSH1gNw":已存在索引的 UUID。
"index": "zs_index":已存在索引的名称。
"type": "resource_already_exists_exception":总体错误类型,与根本原因相同。
"reason": "index [zs_index/dCMAgdlqTeaihB4JSH1gNw] already exists":再次指明索引已经存在的原因。
"index_uuid": "dCMAgdlqTeaihB4JSH1gNw":重复指定已存在索引的 UUID。
"index": "zs_index":重复指定已存在索引的名称。
"status": 400:HTTP 状态码,表示客户端请求错误
- 查看索引: GET请求 IP:9200/索引名
json
{
"zs_index": {
"aliases": {},
"mappings": {},
"settings": {
"index": {
"routing": {
"allocation": {
"include": {
"_tier_preference": "data_content"
}
}
},
"number_of_shards": "1",
"provided_name": "zs_index",
"creation_date": "1719699272706",
"number_of_replicas": "1",
"uuid": "dCMAgdlqTeaihB4JSH1gNw",
"version": {
"created": "8505000"
}
}
}
}
}
"aliases": {}:索引的别名列表为空,表示该索引当前没有别名。
"mappings": {}:索引的映射为空对象,即没有定义特定的字段映射。
"settings":索引的设置信息:
"index":
"routing":
"allocation":
"include":
"_tier_preference": "data_content":指定索引分配时偏好的数据内容层级。
"number_of_shards": "1":该索引被分成了一个分片。
"provided_name": "zs_index":索引的提供的名称为 "zs_index"。
"creation_date": "1719699272706":索引的创建日期的时间戳形式。
"number_of_replicas": "1":该索引有一个副本。
"uuid": "dCMAgdlqTeaihB4JSH1gNw":索引的唯一标识符 UUID。
"version":
"created": "8505000":索引的版本信息,表示索引在 Elasticsearch 版本 "8505000" 中创建。
- 查看所有索引: GET请求 IP:9200/_cat/indices?v
txt
health status index uuid pri rep docs.count docs.deleted store.size pri.store.size dataset.size
yellow open zs_index dCMAgdlqTeaihB4JSH1gNw 1 1 0 0 249b 249b 249b
health: 索引的健康状态,此处为 "yellow",表示所有预期的分片都可用,但副本尚未分配。
status: Elasticsearch 的状态指示符,这里是 "open",表示索引是打开状态,可以接收读写操作。
index: 索引名。
uuid: 索引的唯一标识符。
pri: 主分片数为 1,即索引被分成了一个主分片。
rep: 副本数为 1,表示每个主分片有一个副本。
docs.count: 文档数量为 0,当前索引中的文档总数。
docs.deleted: 已删除的文档数量为 0。
store.size: 存储大小为 249b,索引占用的物理存储空间。
pri.store.size: 主分片的存储大小,也是 249b。
dataset.size: 数据集大小为 249b,即索引的数据集大小。
- 删除索引 DELETE方式 IP:9200/索引名
json
{
"acknowledged": true
}
返回true表示成功执行。
文档增删改查操作
- 增文档(数据): 方式1:POST请求 IP:9200/索引名/_doc/可选参数,数据唯一标识 方式2:PUT请求 IP:9200/索引名/_create/必填唯一标识符 由于方式2的put请求是幂等,所以再次请求会报错
json
这是存入的数据
{
"id":1,
"content":"C是世界上最好的编程语言"
}
这是返回的数据,若用户指定id,则id处显示的是用户指定的id
{
"_index": "zs_index",
"_id": "0mMsZpABZdTHCHXLZQhu",
"_version": 1,
"result": "created",
"_shards": {
"total": 2,
"successful": 1,
"failed": 0
},
"_seq_no": 0,
"_primary_term": 1
}
"_index": "zs_index": 表示文档被添加到了名为zs_index的索引中。
"_id": "0mMsZpABZdTHCHXLZQhu": 是新添加的文档的ID。在Elasticsearch中,每个文档都有一个唯一的ID,用于唯一标识和检索该文档。
"_version": 1: 表示该文档的版本号是1。每当文档被更新时,版本号会增加,这有助于跟踪文档的更改历史。
"result": "created": 表示操作的结果是创建了一个新的文档。
"_shards": 这个字段提供了关于索引操作的分片信息。
"total": 2: 表示总共有2个分片参与了这次索引操作(通常是一个主分片和其副本)。
"successful": 1: 表示有1个分片成功完成了索引操作。在yellow健康状态的索引中,这通常意味着主分片成功了,但副本分片可能还没有数据(因为它是yellow状态,副本可能还没有分配或同步)。
"failed": 0: 表示没有分片失败。
"_seq_no": 0: 是文档在Lucene段中的序列号,用于在内部跟踪文档的版本和顺序。
"_primary_term": 1: 主要术语(primary term)是与_seq_no一起使用的,用于确保文档版本的一致性,特别是在主节点更换时。
- 改文档(数据): 方式1(用于覆盖老数据):POST请求 IP:9200/索引名/_doc/唯一标识 方式2(用于覆盖老数据):PUT请求 IP:9200/索引名/_doc/唯一标识 方式3(用于修改局部数据):POST请求 IP:9200/索引名/_update/唯一标识
json
方式1,若有id号,再次执行增文档操作,可自动将create操作编程update操作。
更新数据
{
"id":1,
"content":"C是世界上最好的编程语言"
}
方式2,请求内容同方式1
方式3,因为要修改局部数据,所以必须告知ES修改那块的局部数据,以下:第一层花括号和doc是固定格式。
{
"doc" : {
"content": "C是最好的编程语言"
}
}
3种方式的响应格式一致:
{
"_index": "zs_index",
"_id": "1",
"_version": 17,
"result": "updated",
"_shards": {
"total": 2,
"successful": 1,
"failed": 0
},
"_seq_no": 24,
"_primary_term": 1
}
"_index": "zs_index": 表示被更新的文档位于名为zs_index的索引中。
"_id": "1": 是被更新的文档的唯一ID。
"_version": 17: 表示该文档的版本号已更新为17。版本号在每次更新时增加,用于跟踪文档的变化历史。
"result": "updated": 表示更新操作已成功执行,文档被更新了。
"_shards": 提供了关于更新操作涉及的分片信息。
"total": 2: 表示总共有2个分片参与了更新操作(通常是一个主分片和其副本)。
"successful": 1: 表示有1个分片成功完成了更新操作。在yellow健康状态的索引中,这意味着主分片成功了,但副本分片可能尚未同步数据。
"failed": 0: 表示没有分片失败。
"_seq_no": 24: 是文档在Lucene段中的序列号,用于内部跟踪文档版本和顺序。
"_primary_term": 1: 主要术语(primary term)与_seq_no一起使用,确保文档版本的一致性,特别是在主节点更换时。
- 查询单条数据: GET请求 IP:9200/索引名/_doc/唯一标识
json
{
"_index": "zs_index",
"_id": "1",
"_version": 8,
"_seq_no": 14,
"_primary_term": 1,
"found": true,
"_source": {
"id": 1,
"content": "C是世界上最好的编程语言"
}
}
"_seq_no": 14: 是文档在Lucene段中的序列号,用于内部跟踪文档版本和顺序。
"_primary_term": 1: 主要术语(primary term)与_seq_no一起使用,确保文档版本的一致性,尤其是在主节点更换时。
"found": true: 表示Elasticsearch成功找到了指定ID的文档,若为false,表示未找到。
"_source": 包含了文档的实际内容。
- 查询多条数据: GET请求 IP:9200/索引名/_search
json
{
"took": 137,
"timed_out": false,
"_shards": {
"total": 1,
"successful": 1,
"skipped": 0,
"failed": 0
},
"hits": {
"total": {
"value": 8,
"relation": "eq"
},
"max_score": 1,
"hits": [
{
"_index": "zs_index",
"_id": "1",
"_score": 1,
"_source": {
"id": 1,
"content": "C是世界上最好的编程语言"
}
},
{
"_index": "zs_index",
"_id": "02M0ZpABZdTHCHXLjAgN",
"_score": 1,
"_source": {
"id": 1,
"content": "C是世界上最好的编程语言"
}
}
]
}
}
"took": 137: 表示搜索操作耗费了137毫秒。
"timed_out": false: 表示搜索操作未超时。
"_shards": 提供了关于搜索操作涉及的分片信息。
"total": 1: 表示总共有1个分片参与了搜索操作。
"successful": 1: 表示所有参与的分片都成功完成了搜索。
"skipped": 0: 表示没有分片被跳过。
"failed": 0: 表示没有分片失败。
"hits": 包含了搜索结果的详细信息。
"total": {"value": 8, "relation": "eq"}: 表示符合搜索条件的文档总数为8个。
"value": 8: 具体的文档数。
"relation": "eq": 表示与总数值相等,即已经获取了所有匹配的文档。
"hits"数组: 包含了每个匹配文档的详细信息。
每个文档对象包括了:
"_index": "zs_index": 文档所属的索引名称。
"_id": 文档的唯一ID。
"_score": 1: 文档的匹配分数,此处为1(最高分)。
"_source": 包含了文档的实际内容。
- 删除数据 DELETE请求 IP:9200/索引名/_doc/唯一标识
json
{
"_index": "zs_index",
"_id": "1",
"_version": 24,
"result": "not_found",
"_shards": {
"total": 2,
"successful": 1,
"failed": 0
},
"_seq_no": 31,
"_primary_term": 1
}
"result": "not_found": 表示更新操作未找到指定的文档,若是deleted,表示成功删除。
_shards": 提供了关于更新操作涉及的分片信息。
"total": 2: 表示总共有 2 个分片参与了更新操作(通常是一个主分片和其副本)。
"successful": 1: 表示有 1 个分片成功完成了更新操作。在索引状态为 yellow 时,这可能意味着主分片成功了,但副本分片可能尚未同步数据。
"failed": 0: 表示没有分片失败。
"_seq_no": 31: 是文档在 Lucene 段中的序列号,用于内部跟踪文档版本和顺序。
"_primary_term": 1: 主要术语(primary term)与 _seq_no 一起使用,确保文档版本的一致性,特别是在主节点更换时。
文档复杂查询操作
- 通过关键词查询: 方式1:GET请求 IP:9200/索引名/_search?q=文档字段名:要搜索的关键字 方式2:GET请求 IP:9200/索引名/_search 并添加请求body
{ "query":{ "match": { "文档字段名":"要搜索的关键字" } } }
json
{
"took": 8,
"timed_out": false,
"_shards": {
"total": 1,
"successful": 1,
"skipped": 0,
"failed": 0
},
"hits": {
"total": {
"value": 6,
"relation": "eq"
},
"max_score": 0.074107975,
"hits": [
{
"_index": "zs_index",
"_id": "0mMsZpABZdTHCHXLZQhu",
"_score": 0.074107975,
"_source": {
"id": 1,
"content": "C是世界上最好的编程语言"
}
}
]
}
}
took: 查询花费的时间,单位为毫秒。在这个例子中,值为8,表示查询执行花费了8毫秒时间。
timed_out: 表示查询是否超时。在这个例子中,值为false,表示查询未超时。
_shards: 分片相关信息,包括:
total: 总分片数,这里是1个分片。
successful: 成功的分片数,这里是1个分片。
skipped: 被跳过的分片数,这里是0个分片。
failed: 失败的分片数,这里是0个分片。
hits: 查询命中的结果集信息,包含:
total: 总命中数,这里是6。
max_score: 结果集中最高得分,这里是0.074107975。
hits: 包含具体的命中文档数组。
每个文档包含以下信息:
_index: 文档所在的索引。
_id: 文档的唯一标识符。
_score: 文档的得分。
_source: 存储实际数据的字段。
-
分页查询: GET请求 IP:9200/索引名/_search body体添加
{ "query": { "match": { "文档字段名":"要搜索的关键字" } }, "from":0, "size":2 }
其中,from为起始位置偏移量,size为每页显示的条数。 from算法:(页码 -1)* size = form。 第1页:(1 - 1)* 2 = 0,所以from为0。 第2页:(2 - 1)* 2 = 2,所以from为2。 响应结果同上。 -
只显示数据的部分字段: GET请求 IP:9200/索引名/_search body体添加_source项即可
{ "query": { "match": { "文档字段名":"要搜索的关键字" } }, "_source":["id"] }
响应结果同上。 -
排序: GET请求 IP:9200/索引名/_search body体添加sort项即可
{ "query": { "match": { "文档字段名":"要搜索的关键字" } }, "sort":{ "排序的字段名":{ "order":"asc" } } }
注意,这个将要排序的字段,可以不被展示出来也能排序(_source控制项) 响应结果同上。 -
多条件and或or查询,区间查询 GET请求 IP:9200/索引名/_search 如下,需添加以下body,表示查询content字段为C语言和(&&)C++语言(C++语言会被拆分),并且content>1(随意测试)的数据。 若替换must为should,则表示或(or)之意。
json
{
"query": {
"bool": {
"must": [
{
"match": {
"content": "C语言"
}
},
{
"match": {
"content": "C++语言"
}
}
],
"filter": {
"range": {
"content": {
"gt": 1
}
}
}
}
}
}
响应结果同上。
- 全文精准匹配 GET请求 IP:9200/索引名/_search 仍需添加如下body
{ "query":{ "match_phrase" :{ "字段名":"要搜索的关键字" } } }
响应结果同上。 - 查询到的结果高亮显示 GET请求 IP:9200/索引名/_search 仍需添加如下body
{ "query":{ "match_phrase" :{ "字段名":"要搜索的关键字" } }, "highlight":{ "fields":{ "字段名":{} } } }
响应结果同上。
聚合查询
- 求指定字段平均值 由于聚合函数过多,逐一说明会让篇幅变的很长,因此推荐看官方文档:www.elastic.co/guide/en/el... GET请求 IP:9200/索引名/_search 仍需添加如下body
{ "aggs" : { "id_group_avg" : { "avg" : { "field" : "字段名" } } }, "size":0 }
其中,id_group_avg为自定义名称,size:0表示去掉对文档数据的返回。
json
{
"took": 35,
"timed_out": false,
"_shards": {
"total": 1,
"successful": 1,
"skipped": 0,
"failed": 0
},
"hits": {
"total": {
"value": 6,
"relation": "eq"
},
"max_score": null,
"hits": []
},
"aggregations": {
"id_group_avg": {
"value": 1
}
}
}
took: 查询花费的时间,单位是毫秒。这里是 35 毫秒。
timed_out: 查询是否超时。这里显示为 false,表示查询在规定时间内完成。
_shards: 这个对象提供关于查询在分片上的执行情况的详细信息:
total: 总分片数。
successful: 成功完成查询的分片数。
skipped: 跳过的分片数。
failed: 查询失败的分片数。
在这个例子中,总分片数为 1,且成功完成了查询。
hits: 包含有关查询匹配的文档信息:
total: 文档匹配的总数。
value: 匹配的文档数,这里是 6。
relation: 匹配关系,这里是 "eq" 表示精确匹配。
max_score: 最高得分,如果不需要计算得分则为 null。
hits: 实际匹配的文档数组。在这个例子中是空的,因为没有具体的文档数据。
aggregations: 聚合结果信息:
id_group_avg: 聚合名称,这里的值为 1。具体的聚合结果会根据你的查询和聚合定义而有所不同。
分词与不分词的控制
这块由于涉及到字段的改动,所以需要重新建立索引,并且添加了映射(mapping)的概念
json
重新建立一个people索引
PUT请求 IP:9200/people
再次请求,添加映射
IP:9200/people/_mapping
{
"properties" :{
"name" : {
"type":"text",
"index":true
},
"sex" : {
"type":"keyword",
"index":true
},
"tel" : {
"type":"keyword",
"index":false
}
}
}
上方的index指的是是否为这条数据添加索引。
type是索引类型,text代表支持分词查询(MySQL like '%kw%'),keyword代表不可分词查询 (MySQL = 'kw')。
然后添加三条数据
PUT IP:9200/people/_create/1
{
"name":"张三",
"sex":"男性",
"tel":"18888888888"
}
PUT IP:9200/people/_create/2
{
"name":"李四",
"sex":"女性",
"tel":"16666666666"
}
PUT IP:9200/people/_create/3
{
"name":"王五",
"sex":"男性",
"tel":"18866668888"
}
搜索
GET IP:9200/people/_search
{
"query" :{
"match" :{
"sex" : "男" 把性去掉,搜索不到数据
}
}
}
GET IP:9200/people/_search
{
"query" :{
"match" :{
"name" : "张" 把三去掉,可以搜索到数据
}
}
}
GET IP:9200/people/_search
{
"query" :{
"match" :{
"tel" : "188" 若输入手机号前3位,则搜不到数据,输入完整的手机号,则可以搜索到数据
}
}
}
PHP Api调用
官方文档:www.elastic.co/guide/en/el... 某些ES Api(例如创建索引)不能重复执行,重复执行会报错,所以在执行写操作的上游做判断,或者使用try catch。
php
composer require elasticsearch/elasticsearch
推荐安装symfony/var-dumper,用于dd()或dump()执行,美化输出。
新建PHP文件,以下代码数据为公共部分。
include './vendor/autoload.php';
use Elastic\Elasticsearch\ClientBuilder;
//连接ES
$client = ClientBuilder::create()->setHosts(['192.168.0.183:9200'])->build();
//若es有密码,则需要添加一个setBasicAuthentication()方法。
$client = ClientBuilder::create()->setHosts(['192.168.0.183:9200'])->setBasicAuthentication('elastic', '123456')->build();
PHP ES Api针对Index增删改查
- 创建
php
返回bool
$response = $client->indices()->create([
'index' => 'php_index'
]);
$response->asBool();
- 查询 判断索引是否存在
php
返回bool
$response = $client->indices()->exists(['index' => 'php_index']);
dd($response->asBool());
- 查询 查看索引相关信息
php
返回array
$response = $client->indices()->get(['index' => 'php_index']);
dd($response->asArray());
- 删除
php
返回bool
$response = $client->indices()->delete(['index' => 'php_index']);
dd($response->asBool());
- 修改 索引作为基础性的数据支撑,一般不做改动。
PHP ES Api针对Mapping增删改查
- 类型可参考官方文档:www.elastic.co/guide/en/el...
- 增 创建索引后
php
返回bool
$params = [
'index' => 'php_index',
'body' => [
'properties' => [
'name' => [
'type' => 'text',
],
]
]
];
$response = $client->indices()->putMapping($params);
dd($response->asBool());
- 增 创建索引时
php
返回bool
$params = [
'index' => 'php_index',
'body' => [
'mappings' => [
'properties' => [
'title' => [
'type' => 'text',
],
'content' => [
'type' => 'text',
],
]
]
]
];
$response = $client->indices()->create($params);
dd($response->asBool());
- 查 所有索引
php
返回数组
$response = $client->indices()->getMapping();
dd($response->asArray());
- 查 指定索引
php
返回数组
$response = $client->indices()->getMapping(['index' => 'php_index']);
dd($response->asArray());
- 删 请直接删除索引。
- 改 请重新建立索引,在新索引基础上做映射的修改。
PHP ES Api针对Doc增删改
- 索引与映射如下:
php
准备四个直辖市的名称,简介,人口和面积大小。
$params = [
'index' => 'php_index',
'body' => [
'mappings' => [
'properties' => [
'city' => [
'type' => 'keyword',
],
'description' => [
'type' => 'text',
],
'population' => [
'type' => 'integer'
],
'area' => [
'type' => 'integer'
],
]
]
]
];
$response = $client->indices()->create($params);
dd($response->asArray());
- 增 单条 请记忆这4个直辖市的数据保存格式,下文基本每个演示都要用 一级数组下有个id属性,若省去,ES会默认给这条数据加一个id。不推荐。推荐使用MySQL的数据id作为ES的id。
php
返回bool
$params = [
'index' => 'php_index',
'id' => 1,
'body' => [
'id' => 1,
'city' => '北京市',
'description' => '北京市(Beijing),简称"京",古称燕京、北平,是中华人民共和国首都、直辖市、国家中心城市、超大城市, 国务院批复确定的中国政治中心、文化中心、国际交往中心、科技创新中心, 中国历史文化名城和古都之一,世界一线城市',
'population' => '2186',
'area' => '16411',
]
];
$response = $client->index($params);
dd($response->asBool());
再增加3条数据
$params = [
'index' => 'php_index',
'id' => 2,
'body' => [
'id' => 2,
'city' => '上海市',
'description' => '上海市(Shanghai City),简称"沪" ,别称"申",中华人民共和国直辖市、国家中心城市、超大城市、上海大都市圈核心城市、国家历史文化名城 [206],是中国共产党的诞生地。上海市入围世界Alpha+城市, 基本建成国际经济、金融、贸易、航运中心,形成具有全球影响力的科技创新中心基本框架。截至2022年12月底,上海市辖16个区,107个街道、106个镇、2个乡。',
'population' => '2487',
'area' => '6341',
]
];
$params = [
'index' => 'php_index',
'id' => 3,
'body' => [
'id' => 3,
'city' => '天津市',
'description' => '天津市(Tianjin City),简称"津",别称津沽、津门,是中华人民共和国省级行政区、直辖市、国家中心城市、超大城市 [222],地处中华人民共和国华北地区,海河流域下游,东临渤海,北依燕山,西靠首都北京市,其余均与河北省相邻。截至2023年10月,天津市共辖16个区。',
'population' => '1364',
'area' => '11966',
]
];
$params = [
'index' => 'php_index',
'id' => 4,
'body' => [
'id' => 4,
'city' => '重庆市',
'description' => '重庆市,简称"渝", 别称山城、江城,是中华人民共和国直辖市、国家中心城市、超大城市,国务院批复的国家重要中心城市之一、长江上游地区经济中心, 国际消费中心城市,全国先进制造业基地、西部金融中心、西部科技创新中心、 国际性综合交通枢纽城市和对外开放门户,辖38个区县',
'population' => '3191',
'area' => '82400',
]
];
- 增 多条
php
返回数组
//假设MySQL查询出来的数据如下
$mysql_data = [
[
'id' => 1024,
'city' => 'xx市',
'description' => 'xxxx',
'population' => '6666',
'area' => '6666',
],
[
'id' => 1025,
'city' => 'yy市',
'description' => 'yyyy',
'population' => '8888',
'area' => '8888',
]
];
//由于ES插入的要求,需要将插入数据的格式转化,为此可以封装一个方法
function esBatchInsert($index_name, $mysql_data) {
$params = [];
foreach($mysql_data as $v) {
$params['body'][] = ['index' => ['_index' => $index_name, '_id' => $v['id']],];
$params['body'][] = $v;
}
return $params;
}
$response = $client->bulk(esBatchInsert('php_index', $mysql_data));
dd($response->asArray());
可根据返回的数据再次循环,排查失败掉的漏网之鱼
- 删 单条
php
返回bool
$params = [
'index' => 'php_index',
'id' => '1025'
];
$response = $client->delete($params);
dd($response->asBool());
- 删 多条
php
方式1:
返回mixed
for($i = 1000; $i < 1050; $i++) { //模拟要删除这些数据
$params = [
'index' => 'php_index',
'id' => $i
];
if(! $client->exists($params)->asBool()) {
continue;
}
$response = $client->delete($params)->asBool();
if(! $response) {
//若删除失败,请添加其它操作,记录日志或存入队列,进行重试或者人工介入
}
}
方式2:
返回mixed
for($i = 1000; $i < 1050; $i++) { //模拟要删除这些数据
$params['body'][] = [
'delete' => [
'_index' => 'php_index',
'_id' => $i,
]
];
}
$response = $client->bulk($params)->asArray();
if ($response['errors']) {
foreach ($response['items'] as $item) {
if (isset($item['delete']['status']) && ($item['delete']['status'] != 200)) {
//若删除失败,请添加其它操作,记录日志或存入队列,进行重试或者人工介入
}
}
} else {
echo "批量删除成功!";
}
- 删 文档的某个字段
php
返回bool
$params = [
'index' => 'php_index',
'id' => 1,
'body' => [
'script' => [
'source' => 'ctx._source.remove(params.field)',
'params' => [
'field' => '要删除的字段名'
]
]
]
];
$response = $client->update($params);
dd($response->asBool());
- 改 直接修改
php
返回bool
$params = [
'index' => 'php_index',
'id' => 1,
'body' => [
'doc' => [
'city' => '北京' //这里是要修改的字段,把北京市改为北京
]
]
];
$response = $client->update($params);
dd($response->asBool());
- 改 自增
php
返回bool
官方文档演示有误,请按照以下正确写法。
$params = [
'index' => 'php_index',
'id' => 1,
'body' => [
'script' => [
//表达式
'source' => 'ctx._source.population += params.population', //给北京人口加4万,population为自定义文档字段,其余字符固定写法。
//表达式所使用的变量
'params' => [
'population' => 4
],
],
]
];
$response = $client->update($params);
dd($response->asBool());
- 改 若文档不存在,则插入
php
$params = [
'index' => 'php_index',
'id' => 60, //若id对应的文档不存在,则利用upsert段的数据,重新生成一个id为60的文档。
'body' => [
'doc' => [
'city' => '台北市'
],
'upsert' => [
'append_field' => 1
],
]
];
$response = $client->update($params);
dd($response->asBool());
- 改 批量
php
//假设以下数据时数据表中查询出来的字段,要修改以下内容
$mysql_data = [
['id' => 1, 'city' => '北京'],
['id' => 2, 'city' => '上海'],
];
//可以封装一个方法,格式化数据
function esBatchUpdate($index_name, $update_data) {
if(! $update_data) {
return [];
}
$arr = [];
foreach($update_data as $v) {
$arr[] = ['update' => ['_index' => $index_name, '_id' => $v['id']]];
unset($v['id']);
$arr[] = ['doc' => $v];
}
return ['body' => $arr];
}
$response = $client->bulk(esBatchUpdate('php_index', $mysql_data));
$response = $response->asArray();
//处理
if ($response['errors']) {
foreach ($response['items'] as $item) {
if (isset($item['update']['status']) && ($item['update']['status'] != 200)) {
//若删除失败,请添加其它操作,记录日志或存入队列,进行重试或者人工介入
}
}
} else {
echo "批量删除成功!";
}
- 改 追加新的字段
php
$params = [
'index' => 'php_index',
'id' => '1',
'body' => [
'doc' => [
'new_field' => 'new_value'
],
]
];
$response = $client->update($params);
- 改 删除某些字段
php
返回bool
$params = [
'index' => 'php_index',
'id' => 1,
'body' => [
'script' => [
'source' => 'ctx._source.remove(params.field)',
'params' => [
'field' => '要删除的字段名'
]
]
]
];
$response = $client->update($params);
dd($response->asBool());
PHP ES Api针对Doc高级查询
查询关键词官方文档:www.elastic.co/guide/en/el...
- 指定id查找
php
返回string
$params = [
'index' => 'php_index',
'id' => 1,
];
$response = $client->get($params);
echo $response->asString();
得到以下结果
{
"_index": "php_index",
"_id": "1",
"_version": 1,
"_seq_no": 0,
"_primary_term": 1,
"found": true,
"_source": {
"id": 1,
"city": "北京市",
"description": "北京市(Beijing),简称"京",古称燕京、北平,是中华人民共和国首都、直辖市、国家中心城市、超大城市, 国务院批复确定的中国政治中心、文化中心、国际交往中心、科技创新中心, 中国历史文化名城和古都之一,世界一线城市",
"population": "2186",
"area": "16411"
}
}
- 查找全部
php
返回array
$response['hits']['total']['value']可获取条数
$params = [
'index' => 'php_index',
'body' => [
'query' => [
'match_all' => new StdClass
]
]
];
$response = $client->search($params);
dd($response->asArray());
- 指定指定部分id的数据。
php
返回数组
$response['hits']['total']['value']可获取条数
$params = [
'index' => 'php_index',
'body' => [
'query' => [
'ids' => [
'values' => [1, 2]
]
]
]
];
$response = $client->search($params);
dd($response->asArray());
- 分页查询
php
返回数组
//传输的页码
$page = 2;
$size = 2;
//偏移量算法
$offset = ($page -1 ) * $size;
$params = [
'index' => 'php_index',
'body' => [
'from' => $offset,
'size' => $size,
// 可以添加其他查询条件
'query' => [
'match_all' => new \stdClass()
]
]
];
$response = $client->search($params);
dd($response->asArray());
- 返回指定字段
php
返回数组
$params = [
'index' => 'php_index',
'body' => [
'_source' => ['description'], //自定义字段
'query' => [
'match_all' => new \stdClass()
]
]
];
$response = $client->search($params);
dd($response->asArray());
- 判断是否存在
php
返回bool
$params = [
'index' => 'php_index',
'id' => 10
];
$response = $client->exists($params);
- 获取条数
php
返回int
$params = [
'index' => 'php_index',
'body' => [
'query' => [
'match_all' => new StdClass
]
]
];
$response = $client->count($params);
dd($response->asArray()['count'] ?? 0);
- 高亮查询(类比百度词条对关键字的标红行为)
php
返回string
echo "<style>em{color:red}</style>";
$params = [
'index' => 'php_index',
'body' => [
'query' => [
'match' => [
'description' => '北京' //返回该字段含有北京或北或京的文字。
]
],
'highlight' => [
'fields' => [
'city' => ['pre_tags' => ['<em>'], 'post_tags' => ['</em>'],], //配置要高亮的字段
'description' => ['pre_tags' => ['<em>'], 'post_tags' => ['</em>'],] //配置要高亮的字段
]
]
]
];
$response = $client->search($params);
print_r($response->asString());
返回格式如下,具体要用那个字段,看具体需求
<style>em{color:red}</style>
{
"took":13,
"timed_out":false,
"_shards":{
"total":1,
"successful":1,
"skipped":0,
"failed":0
},
"hits":{
"total":{
"value":2,
"relation":"eq"
},
"max_score":2.9070516,
"hits":[
{
"_index":"php_index",
"_id":"1",
"_score":2.9070516,
"_source":{
"id":1,
"city":"北京",
"description":"北京市(Beijing),简称"京",古称燕京、北平,是中华人民共和国首都、直辖市、国家中心城市、超大城市, 国务院批复确定的中国政治中心、文化中心、国际交往中心、科技创新中心, 中国历史文化名城和古都之一,世界一线城市",
"population":2198,
"area":"16411",
"new_field":"new_value"
},
"highlight":{
"description":["<em>北</em><em>京</em>市(Beijing),简称"<em>京</em>",古称燕<em>京</em>、<em>北</em>平,是中华人民共和国首都、直辖市、国家中心城市、超大城市, 国务院批复确定的中国政治中心、文化中心、国际交往中心、科技创新中心, 中国历史文化名城和古都之一"]
}
},
{
"_index":"php_index",
"_id":"3",
"_score":2.5460577,
"_source":{
"id":3,
"city":"天津市",
"description":"天津市(Tianjin City),简称"津",别称津沽、津门,是中华人民共和国省级行政区、直辖市、国家中心城市、超大城市 [222],地处中华人民共和国华北地区,海河流域下游,东临渤海,北依燕山,西靠首都北京市,其余均与河北省相邻。截至2023年10月,天津市共辖16个区。",
"population":"1364",
"area":"11966"
},
"highlight":{
"description":["天津市(Tianjin City),简称"津",别称津沽、津门,是中华人民共和国省级行政区、直辖市、国家中心城市、超大城市 [222],地处中华人民共和国华<em>北</em>地区,海河流域下游,东临渤海,<em>北</em>依燕山,西靠首都<em>北</em><em>京</em>市",",其余均与河<em>北</em>省相邻。"]
}
}
]
}
}
- 限量 可参考分页逻辑(类比MySQL limit)
php
返回array
$params = [
'index' => 'php_index',
'body' => [
'query' => [
'match_all' => new stdClass
],
'from' => 0,
'size' => 1,
]
];
$response = $client->search($params);
dd($response->asArray());
- 定值查找 (类比MySQL wher filed = 'kw') keyword 或 integer 等非分词字段:可用 term 精确匹配。如果字段是 text 类型,那么 term 查询无法找到预期的匹配结果。 text 类型并且你想要精确匹配,可以使用 match_phrase 查询
php
方式1 针对integer字段的精准匹配
$params = [
'index' => 'php_index',
'body' => [
'query' => [
'term' => [
'city' => '北京市' //北京或北或京无法查询出指定数据
]
]
]
];
$response = $client->search($params);
dd($response->asArray());
- 分词查找(类比MySQL where filed like '%kw%' or filed like '%k%' or filed like '%w%')
php
方式1
返回array
这种方式仅支持text类型
$params = [
'index' => 'php_index',
'body' => [
'query' => [
'match' => [
'description' => '北京'
]
]
]
];
$response = $client->search($params);
dd($response->asArray());
方式2
返回array
非text类型,可手动分词
$params = [
'index' => 'php_index',
'body' => [
'query' => [
'bool' => [
'should' => [ //or
[
'match' => ['city' => '北京']
],
[
'match' => ['city' => '北京市']
]
],
'minimum_should_match' => 1
//minimum_should_match 设置为 1,表示至少需要匹配一个 should 子句中的条件
]
]
]
];
$response = $client->search($params);
dd($response->asArray());
- 模糊匹配 (类比MySQL where filed like '%kw%')wildcard性能可能不如其它类型的查询,如match查询,因为wildcard查询需要对每个文档的字段值进行模式匹配
php
方式1,针对keyword mapping
返回array
$params = [
'index' => 'php_index',
'body' => [
'query' => [
'wildcard' => [
'city' => '*北京*' //*表示任意字符,?表示任意一个字符
]
]
]
];
$response = $client->search($params);
dd($response->asArray());
方式2,针对text mapping,并非严格意义上的MySQL where filed like '%kw%',而是 where filed like '%kw%' or filed like '%k%' or filed like '%w%'
返回array
$params = [
'index' => 'php_index',
'body' => [
'query' => [
'match' => [
'description' => '北京'
]
]
]
];
$response = $client->search($params);
dd($response->asArray());
- 前缀查找 (类比MySQL where filed like 'kw%')针对keyword类型的字段有效
php
返回array
$params = [
'index' => 'php_index',
'body' => [
'query' => [
'prefix' => [
'city' => '北'
]
]
]
];
$response = $client->search($params);
- 后缀查找 (类比MySQL where filed like '%kw')针对keyword字段有效
php
$params = [
'index' => 'php_index',
'body' => [
'query' => [
'wildcard' => [
'city' => '*京市'
]
]
]
];
$response = $client->search($params);
- 区间查找(类比MySQL where field <、<=、>、>=、between)
php
返回array
<是lt、<=是lte、>是gt、>=是gte
$params = [
'index' => 'php_index',
'body' => [
'query' => [
'range' => [
'area' => [ //面积大于1000平方千米的城市
'gt' => 1000
]
]
]
]
];
$response = $client->search($params);
dd($response->asArray());
返回array
between
$params = [
'index' => 'php_index',
'body' => [
'query' => [
'range' => [
'area' => [ //获取面积大于1000平方千米,但在10000平方千米以内的城市数据
'gt' => 1000,
'lt' => 10000,
]
]
]
]
];
$response = $client->search($params);
dd($response->asArray());
- 正则匹配(类比MySQL where field regexp 'xxx')针对keyword字段有效
php
$params = [
'index' => 'php_index',
'body' => [
'query' => [
'regexp' => [
'city' => '.*北京.*' //搜索包含北京关键字的字段
]
]
]
];
$response = $client->search($params);
dd($response->asArray());
.*: 匹配任意数量的任意字符
.: 匹配任意单个字符。
*: 匹配前面的元素零次或多次。
+: 匹配���面的元素一次或多次。
?: 匹配前面的元素零次或一次。
^: 匹配字符串的开头。
$: 匹配字符串的结尾。
[...]: 匹配方括号中的任意字符。
{n}: 匹配前面的元素恰好 n 次。
{n,}: 匹配前面的元素至少 n 次。
{n,m}: 匹配前面的元素至少 n 次,但不超过 m 次。
- 取反查找(类比MySQL where filed != 'kw')针对text类型的字段无效
php
返回bool
$params = [
'index' => 'php_index',
'body' => [
'query' => [
'bool' => [
'must_not' => [
'term' => [
'city' => '北京市' //返回不是北京市的数据
]
]
]
]
]
];
$response = $client->search($params);
dd($response->asArray());
$params = [
'index' => 'php_index',
'body' => [
'query' => [
'bool' => [
'must_not' => [
'range' => [
'area' => [ //面积不小于1000平方千米的城市
'lt' => 1000
]
]
]
]
]
]
];
$response = $client->search($params);
dd($response->asArray());
- 多条件and查找(类比MySQL where expression1 and expression2)
php
返回array
$params = [
'index' => 'php_index',
'body' => [
'query' => [
'bool' => [
'must' => [ //返回city字段是北京市,并且描述带有首都的数据
['term' => ['city' => '北京市']],
['match' => ['description' => '首都']],
]
]
]
]
];
$response = $client->search($params);
dd($response->asArray());
- 多条件or查找(类比MySQL where expression1 or expression2)
php
返回array
$params = [
'index' => 'php_index',
'body' => [
'query' => [
'bool' => [
'should' => [ //查询城市名北京市,或者描述含有沪的描述内容
['term' => ['city' => '北京市']],
['match' => ['description' => '沪']],
]
]
]
]
];
$response = $client->search($params);
dd($response->asArray());
- and 和 or 共同使用
php
$params = [
'index' => 'php_index',
'body' => [
'query' => [
'bool' => [ //查询城市名为北京市或上海市,并且描述带有京字的数据
'must' => [
[
'bool' => [
'should' => [
['term' => ['city' => '北京市']],
['term' => ['city' => '上海市']]
]
]
],
['match' => ['description' => '京']]
]
]
]
]
];
$response = $client->search($params);
dd($response->asArray());
- 排序(类比MySQL Order By)
php
单字段排序
$params = [
'index' => 'php_index',
'body' => [
'query' => [
'match_all' => new StdClass,
],
'sort' => [ //四个直辖市数据按照区域大小排名
['area' => ['order' => 'asc']] //asc或desc
]
]
];
$response = $client->search($params);
dd($response->asArray());
多字段排序
$params = [
'index' => 'php_index',
'body' => [
'query' => [
'match_all' => new StdClass,
],
'sort' => [ //区域按照降序,人口按照升序排,条件不会冲突,回想MySQL order by那样,合并处理。
['area' => ['order' => 'asc']], //asc或desc
['population' => ['order' => 'desc']], //asc或desc
]
]
];
- 聚合函数(类比MySQL聚合函数)
php
返回bool
$params = [
'index' => 'php_index',
'body' => [
'size' => 0, // 设置为0表示不返回实际的文档,仅返回聚合结果
'aggs' => [
'population_data' => [ //这个key为自定义名称
'avg' => [ //返回4个直辖市平均人口
'field' => 'population'
]
]
]
]
];
$response = $client->search($params);
dd($response->asArray());
avg : 平均值
sum :总和
min : 最小值
max :最大值
没有count。
- 分组(类比MySQL Group By)
php
返回string
$params = [
'index' => 'php_index',
'body' => [
'size' => 0, // 不返回文档,只返回聚合结果
'aggs' => [
'city_group' => [ //自定义名称
'terms' => [
'field' => 'city',
'size' => 10 // 聚合结果的数量限制
]
]
]
]
];
$response = $client->search($params)->asArray();
$aggregations = $response['aggregations']['city_group']['buckets'];
foreach ($aggregations as $bucket) {
echo "城市名:" . $bucket['key'] . " - 本组组对应的数量:" . $bucket['doc_count'] . "\n";
}
城市名:上海市 - 本组组对应的数量:1
城市名:北京市 - 本组组对应的数量:1
城市名:天津市 - 本组组对应的数量:1
城市名:重庆市 - 本组组对应的数量:1
- 合并(类比MySQL union) 用PHP array_merge实现吧,这对于ES不适用。
- 指定数据靠前(类比竞价排名)
php
返回array
个人还是推荐使用自定义字段,因为['hits']['_score']字段得出来分数不可控。
搜索城市,原先是北京靠前,现在通过修改权重,使其上海靠前
$params = [
'index' => 'php_index',
'body' => [
'query' => [
'function_score' => [
'query' => [
'bool' => [
'should' => [
['term' => ['city' => '北京市']],
['match' => ['description' => '沪']]
]
]
],
'functions' => [
[
'filter' => [
'match' => ['description' => '沪']
],
'weight' => 2 // 增加包含"沪"的文档的权重
]
],
'boost_mode' => 'sum'
]
],
'sort' => [
'_score' => [
'order' => 'desc' // 按照得分降序排序
]
]
]
];
$response = $client->search($params);
dd($response->asArray());
boost_mode设定了如何将查询的基础得分(由 query 部分确定)与功能得分(由 functions 部分计算)进行组合。以下是几种常用的 boost_mode 设置:
multiply: 基础得分与功能得分相乘。
replace: 功能得分替代基础得分。
sum: 基础得分与功能得分相加。
avg: 基础得分与功能得分的平均值。
max: 取基础得分与功能得分中的最大值。
Painless
- 极简概括:是一种简单、安全的、服务于Elasticsearch的脚本语言。类比Redis或Nginx中的Lua,某些组件嵌入脚本语言用于实现复杂的逻辑,这并不罕见。
- 官方文档:www.elastic.co/guide/en/el...
- 使用场景:针对ES,例如上文在更新文档中,请求文档script段中的source段,都用的painless表达式。
- 额外补充:由于painless语法内容过多,且比较简单,整个记录下来需要2万字,成本问题,因此读者推荐看手册。
- 简单举例:
php
//counter子对岸自增
ctx._source.counter += params.count
//if else 判断
if (ctx._source.someField > 10) {
ctx._source.anotherField = ctx._source.someField * params.multiplier;
} else {
ctx._source.anotherField = params.defaultValue;
}
IK中问分词与高级索引创建
- 使用理由:ES默认的分词器对中文不友好,英文分词器会把中文每个字分开,因此需要专门的中文分词器。
- 分词器的服务对象是映射,而不是索引。
- 安装:
sh
关闭ES
执行以下代码,注意版本号的问题
bin/elasticsearch-plugin install https://get.infini.cloud/elasticsearch/analysis-ik/8.14.1
进入交互界面输入Y。
之后启动ES
- ik_max_word与ik_smart分词精度控制演示:
json
演示分词:
GET IP:9200/_analyze
传入以下内容
{
"text":"射雕英雄传",
"analyzer":"ik_smart"
}
返回
{
"tokens": [
{
"token": "射雕英雄传",
"start_offset": 0,
"end_offset": 5,
"type": "CN_WORD",
"position": 0
}
]
}
若使用ik_max_word
{
"text":"射雕英雄传",
"analyzer":"ik_max_word"
}
则返回
{
"tokens": [
{
"token": "射雕英雄传",
"start_offset": 0,
"end_offset": 5,
"type": "CN_WORD",
"position": 0
},
{
"token": "射雕",
"start_offset": 0,
"end_offset": 2,
"type": "CN_WORD",
"position": 1
},
{
"token": "英雄传",
"start_offset": 2,
"end_offset": 5,
"type": "CN_WORD",
"position": 2
},
{
"token": "英雄",
"start_offset": 2,
"end_offset": 4,
"type": "CN_WORD",
"position": 3
},
{
"token": "传",
"start_offset": 4,
"end_offset": 5,
"type": "CN_CHAR",
"position": 4
}
]
}
- 配置自定义分词:
php
有些场景,有很多的专业用语,但是IK分词器把它拆分开,就显得不是很精准,因此可以添加自定义分词解决。
vim ES安装目录/config/analysis-ik/IKAnalyzer.cfg.xml
在<entry key="ext_dict"></entry>的双标签中间写入文件名,例如
<entry key="ext_dict">self_words.dic</entry>
vim self_words.dic
逐行添加自定义词汇
重启ES。
- PHP使用:
php
返回bool
$params = [
'index' => 'test_index',
'body' => [
'settings' => [
'analysis' => [
'analyzer' => [
'analyzer_ik_max_word' => [
'type' => 'ik_max_word' //ik分词器内置关键配置,更多的分词结果
],
'analyzer_ik_smart' => [
'type' => 'ik_smart' //ik分词器内置关键配置,更快的分词结果
]
]
]
],
'mappings' => [
'properties' => [
'content' => [
'type' => 'text',
'analyzer' => 'analyzer_ik_smart', // 设置索引时的分词器
'search_analyzer' => 'analyzer_ik_smart' // 设置搜索时的分词器
]
]
]
]
];
$response = $client->indices()->delete(['index' => 'test_index']);
dump($response->asBool());
- 进阶用法,添加过滤(不生效):
php
返回bool
这里尝试创建了一个更复杂的索引,添加了过滤器,但是不生效,不知道是那里的问题。
如下,按照以下索引的配置,过滤后的结果,应当是"C世界上最好编程语言",然后再分词,可却不生效。
GET IP:9200/test_index/_analyze
{
"analyzer":"self_ik_max_word",
"text" : "PHP是世界上最好的编程语言"
}
$params = [
'index' => 'test_index', // 指定要创建的索引名称
'body' => [
'settings' => [ // 配置索引的设置
'analysis' => [ // 分析器设置
'char_filter' => [ // 字符过滤器设置
'self_char_filter' => [ // 自定义字符过滤器名称
'type' => 'mapping', // 过滤器类型为映射
'mappings' => ['PHP => C'] // 替换分词的字符
]
],
'filter' => [ // 过滤器设置
'self_filter' => [ // 自定义停用词过滤器名称
'type' => 'stop', // 过滤器类型为停用词
'stopwords' => ['是', '的'] // 停用词列表
]
],
'analyzer' => [ // 分析器设置
'self_ik_max_word' => [ // IK 分词器名称
'type' => 'ik_max_word', // 使用 IK 分词器的最大分词模式
'char_filter' => ['html_strip', 'self_char_filter'], // html_strip过滤器会把html标签忽略,但html转义字符仍旧生效( 仍旧是空格),且会把<br/>转化为\n
'filter' => ['lowercase', 'self_filter'] //lowercase过滤器是将大写字母变为小写
],
'self_ik_smart' => [ // IK 分词器名称
'type' => 'ik_smart', // 使用 IK 分词器的快速分词模式
'char_filter' => ['html_strip', 'self_char_filter'], // html_strip过滤器会把html标签忽略,但html转义字符仍旧生效( 仍旧是空格),且会把<br/>转化为\n
'filter' => ['lowercase', 'self_filter'] //lowercase过滤器是将大写字母变为小写
]
]
]
],
'mappings' => [ // 配置索引的映射
'properties' => [ // 文档字段的属性设置
'content' => [ // 文档中的字段名称
'type' => 'text', // 字段类型为文本
'analyzer' => 'self_ik_max_word', // 设置索引时的分词器
'search_analyzer' => 'self_ik_max_word' // 设置搜索时的分词器
]
]
]
]
];
$response = $client->indices()->create($params);
dd($response->asBool());
ELK
- 概念:ES结合LK作为ELK(Elasticsearch(搜索), Logstash(采集转换), Kibana(分析))组合,可用于实时监控、分析和可视化大量日志和事件数据,如系统日志、应用程序日志、网络流量日志等。
- 构成
- Elasticsearch:一个分布式搜索引擎,提供强大的搜索功能和实时的数据分析能力。
- Logstash:一个数据处理管道,用于收集、解析和转发日志数据。
- Kibana:一个数据可视化工具,帮助用户通过图形化界面查看和分析 Elasticsearch 中的数据。
- 作用:
- 日志管理:集中化日志收集:通过Logstash收集来自不同系统和应用的日志,统一存储在Elasticsearch中。
- 日志分析:利用Kibana对日志数据进行实时分析和可视化,帮助发现系统问题和异常。
- 实时监控:跟踪应用程序的性能指标,实时查看应用的健康状况。
- 性能瓶颈检测:通过分析日志数据,识别和解决性能瓶颈。
- 安全事件分析:监控和分析系统中的安全事件,检测异常行为。
- 合规性审计:记录和分析系统日志,以满足合规性要求。
- 数据可视化:通过Kibana创建各种图表和仪表盘,帮助业务分析师理解数据趋势和模式。
- 用户行为分析:分析用户的操作日志,优化用户体验和产品设计。
- 服务器监控:跟踪服务器的性能指标,如CPU使用率、内存使用情况和磁盘空间。
- 应用状态监控:监控应用程序的运行状态和日志,以确保正常运行。
- 问题诊断:利用Elasticsearch存储的日志数据,快速定位和解决系统故障。
- 根因分析:分析相关日志,帮助找到问题的根本原因。
- 对于PHP而言:几乎用不到,这是Java和大数据方向的。
Kibana
- 极简概括:开源的可视化控制ES的组件。
- 官方文档:www.elastic.co/guide/en/ki...
- 安装:
sh
保证ES服务已启动。
防火墙开启5601端口
firewall-cmd --add-port=5601/tcp --zone=public --permanent
systemctl restart firewalld
下载与解压
curl -O https://artifacts.elastic.co/downloads/kibana/kibana-8.14.1-linux-x86_64.tar.gz
tar zxf kibana-8.14.1-linux-x86_64.tar.gz
权限配置
chown -R es:es kibana-8.14.1
切换用户
su es
kibana不支持elastic用户,所以需要创建新用户,并赋予超级管理员角色,并赋予kibana_system角色
elasticsearch-users useradd zs
elasticsearch-users roles -a superuser zs
少了这一步报错,让我搞了4个小时。
elasticsearch-users roles -a kibana_system zs
修改配置
vim kibana-8.14.1/config/kibana.yml
elasticsearch.username: "zs"
elasticsearch.password: "123456"
elasticsearch.hosts: ["ES IP:9200"]
i18n.locale: "zh-CN"
启动
kibana-8.14.1/bin/kibana
过2分钟后,访问http://IP:5601
4种和数据库同步方案
- 不妨先讲一讲业务层是怎么使用ES的读功能的: 以电商系统为例,用到ES的原因,一个是商品数量庞大,一个是分词有助于展示更好的结果,上架的商品因为关键词误差搜不到,这就是损失。 例如商品列表数据的展示,可将价格,名称,描述,主图片,标签,id等其他数据存入ES,然后展示。 当用户点击某个商品时,根据id进行哈希运算,获取商品数据在那个MySQL分表中,利用id主键索引极速查询的特性,快速获取商品数据。
- 同步双写:MySQL和ES同步更新
- 优点:实现简单,实时性高。
- 缺点:耦合度高,其中一个组件异常可能会影响另一个。
- 异步双写:先同步MySQL,再用MQ同步ES。
- 优点:优雅,由于MQ(非Redis实现的MQ)具有高可用机制,因此ES消费失败可以重试。
- 缺点:多了一个MQ,就多了一层运维成本。有延迟。
- 自动化任务,定时遍历SQL:用时间戳做标识符,用于区分哪些数据未同步,没有同步就用脚本定时同步到ES。
- 优点:业务逻辑层不需要额外的针对ES做写操作。
- 缺点:实时性不够,对MySQL压力大。
- 使用Canal基于Binlog进行接近实时的同步,使用Canal监听MySQL Binlog,并部署同步ES数据的脚本,从而自动化保持同步。也可直接利用Canal同步ES。相关链接:github.com/alibaba/can...。
- 优点:实时性高,对业务层代码无侵入。
- 缺点:多了一个Canal,就多了一层运维成本。
高并发下ES本身一致性解决方案
- 问题:与上文讲的数据库一致性,不是一个东西。这里讲的是并发下ES本身更新数据导致的一致性问题。例如并发过来的两个请求,查询到结果是10,都想要-1,等两个执行完毕后,结果不是8而是9,那么就出现了数据一致性问题。
- ES之外的解决方案:分布式锁。或非分布式环境下编程语言自带的具有排它性的锁。
- ES乐观锁解决方案1:
php
背景:先创建一个num_test索引,并添加名为num的int类型的映射。并插入一条数据。
流程:当进行数据更新时,先做一次查询(get方法,不是search方法),获取相关的_primary_term,_seq_no值。
当更新数据时,添加对应的版本号,如果ES检测到版本号不对,则会报错,如下:
$params = [
'index' => 'num_test',
'id' => 1,
'body' => [
'doc' => [
'num' => 10
]
],
'if_seq_no' => 3, // 使用序列号
'if_primary_term' => 1, // 使用主分片术语
];
try {
$response = $client->update($params);
} catch (\Exception $exception) {
echo '出错了,这里重试查询后再更新,或者记录错误等其它操作。。。'
}
- ES乐观锁解决方案2(不生效,请勿使用):
php
$params = [
'index' => 'num_test',
'id' => 1,
'body' => [
'doc' => [
'num' => 1800
]
],
'version' => 40, // 提供外部版本号
'version_type' => 'external' // 使用外部版本号
];
try {
$response = $client->update($params); //版本不生效的方案,不推荐使用
} catch (\Exception $exception) {
dump('出错了,这里进行重试,或者记录错误,等其它操作');
}
- ES应对高并发写的报错问题(和上文内容不是一回事):ES针对大量的并发过来的写请求,ES支持的并不好,ES底层采用乐观锁的形式,这会导致ES内部在频繁并发写入时内部维护版本号冲突,也就是说更新前查询出来的版本号,比当前实际的版本号小(被其它并发过来的请求增加了版本号),那就会报错,这也就是所谓的ES报版本冲突的错误的问题,对于这种场景,可添加重试次数,和业务层的异常获取作为兜底策略。重试代码示例如下:
php
$params = [
'index' => 'index',
'id' => '10',
'body' => [
'doc' => [
'field1' => 'new value1',
'field2' => 'new value2'
]
],
'retry_on_conflict' => 3 // 设置重试次数
];
try {
$response = $client->update($params);
} catch (Exception $e) {
// 处理异常,可以选择记录日志或执行其它操作,这个catch是用来重试3次还报错的兜底策略。
}
为什么不用ES替代MySQL
- ES没有MySQL的事务机制,高可用无法保证。
- ES没有MySQL的关系型侧重,MySQL有强大的关联策略,MySQL join多张表时,ES需要手动实现。
- ES的定位是快速索引快速查找,并非有过多高可用存储的机制,还是需要配合MySQL使用。
EQL
- 极简概括:Event Query Language用于在ES中进行事件数据查询的类SQL语言。
- 解决问题:为了更方便地分析时间序列数据和事件流数据,特别适用于安全事件、日志数据和监控数据的分析。
- 弃用原因:多用于快速调试。毕竟ES不是MySQL,SQL API 并不是ES中所有功能的完整替代品,有些复杂的查询和功能可能需要使用原生的ES查询 DSL(ES领域或问题域设计的编程语言或语法)。
- 简单示例:要查询索引下的一条数据
json
POST IP:9200/_sql?format=json //类型可未txt,用制表符更直观的展示
{
"query": "SELECT * FROM php_index WHERE city = '北京市'"
}
结果:
{
"columns": [
{
"name": "_boost",
"type": "float"
},
{
"name": "area",
"type": "integer"
},
{
"name": "city",
"type": "keyword"
},
{
"name": "description",
"type": "text"
},
{
"name": "id",
"type": "long"
},
{
"name": "population",
"type": "integer"
}
],
"rows": [
[
null,
16411,
"北京市",
"北京市(Beijing),简称"京",古称燕京、北平,是中华人民共和国首都、直辖市、国家中心城市、超大城市, 国务院批复确定的中国政治中心、文化中心、国际交往中心、科技创新中心, 中国历史文化名城和古都之一,世界一线城市",
1,
2186
]
]
}
- 演示2:查询所有索引:
json
POST IP:9200/_sql?format=txt
{
"query": "show tables"
}
catalog | name | type | kind
---------------+--------------------------------------------------+---------------+---------------
zs_es_cluster |.alerts-default.alerts-default |VIEW |ALIAS
zs_es_cluster |.alerts-ml.anomaly-detection-health.alerts-default|VIEW |ALIAS
zs_es_cluster |.alerts-ml.anomaly-detection.alerts-default |VIEW |ALIAS
zs_es_cluster |.alerts-observability.apm.alerts-default |VIEW |ALIAS
zs_es_cluster |.alerts-observability.logs.alerts-default |VIEW |ALIAS
zs_es_cluster |.alerts-observability.metrics.alerts-default |VIEW |ALIAS
zs_es_cluster |.alerts-observability.slo.alerts-default |VIEW |ALIAS
zs_es_cluster |.alerts-observability.threshold.alerts-default |VIEW |ALIAS
zs_es_cluster |.alerts-observability.uptime.alerts-default |VIEW |ALIAS
zs_es_cluster |.alerts-security.alerts-default |VIEW |ALIAS
zs_es_cluster |.alerts-stack.alerts-default |VIEW |ALIAS
zs_es_cluster |.alerts-transform.health.alerts-default |VIEW |ALIAS
zs_es_cluster |.kibana-observability-ai-assistant-conversations |VIEW |ALIAS
zs_es_cluster |.kibana-observability-ai-assistant-kb |VIEW |ALIAS
zs_es_cluster |.siem-signals-default |VIEW |ALIAS
zs_es_cluster |my_index |TABLE |INDEX
zs_es_cluster |num_test |TABLE |INDEX
zs_es_cluster |php_index |TABLE |INDEX
zs_es_cluster |test_index |TABLE |INDEX
zs_es_cluster |zs_index |TABLE |INDEX