跳到主要内容
Elasticsearch 核心概念、Kibana 测试与 C++ 客户端封装实战 | 极客日志
C++
Elasticsearch 核心概念、Kibana 测试与 C++ 客户端封装实战 综述由AI生成 Elasticsearch 作为开源分布式搜索引擎,在文本搜索场景下比传统数据库更高效。本文梳理了 ES 的核心概念如索引、映射及字段类型,演示了基于 Kibana 的安装配置与基础测试流程。重点展示了如何通过 C++ 结合 cpr 与 jsoncpp 库构建轻量级客户端,并对原生 API 进行二次封装,实现链式调用以简化复杂查询逻辑,提升开发效率。
极光 发布于 2026/3/15 更新于 2026/4/26 6 浏览核心概念
Elasticsearch(简称 ES)是一个开源的分布式搜索引擎,具备零配置、自动发现、索引分片及副本机制等特点。相比传统数据库,它在文本搜索场景下更为高效实用。
数据库通常采用二叉搜索树策略,在模糊匹配时效率较低;而 ES 擅长分词搜索,例如'你好,世界'会被拆解为多个词条进行检索。
ES 核心概念
索引(Index) :拥有相似特征的文档集合,类似 MySQL 中的库。
类型(Type) :索引的逻辑分类,类似表结构(注:新版 ES 中已弃用,建议一个索引管理一种数据)。
字段(Field) :文档数据的属性标识,类似列。
字段类型说明
分类 类型 备注 字符串 text, keywordtext 会分词生成索引;keyword 不分词,仅精确匹配 整形 integer, long, short, byte- 浮点 double, float- 逻辑 booleantrue 或 false 日期 date, date_nanos支持时间戳或标准格式 二进制 binary通常只存储不索引
映射(Mapping)
映射定义了文档中每个字段的类型及处理方式。通过 enabled 可控制是否参与搜索分析,通过 boost 可设置字段权重。例如在音乐软件搜索中,可将歌名设为最高权重。
常见映射参数如下:
名称 默认值 备注 enabledtrue false 时仅作存储 indextrue false 时不构建倒排索引 dynamictrue 控制未定义字段的自动更新 doc_valuetrue 用于聚合和排序,分词字段不可用 analyzerstandard 指定分词器,如 ik boost1.0 字段级别分数加权
安装与配置
使用 apt 安装 Elasticsearch 7.x 版本。
curl -s https://artifacts.elastic.co/GPG-KEY-elasticsearch | gpg --no-default-keyring --keyring gnupg:/etc/apt/trusted.gpg.d/elasticsearch.gpg --import
| /etc/apt/sources.list.d/elasticsearch.list
apt update
apt-get install elasticsearch=7.17.21
systemctl start elasticsearch
/usr/share/elasticsearch/bin/elasticsearch-plugin install https://get.infini.cloud/elasticsearch/analysis-ik/7.17.21
sudo
echo
"deb https://artifacts.elastic.co/packages/7.x/apt stable main"
sudo
tee
sudo
sudo
sudo
sudo
修改配置文件 /etc/elasticsearch/elasticsearch.yml 以支持外网访问:
network.host: 0.0 .0 .0
http.port: 9200
cluster.initial_master_nodes: ["node-1" ]
Kibana 是 Elastic Stack 的数据可视化平台。
sudo apt install kibana
sudo systemctl start kibana
sudo systemctl enable kibana
sudo systemctl status kibana
浏览器访问 http://<your-ip>:5601 即可进入 Kibana 界面。
测试示例 在 Kibana 的控制台中执行以下请求来创建索引并定义映射。
POST /user/_doc
{
"settings" : {
"analysis" : {
"analyzer" : {
"ik" : { "tokenizer" : "ik_max_word" }
}
}
} ,
"mappings" : {
"dynamic" : true ,
"properties" : {
"昵称" : { "type" : "text" , "analyzer" : "ik_max_word" } ,
"用户 ID" : { "type" : "keyword" , "analyzer" : "standard" } ,
"手机号" : { "type" : "keyword" , "analyzer" : "standard" } ,
"描述" : { "type" : "text" , "enabled" : false } ,
"头像 ID" : { "type" : "keyword" , "enabled" : false }
}
}
}
POST /user/_doc/_bulk
{ "index" : { "_id" : "1" } }
{ "user_id" : "USER4b8a2aaa-2df8654a-7eb4bb65-e3507f66" , "nickname" : "昵称 1" , "phone" : "手机号 1" , "description" : "签名 1" , "avatar_id" : "头像 1" }
{ "index" : { "_id" : "2" } }
{ "user_id" : "USER14eeea5-442771b9-0262e455-e4663d1d" , "nickname" : "昵称 2" , "phone" : "手机号 2" , "description" : "签名 2" , "avatar_id" : "头像 2" }
{ "index" : { "_id" : "3" } }
{ "user_id" : "USER4b8a6734-03a124f0-996c169d-d05c1869" , "nickname" : "昵称 3" , "phone" : "手机号 3" , "description" : "签名 3" , "avatar_id" : "头像 3" }
{ "index" : { "_id" : "4" } }
{ "user_id" : "USER186ade83-4460d4a6-8c08068f-83127b5d" , "nickname" : "昵称 4" , "phone" : "手机号 4" , "description" : "签名 4" , "avatar_id" : "头像 4" }
{ "index" : { "_id" : "5" } }
{ "user_id" : "USER6f1d9074-c33891cf-23bf5a83-57189a19" , "nickname" : "昵称 5" , "phone" : "手机号 5" , "description" : "签名 5" , "avatar_id" : "头像 5" }
{ "index" : { "_id" : "6" } }
{ "user_id" : "USER97605c64-9833ebb7-d0455353-35a59195" , "nickname" : "昵称 6" , "phone" : "手机号 6" , "description" : "签名 6" , "avatar_id" : "头像 6" }
注意:Bulk 模式下每条数据需按'索引声明行 + 数据行'格式书写,且每行单独占一行。
POST /user/_doc/_search
{
"query" : { "match_all" : { } }
}
条件搜索(排除特定用户 ID,并在昵称、ID、手机号中模糊匹配):
GET /user/_doc/_search?pretty
{
"query" : {
"bool" : {
"must_not" : [
{
"terms" : {
"user_id.keyword" : [ "USER4b862aaa-2df8654a-7eb4bb65-e3597766" , "USER14eeeaa5-442771b9-0262e455-e4663d1d" , "USER484a6734-93a124f0-996c169d-d05c1869" ]
}
}
] ,
"should" : [
{ "match" : { "user_id" : "昵称" } } ,
{ "match" : { "nickname" : "昵称" } } ,
{ "match" : { "phone" : "昵称" } }
]
}
}
}
注意:user_id 等唯一标识字段应使用 .keyword 后缀,避免分词导致匹配失败。
客户端 API 使用 在 C++ 开发中,我们可以使用 elasticlient 结合 cpr 库来调用 ES 接口。主要包含以下操作:
构造函数 :初始化客户端实例,传入地址列表及超时时间。
search :执行搜索查询,返回匹配的文档列表。
index :创建索引或新增/更新数据。
remove :删除指定 ID 的文档。
常用参数包括 indexName(索引名)、docType(文档类型)、id(文档唯一标识)、body(JSON 请求体)及 routing(路由键)。返回值通常为 cpr::Response,可通过 status_code 判断执行结果。
#include <elasticlient/client.h>
#include <cpr/cpr.h>
#include <iostream>
int main () {
elasticlient::Client client ({"http://127.0.0.1:9200" }) ;
try {
auto ret = client.search ("/user" , "_doc" , "{\"query\": { \"match_all\":{} }}" );
std::cout << ret.status_code << std::endl;
std::cout << ret.text << std::endl;
} catch (std::exception& ex) {
std::cout << "请求失败:" << ex.what () << std::endl;
return -1 ;
}
return 0 ;
}
二次封装源码 原生 ES API 需要手动构造复杂的 JSON 查询,容易出错且维护成本高。通过二次封装,可以利用链式调用简化操作。
这里我们利用 jsoncpp 处理序列化与反序列化,并将索引创建、数据增删查拆分为独立类实现。以下是完整的封装示例:
#include <elasticlient/client.h>
#include <json/json.h>
#include <cpr/cpr.h>
#include <iostream>
#include <vector>
#include "../spdlog/logger.hpp"
bool Serialize (const Json::Value &val, std::string &out) {
auto nsw = Json::StreamWriterBuilder ().newStreamWriter ();
std::stringstream ss;
int ret = nsw->write (val, &ss);
if (ret != 0 ) {
std::cout << "序列化失败" ;
return false ;
}
out = ss.str ();
return true ;
}
bool UnSerialize (const std::string &str, Json::Value &val) {
auto crb = Json::CharReaderBuilder ().newCharReader ();
std::string erro;
bool ret = crb->parse (str.c_str (), str.c_str () + str.size (), &val, &erro);
if (ret == false ) {
std::cout << "反序列化失败" << std::endl;
return false ;
}
return true ;
}
class ESIndex {
public :
ESIndex (const std::shared_ptr<elasticlient::Client> client, std::string name, const std::string type = "_doc" )
: _client(client), _name(name), _type(type) {
Json::Value ik; ik["tokenizer" ] = "ik_max_word" ;
Json::Value analyzer; analyzer["ik" ] = ik;
Json::Value analysis; analysis["analyzer" ] = analyzer;
Json::Value settings; settings["analysis" ] = analysis;
_index["settings" ] = settings;
}
ESIndex append (const std::string &key, const std::string &type = "text" , const std::string &analyzer = "ik_max_word" , bool enabled = true ) {
Json::Value data;
data["type" ] = type;
data["analyzer" ] = analyzer;
data["enabled" ] = enabled;
_properties[key] = data;
return *this ;
}
bool create () {
Json::Value mappings;
mappings["dynamic" ] = true ;
mappings["properties" ] = _properties;
_index["mappings" ] = mappings;
std::string body;
Serialize (_index, body);
try {
auto ret = _client->index (_name, _type, "" , body);
if (ret.status_code < 200 || ret.status_code >= 300 ) {
LOG_ERR ("创建索引 {} 失败" , _name);
return false ;
}
} catch (std::exception &e) {
LOG_ERR ("创建索引 {} 失败:{}" , _name, e.what ());
return false ;
}
return true ;
}
private :
std::string _name;
std::string _type;
Json::Value _properties;
Json::Value _index;
std::shared_ptr<elasticlient::Client> _client;
};
class ESInsert {
public :
ESInsert (const std::shared_ptr<elasticlient::Client> client, std::string name, const std::string type = "_doc" )
: _client(client), _name(name), _type(type) {}
template <typename T>
ESInsert& append (const std::string& key, const T& val) {
_item[key] = val;
return *this ;
}
bool insert (const std::string& id = "" ) {
std::string data;
bool ret = Serialize (_item, data);
if (ret == false ) return false ;
try {
auto rsp = _client->index (_name, _type, id, data);
if (rsp.status_code < 200 || rsp.status_code >= 300 ) {
LOG_ERR ("数据插入失败:{}" , rsp.status_code);
return false ;
}
} catch (std::exception& e) {
LOG_ERR ("数据插入失败:{}" , e.what ());
return false ;
}
return true ;
}
private :
std::string _name;
std::string _type;
Json::Value _item;
std::shared_ptr<elasticlient::Client> _client;
};
class ESRemove {
public :
ESRemove (const std::shared_ptr<elasticlient::Client> client, std::string name, const std::string type = "_doc" )
: _client(client), _name(name), _type(type) {}
bool remove (std::string id) {
try {
auto rsp = _client->remove (_name, _type, id);
if (rsp.status_code < 200 || rsp.status_code >= 300 ) {
LOG_ERR ("数据删除失败:{}" , rsp.status_code);
return false ;
}
} catch (std::exception& e) {
LOG_ERR ("数据删除失败:{}" , e.what ());
return false ;
}
return true ;
}
private :
std::string _name;
std::string _type;
std::shared_ptr<elasticlient::Client> _client;
};
class ESSearch {
public :
ESSearch (const std::shared_ptr<elasticlient::Client> client, 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>& data) {
Json::Value mnt;
for (auto x : data) mnt[key].append (x);
Json::Value terms; terms["terms" ] = mnt;
_must_not.append (terms);
return *this ;
}
ESSearch& append_must_terms (const std::string& key, const std::string& val) {
Json::Value mt; mt[key] = val;
Json::Value terms; terms["terms" ] = mt;
_must.append (terms);
return *this ;
}
ESSearch& append_must_match (const std::string& key, const std::string& val) {
Json::Value mm; mm[key] = val;
Json::Value match; match["match" ] = mm;
_must.append (match);
return *this ;
}
ESSearch& append_should_match (const std::string& key, const std::string& val) {
Json::Value sm; sm[key] = val;
Json::Value match; match["match" ] = sm;
_should.append (match);
return *this ;
}
Json::Value search () {
Json::Value data;
if (!_must_not.empty ()) data["must_not" ] = _must_not;
if (!_must.empty ()) data["must" ] = _must;
if (!_should.empty ()) data["should" ] = _should;
Json::Value bl; bl["bool" ] = data;
Json::Value query; query["query" ] = bl;
std::string body;
bool ret = Serialize (query, body);
if (ret == false ) {
LOG_ERR ("序列化失败" );
return Json::Value ();
}
cpr::Response rsp;
try {
rsp = _client->search (_name, _type, body);
if (rsp.status_code < 200 || rsp.status_code >= 300 ) {
LOG_ERR ("搜索失败:{}" , rsp.status_code);
return Json::Value ();
}
} catch (std::exception& e) {
LOG_ERR ("搜索失败:{}" , e.what ());
return Json::Value ();
}
Json::Value val;
ret = UnSerialize (rsp.text, val);
if (ret == false ) {
LOG_ERR ("反序列化失败" );
return Json::Value ();
}
return val["hits" ]["hits" ];
}
private :
std::string _name;
std::string _type;
Json::Value _must_not;
Json::Value _must;
Json::Value _should;
std::shared_ptr<elasticlient::Client> _client;
};
相关免费在线工具 Base64 字符串编码/解码 将字符串编码和解码为其 Base64 格式表示形式即可。 在线工具,Base64 字符串编码/解码在线工具,online
Base64 文件转换器 将字符串、文件或图像转换为其 Base64 表示形式。 在线工具,Base64 文件转换器在线工具,online
Markdown转HTML 将 Markdown(GFM)转为 HTML 片段,浏览器内 marked 解析;与 HTML转Markdown 互为补充。 在线工具,Markdown转HTML在线工具,online
HTML转Markdown 将 HTML 片段转为 GitHub Flavored Markdown,支持标题、列表、链接、代码块与表格等;浏览器内处理,可链接预填。 在线工具,HTML转Markdown在线工具,online
JSON 压缩 通过删除不必要的空白来缩小和压缩JSON。 在线工具,JSON 压缩在线工具,online
JSON美化和格式化 将JSON字符串修饰为友好的可读格式。 在线工具,JSON美化和格式化在线工具,online