
🔥草莓熊Lotso: 个人主页
❄️个人专栏: 《C++知识分享》 《Linux 入门到实践:零基础也能懂》
✨生活是默默的坚持,毅力是永久的享受!
🎬 博主简介:

文章目录
- 前言:
- [一、ES 的安装与介绍](#一、ES 的安装与介绍)
-
- [1.1 ES 是什么](#1.1 ES 是什么)
- [1.2 ES 服务端安装](#1.2 ES 服务端安装)
-
- 步骤1:添加软件源与密钥
- 步骤2:安装指定版本
- 步骤3:启动服务
- 步骤4:网络配置(可选)
- [步骤5:安装 IK 中文分词器](#步骤5:安装 IK 中文分词器)
- [步骤6:安装 Kibana(可选)](#步骤6:安装 Kibana(可选))
- [1.3 C++ 客户端 elasticlient 安装](#1.3 C++ 客户端 elasticlient 安装)
- [二、ES 核心概念扫盲](#二、ES 核心概念扫盲)
-
- [2.1 概念对应关系](#2.1 概念对应关系)
- [2.2 核心概念详解](#2.2 核心概念详解)
- [2.3 实战 DSL 示例](#2.3 实战 DSL 示例)
- [三、ES C++ 客户端接口介绍](#三、ES C++ 客户端接口介绍)
-
- [3.1 Client 客户端类](#3.1 Client 客户端类)
- [3.2 Response 响应类](#3.2 Response 响应类)
- [3.3 两个必踩的基础坑](#3.3 两个必踩的基础坑)
- [四、Jsoncpp 序列化与反序列化](#四、Jsoncpp 序列化与反序列化)
-
- [4.1 核心类](#4.1 核心类)
- [4.2 序列化样例](#4.2 序列化样例)
- [4.3 反序列化样例](#4.3 反序列化样例)
- [五、ES 客户端基础调用样例](#五、ES 客户端基础调用样例)
- [六、ES 客户端 API 二次封装](#六、ES 客户端 API 二次封装)
-
- [6.1 索引创建类 ESIndex](#6.1 索引创建类 ESIndex)
- [6.2 数据新增类 ESInsert](#6.2 数据新增类 ESInsert)
- [6.3 数据删除类 ESRemove](#6.3 数据删除类 ESRemove)
- [6.4 数据查询类 ESSearch](#6.4 数据查询类 ESSearch)
- 七、封装组件编译与测试
-
- [7.1 完整测试代码](#7.1 完整测试代码)
- [7.2 编译与踩坑记录](#7.2 编译与踩坑记录)
- 八、核心要点总结
- 结尾:
前言:
做即时通讯系统,有两个功能绕不开全文检索:用户昵称搜索、聊天历史消息搜索。如果直接用 MySQL 的 like 模糊查询,数据量上来之后全表扫描的效率根本没法看。所以我们这套微服务聊天系统里,引入了 Elasticsearch 来专门承担全文检索的工作。
很多 C++ 开发者接触 ES 比较少,不知道怎么和 C++ 项目结合。本文就顺着项目落地的完整路径,从 ES 服务端安装、核心概念扫盲,到 C++ 客户端选型、编译,再到 JSON 工具、原生接口调用,最后封装成业务级别的 CRUD 接口。全程附带实战踩坑和源码解读,跟着走一遍就能把 ES 集成到自己的 C++ 项目里。
一、ES 的安装与介绍
1.1 ES 是什么
Elasticsearch(简称 ES)是一款开源的分布式搜索引擎,基于 Java 开发,底层以 Lucene 为核心实现索引和搜索能力,对外提供简洁的 RESTful 接口。它的特点很鲜明:分布式、零配置自动发现、索引自动分片、副本机制、近实时搜索、扩展性极强,可以横向扩展到上百台服务器,处理 PB 级数据。
它和传统数据库最核心的区别在于搜索原理:
- 关系型数据库做模糊查询,基本是全表遍历匹配,数据量大了之后性能急剧下降
- ES 会对文本做分词,建立倒排索引,搜索的时候直接命中索引,效率高出几个数量级
举个最简单的例子:文本"你好,比特",数据库用 %你好% 要逐行扫描;而 ES 会分词成"你、好、比特、你好"并建立索引,搜索时直接定位到对应文档,这也是全文检索的核心价值。
1.2 ES 服务端安装
我们用 7.17.21 版本,Ubuntu 下通过 apt 安装,步骤如下:
步骤1:添加软件源与密钥
# 添加GPG密钥
curl -s [https://artifacts.elastic.co/GPG-KEY-elasticsearch](https://artifacts.elastic.co/GPG-KEY-elasticsearch) | sudo gpg --no-default-keyring --keyring gnupg-ring:/etc/apt/trusted.gpg.d/elasticsearch.gpg --import
# 添加镜像源
echo "deb [https://artifacts.elastic.co/packages/7.x/apt](https://artifacts.elastic.co/packages/7.x/apt) stable main" | sudo tee /etc/apt/sources.list.d/elasticsearch.list
# 更新软件包列表
sudo apt update
步骤2:安装指定版本
sudo apt-get install elasticsearch=7.17.21
步骤3:启动服务
# 启动服务
sudo systemctl start elasticsearch
# 设置开机自启
sudo systemctl enable elasticsearch
步骤4:网络配置(可选)
默认 ES 只允许本地访问,如果需要外网访问,修改配置文件 /etc/elasticsearch/elasticsearch.yml:
network.host: 0.0.0.0
http.port: 9200
cluster.initial_master_nodes: ["node-1"]
注意:生产环境不要直接暴露公网,一定要加安全认证,开发环境本地用默认配置即可。
步骤5:安装 IK 中文分词器
默认分词器对中文支持很差,必须安装 IK 分词器:
sudo /usr/share/elasticsearch/bin/elasticsearch-plugin install [https://get.infini.cloud/elasticsearch/analysis-ik/7.17.21](https://get.infini.cloud/elasticsearch/analysis-ik/7.17.21)
安装完重启 ES 生效。
步骤6:安装 Kibana(可选)
Kibana 是 ES 的可视化工具,自带 Dev Tools 可以很方便地写 DSL 调试查询:
sudo apt install kibana
sudo systemctl start kibana
sudo systemctl enable kibana
默认端口 5601,浏览器访问即可使用。
1.3 C++ 客户端 elasticlient 安装
C++ 的 ES 客户端选择不多,我们选用 elasticlient,轻量够用。
下载与编译
# 克隆代码
git clone [https://github.com/seznam/elasticlient](https://github.com/seznam/elasticlient)
cd elasticlient
# 更新子模块
git submodule update --init --recursive
# 编译安装
mkdir build && cd build
cmake ..
make -j$(nproc)
sudo make install
常见踩坑
cmake 阶段大概率会报错 MHD_INCLUDE_DIR NOTFOUND,这是缺少依赖库,安装即可:
sudo apt-get install libmicrohttpd-dev
安装完重新 cmake 就能通过了。
二、ES 核心概念扫盲
上来直接写代码很容易写得一团糟,先把核心概念理清楚,对应关系型数据库做个类比,大家理解起来会快很多。
2.1 概念对应关系
| 关系型数据库 | Elasticsearch | 说明 |
|---|---|---|
| Database(库) | Index(索引) | 一类相似文档的集合,比如用户索引、消息索引 |
| Table(表) | Type(类型) | 索引下的逻辑分类,7.x 后基本弃用,统一用 _doc |
| Row(行) | Document(文档) | 一条完整的数据,JSON 格式 |
| Column(列) | Field(字段) | 文档里的单个属性 |
2.2 核心概念详解
索引(Index)
一个索引就是一组相似特征文档的集合,比如 user 索引存所有用户数据,message 索引存所有聊天消息。索引名必须全部小写,操作的时候通过索引名指定。
类型(Type)
索引下的逻辑分类,早期版本用来细分数据,现在已经处于弃用状态,统一使用默认的 _doc 即可。
字段与数据类型
字段就是文档的属性,对应数据库的列。常用类型:
| 分类 | 类型 | 说明 |
|---|---|---|
| 字符串 | text |
会分词,建立倒排索引,用于模糊搜索、全文检索 |
| 字符串 | keyword |
不分词,只能精确匹配,用于过滤、排序、精确查找 |
| 数值 | integer long short byte |
整数类型 |
| 浮点 | double float |
浮点类型 |
| 布尔 | boolean |
true/false |
| 日期 | date |
支持日期字符串、时间戳格式 |
| 二进制 | binary |
仅存储,不索引 |
重点区分 text 和 keyword :这是新手最容易搞混的点。比如用户 ID、手机号这种需要精确匹配的,一定要用
keyword;昵称、个人签名、消息内容这种需要模糊搜索的,才用text。用错了要么搜不到,要么性能差。
映射(Mapping)
映射相当于数据库的表结构定义,用来指定每个字段的类型、分词器、是否索引、是否存储等规则。设计好映射对性能影响非常大。
常用映射配置项:
| 配置项 | 默认值 | 说明 |
|---|---|---|
enabled |
true | 设为 false 则仅存储、不建索引,节省空间,比如头像、签名这种不用来搜索的字段 |
index |
true | 是否构建倒排索引,设为 false 无法被搜索 |
dynamic |
true | 是否自动映射未定义的字段,开启的话新增字段会自动猜类型 |
analyzer |
standard | 指定分词器,中文要设为 ik_max_word |
boost |
1.0 | 字段级别的权重,影响搜索得分排序 |
文档(Document)
文档是 ES 中可被索引的基础单元,JSON 格式。一个索引里可以存储任意多的文档,每条文档都有一个唯一 ID。
2.3 实战 DSL 示例
结合我们聊天系统的用户搜索场景,看三段最常用的 DSL。
创建索引与映射
PUT /user
{
"settings": {
"analysis": {
"analyzer": {
"ik": {
"tokenizer": "ik_max_word"
}
}
}
},
"mappings": {
"dynamic": true,
"properties": {
"nickname": {
"type": "text",
"analyzer": "ik_max_word"
},
"user_id": {
"type": "keyword"
},
"phone": {
"type": "keyword"
},
"description": {
"type": "text",
"enabled": false
},
"avatar_id": {
"type": "keyword",
"enabled": false
}
}
}
}
这段代码创建了 user 索引,配置了 IK 中文分词器,昵称字段支持分词搜索,用户 ID 和手机号精确匹配,描述和头像只存不搜。
批量新增数据
POST /user/_doc/_bulk
{"index":{"_id":"1"}}
{"user_id":"USER001","nickname":"昵称1","phone":"13300001111","description":"签名1","avatar_id":"头像1"}
{"index":{"_id":"2"}}
{"user_id":"USER002","nickname":"昵称2","phone":"13300002222","description":"签名2","avatar_id":"头像2"}
用 _bulk 批量接口,两行一条数据:第一行指定操作和 ID,第二行是实际数据。
布尔查询(用户搜索场景)
GET /user/_doc/_search
{
"query": {
"bool": {
"must_not": [
{
"terms": {
"user_id.keyword": [
"USER001",
"USER002"
]
}
}
],
"should": [
{"match": {"user_id": "昵称"}},
{"match": {"nickname": "昵称"}},
{"match": {"phone": "昵称"}}
]
}
}
}
这就是用户搜索的底层逻辑:
must_not:排除已经是好友的用户,对应 SQL 里的NOT INshould:三个字段任意匹配一个就行,对应 SQL 里的OR- 用户输入关键字,同时匹配 ID、昵称、手机号,就是全局搜索框的效果
三、ES C++ 客户端接口介绍
elasticlient 库的接口很简洁,核心就一个客户端类和一个响应类。
3.1 Client 客户端类
核心操作接口:
class Client {
public:
// 构造函数:传入ES节点地址列表,超时时间
explicit Client(const std::vector<std::string>& hostUrlList, std::int32_t timeout = 6000);
// 创建索引、新增/更新文档
cpr::Response index(const std::string& indexName, const std::string& docType,
const std::string& id, const std::string& body);
// 搜索查询
cpr::Response search(const std::string& indexName, const std::string& docType,
const std::string& body);
// 删除文档
cpr::Response remove(const std::string& indexName, const std::string& docType,
const std::string& id);
// 获取单条文档
cpr::Response get(const std::string& indexName, const std::string& docType,
const std::string& id);
};
3.2 Response 响应类
响应用的是 cpr 库的 Response 类,我们最常用两个成员:
status_code:HTTP 状态码,200-299 代表成功,400 是请求格式错,404 是不存在,5xx 是服务端错text:响应体字符串,也就是 ES 返回的 JSON 结果
3.3 两个必踩的基础坑
- 地址末尾必须加斜杠 比如要写成
"[http://127.0.0.1:9200/](http://127.0.0.1:9200/)",不能漏了最后的/。不然内部拼接路径会变成9200user而不是9200/user,直接请求失败。 - 必须捕获异常 网络不通、ES 报错、参数错误都会抛出异常,不做捕获程序会直接崩溃。所有调用都要包在
try-catch里。
四、Jsoncpp 序列化与反序列化
ES 全部用 JSON 交互,我们需要一个 JSON 库来做数据的序列化和反序列化。elasticlient 已经依赖了 jsoncpp,不用额外安装。
4.1 核心类
Json::Value:通用 JSON 容器,可以表示对象、数组、字符串、数字等所有类型。支持[]运算符访问字段,append()方法添加数组元素。Json::StreamWriter:序列化类,把Json::Value序列化为 JSON 字符串,通过StreamWriterBuilder创建。Json::CharReader:反序列化类,把 JSON 字符串解析为Json::Value,通过CharReaderBuilder创建。
4.2 序列化样例
#include <json/json.h>
#include <iostream>
#include <sstream>
#include <memory>
bool Serialize(const Json::Value& val, std::string& dst) {
Json::StreamWriterBuilder swb;
std::unique_ptr<Json::StreamWriter> sw(swb.newStreamWriter());
std::stringstream ss;
int ret = sw->write(val, &ss);
if (ret != 0) {
std::cout << "JSON序列化失败!" << std::endl;
return false;
}
dst = ss.str();
return true;
}
int main() {
Json::Value stu;
stu["姓名"] = "张三";
stu["年龄"] = 18;
stu["成绩"].append(88);
stu["成绩"].append(89.5);
stu["成绩"].append(99);
std::string stu_str;
Serialize(stu, stu_str);
std::cout << stu_str << std::endl;
return 0;
}
说明:默认中文会输出 Unicode 编码,可以通过
swb.settings_["emitUTF8"] = true;设置输出 UTF-8 中文,不同版本略有差异。
4.3 反序列化样例
bool UnSerialize(const std::string& src, Json::Value& val) {
Json::CharReaderBuilder crb;
std::unique_ptr<Json::CharReader> cr(crb.newCharReader());
std::string err;
bool ret = cr->parse(src.c_str(), src.c_str() + src.size(), &val, &err);
if (!ret) {
std::cout << "JSON反序列化失败: " << err << std::endl;
return false;
}
return true;
}
int main() {
// ... 接上一段的stu_str ...
Json::Value val;
UnSerialize(stu_str, val);
std::cout << val["姓名"].asString() << std::endl;
std::cout << val["年龄"].asInt() << std::endl;
int sz = val["成绩"].size();
for (int i = 0; i < sz; ++i) {
std::cout << val["成绩"][i].asFloat() << std::endl;
}
return 0;
}
五、ES 客户端基础调用样例
先写一个最简单的查询程序,走一遍原生接口的完整流程。
#include <elasticlient/client.h>
#include <cpr/cpr.h>
#include <iostream>
int main() {
// 1. 构造ES客户端,注意地址末尾的斜杠!
elasticlient::Client client({"[http://127.0.0.1:9200/](http://127.0.0.1:9200/)"});
try {
// 2. 发起搜索请求,查询所有文档
auto rsp = client.search("user", "_doc", "{\"query\":{\"match_all\":{}}}");
// 3. 打印状态码和响应正文
std::cout << "状态码: " << rsp.status_code << std::endl;
std::cout << "响应: " << rsp.text << std::endl;
} catch (std::exception& e) {
std::cout << "请求失败: " << e.what() << std::endl;
return -1;
}
return 0;
}
编译命令:
g++ -std=c++17 main.cc -o main -lcpr -lelasticlient
能正常输出状态码 200 和 JSON 结果,说明基础调用就通了。
六、ES 客户端 API 二次封装
原生接口每次都要手拼 JSON 字符串,既容易出错也不好复用。我们把它封装成四个业务类,分别对应索引创建、数据新增、数据删除、数据查询,屏蔽底层 DSL 细节,上层业务只需要传参数。
为什么要分成四个类?因为不同的微服务节点需要的功能不一样:用户服务只需要新增和查询,管理服务才需要建索引。按需依赖,不用把所有操作都揉在一起。
6.1 索引创建类 ESIndex
负责创建索引,自动配置 IK 分词器,链式添加字段。
#pragma once
#include <elasticlient/client.h>
#include <json/json.h>
#include <memory>
#include <string>
#include "logger.hpp"
class ESIndex {
public:
ESIndex(std::shared_ptr<elasticlient::Client> client,
const std::string& name, const std::string& type = "_doc")
: _client(client), _name(name), _type(type) {
// 构造函数里直接配置好IK分词器
Json::Value ik;
ik["tokenizer"] = "ik_max_word";
Json::Value analyzer;
analyzer["ik"] = ik;
Json::Value analysis;
analysis["analyzer"] = analyzer;
_index["settings"]["analysis"] = analysis;
}
// 链式添加字段
ESIndex& append(const std::string& key,
const std::string& type = "text",
const std::string& analyzer = "ik_max_word",
bool enabled = true) {
Json::Value field;
field["type"] = type;
if (type == "text") {
field["analyzer"] = analyzer;
}
if (!enabled) {
field["enabled"] = enabled;
}
_properties[key] = field;
return *this;
}
// 执行创建
bool create() {
Json::Value mappings;
mappings["dynamic"] = true;
mappings["properties"] = _properties;
_index["mappings"] = mappings;
std::string body;
if (!Serialize(_index, body)) {
LOG_ERROR("索引序列化失败");
return false;
}
try {
auto rsp = _client->index(_name, _type, "", body);
if (rsp.status_code < 200 || rsp.status_code >= 300) {
LOG_ERROR("创建索引{}失败,状态码:{}", _name, rsp.status_code);
return false;
}
} catch (std::exception& e) {
LOG_ERROR("创建索引{}异常: {}", _name, e.what());
return false;
}
LOG_DEBUG("索引{}创建成功", _name);
return true;
}
private:
std::shared_ptr<elasticlient::Client> _client;
std::string _name;
std::string _type;
Json::Value _index;
Json::Value _properties;
};
代码解读:
- 构造函数直接把 IK 分词器配置好,业务层不用关心分词器细节
- 采用链式调用,写起来流畅:
index.append("nickname").append("phone", "keyword") - 统一做异常捕获和日志,出错了能快速定位
6.2 数据新增类 ESInsert
负责单条数据的新增或更新,同样链式调用。
class ESInsert {
public:
ESInsert(std::shared_ptr<elasticlient::Client> client,
const std::string& name, const std::string& type = "_doc")
: _client(client), _name(name), _type(type) {}
ESInsert& append(const std::string& key, const std::string& val) {
_item[key] = val;
return *this;
}
bool insert(const std::string& id = "") {
std::string body;
if (!Serialize(_item, body)) {
LOG_ERROR("数据序列化失败");
return false;
}
try {
auto rsp = _client->index(_name, _type, id, body);
if (rsp.status_code < 200 || rsp.status_code >= 300) {
LOG_ERROR("新增数据失败,状态码:{}", rsp.status_code);
return false;
}
} catch (std::exception& e) {
LOG_ERROR("新增数据异常: {}", e.what());
return false;
}
return true;
}
private:
std::shared_ptr<elasticlient::Client> _client;
std::string _name;
std::string _type;
Json::Value _item;
};
说明:ES 的 index 接口本身就是 upsert 语义:ID 存在就是更新,不存在就是新增。
6.3 数据删除类 ESRemove
最简单的一个类,按 ID 删除文档。
class ESRemove {
public:
ESRemove(std::shared_ptr<elasticlient::Client> client,
const std::string& name, const std::string& type = "_doc")
: _client(client), _name(name), _type(type) {}
bool remove(const std::string& id) {
try {
auto rsp = _client->remove(_name, _type, id);
if (rsp.status_code < 200 || rsp.status_code >= 300) {
LOG_ERROR("删除数据{}失败,状态码:{}", id, rsp.status_code);
return false;
}
} catch (std::exception& e) {
LOG_ERROR("删除数据{}异常: {}", id, e.what());
return false;
}
return true;
}
private:
std::shared_ptr<elasticlient::Client> _client;
std::string _name;
std::string _type;
};
6.4 数据查询类 ESSearch
最复杂也最核心的类,封装布尔查询,支持排除条件和多字段匹配。
class ESSearch {
public:
ESSearch(std::shared_ptr<elasticlient::Client> client,
const std::string& name, const std::string& type = "_doc")
: _client(client), _name(name), _type(type) {}
// 添加必须排除的精确值列表
ESSearch& append_must_not_terms(const std::string& key, const std::vector<std::string>& vals) {
Json::Value fields;
for (const auto& val : vals) {
fields[key].append(val);
}
Json::Value terms;
terms["terms"] = fields;
_must_not.append(terms);
return *this;
}
// 添加应该匹配的字段
ESSearch& append_should_match(const std::string& key, const std::string& val) {
Json::Value field;
field[key] = val;
Json::Value match;
match["match"] = field;
_should.append(match);
return *this;
}
// 执行搜索,返回命中的文档数组
Json::Value search() {
// 构造bool查询条件
Json::Value cond;
if (!_must_not.empty()) {
cond["must_not"] = _must_not;
}
if (!_should.empty()) {
cond["should"] = _should;
}
Json::Value query;
query["bool"] = cond;
Json::Value root;
root["query"] = query;
std::string body;
if (!Serialize(root, body)) {
LOG_ERROR("查询序列化失败");
return Json::Value();
}
try {
auto rsp = _client->search(_name, _type, body);
if (rsp.status_code < 200 || rsp.status_code >= 300) {
LOG_ERROR("查询失败,状态码:{}, 请求体:{}", rsp.status_code, body);
return Json::Value();
}
// 解析响应,取出hits.hits层
Json::Value json_res;
if (!UnSerialize(rsp.text, json_res)) {
LOG_ERROR("查询结果反序列化失败");
return Json::Value();
}
return json_res["hits"]["hits"];
} catch (std::exception& e) {
LOG_ERROR("查询异常: {}", e.what());
return Json::Value();
}
}
private:
std::shared_ptr<elasticlient::Client> _client;
std::string _name;
std::string _type;
Json::Value _must_not;
Json::Value _should;
};
代码解读:
- 对应业务场景:
must_not排除已有好友,should多字段模糊匹配 - 重点:返回的是
hits.hits数组,每个元素的_source字段才是原始文档数据 - 这是新手最高频的坑:ES 的响应结构是两层 hits,少一层都拿不到数据
七、封装组件编译与测试
现在写一个完整的测试程序,把四个类串起来,验证全链路是否正常。
7.1 完整测试代码
#include "../common/es.hpp"
#include <gflags/gflags.h>
DEFINE_bool(run_mode, false, "运行模式:false调试,true发布");
DEFINE_string(log_file, "", "日志文件路径");
DEFINE_int32(log_level, 0, "日志级别");
int main(int argc, char* argv[]) {
google::ParseCommandLineFlags(&argc, &argv, true);
init_logger(FLAGS_run_mode, FLAGS_log_file, FLAGS_log_level);
// 创建ES客户端
std::vector<std::string> hosts = {"[http://127.0.0.1:9200/](http://127.0.0.1:9200/)"};
auto client = std::make_shared<elasticlient::Client>(hosts);
// 1. 创建索引
bool ret = ESIndex(client, "test_user", "_doc")
.append("nickname")
.append("phone", "keyword", "standard", true)
.create();
if (!ret) {
LOG_ERROR("索引创建失败");
return -1;
}
LOG_INFO("索引创建成功");
// 2. 新增数据
ret = ESInsert(client, "test_user", "_doc")
.append("nickname", "张三")
.append("phone", "15566667777")
.insert("0001");
if (!ret) {
LOG_ERROR("数据新增失败");
return -1;
}
LOG_INFO("数据新增成功");
// 3. 更新数据(同ID就是更新)
ret = ESInsert(client, "test_user", "_doc")
.append("nickname", "张三")
.append("phone", "13344445555")
.insert("0001");
if (!ret) {
LOG_ERROR("数据更新失败");
return -1;
}
LOG_INFO("数据更新成功");
// 4. 查询数据
Json::Value users = ESSearch(client, "test_user", "_doc")
.append_should_match("phone.keyword", "13344445555")
.search();
if (users.empty() || !users.isArray()) {
LOG_ERROR("查询结果为空或格式错误");
return -1;
}
LOG_INFO("数据检索成功,命中{}条", users.size());
for (size_t i = 0; i < users.size(); ++i) {
LOG_INFO("nickname: {}", users[i]["_source"]["nickname"].asString());
}
// 5. 删除数据
ret = ESRemove(client, "test_user", "_doc").remove("0001");
if (!ret) {
LOG_ERROR("数据删除失败");
return -1;
}
LOG_INFO("数据删除成功");
return 0;
}
7.2 编译与踩坑记录
编译命令:
g++ -std=c++17 main.cc -o main -lcpr -lelasticlient -lspdlog -lfmt -lgflags -ljsoncpp
测试过程中的典型坑
- 查询报 400 错误 一开始写查询的时候,很容易少写外层的
query,直接把bool放根节点,ES 不认识就会 400。一定要记住:查询条件都要包在query里面。 - 精确匹配搜不到 用 keyword 类型的字段做匹配,字段名要加
.keyword后缀,比如phone.keyword。不然会默认去匹配分词结果,对不上精确值。 - 拿到的结果是空的 90% 的情况是少了一层
_source。ES 的结果结构是hits.hits[]._source,两层 hits 再加 _source,少任何一层都拿不到真实数据。 - 中文搜索结果乱七八糟 没装 IK 分词器的时候,默认按单字切,搜"张伟"会把"张三丰""李伟"都搜出来。这不是优化问题,是功能能不能用的问题,中文场景一定要装 IK 分词器,并且在 mapping 里指定好。
八、核心要点总结
- 字段类型设计是根基 :
text还是keyword,要不要建索引,用什么分词器,设计 mapping 的时候就要想清楚。用错了要么性能差,要么结果不准,后期改代价很大。 - 二次封装提升工程化效率:屏蔽 DSL 拼接细节,统一异常和日志,业务层只关心数据,不用关心 ES 语法,既能少写重复代码,也能降低出错概率。
- 响应结构记牢两层 hits :真实数据在
hits.hits[]._source里,这是新手最高频的踩坑点,没有之一。 - 中文必须配 IK 分词器:默认分词器不适合中文,搜索质量会非常差,这是必做项,不是可选项。
- 基础细节不能漏:地址末尾的斜杠、异常捕获,都是很小的点,但漏了要么跑不通要么直接崩。
结尾:
html
🍓 我是草莓熊 Lotso!若这篇技术干货帮你打通了学习中的卡点:
👀 【关注】跟我一起深耕技术领域,从基础到进阶,见证每一次成长
❤️ 【点赞】让优质内容被更多人看见,让知识传递更有力量
⭐ 【收藏】把核心知识点、实战技巧存好,需要时直接查、随时用
💬 【评论】分享你的经验或疑问(比如曾踩过的技术坑?),一起交流避坑
🗳️ 【投票】用你的选择助力社区内容方向,告诉大家哪个技术点最该重点拆解
技术之路难免有困惑,但同行的人会让前进更有方向~愿我们都能在自己专注的领域里,一步步靠近心中的技术目标!
结语:到这里,Elasticsearch 从服务端安装到 C++ 客户端二次封装的完整链路就跑通了。在我们的即时通讯系统里,用户搜索、全局消息搜索这两个核心场景,都是基于这套封装实现的,性能和准确性都能满足业务需求。ES 本身的玩法还有很多,比如分页、高亮、聚合分析,后续做消息的多维度统计的时候还会深入。接下来我们会继续把 ES 集成到微服务的各个业务子服务里,完整落地用户搜索和消息搜索功能。后续也会分享更多 C++ 后端、微服务架构的实战内容,欢迎关注交流。
✨把这些内容吃透超牛的!放松下吧✨ ʕ˘ᴥ˘ʔ づきらど
