用 C++ ElasticClient 解决 MySQL 模糊查询性能瓶颈,实测提升 100 倍

ES 介绍
在开发高并发后端、搜索服务、日志分析平台时,你是否也遇到过这些难题:想实现全文检索,用传统数据库模糊查询性能极差【特指的就是 MySQL(以及 Oracle、SQL Server 这类传统关系型数据库)】,数据量稍大就直接卡死;需要存储海量日志、指标、业务数据,还要秒级查询、聚合分析,关系型数据库根本扛不住;要搭建分布式高可用服务,手写分片、副本、负载均衡,底层代码复杂到崩溃;数据量从 GB 涨到 PB,服务需要无缝扩容,传统存储方案要么停机维护,要么架构重构;多终端、多语言系统需要统一调用搜索接口,还要兼容 JSON 数据,手写适配代码繁琐又易出错。
尤其是在大数据、微服务、实时分析场景:电商商品搜索、日志监控、用户行为分析、APP 全局检索需要秒级、精准、全文匹配,SQL 查询满足不了实时性;海量业务数据需要分布式存储、自动容错,单机服务随时有宕机风险;需要对非结构化数据(文本、JSON、日志)直接存储检索,传统数据库表结构设计极其复杂;线上服务需要实时监控、快速排查问题,手写存储查询系统没有可视化能力,排障效率极低。轻则功能无法实现,重则服务因查询延迟、数据丢失、扩容困难导致全线崩溃。
而Elasticsearch(简称 ES) 正是为解决这些痛点而生 —— 这款基于 Lucene 的开源分布式实时搜索与数据分析引擎,核心就是让你用极简的 RESTful API,快速实现海量数据存储、近实时检索、复杂聚合分析、高可用分布式部署,它就像给你的系统装上了一个 “智能数据引擎”,天生支持分布式、自动分片、副本容错、全文索引,无需手写底层存储和搜索逻辑,堪称大数据检索、日志分析、全文搜索的 “神器”,广泛应用于电商搜索、日志监控、安全审计、推荐系统、企业级数据检索等核心业务场景。
光说不练假把式,我们用一个 “快速上手 ES + 基础增删改查” 的例子,直观感受 ES 的便捷性:(基于 RESTful 接口,无需复杂配置,开箱即用)
如果不清楚 ES 核心概念,可以先简单了解:ES 是面向文档的分布式搜索引擎,存储 JSON 格式数据,支持近实时检索与复杂分析 😊
第一步:启动 ES 服务
ES 基于 Java 开发,解压即用,零配置启动,无需搭建复杂环境:
# 启动ES(单节点)
./bin/elasticsearch
启动成功后,访问 http://127.0.0.1:9200,即可看到 ES 服务信息,自带可视化交互能力。
第二步:基础操作(RESTful 接口,全语言兼容)
ES 原生支持GET/PUT/POST/DELETE标准接口,所有操作无需 SDK,直接调用即可,极简高效。
1. 创建索引(相当于数据库的 “表”)
PUT /my_index
{
"settings": {
"number_of_shards": 1, // 自动分片
"number_of_replicas": 1 // 自动副本,高可用
}
}
2. 插入文档(相当于数据库的 “行数据”,存储 JSON)
POST /my_index/_doc/1
{
"name": "ES入门教程",
"type": "搜索引擎",
"content": "ES是分布式实时搜索分析引擎,支持海量数据存储与秒级检索",
"create_time": "2026-03-06"
}
3. 根据 ID 查询文档
GET /my_index/_doc/1
4. 全文检索(核心能力,模糊匹配 + 智能评分)
GET /my_index/_search
{
"query": {
"match": {
"content": "分布式 实时检索"
}
}
}
5. 修改文档
POST /my_index/_doc/1/_update
{
"doc": {
"content": "ES是开源分布式Lucene搜索引擎,支持PB级数据、近实时查询"
}
}
6. 删除文档
DELETE /my_index/_doc/1
第三步:查看集群状态(自带分布式监控)
GET /_cluster/health
返回结果直接展示节点数、分片状态、是否健康,分布式集群状态一目了然。
当你想新增业务数据时,直接插入 JSON 文档,无需提前定义表结构;想实现全文搜索,一行查询语句就能完成模糊匹配、分词检索、结果排序;需要扩容,直接新增节点,ES 自动发现、自动均衡数据,无需停机;需要高可用,开启副本机制,单个节点宕机不影响服务;需要做数据分析,内置聚合函数,轻松统计、分组、报表生成,无需手写复杂计算逻辑。
这就是 ES 的核心价值:把分布式存储、全文检索、数据分析的复杂底层全部封装,让开发者零底层成本实现高性能搜索服务,同时天然支持分布式、高可用、易扩容,是处理海量数据、实时检索、日志分析的 “工业级标准方案”。
除了核心的搜索能力,ES 还内置超多企业级特性,覆盖全场景需求:
- 近实时:数据写入后 1 秒内即可检索,延迟极低;
- 分布式架构:自动分片、自动副本、自动负载均衡,支持上百台服务器集群;
- 海量存储:水平扩展轻松支撑 PB 级数据;
- 全文检索:智能分词、模糊匹配、相关性评分、高亮显示;
- 数据聚合:支持求和、平均值、分组、桶聚合,快速生成报表;
- 多数据源兼容:支持 Logstash、Kafka、Beats 等无缝接入;
- 高可用容错:节点故障自动切换,数据不丢失、服务不中断;
- RESTful API:所有语言、所有系统都能直接调用,无适配成本。
从搭建个人博客搜索,到支撑电商亿级商品检索、企业 PB 级日志分析,ES 都能完美胜任,真正做到简单易用、性能强悍、稳定可靠!
ES 检索原理
正排索引
正排索引,也称为前向索引,是一种将文档或数据记录按照某种特定顺序组织的索引机制。在正排索引中,索引的键通常是文档的标识符,如文档 ID,而索引的值则包含文档的详细信息,例如标题、内容摘要、发布日期等。这种结构使得正排索引非常适合执行基于特定标识符的查找操作。正排索引的优点在于能够直接根据文档 ID 快速访问文档,适合于需要按照文档顺序进行操作的场景。文档内容: 即通过不同字段组成的完整数据,这里一行数据代表一个文档

