介绍
安装
安装 Elasticsearch
# 添加仓库秘钥
wget -qO - https://artifacts.elastic.co/GPG-KEY-elasticsearch | sudo apt-key add -
# 上边的添加方式会导致一个 apt-key 的警告,但是不影响
# 添加镜像源仓库
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
重新启动 elasticsearch,查看其是否正常运行
sudo systemctl start elasticsearch
sudo systemctl status elasticsearch.service
sudo vim /etc/elasticsearch/elasticsearch.yml
network.host: 0.0.0.0
http.port: 9200
cluster.initial_master_nodes: ["node-1"]

安装 Kibana
Kibana 是 Elasticsearch 的官方数据可视化和管理工具,通常与 Elasticsearch 配合使用。以下是关于它的核心解释和常见使用场景:
Kibana 的核心作用
-
数据可视化:通过图表、仪表盘(Dashboards)展示 Elasticsearch 中的索引数据,支持柱状图、折线图、地图、词云等多种可视化形式。
-
数据探索:使用 Discover 功能直接搜索和过滤 Elasticsearch 中的数据,支持全文搜索、字段过滤、时间范围筛选等。
-
索引管理:在 Management 中管理 Elasticsearch 的索引、设置索引生命周期(ILM)、定义字段映射(Mapping)等。
-
监控与告警:监控 Elasticsearch 集群的健康状态(如节点状态、分片分布),配置告警规则(Alerting),例如磁盘空间不足时触发通知。
-
开发工具:内置 Dev Tools,可直接编写和执行 Elasticsearch 的 REST API 请求(如
GET /_cat/indices
)。
sudo apt install kibana
根据需要配置 Kibana。配置文件通常位于 /etc/kibana/kibana.yml。可能需要设置如服务器地址、端口、Elasticsearch URL 等。
sudo vim /etc/kibana/kibana.yml
#添加以下配置
elasticsearch.host: "http://localhost:9200"
server.port: 5601
server.host: "0.0.0.0"
sudo systemctl restart kibana
sudo systemctl enable kibana
sudo systemctl status kibana

ES 客户端的安装
# 克隆代码
git clone https://github.com/seznam/elasticlient
# 切换目录
cd elasticlient
# 更新子模块
git submodule update --init --recursive
# 编译代码
mkdir build
cd build
# 需要安装 MicroHTTPD 库
sudo apt-get install libmicrohttpd-dev
cmake -DCMAKE_INSTALL_PREFIX=/usr ..
make
# 安装
make install
ES 核心概念
索引(Index)
类型(Type)
字段(Field)
分类 |
类型
|
备注
|
字符串
|
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 |
映射(mapping)
名称 |
数值
| 备注 |
enabled
| true(默认) | false |
是否仅作存储,不做搜索和分析
|
index |
true(
默认
) | false
| 是否构建倒排索引(决定了是否分词,是否被索引) |
index_option
| ||
dynamic |
true(默认)| false
| 控制 mapping 的自动更新 |
doc_value
| true(默认) | false |
是否开启
doc_value
,用户聚合和排序分析,分词字段不能使用
|
fielddata | fielddata: {"format": "disabled"} |
是否为
text
类型启动
fielddata,实现排序和聚合分析。针对分词字段,参与排序或聚合时能提高性能,不分词字段统一建议使用 doc_value。
|
store
| true | false(默认) | 是否单独设置此字段的存储而从 _source 字段中分离,只能搜索,不能获取值 |
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
|
是否自动识别日期类型
|
文档 (document)

