Elasticsearch 核心概念、Kibana 测试与 C++ 客户端封装
本文介绍了 Elasticsearch 的核心概念如索引、字段类型及映射配置,详细说明了在 Linux 环境下的安装、配置及 IK 分词器插件部署步骤。通过 Kibana 进行了数据插入与搜索的测试验证。重点阐述了如何利用 C++ 结合 cpr 和 jsoncpp 库对 ES 原生 RESTful API 进行二次封装,设计了索引创建、数据增删查改的类接口,降低了开发复杂度并提升了查询效率。

本文介绍了 Elasticsearch 的核心概念如索引、字段类型及映射配置,详细说明了在 Linux 环境下的安装、配置及 IK 分词器插件部署步骤。通过 Kibana 进行了数据插入与搜索的测试验证。重点阐述了如何利用 C++ 结合 cpr 和 jsoncpp 库对 ES 原生 RESTful API 进行二次封装,设计了索引创建、数据增删查改的类接口,降低了开发复杂度并提升了查询效率。

Elasticsearch,简称 ES,是一个开源分布式搜索引擎。其特点包括:分布式、零配置、自动发现、索引自动分片、索引副本机制、RESTful 风格接口、多数据源、自动搜索负载等。ES 类似数据库,相比传统数据库,它在搜索功能上更为实用、高效。
在搜索上与数据库的区别? 数据库的搜索策略类似二叉搜索树,但在文本搜索场景下,只能使用 LIKE 模糊匹配,效率较低。而 ES 主要做分词搜索,比如'你好,世界',会被分成:'你'、'好'、'世'、'界'、'你好'、'世界'等。
ES 核心概念
字段类型:
| 分类 | 类型 | 备注 |
|---|---|---|
| 字符串 | text, keyword | text 会被分词生成索引;keyword 不会被分词生成索引,只能精确值搜索 |
| 整形 | integer, long, short, byte | - |
| 浮点 | double, float | - |
| 逻辑 | boolean | true 或 false |
| 日期 | date, date_nanos | '2018-01-13'或'2018-01-13 12:10:30'或者时间戳,即 1970 到现在的秒数/毫秒数 |
| 二进制 | binary | 二进制通常只存储,不索引 |
| 范围 | range | - |
字符串是最常用的字段类型。
提示:ES 中的类型基本上已经被弃用,通常是一个 ES 索引管理一种数据。
映射
它定义了每条数据记录(文档)中的每个字段(Field)应该是什么类型,以及应该如何被处理,对数据的处理方式和规则做一些限制。
比如该字段是否做搜索分析,比如我们在搜索好友时不会通过性别或个性签名去搜索到好友,所以这些字段不用做搜索分析。(通过 enabled 设置)
又或者在音乐软件上做搜索,那么用户想搜的不一定是歌名,也可以把歌手,用户名,歌词等等进行搜索分析,而我们可以为这些字段设置权重,把歌名做最高权重,然后依次根据需要做不同权重。(通过 boost 设置)
这就是映射的意义与重要性,如下 ES 的 映射参数:
| 名称 | 数值 | 备注 |
|---|---|---|
enabled | true(默认) | false |
index | true(默认) | false |
index_option | - | - |
dynamic | true(缺省) | false |
doc_value | true(默认) | false |
fielddata | 'fielddata': {'format': 'disabled'} | 是否为 text 类型启动 fielddata,实现排序和聚合分析针对分词字段,参与排序或聚合时能提高性能,不分词字段统一建议使用 doc_value |
store | true | false(默认) |
coerce | true(默认) | false |
analyzer | 'analyzer':'ik' | 指定分词器,默认分词器为 standard analyzer |
boost | 'boost': 1.23 | 字段级别的分数加权,默认值是 1.0 |
fields | 'fields': { 'raw': { 'type': 'text', 'index': 'not_analyzed' } } | 对一个字段提供多种索引模式,同一个字段的值可对应两种索引模式,一种分词索引,一种不分词索引 |
data_detection | true(默认) | false |
# 添加仓库秘钥
wget -qO - https://artifacts.elastic.co/GPG-KEY-elasticsearch | sudo apt-key add -
# 上边的添加方式会导致一个 apt-key 的警告,如果不想报警告使用下边这个
curl -s https://artifacts.elastic.co/GPG-KEY-elasticsearch | sudo gpg --no-default-keyring --keyring gnupg:/etc/apt/trusted.gpg.d/elasticsearch.gpg --import
# 添加镜像源仓库
echo "deb https://artifacts.elastic.co/packages/7.x/apt stable main" | sudo tee /etc/apt/sources.list.d/elasticsearch.list
# 更新软件包列表
sudo apt update
# 安装 es
sudo apt-get install elasticsearch=7.17.21
# 启动 es
sudo systemctl start elasticsearch
# 安装 ik 分词器插件
sudo /usr/share/elasticsearch/bin/elasticsearch-plugin install https://get.infini.cloud/elasticsearch/analysis-ik/7.17.21
启动 ES
sudo systemctl start elasticsearch
配置外网访问
vim /etc/elasticsearch/elasticsearch.yml
# 新增配置
network.host: 0.0.0.0
http.port: 9200
cluster.initial_master_nodes: ["node-1"]
Kibana 是 Elastic Stack 技术栈中的一个开源数据分析与可视化平台。它作为一个基于 Web 的图形界面,为用户提供了对存储在 Elasticsearch 中的数据进行探索、可视化、交互和管理的能力。
安装 Kibana:
# 使用 apt 命令安装 Kibana。
sudo apt install kibana
# 配置 Kibana(可选):
# 根据需要配置 Kibana。配置文件通常位于 /etc/kibana/kibana.yml。可能需要
# 设置如服务器地址、端口、Elasticsearch URL 等。
sudo vim /etc/kibana/kibana.yml
# 例如,你可能需要设置 Elasticsearch 服务的 URL:大概 32 行左右
elasticsearch.host: "http://localhost:9200"
# 启动 Kibana 服务:
# 安装完成后,启动 Kibana 服务。
sudo systemctl start kibana
# 设置开机自启(可选):
# 如果你希望 Kibana 在系统启动时自动启动,可以使用以下命令来启用自启动。
sudo systemctl enable kibana
# 验证安装:
# 使用以下命令检查 Kibana 服务的状态。
sudo systemctl status kibana
# 访问 Kibana:
# 在浏览器中访问 Kibana,通常是 http://<your-ip>:5601
通过网页访问 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" },
"手机号":
post:以请求的方式提交数据user:索引名称(存储用户数据的库)_doc:类型(此处为文档类型标识)analyzer:分词器设置,ik 为中文分词器,tokenizer 用于指定分词粒度,ik_max_word 表示以最大的粒度进行分词。mapping:表示下面要描述的映射关系dynamic:true 表示未定义的字段会自动添加到映射并使用默认配置type
text:是一个文本类型keyword:是一个文本类型,但是是关键字不进行分词analyzer
standard:默认标准分词器ik_max_word:中文分词器数据插入
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"}
注意:每条数据需按'索引声明行 + 数据行'的格式书写,且每行需单独占一行。
数据搜索
搜索所有数据:
POST /user/_doc/_search
{
"query": {
"match_all": {}
}
}
以昵称这个'关键词'进行条件搜索:
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"
must_not:描述必须不包含的项,反向搜索should:描述应该遵循的条件,表示在以下任意一个字段中出现'昵称'则匹配成功。注意:如上过滤条件中,user_id 需要加 keyword,表示不进行分词。不加该字段默认是分词匹配,分词字段与不分词字段无法直接匹配。
构造函数 该接口用于创建和初始化
Elasticsearch客户端实例。hostUrlList:传入地址列表,即http://ip:port。如果有多个地址,可以传入列表。timeout:为连接超时时间,通常用缺省值。
查询数据
search 接口用于在指定索引中执行搜索查询,根据查询条件返回匹配的文档列表。
创建索引和新增数据
index 接口用于在 Elasticsearch 中创建索引,或新增数据和更新数据,通过指定索引名称、文档 ID 和内容来存储数据。
删除数据
remove 接口用于从 Elasticsearch 索引中删除指定 ID 的文档,永久移除目标数据。
关于以上三个接口的参数:
indexName:索引名称
docType:指定文档类型
id:用于标识一个索引内键值的唯一性
body:正文部分,用于创建索引、搜索或添加数据的 JSON 字符串
routing:路由键,用于控制文档存储到哪个分片,通常就用缺省值
返回值类型为 cpr::Response,通常会使用它的 status_code 字段(是一个状态码,用于判断函数执行是否成功)和 text 字段(返回的正文部分,是一个 Json 字符串)。
搜索接口的使用测试:
#include <elasticlient/client.h>
#include <cpr/cpr.h>
#include <iostream>
int main() {
elasticlient::Client client({"http://127.0.0.1:9200"});
try {
// 注意需要在 9200 后加 / 或在 user 前面加 /,要不然会成为无效的 URI
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;
}
Elasticsearch 原生 API 需要开发者手动构造复杂的 JSON 查询语句,成本高且容易出错。我们可以对它进行二次封装,通过简洁的方法链式调用完成复杂查询,大大降低使用门槛,提升开发效率。
由于索引创建、数据查询、数据删除等操作可能分布在不同服务器执行,因此将它们拆分为多个类分别实现。如下:
#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) {
// 通过工厂类构建 StreamWriter
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);
(ret == ) {
std::cout << << std::endl;
;
}
;
}
{
:
( std::shared_ptr<elasticlient::Client> client, std::string name, std::string type = )
: _client(client), _name(name), _type(type) {
Json::Value ik;
ik[] = ;
Json::Value analyzer;
analyzer[] = ik;
Json::Value analysis;
analysis[] = analyzer;
Json::Value settings;
settings[] = analysis;
_index[] = settings;
}
{
Json::Value data;
data[] = type;
data[] = analyzer;
data[] = enabled;
_properties[key] = data;
*;
}
{
Json::Value mappings;
mappings[] = ;
mappings[] = _properties;
_index[] = mappings;
std::string body;
(_index, body);
{
ret = _client->(_name, _type, , body);
(ret.status_code < || ret.status_code >= ) {
(, _name);
;
}
} (std::exception &e) {
(, _name, e.());
;
}
;
}
:
std::string _name;
std::string _type;
Json::Value _properties;
Json::Value _index;
std::shared_ptr<elasticlient::Client> _client;
};
{
:
( std::shared_ptr<elasticlient::Client> client, std::string name, std::string type = )
: _client(client), _name(name), _type(type) {}
{
_item[key] = val;
*;
}
{
std::string data;
ret = (_item, data);
(ret == ) ;
{
rsp = _client->(_name, _type, id, data);
(rsp.status_code < || rsp.status_code >= ) {
(, rsp.status_code);
;
}
} (std::exception& e) {
(, e.());
;
}
;
}
:
std::string _name;
std::string _type;
Json::Value _item;
std::shared_ptr<elasticlient::Client> _client;
};
{
:
( std::shared_ptr<elasticlient::Client> client, std::string name, std::string type = )
: _client(client), _name(name), _type(type) {}
{
{
rsp = _client->(_name, _type, id);
(rsp.status_code < || rsp.status_code >= ) {
(, rsp.status_code);
;
}
} (std::exception& e) {
(, e.());
;
}
;
}
:
std::string _name;
std::string _type;
std::shared_ptr<elasticlient::Client> _client;
};
{
:
( std::shared_ptr<elasticlient::Client> client, std::string name, std::string type = )
: _client(client), _name(name), _type(type) {}
{
Json::Value mnt;
( x : data) mnt[key].(x);
Json::Value terms;
terms[] = mnt;
_must_not.(terms);
*;
}
{
Json::Value mt;
mt[key] = val;
Json::Value terms;
terms[] = mt;
_must.(terms);
*;
}
{
Json::Value mm;
mm[key] = val;
Json::Value match;
match[] = mm;
_must.(match);
*;
}
{
Json::Value sm;
sm[key] = val;
Json::Value match;
match[] = sm;
_should.(match);
*;
}
{
Json::Value data;
(!_must_not.()) data[] = _must_not;
(!_must.()) data[] = _must;
(!_should.()) data[] = _should;
Json::Value bl;
bl[] = data;
Json::Value query;
query[] = bl;
std::string body;
ret = (query, body);
(ret == ) {
();
Json::();
}
cpr::Response rsp;
{
rsp = _client->(_name, _type, body);
(rsp.status_code < || rsp.status_code >= ) {
(, rsp.status_code);
Json::();
}
} (std::exception& e) {
(, e.());
Json::();
}
Json::Value val;
ret = (rsp.text, val);
(ret == ) {
();
Json::();
}
val[][];
}
:
std::string _name;
std::string _type;
Json::Value _must_not;
Json::Value _must;
Json::Value _should;
std::shared_ptr<elasticlient::Client> _client;
};

微信公众号「极客日志」,在微信中扫描左侧二维码关注。展示文案:极客日志 zeeklog
使用加密算法(如AES、TripleDES、Rabbit或RC4)加密和解密文本明文。 在线工具,加密/解密文本在线工具,online
将字符串编码和解码为其 Base64 格式表示形式即可。 在线工具,Base64 字符串编码/解码在线工具,online
将字符串、文件或图像转换为其 Base64 表示形式。 在线工具,Base64 文件转换器在线工具,online
将 Markdown(GFM)转为 HTML 片段,浏览器内 marked 解析;与 HTML 转 Markdown 互为补充。 在线工具,Markdown 转 HTML在线工具,online
将 HTML 片段转为 GitHub Flavored Markdown,支持标题、列表、链接、代码块与表格等;浏览器内处理,可链接预填。 在线工具,HTML 转 Markdown在线工具,online
通过删除不必要的空白来缩小和压缩JSON。 在线工具,JSON 压缩在线工具,online