正排索引的结构:正排索引的 “键” 是文档 ID(比如图里的 1、2、3),“值” 是这个 ID 对应的完整文档内容(也就是那一行数据,包含年龄、姓名等所有字段)。
它的工作方式:“当想找 “文档 ID=1” 的内容时,正排索引会直接定位到这一行,把 {"年龄":18, "姓名":"张三"} 这整条数据返回给你。它的核心是:用 ID 找整条记录,而不是用字段值去反查 ID。
- 左边的 “正排索引” 框,就像一个映射表:
1 → {18, 张三},2 → {20, 张老八},3 → {18, 李三}。 - 右边的表格,就是正排索引里存储的 “文档内容”,每一行就是一个完整文档。
正排索引就是 “按文档 ID 存,按文档 ID 取” 的结构,它能让你通过 ID 快速拿到整条数据,但如果你想反过来问 “年龄 = 18 的人有谁?”,它就需要把所有文档都扫一遍,效率很低。
倒排索引
倒排索引,又称反向索引或逆向索引,是按照文档中的词汇来组织数据的索引方法。在倒排索引中,每个独特的词汇都会有一个索引条目,该条目包含指向包含该词汇的所有文档的指针或引用。这使得倒排索引非常适合全文搜索,能够快速找到包含特定关键词的文档。倒排索引则适合于全文搜索,可以快速找到包含特定关键词的所有文档,索引的大小相对较小,因为它只记录关键词和文档的映射关系。但是,倒排索引不能直接通过索引访问文档,需要结合正排索引来获取文档的详细信息。

倒排索引的结构:倒排索引的 “键” 是字段值或关键字(比如图里的年龄 18、20,或者姓名里的 张、老、三 等),“值” 是包含这个值的文档 ID 列表(比如 [1, 3]、[2])。
它的工作方式:当我们呢想查询 “年龄 = 18 的人有谁?” 时,倒排索引会直接定位到 18 这个键,返回对应的文档 ID 列表 [1, 3],然后再通过正排索引去获取这两个 ID 对应的完整文档。它的核心是:用字段值或关键字,反查包含它的文档 ID
- 左边的 “倒排索引” 框,维护了两套映射关系:
- 按年龄字段:
18 → [1, 3],20 → [2] - 按姓名字段分词后的关键字:
张 → [1, 2],老 → [2],三 → [1, 3],老八 → [2],八 → [2]
- 按年龄字段:
- 右边的两个表格,就是倒排索引中存储的 “关键字 → 文档 ID 列表” 映射表。
倒排索引就是 “用内容找文档” 的结构,它能让你通过关键词、字段值快速定位到包含这些内容的文档,这也是 ES 能实现高效全文检索的核心原理。
ES 核心概念

