ElasticSearch第1讲(4万字详解 Linux下安装、原生调用、API调用超全总结、Painless、IK分词器、4种和数据库同步方案、高并发下一致性解决方案、Kibana、 ELK)

ElasticSearch

  • 官方文档:https://www.elastic.co/guide/en/elasticsearch/reference/current/getting-started.html
  • 非官方中文文档:https://learnku.com/docs/elasticsearch73/7.3
  • 极简概括:基于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

  • 系统配置,用于开启防火墙,创建用户,和大数据情况下提升性能。
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
  • 安装相关
下载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和端口对外暴露,这不是一种安全的行为。
注意,要部署集群,各个节点密码应当一致。

注意配置格式,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/
{
    "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/索引名
{
    "acknowledged": true,
    "shards_acknowledged": true,
    "index": "zs_index"
}

"acknowledged": true:指示请求是否被成功接受和处理。
"shards_acknowledged": true:指示所有分片是否已经确认请求。
"index": "zs_index":这表示操作涉及的索引名称为 “zs_index”。
  • 创建索引:
    重复创建,报错说明:
{
    "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/索引名
{
    "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
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/索引名
{
    "acknowledged": true
}

返回true表示成功执行。

文档增删改查操作

  • 增文档(数据):
    方式1:POST请求 IP:9200/索引名/_doc/可选参数,数据唯一标识
    方式2:PUT请求 IP:9200/索引名/_create/必填唯一标识符 由于方式2的put请求是幂等,所以再次请求会报错
这是存入的数据
{
    "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/唯一标识
方式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/唯一标识
{
    "_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
{
    "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/唯一标识
{
    "_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": { "文档字段名":"要搜索的关键字" } } }
{
    "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)之意。

{
    "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":{ "字段名":{} } } }
    响应结果同上。

聚合查询

{
    "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)的概念

重新建立一个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调用

官方文档:https://www.elastic.co/guide/en/elasticsearch/client/php-api/current/getting-started-php.html#_installation
某些ES Api(例如创建索引)不能重复执行,重复执行会报错,所以在执行写操作的上游做判断,或者使用try catch。

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增删改查

  • 创建
返回bool
$response = $client->indices()->create([
    'index' => 'php_index'
]);
$response->asBool();
  • 查询 判断索引是否存在
返回bool
$response = $client->indices()->exists(['index' => 'php_index']);
dd($response->asBool());
  • 查询 查看索引相关信息
返回array
$response = $client->indices()->get(['index' => 'php_index']);
dd($response->asArray());
  • 删除
返回bool
$response = $client->indices()->delete(['index' => 'php_index']);
dd($response->asBool());
  • 修改
    索引作为基础性的数据支撑,一般不做改动。

PHP ES Api针对Mapping增删改查

返回bool
$params = [
    'index' => 'php_index',
    'body' => [
        'properties' => [
            'name' => [
                'type' => 'text',
            ],
        ]
    ]
];
$response = $client->indices()->putMapping($params);

dd($response->asBool());
  • 增 创建索引时
返回bool
$params = [
    'index' => 'php_index',
    'body' => [
        'mappings' => [
            'properties' => [
                'title' => [
                    'type' => 'text',
                ],
                'content' => [
                    'type' => 'text',
                ],
            ]
        ]
    ]
];
$response = $client->indices()->create($params);
dd($response->asBool());
  • 查 所有索引
返回数组
$response = $client->indices()->getMapping();
dd($response->asArray());
  • 查 指定索引
返回数组
$response = $client->indices()->getMapping(['index' => 'php_index']);
dd($response->asArray());

  • 请直接删除索引。

  • 请重新建立索引,在新索引基础上做映射的修改。

PHP ES Api针对Doc增删改

  • 索引与映射如下:
准备四个直辖市的名称,简介,人口和面积大小。
$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。
返回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',
    ]
];
  • 增 多条
返回数组
//假设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());
可根据返回的数据再次循环,排查失败掉的漏网之鱼
  • 删 单条
返回bool
$params = [
    'index' => 'php_index',
    'id'    => '1025'
];
$response = $client->delete($params);
dd($response->asBool());
  • 删 多条
方式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 "批量删除成功!";
}
  • 删 文档的某个字段
返回bool
$params = [
    'index' => 'php_index',
    'id' => 1,
    'body' => [
        'script' => [
            'source' => 'ctx._source.remove(params.field)',
            'params' => [
                'field' => '要删除的字段名'
            ]
        ]
    ]
];

$response = $client->update($params);
dd($response->asBool());
  • 改 直接修改
返回bool
$params = [
    'index' => 'php_index',
    'id'    => 1,
    'body'  => [
        'doc' => [
            'city' => '北京' //这里是要修改的字段,把北京市改为北京
        ]
    ]
];

$response = $client->update($params);
dd($response->asBool());
  • 改 自增
返回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());
  • 改 若文档不存在,则插入
$params = [
    'index' => 'php_index',
    'id'    => 60, //若id对应的文档不存在,则利用upsert段的数据,重新生成一个id为60的文档。
    'body'  => [
        'doc' => [
            'city' => '台北市'
        ],
        'upsert' => [
            'append_field' => 1
        ],
    ]
];

$response = $client->update($params);
dd($response->asBool());
  • 改 批量
//假设以下数据时数据表中查询出来的字段,要修改以下内容
$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 "批量删除成功!";
}
  • 改 追加新的字段
$params = [
    'index' => 'php_index',
    'id' => '1',
    'body' => [
        'doc' => [
            'new_field' => 'new_value'
        ],
    ]
];

$response = $client->update($params);

  • 改 删除某些字段
返回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高级查询

查询关键词官方文档:https://www.elastic.co/guide/en/elasticsearch/reference/current/term-level-queries.html

  • 指定id查找
返回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"
    }
}
  • 查找全部