ES 客户端的使用示例
在浏览器中访问 Kibana,通常是 http://<your-ip>:5601,在工具页面进行编码,使用以下语句:
#创建索引并配置字段和映射
POST /user/_doc
{
"settings" : {
"analysis" : {
"analyzer" : {
"ik" : {
"tokenizer" : "ik_max_word"
}
}
}
},
"mappings":
{
"dynamic" : true,
"properties":
{
"nickname" : {
"type" : "text",
"analyzer" : "ik_max_word"
},
"user_id" : {
"type" : "keyword",
"analyzer" : "standard"
},
"phone" : {
"type" : "keyword",
"analyzer" : "standard"
},
"description" : {
"type" : "text",
"enabled" : false
},
"avatar_id":
{
"type" : "keyword",
"enabled" : false
}
}
}
}
#新增数据
POST /user/_doc/_bulk
{"index":{"_id":"1"}}
{"user_id" : "USER4b862aaa-2df8654a-7eb4bb65-e3507f66","nickname" : "昵称 1","phone" : "手机号 1","description" : "签名 1","avatar_id" : "头像 1"}
{"index":{"_id":"2"}}
{"user_id" : "USER14eeeaa5-442771b9-0262e455-e4663d1d","nickname" : "昵称 2","phone" : "手机号 2","description" : "签名 2","avatar_id" : "头像 2"}
{"index":{"_id":"3"}}
{"user_id" : "USER484a6734-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" : "USER6f19d074-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"}
main.cc
#include <elasticlient/client.h>
#include <cpr/cpr.h>
#include <iostream>
int main()
{
// 1. 构建ES客户端
elasticlient::Client client({"http://127.0.0.1:9200/"});
// 2. 发起搜索请求
try
{
auto rsp = client.search("user", "_doc", "{\"query\":{\"match_all\":{}}}");
std::cout << rsp.status_code << std::endl;
std::cout << rsp.text << std::endl;
}
catch (const std::exception &e)
{
std::cerr << "请求失败:" << e.what() << '\n';
return -1;
}
return 0;
}
makefile
main : main.cc
g++ -o $@ $^ -std=c++17 -lcpr -lelasticlient
ES 客户端 API 二次封装
- 索引构造过程的封装:索引正文构造过程,大部分正文都是固定的,唯一不同的地方是各个字段不同的名称以及是否只存储不索引这些选项,因此重点关注以下几个点即可:
- 字段类型:type : text / keyword (目前只用到这两个类型)
- 是否索引:enable : true/false
- 索引的话分词器类型: analyzer : ik_max_word / standard
- 新增文档构造过程的封装:新增文档其实在常规下都是单条新增,并非批量新增,因此直接添加字段和值就行
- 文档搜索构造过程的封装:搜索正文构造过程,我们默认使用条件搜索,我们主要关注的两个点:
- 应该遵循的条件是什么:should 中有什么
- 条件的匹配方式是什么:match 还是 term/terms,还是 wildcard
- 过滤的条件字段是什么:must_not 中有什么
- 过滤的条件字段匹配方式是什么:match 还是 wildcard,还是 term/terms
#pragma once
#include <elasticlient/client.h>
#include <json/json.h>
#include <iostream>
#include <memory>
#include <sstream>
#include <cpr/cpr.h>
#include "logger.hpp"
bool Serialize(const Json::Value &val, std::string &dst)
{
// 先定义Json::StreamWriter 工厂类 Json::StreamWriterBuilder
Json::StreamWriterBuilder swb;
swb.settings_["emitUTF8"] = true;
std::unique_ptr<Json::StreamWriter> sw(swb.newStreamWriter());
// 通过Json::StreamWriter中的write接口进行序列化
std::stringstream ss;
int ret = sw->write(val, &ss);
if (ret != 0)
{
std::cout << "Json反序列化失败!\n";
return false;
}
dst = ss.str();
return true;
}
bool UnSerialize(const std::string &src, Json::Value &val)
{
Json::CharReaderBuilder crb;
crb.settings_["emitUTF8"] = true;
std::unique_ptr<Json::CharReader> cr(crb.newCharReader());
std::string error;
bool ret = cr->parse(src.c_str(), src.c_str() + src.size(), &val, &error);
if (ret == false)
{
std::cout << "json反序列化失败: " << error << std::endl;
return false;
}
return true;
}
class ESIndex
{
public:
ESIndex(std::shared_ptr<elasticlient::Client> &client,
const std::string &name, const std::string &type = "_doc") : _name(name), _type(type), _client(client)
{
Json::Value analysis;
Json::Value analyzer;
Json::Value ik;
Json::Value tokenizer;
tokenizer["tokenizer"] = "ik_max_word";
ik["ik"] = tokenizer;
analyzer["analyzer"] = ik;
analysis["analysis"] = analyzer;
_index["settings"] = analysis;
}
ESIndex &append(const std::string &key, const std::string &type = "text",
const std::string &analyzer = "ik_max_word", bool enabled = true)
{
Json::Value fields;
fields["type"] = type;
fields["analyzer"] = analyzer;
if (enabled == false)
fields["enabled"] = false;
_properties[key] = fields;
return *this;
}
bool create(const std::string &index_id = "default_index_id")
{
Json::Value mappings;
mappings["dynamic"] = true;
mappings["properties"] = _properties;
_index["mappings"] = mappings;
std::string body;
bool ret = Serialize(_index, body);
if (ret == false)
{
LOG_ERROR("索引序列化失败!");
return false;
}
LOG_DEBUG("{}", body);
try
{
auto rsp = _client->index(_name, _type, index_id, body);
if (rsp.status_code < 200 || rsp.status_code >= 300)
{
LOG_ERROR("创建ES索引{}失败,响应状态码异常:{}", _name, rsp.status_code);
return false;
}
}
catch (const std::exception &e)
{
LOG_ERROR("创建ES索引{}失败:{}", _name, e.what());
return false;
}
return true;
}
private:
std::shared_ptr<elasticlient::Client> _client;
std::string _name;
std::string _type;
Json::Value _index;
Json::Value _properties;
};
class ESInsert
{
public:
ESInsert(std::shared_ptr<elasticlient::Client> &client,
const std::string &name, const std::string &type = "_doc") : _name(name), _type(type), _client(client)
{
}
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 body;
bool ret = Serialize(_item, body);
if (ret == false)
{
LOG_ERROR("索引序列化失败!");
return false;
}
LOG_DEBUG("{}", body);
try
{
auto rsp = _client->index(_name, _type, id, body);
if (rsp.status_code < 200 || rsp.status_code >= 300)
{
LOG_ERROR("新增数据{}失败,响应状态码异常:{}", body, rsp.status_code);
return false;
}
}
catch (const std::exception &e)
{
LOG_ERROR("新增数据{}失败:{}", body, e.what());
return false;
}
return true;
}
private:
std::shared_ptr<elasticlient::Client> _client;
std::string _name;
std::string _type;
Json::Value _item;
};
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 (const 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;
};
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_must_term(const std::string &key, const std::string &val)
{
Json::Value field;
field[key] = val;
Json::Value term;
term["terms"] = field;
_must.append(term);
return *this;
}
ESSearch &append_must_match(const std::string &key, const std::string &val)
{
Json::Value field;
field[key] = val;
Json::Value match;
match["match"] = field;
_must.append(match);
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()
{
Json::Value cond;
if (_must_not.empty() == false)
cond["must_not"] = _must_not;
if (_must.empty() == false)
cond["must"] = _must;
if (_should.empty() == false)
cond["should"] = _should;
Json::Value query;
query["bool"] = cond;
Json::Value root;
root["query"] = query;
std::string body;
bool ret = Serialize(root, body);
if (ret == false)
{
LOG_ERROR("索引序列化失败!");
return Json::Value();
}
LOG_DEBUG("{}", body);
cpr::Response rsp;
try
{
rsp = _client->search(_name, _type, body);
if (rsp.status_code < 200 || rsp.status_code >= 300)
{
LOG_ERROR("检索数据{}失败,响应状态码异常:{}", body, rsp.status_code);
return Json::Value();
}
}
catch (const std::exception &e)
{
LOG_ERROR("删除数据{}失败:{}", body, e.what());
return Json::Value();
}
LOG_DEBUG("检索响应正文:{}", rsp.text);
Json::Value json_rsp;
ret = UnSerialize(rsp.text, json_rsp);
if (ret == false)
{
LOG_ERROR("检索数据 {} 结果反序列化失败", rsp.text);
return Json::Value();
}
return json_rsp["hits"]["hits"];
}
private:
std::shared_ptr<elasticlient::Client> _client;
std::string _name;
std::string _type;
Json::Value _must_not;
Json::Value _must;
Json::Value _should;
};
makefile
main : main.cc
g++ -std=c++17 $^ -o $@ -lcpr -lelasticlient -lspdlog -lfmt -lgflags -ljsoncpp