一个索引就是一组相似文档的集合,相当于 MySQL 的一个数据库。
-
名字必须全小写
-
用来对文档做:增删改查
假设我们做一个电商系统,可以建 3 个索引:
-
user(用户数据) -
product(商品数据) -
order(订单数据)
就像 MySQL 建 3 个库:user_db、product_db、order_db。
类型是一个索引里的逻辑分类,相当于 MySQL 的一张表。一个索引可以分多个类型。
索引 user 里可以建:
-
customer(普通用户) -
admin(管理员)
就像 MySQL 一个库里有 customer 表、admin 表。
只是说当前新版本他没有类型的概念了,就是一库一表,单独索引就可以表示一个库一个表,没有一库多表了!
字段就是文档的属性,相当于 MySQL 的列。
|
分类 |
类型 |
说明 + 实例 |
|---|---|---|
|
字符串 |
text, keyword |
text:会分词;keyword:不分词 |
|
整形 |
integer, long |
年龄、ID |
|
浮点 |
double, float |
价格、体重 |
|
逻辑 |
boolean |
true / false |
|
日期 |
date |
"2025-01-01" 或时间戳 |
|
二进制 |
binary |
图片、文件 |
|
范围 |
range |
10~20 岁、50~100 元 |
一个用户文档字段:
{
"name": "张三", // text 类型
"age": 20, // integer
"phone": "13800138000", // keyword
"is_vip": true // boolean
}
映射(mapping) 就是给字段定规则:类型、是否分词、是否索引、是否自动转换、分词器…
|
参数 |
说明 |
例子 |
|---|---|---|
|
enabled |
是否只存储、不搜索 |
false:只存不查 |
|
index |
是否建倒排索引(能否被搜索) |
false:不能搜索 |
|
dynamic |
是否自动新增字段 |
false:不允许乱加字段 |
|
doc_value |
能否排序 / 聚合(分词字段不能用) |
true:可以排序 |
|
analyzer |
分词器 |
ik:中文分词 |
|
coerce |
自动类型转换 |
"18" → 转数字 18 |
实例(定义一个用户映射)
{
"name": { "type": "text", "analyzer": "ik" },
"age": { "type": "integer" },
"phone": { "type": "keyword", "index": true }
}
文档 = ES 里的一行数据用 JSON 表示,是可被索引的最小单元。
一个用户文档:
{
"id": 1001,
"name": "张三",
"age": 22,
"phone": "13812345678"
}
其中我们要注意:
text 会把一句话切成词语(分词),用来搜索;keyword 保持原样不切,用来精确匹配。
昵称:张三是程序员
text 分词:张三 / 是 / 程序员搜:张三 → 能找到搜:程序员 → 能找到
手机号:13900001111
keyword 不分词搜:13900001111 → 能找到搜:139 → 找不到
text:会分词,适合:内容、标题、描述、姓名、文章,支持:模糊搜索
keyword:不分词,适合:手机号、ID、编号、状态、枚举、订单号,支持:精确匹配
ES 的可视化客户端【用于测试】
我们知道了 Elasticsearch 的核心概念、索引 / 文档 / 字段的定义规则,也清楚了 text 与 keyword 分词、mapping 映射设置 等底层原理。但光理解理论还不够,我们需要一个可视化、可直接操作的工具,来真正上手验证 ES 的各项功能、直观看到索引与数据的真实效果。
而 Kibana 就是 Elastic Stack 官方提供的可视化操作工具【Kibana = Elasticsearch 的客户端 + 可视化操作界面】,它可以直接连接 Elasticsearch,让我们通过简单的控制台指令,完成索引创建、数据增删改查、搜索测试等全部操作。接下来,我们就从 Kibana 访问登录 开始,一步步把前面学到的 ES 理论落地为实际操作。
通过浏览器访问 Kibana 是连接 ES 的第一步,访问地址如下:http://192.168.65.128:5601/
⚠️ 关键注意:这里的
192.168.65.128是教程示例的服务器 IP,必须替换为你自己部署 Elasticsearch 的主机真实 IP,否则浏览器会提示 “URL 拼写可能存在错误,请检查”,无法正常打开页面。ES 端口是 9200,Kibana 端口是 5601!
访问成功后进入登录界面,使用开发环境默认账号密码登录:
- 用户名:elastic
- 密码:123456(未修改默认配置时直接使用)
登录完成,我们就可以进入 Kibana 开发工具,正式开始 Elasticsearch 的实操测试。
操作指令
索引操作(建表 / 删表)
1. 创建索引(建表 + 定义字段规则)
PUT /student
{
"settings": "索引的配置(分片、分词器)",
"mappings": "定义表里有哪些字段、什么类型"
}
PUT:创建 / 修改的请求方式
/student:索引名(表名)
settings:索引的全局设置(分词、分片)
mappings:最重要,定义字段规则
type: text:文本,可分词搜索type: keyword:精确值,不分词type: integer:数字type: date:日期index: false:不能被搜索
// 创建一个名为 student 的索引(相当于建数据库表)
PUT /student
{
// ======================
// settings:索引的配置
// 分片、副本、分词器 都在这里设置
// ======================
"settings": {
"number_of_shards": 1, // 主分片数量(单机学习设1)
"number_of_replicas": 0, // 副本分片数量(单机学习设0)
"analysis": { // 分词器配置
"analyzer": {
"default": { // 使用默认分词器
"type": "standard"
}
}
}
},
// ======================
// mappings:表结构定义
// 字段名、字段类型 在这里设置
// ======================
"mappings": {
"properties": {
"name": {
"type": "text" // 姓名:text类型(可分词、可模糊搜索)
},
"age": {
"type": "integer" // 年龄:整数类型
},
"phone": {
"type": "keyword" // 手机号:keyword类型(精确匹配、不分词)
}
}
}
}
2. 删除索引(删表)
DELETE /student
- DELETE:删除操作
- 直接删除整个索引(所有数据都会没)
新增数据(往表里插数据)
1. 单条新增
POST /student/_doc/1
{
"name": "张三",
"age": 18
}
POST:添加数据
/student/_doc/1
- student:索引
- _doc:固定文档类型
- 1:这条数据的唯一 ID
大括号里:就是字段和值
1. 为什么必须写
/_doc/?表建好了,ES 规定死了:必须用这个固定格式门牌号,才能往里存数据!这是语法规则,必须照写。
2. 为什么必须写
1、2、3ID?一张表里会有几百几千个学生,必须每个人一个唯一的身份证号(ID),不然 ES 根本分不清谁是谁!
2. 批量新增(一次加多条)
POST /student/_doc/_bulk
{"index":{"_id":"2"}}
{"name":"李四","age":22}
- _bulk:批量操作
- 一行指令 + 一行数据
index:表示新增_id:数据 ID
选定 id 进行数据统一添加
删除数据
1. 根据 ID 删一条
DELETE /student/_doc/1
- 直接删除 ID=1 的数据
2. 批量删 ID
POST /student/_doc/_bulk
{"delete":{"_id":"2"}}
{"delete":{"_id":"3"}}
delete:删除操作
3. 按条件删除(比如删除所有叫李四的)
POST /student/_delete_by_query
{
"query": {
"match": {
"name": "李四"
}
}
}
query:查询条件match:模糊匹配- 意思:删除 name="李四" 的所有数据
更新数据(修改)
1. 按 ID 修改
POST /student/_update/1
{
"doc": {
"age": 20
}
}
_update:更新doc:固定写法,表示只改部分字段
2. 批量更新
POST /student/_doc/_bulk
{"update":{"_id":"1"}}
{"doc":{"age":20}}
查询数据
说明:query 是 ES 查询的固定根节点,表示 “查询条件块”。所有的搜索、匹配、过滤、条件判断,都必须写在 query 内部。它就相当于 SQL 里的 WHERE,没有 query 就无法写查询条件。
其余节点都有其自己的含义,不是用户的数据导致的!
1. 查询所有数据
说明:查询索引内全部数据,match_all 代表无条件匹配所有,query 包裹查询条件。
POST /student/_search
{
"query": {
"match_all": {}
}
}
返回结果
{
"hits": {
"total": { "value": 3 },
"hits": [
{ "_id": "1", "_source": { "name": "张三", "age": 18, "phone": "15511111111" } },
{ "_id": "2", "_source": { "name": "李四", "age": 22, "phone": "15522222222" } },
{ "_id": "3", "_source": { "name": "王五", "age": 20, "phone": "15533333333" } }
]
}
}
2. term 精确匹配
说明:query 内使用 term 实现完全相等匹配,适合手机号、ID 等 keyword 类型,必须完全一致才会命中。
POST /student/_search
{
"query": {
"term": {
"phone": "15511111111"
}
}
}
返回结果
{
"hits": {
"total": { "value": 1 },
"hits": [
{ "_id": "1", "_source": { "name": "张三", "age": 18, "phone": "15511111111" } }
]
}
}
3. terms 多值精确匹配
说明:query 内使用 terms 一次匹配多个值,满足任意一个即命中,类似 SQL 的 IN 查询。
POST /student/_search
{
"query": {
"terms": {
"phone": ["15511111111", "15522222222"]
}
}
}
返回结果
{
"hits": {
"total": { "value": 2 },
"hits": [
{ "_id": "1", "_source": { "name": "张三", "phone": "15511111111" } },
{ "_id": "2", "_source": { "name": "李四", "phone": "15522222222" } }
]
}
}
4. match 模糊分词搜索
说明:query 内使用 match 对文本分词后搜索,适合姓名、内容等 text 类型,支持模糊匹配。
POST /student/_search
{
"query": {
"match": {
"name": "张三"
}
}
}
返回结果
{
"hits": {
"total": { "value": 1 },
"hits": [
{ "_id": "1", "_source": { "name": "张三", "age": 18 } }
]
}
}
5. range 范围查询
说明:query 内使用 range 做数字 / 日期范围查询,gte = 大于等于,lte = 小于等于。
POST /student/_search
{
"query": {
"range": {
"age": {
"gte": 18,
"lte": 22
}
}
}
}
返回结果
{
"hits": {
"total": { "value": 3 },
"hits": [
{ "_id": "1", "_source": { "name": "张三", "age": 18 } },
{ "_id": "2", "_source": { "name": "李四", "age": 22 } },
{ "_id": "3", "_source": { "name": "王五", "age": 20 } }
]
}
}
6. multi_match 多字段搜索
说明:query 内使用 multi_match 在多个字段中同时搜索同一个关键词,适合全局搜索场景。
POST /student/_search
{
"query": {
"multi_match": {
"query": "张三",
"fields": ["name", "phone"]
}
}
}
返回结果
{
"hits": {
"total": { "value": 1 },
"hits": [
{ "_id": "1", "_source": { "name": "张三", "phone": "15511111111" } }
]
}
}
7. bool 多条件组合查询
说明:query 内使用 bool 组合多个条件,must = 必须满足(AND),must_not = 不能满足,should = 满足一个即可(OR)。
POST /student/_search
{
"query": {
"bool": {
"must": [
{ "match": { "name": "张三" } },
{ "range": { "age": { "gte": 18 } } }
],
"must_not": [
{ "term": { "phone": "15599999999" } }
],
"should": [
{ "term": { "phone": "15511111111" } }
]
}
}
}
返回结果
{
"hits": {
"total": { "value": 1 },
"hits": [
{ "_id": "1", "_source": { "name": "张三", "age": 18, "phone": "15511111111" } }
]
}
}
8. 排序 + 分页 + 字段过滤
说明:在查询外层(不属于 query)进行排序、分页、字段过滤,用于列表展示。
POST /student/_search
{
"query": { "match_all": {} },
"sort": { "age": "desc" },
"from": 0,
"size": 2,
"_source": ["name", "age"]
}
返回结果
{
"hits": {
"total": { "value": 3 },
"hits": [
{ "_id": "2", "_source": { "name": "李四", "age": 22 } },
{ "_id": "3", "_source": { "name": "王五", "age": 20 } }
]
}
}
Elasticsearch C++ 客户端 SDK 实战(elasticlient)
在项目开发中,我们不会直接手动发送 HTTP 请求操作 ES,而是通过C++ 客户端 SDK 与 Elasticsearch 服务器交互。
ES 的 C++ 客户端选择较少,elasticlient 是轻量、稳定、易用的开源库,基于 CPR 网络库实现,支持索引创建、数据增删改查、批量操作、条件搜索等完整功能,是 C++ 项目接入 ES 的首选方案。
核心结构与接口详解
在使用客户端前,我们先掌握两个最核心的结构:响应结果 和 客户端操作接口。
响应结果结构 cpr::Response
所有 ES 操作(查询、新增、删除等)都会返回这个对象,它封装了 HTTP 响应的所有信息。
namespace cpr {
class Response {
public:
long status_code; // HTTP 状态码(200=成功,404=不存在,500=错误)
std::string text; // ES 返回的原始数据(JSON字符串)
Header header; // 响应头
Url url; // 请求地址
double elapsed; // 请求耗时
Cookies cookies; // 会话cookies
Error error; // 错误信息
std::string raw_header; // 原始响应头
std::string status_line; // 状态行
std::string reason; // 失败原因
};
}
✅ 开发最常用两个字段:
status_code:判断请求是否成功(200~299 为成功)text:获取 ES 返回的 JSON 数据(查询结果、错误信息都在这里)
客户端核心接口 elasticlient::Client
Client 是 elasticlient 的核心类,封装了所有 ES 操作,只需实例化一次,全局使用。
1. 构造函数
// 传入 ES 节点地址列表 + 超时时间(默认6秒)
Client(const std::vector<std::string> &hostUrlList, std::int32_t timeout = 6000);
- 支持集群多节点,自动故障转移
- 地址格式:
http://用户名:密码@IP:端口/
2. 搜索数据(最常用)
// 执行搜索查询
cpr::Response search(
const std::string &indexName, // 索引名(表名)
const std::string &docType, // 文档类型(新版固定填 _doc)
const std::string &body, // 查询JSON语句
const std::string &routing = "" // 路由(默认不填)
);
3. 获取单条数据
cpr::Response get(
const std::string &indexName,
const std::string &docType,
const std::string &id, // 数据ID
const std::string &routing = ""
);
4. 创建索引 / 新增数据 / 全量更新
cpr::Response index(
const std::string &indexName,
const std::string &docType,
const std::string &id, // 数据ID
const std::string &body, // 数据JSON
const std::string &routing = ""
);
✅ 一个方法三用:
- ID 不存在 = 新增数据
- ID 已存在 = 全量覆盖更新
- 传入索引结构 = 创建索引
5. 删除单条数据
cpr::Response remove(
const std::string &indexName,
const std::string &docType,
const std::string &id // 要删除的数据ID
);
项目实战:完整 C++ 操作 ES 案例
elasticsearch/
├── client.cc # C++ 主程序(索引+增删改查+搜索)
└── makefile # 编译脚本
client.cc
#include <cpr/response.h>
#include <elasticlient/client.h>
#include <iostream>
#include <thread>
#include <chrono>
// ===================== 全局常量 =====================
const std::string index_name = "student"; // 索引=表
const std::string type_name = "_doc"; // 固定类型(新版ES)
const std::string index_id = "default_id"; // 创建索引用的ID
// ===================== 1. 创建索引(建表) =====================
void create_index(elasticlient::Client &client) {
// 索引结构JSON:分片+分词+字段定义
const std::string body = R"({
"settings": {
"analysis": {
"analyzer": {
"ikmax": {
"type": "custom",
"tokenizer": "ik_max_word"
}
}
}
},
"mappings": {
"dynamic": false,
"properties": {
"name": {
"type": "text",
"boost": 3.0,
"analyzer": "ikmax"
},
"age": {
"type": "integer"
},
"phone": {
"type": "keyword"
},
"skills": {
"type": "text"
},
"birth": {
"type": "date",
"index": false
}
}
}
})";
// 发送请求:index方法用于创建索引
auto resp = client.index(index_name, type_name, index_id, body);
// 判断状态码:200~299 为成功
if (resp.status_code < 200 || resp.status_code >= 300) {
std::cout << "创建索引失败:" << resp.status_code
<< " 错误信息:" << resp.text << std::endl;
return;
}
std::cout << "创建索引成功!" << std::endl;
}
// ===================== 2. 新增数据 =====================
void add_data(elasticlient::Client &client) {
// 构造学生数据JSON
const std::string body = R"({
"name": "张三",
"age": 19,
"phone": "13888888888",
"skills": ["java","php","go"],
"birth": "2007-05-21 12:35:32"
})";
// ID=2 新增数据
auto resp = client.index(index_name, type_name, "2", body);
if (resp.status_code < 200 || resp.status_code >= 300) {
std::cout << "新增数据失败:" << resp.text << std::endl;
return;
}
std::cout << "新增数据成功!" << std::endl;
}
// ===================== 3. 全量更新数据 =====================
void mod_data(elasticlient::Client &client) {
// 覆盖式更新:必须传完整字段
const std::string body = R"({
"name": "李四",
"age": 19,
"phone": "13333333333",
"skills": ["java","php","go"],
"birth": "2007-05-21 12:35:32"
})";
// ID=2 已存在 → 执行更新
auto resp = client.index(index_name, type_name, "2", body);
if (resp.status_code < 200 || resp.status_code >= 300) {
std::cout << "更新数据失败:" << resp.text << std::endl;
return;
}
std::cout << "更新数据成功!" << std::endl;
}
// ===================== 4. 删除单条数据 =====================
void remove_data(elasticlient::Client &client) {
// 删除 ID=2 的数据
auto resp = client.remove(index_name, type_name, "2");
if (resp.status_code < 200 || resp.status_code >= 300) {
std::cout << "删除数据失败:" << resp.text << std::endl;
return;
}
std::cout << "删除数据成功!" << std::endl;
}
// ===================== 5. 删除整个索引(删表) =====================
void remove_index(elasticlient::Client &client) {
// 发送 DELETE 请求删除索引
auto resp = client.performRequest(
elasticlient::Client::HTTPMethod::DELETE,
index_name, "");
if (resp.status_code < 200 || resp.status_code >= 300) {
std::cout << "删除索引失败:" << resp.text << std::endl;
return;
}
std::cout << "删除索引成功!" << std::endl;
}
// ===================== 6. 查询所有数据 =====================
void get_data(elasticlient::Client &client) {
// match_all:查询所有数据
const std::string body = R"({
"query": {
"match_all": {}
}
})";
auto resp = client.search(index_name, type_name, body);
if (resp.status_code < 200 || resp.status_code >= 300) {
std::cout << "查询失败:" << resp.text << std::endl;
return;
}
// 输出 ES 返回的完整 JSON 结果
std::cout << "\n====== 查询结果 ======\n" << resp.text << "\n=====================\n" << std::endl;
}
// ===================== 主函数 =====================
int main() {
// ES 连接地址(替换成你的服务器地址)
const std::string url = "http://elastic:123456@192.168.65.128:9200/";
// 初始化客户端
elasticlient::Client client({url});
// 按顺序执行操作(加休眠保证ES同步完成)
create_index(client);
std::this_thread::sleep_for(std::chrono::seconds(1));
add_data(client);
std::this_thread::sleep_for(std::chrono::seconds(1));
mod_data(client);
std::this_thread::sleep_for(std::chrono::seconds(1));
get_data(client); // 查询结果
remove_data(client);
remove_index(client);
return 0;
}
client:client.cc
g++ -std=c++17 $^ -o $@ -lcpr -ljsoncpp -lelasticlient
clean:
rm -rf client
依赖库:
cpr:网络请求jsoncpp:JSON 解析(可选)elasticlient:ES 客户端
./client
创建索引成功!
新增数据成功!
更新数据成功!
====== 查询结果 ======
{
"took":2,
"timed_out":false,
"_shards":{"total":1,"successful":1},
"hits": {
"total": {"value":2},
"hits": [
{ "_id":"default_id", "_source": { 索引结构 } },
{ "_id":"2", "_source": { "name":"李四", ... } }
]
}
}
=====================
删除数据成功!
删除索引成功!
ElasticSearch C++ 客户端封装实战
我们已经学会了使用 elasticlient 原生客户端操作 ES,但是直接拼接 JSON 字符串非常繁琐、容易出错、可读性差。
目标:封装一套面向对象、链式调用、自动生成合法 JSON、无需关心底层格式的 C++ 操作库,让你像写普通代码一样操作 ES。
ElasticSearch 所有操作本质都是 RESTful API + JSON 请求体。
我们要做的事情:
- 把 JSON 结构变成 C++ 类
- 提供链式调用接口
- 自动生成合法 JSON
- 屏蔽底层细节,让使用更简单
1. 基础结构(基类)
Base:所有 JSON 对象的基类,提供通用设置、序列化Object:对象类型({})Array:数组类型([])
2. 索引结构封装
-
Field:字段类型 -
Properties:字段集合 -
Mapping:映射结构 -
Tokenizer/Analyzer/Analysis:分词器 -
Settings:索引配置
3. 查询结构封装
-
QObject/QArray:查询条件基类 -
Bool:多条件组合 -
Query:查询根节点
4. 操作类封装
-
Indexer:创建索引 -
Inserter:插入数据 -
Updater:更新数据 -
Remover:删除数据 -
Searcher:搜索数据
5. 客户端封装
BaseClient:抽象接口ESClient:基于elasticlient实现ESClientFactory:工厂类
基础基类封装
所有 JSON 对象的父类,提供通用功能。
namespace es {
// ------------------------------
// 基础类:所有JSON节点的基类
// ------------------------------
class Base {
public:
using ptr = std::shared_ptr<Base>;
Base(const std::string& key);
// 获取当前节点key
std::string key();
// 设置字段:key -> value
template<typename T>
Base& set(const std::string& key, const T& value);
// 向数组追加元素
template<typename T>
Base& append(const std::string& key, const T& value);
// 序列化为字符串
virtual std::string to_string();
// 获取Json::Value
virtual Json::Value get();
protected:
std::string _key; // 当前节点名称
Json::Value _data; // 数据存储
};
// ------------------------------
// 对象类型:{ ... }
// ------------------------------
class Object : public Base {
public:
using ptr = std::shared_ptr<Object>;
Object(const std::string& key);
// 添加子节点
Object& add(const Base::ptr& child);
// 清空
virtual void clear();
// 获取构建好的JSON
virtual Json::Value get() const override;
// 获取子节点
virtual Base::ptr children(const std::string& key);
protected:
std::unordered_map<std::string, Base::ptr> _children;
};
// ------------------------------
// 数组类型:[ ... ]
// ------------------------------
class Array : public Base {
public:
using ptr = std::shared_ptr<Array>;
Array(const std::string& key);
Array& add(const Base::ptr& child);
virtual void clear();
virtual Json::Value get() const override;
protected:
std::vector<Base::ptr> _children;
};
}
索引结构封装
用于构建 settings + mappings。
// 字段类型:name, age, phone...
class Field : public Base {
public:
using ptr = std::shared_ptr<Field>;
Field(const std::string &name);
Field& type(const std::string &t); // 类型 text/keyword/date
Field& boost(double b); // 权重
Field& index(bool enabled); // 是否可搜索
Field& analyzer(const std::string &a); // 分词器
};
// 字段集合:properties
class Properties : public Object {
public:
using ptr = std::shared_ptr<Properties>;
Properties(): Object("properties"){}
// 创建并获取一个字段
Field::ptr field(const std::string &key);
};
// 映射:mapping
class Mappings : public Object{
public:
using ptr = std::shared_ptr<Mappings>;
Mappings(): Object("mapping"){}
Mappings& dynamic(bool enabled);
Properties::ptr properties();
};
// 分词器类型
class Tokenizer : public Object{
public:
using ptr = std::shared_ptr<Tokenizer>;
Tokenizer(const std::string &name);
Tokenizer& type(const std::string &t = "custom");
Tokenizer& tokenizer(const std::string &t);
};
// 分词器集合
class Analyzer : public Object{
public:
using ptr = std::shared_ptr<Analyzer>;
Analyzer(): Object("analyzer"){}
Tokenizer::ptr tokenizer(const std::string &key);
};
// analysis节点
class Analysis : public Object{
public:
using ptr = std::shared_ptr<Analysis>;
Analysis(): Object("analysis"){}
Analyzer::ptr analyzer();
};
// settings节点
class Settings : public Object{
public:
using ptr = std::shared_ptr<Settings>;
Settings(): Object("settings"){}
Analysis::ptr analysis();
};
查询结构封装
用于构建 query、bool、term、match 等。
// 查询条件对象基类
class QObject : public Object{
public:
using ptr = std::shared_ptr<QObject>;
QObject(const std::string &key): Object(key){}
Base::ptr term(); // 精确查询
Base::ptr terms(); // 多值精确
Base::ptr match(); // 模糊查询
Base::ptr multi_match(); // 多字段查询
};
// 查询条件数组(must/should/must_not)
class QArray : public Array{
public:
using ptr = std::shared_ptr<QArray>;
QArray(const std::string &key): Array(key){}
Base::ptr term();
Base::ptr terms();
Base::ptr match();
Base::ptr multi_match();
};
// bool组合查询
class Bool : public QObject{
public:
using ptr = std::shared_ptr<Bool>;
Bool(): QObject("bool"){}
void minimum_should_match(int count);
QArray::ptr must();
QArray::ptr should();
QArray::ptr must_not();
};
// 查询根节点 query
class Query : public QObject{
public:
using ptr = std::shared_ptr<Query>;
Query(): QObject("query"){}
Query& match_all(); // 查询所有
Bool::ptr bools(); // 组合条件
};
操作类封装
把索引、增删改查封装成类。
// ------------------------------
// 请求基础类(URL 构建)
// ------------------------------
class Request {
public:
using ptr = std::shared_ptr<Request>;
Request(){}
void server(const std::string& addr);
void method(const std::string& method);
void index(const std::string& index_name);
void type(const std::string& doc_type);
void operate(const std::string& op);
std::string server();
std::string method();
std::string index();
std::string type();
std::string operate();
virtual std::string httpUrl();
protected:
std::string _server_addr;
std::string _method;
std::string _index_name;
std::string _doc_type;
std::string _operate;
};
// 创建索引
class Indexer : public Object, public Request{
public:
using ptr = std::shared_ptr<Indexer>;
Indexer(const std::string &index);
Setting::ptr settings();
Mapping::ptr mappings();
};
// 插入数据
class Inserter : public Object, public Request{
public:
using ptr = std::shared_ptr<Inserter>;
Inserter(const std::string &index);
void insert(const std::string &id);
virtual std::string httpUrl() override;
private:
std::string _id;
};
// 删除数据
class Remover : public Object, public Request{
public:
using ptr = std::shared_ptr<Remover>;
Remover(const std::string &index);
std::string id();
void remove(const std::string& id);
Query::ptr query();
virtual std::string httpUrl() override;
private:
std::string _id;
};
// 更新数据
class Updater : public Object, public Request{
public:
using ptr = std::shared_ptr<Updater>;
Updater(const std::string &index);
Object::ptr doc(const std::string &id);
virtual std::string httpUrl() override;
private:
std::string _id;
};
// 查询数据
class Searcher : public Object, public Request{
public:
using ptr = std::shared_ptr<Searcher>;
Searcher(const std::string &index);
void size(int count);
void from(int offset);
Query::ptr query();
};
客户端封装
对接 elasticlient,提供统一调用接口。
// 客户端抽象接口
class BaseClient {
public:
using ptr = std::shared_ptr<BaseClient>;
BaseClient() {}
virtual bool index(ESIndex &obj) = 0;
virtual bool insert(ESInsert &build) = 0;
virtual bool remove(ESRemove &build) = 0;
virtual bool remove(const std::string &index_name);
virtual std::optional<Json::Value> search(ESSearch &build) = 0;
};
// elasticlient 实现
class ESClient : public BaseClient {
public:
using ptr = std::shared_ptr<ESClient>;
ESClient(const std::vector<std::string> &hosts);
virtual bool index(ESIndex &obj);
virtual bool insert(ESInsert &obj);
virtual bool remove(ESRemove &obj);
virtual bool remove(const std::string &index_name);
virtual std::optional<Json::Value> search(ESSearch &obj);
private:
std::shared_ptr<elasticlient::Client> _client;
};
// 工厂类
class ESClientFactory {
public:
static BaseClient::ptr create(const std::vector<std::string> &hosts);
};
使用示例
创建索引
void create_index() {
es::Indexer indexer("student");
// 设置分词器
indexer.settings()->analysis()->analyzer()->tokenizer("ikmax")
->type("custom").tokenizer("ik_max_word");
// 设置字段
auto props = indexer.mappings()->properties();
props->field("name")->type("text")->analyzer("ikmax")->boost(3.0);
props->field("phone")->type("keyword");
props->field("age")->type("integer");
props->field("birth")->type("date")->index(false);
client->index(indexer);
}
插入数据
void insert_data() {
es::Inserter inserter("student");
inserter.insert("1");
inserter.set("name", "张三");
inserter.set("age", 18);
inserter.set("phone", "13888888888");
inserter.append("skills", "C++");
inserter.append("skills", "Go");
client->insert(inserter);
}
条件查询
void search_data() {
es::Searcher searcher("student");
auto query = searcher.query();
// 组合查询
auto bools = query->bools();
bools->must()->match()->set("name", "张三");
bools->must()->range("age")->set("gte", 18);
auto result = client->search(searcher);
}
删除数据
void delete_data() {
es::Remover remover("student");
remover.remove("1");
client->remove(remover);
}
更多推荐




所有评论(0)