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 的人有谁?”,它就需要把所有文档都扫一遍,效率很低。

倒排索引

倒排索引,又称反向索引或逆向索引,是按照文档中的词汇来组织数据的索引方法。在倒排索引中,每个独特的词汇都会有一个索引条目,该条目包含指向包含该词汇的所有文档的指针或引用。这使得倒排索引非常适合全文搜索,能够快速找到包含特定关键词的文档。倒排索引则适合于全文搜索,可以快速找到包含特定关键词的所有文档,索引的大小相对较小,因为它只记录关键词和文档的映射关系。但是,倒排索引不能直接通过索引访问文档,需要结合正排索引来获取文档的详细信息。

倒排索引的结构:倒排索引的 “键” 是字段值或关键字(比如图里的年龄 1820,或者姓名里的 等),“值” 是包含这个值的文档 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_dbproduct_dborder_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. 为什么必须写 123 ID?

一张表里会有几百几千个学生,必须每个人一个唯一的身份证号(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 请求体

我们要做的事情:

  1. 把 JSON 结构变成 C++ 类
  2. 提供链式调用接口
  3. 自动生成合法 JSON
  4. 屏蔽底层细节,让使用更简单

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();
};

查询结构封装

用于构建 querybooltermmatch 等。

// 查询条件对象基类
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);
}

Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