返回array
$response['hits']['total']['value']可获取条数
$params = [
    'index' => 'php_index',
    'body'  => [
        'query' => [
            'match_all' => new StdClass
        ]
    ]
];

$response = $client->search($params);

dd($response->asArray());
  • 指定指定部分id的数据。
返回数组
$response['hits']['total']['value']可获取条数
$params = [
    'index' => 'php_index',
    'body' => [
        'query' => [
            'ids' => [
                'values' => [1, 2]
            ]
        ]
    ]
];

$response = $client->search($params);
dd($response->asArray());
  • 分页查询
返回数组
//传输的页码
$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());
  • 返回指定字段
返回数组
$params = [
    'index' => 'php_index',
    'body' => [
        '_source' => ['description'], //自定义字段
        'query' => [
            'match_all' => new \stdClass()
        ]
    ]
];

$response = $client->search($params);
dd($response->asArray());
  • 判断是否存在
返回bool
$params = [
    'index' => 'php_index',
    'id'    => 10
];
$response = $client->exists($params);
  • 获取条数
返回int
$params = [
    'index' => 'php_index',
    'body' => [
        'query' => [
            'match_all' => new StdClass
        ]
    ]
];

$response = $client->count($params);
dd($response->asArray()['count'] ?? 0);
  • 高亮查询(类比百度词条对关键字的标红行为)
返回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)
返回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 查询
方式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%')
方式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查询需要对每个文档的字段值进行模式匹配
方式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类型的字段有效
返回array
$params = [
    'index' => 'php_index',
    'body'  => [
        'query' => [
            'prefix' => [
                'city' => '北'
            ]
        ]
    ]
];

$response = $client->search($params);
  • 后缀查找 (类比MySQL where filed like '%kw')针对keyword字段有效
$params = [
    'index' => 'php_index',
    'body'  => [
        'query' => [
            'wildcard' => [
                'city' => '*京市'
            ]
        ]
    ]
];

$response = $client->search($params);
  • 区间查找(类比MySQL where field <、<=、>、>=、between)
返回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字段有效
$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类型的字段无效
返回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)
返回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)
返回array
$params = [
    'index' => 'php_index',
    'body' => [
        'query' => [
            'bool' => [
                'should' => [ //查询城市名北京市,或者描述含有沪的描述内容
                    ['term' => ['city' => '北京市']],
                    ['match' => ['description' => '沪']],
                ]
            ]
        ]
    ]
];
$response = $client->search($params);
dd($response->asArray());
  • and 和 or 共同使用
$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)
单字段排序
$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聚合函数)
返回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)
返回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不适用。
  • 指定数据靠前(类比竞价排名)
返回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,某些组件嵌入脚本语言用于实现复杂的逻辑,这并不罕见。
  • 官方文档:https://www.elastic.co/guide/en/elasticsearch/painless/current/painless-guide.html
  • 使用场景:针对ES,例如上文在更新文档中,请求文档script段中的source段,都用的painless表达式。
  • 额外补充:由于painless语法内容过多,且比较简单,整个记录下来需要2万字,成本问题,因此读者推荐看手册。
  • 简单举例:
//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默认的分词器对中文不友好,英文分词器会把中文每个字分开,因此需要专门的中文分词器。
  • 分词器的服务对象是映射,而不是索引。
  • 安装:
关闭ES

执行以下代码,注意版本号的问题
bin/elasticsearch-plugin install https://get.infini.cloud/elasticsearch/analysis-ik/8.14.1
进入交互界面输入Y。

之后启动ES
  • ik_max_word与ik_smart分词精度控制演示:
演示分词:
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
        }
    ]
}


  • 配置自定义分词:
有些场景,有很多的专业用语,但是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使用:
返回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());
  • 进阶用法,添加过滤(不生效):
返回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转义字符仍旧生效(&nbsp;仍旧是空格),且会把<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转义字符仍旧生效(&nbsp;仍旧是空格),且会把<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服务已启动。


防火墙开启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。相关链接:https://github.com/alibaba/canal/wiki/Sync-ES
    • 优点:实时性高,对业务层代码无侵入。
    • 缺点:多了一个Canal,就多了一层运维成本。

高并发下ES本身一致性解决方案

  • 问题:与上文讲的数据库一致性,不是一个东西。这里讲的是并发下ES本身更新数据导致的一致性问题。例如并发过来的两个请求,查询到结果是10,都想要-1,等两个执行完毕后,结果不是8而是9,那么就出现了数据一致性问题。
  • ES之外的解决方案:分布式锁。或非分布式环境下编程语言自带的具有排它性的锁。
  • ES乐观锁解决方案1:
背景:先创建一个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(不生效,请勿使用):
$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报版本冲突的错误的问题,对于这种场景,可添加重试次数,和业务层的异常获取作为兜底策略。重试代码示例如下:
$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领域或问题域设计的编程语言或语法)。
  • 简单示例:要查询索引下的一条数据
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:查询所有索引:
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          

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 205,236评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,867评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,715评论 0 340
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,899评论 1 278
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,895评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,733评论 1 283
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,085评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,722评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,025评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,696评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,816评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,447评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,057评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,009评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,254评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,204评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,561评论 2 343

推荐阅读更多精彩内容