概述
基本了解
Elasticsearch 是一个开源的分布式搜索和分析引擎,基于 Apache Lucene 构建。它提供了对海量数据的快速全文搜索、结构化搜索和分析功能,是目前流行的大数据处理工具之一。主要特点即高效搜索、分布式存储、拓展性强
核心功能
- 全文搜索: 提供对文本数据的快速匹配和排名能力
- 实时数据处理: 支持实时写入、更新、删除和搜索
- 分布式存储: 能够将数据分片(shard)存储在多个节点中,具有高可用性和容错性
- 聚合分析: 支持对大数据进行复杂的统计分析,如平均值、最大值、分组统计等
核心概念
索引
索引是数据的逻辑组织单位,可以类比为数据库中的"数据库"。每个索引由多个文档组成,类似于一本书的目录
索引是查询的入口点,比如当你要查"小说类书籍",会直接到"小说书架"查找,而不是其他书架
文档
文档是数据存储的基本单元,相当于关系型数据库中的"行"。每个文档是以 JSON 格式存储的键值对集合(这一点注意和MySQL进行区分)
每本书可以看作一个文档。每本书有具体的属性,比如书名、作者、出版年份、ISBN 号等
cpp
{
"title": "The Catcher in the Rye",
"author": "J.D. Salinger",
"year": 1951,
"genre": "Fiction"
}
字段
文档中的属性或键值对,比如书的"标题"、"作者"、"出版年份"等;可以简单理解为一本书的详细信息
注意:字段的类型可以指定(例如 text
、keyword
、integer
等),并决定了如何处理这些数据。例如,"标题"是 text
类型,支持全文搜索,而"ISBN" 是 keyword
类型,只支持精确匹配
分片
分片是数据的物理存储 单位。每个索引可以分成多个分片,分布在不同的节点上以提升性能
类似哈利波特系列的书很多,在图书馆中分别放在一楼和二楼,当寻找的时候同时派人去一楼和二楼寻找,这样就可以节省寻找的时间;这也就对应着分片的主要的作用:分片使得查询和存储可以并行处理,提高系统性能;分片还提供了冗余和容错能力(通过副本)
节点
节点是 Elasticsearch 集群中的一个实例。每个节点都有特定的角色,比如主节点、数据节点
例如在图书馆中,可以将每个楼层看作一个节点,可以存储数据,也可以帮助查询;比如,楼层 1 存储小说,楼层 2 存储科技书籍,但两层楼之间可以互相配合
不同的节点可以扮演不同角色
- 主节点(Master Node):负责管理整个图书馆的目录(分片的分布情况)
- 数据节点(Data Node):实际存储书籍和处理搜索请求
- 协调节点(Coordinator Node):负责分发和合并查询结果
集群
集群是一组相互协作的节点,共同存储和处理数据
整个图书馆可以看作是一个 Elasticsearch 集群,包含多个楼层(节点),书架(索引)被分布在各个楼层(节点)上
- 集群中的节点可以随时增加或减少,确保高扩展性
- 当有大批人需要搜索数据时,集群可以通过多个节点的并发处理快速完成任务
ES高性能的原因
倒排索引
倒排索引是 Elasticsearch 搜索速度快的核心技术,它记录的是每个单词在文档中的位置,而不是逐个文档搜索所有内容。
例如有三个文档,记录的内容分别是:文档1: "猫喜欢鱼";文档2: "狗喜欢骨头";文档3: "鱼喜欢水",那么倒排索引就会建立如下索引
cpp
词语 文档ID
喜欢 1, 2, 3
猫 1
狗 2
鱼 1, 3
骨头 2
水 3
当查找喜欢这个单词的时候,Elasticsearch就不会扫描全部的文档,而是直接从倒排索引中找到包含该单词的文档列表即可
综上所述:ES使用倒排索引避免了逐个扫描文档,直接定位到包含目标关键词的文档,查询时间随文档总量的增长几乎不变。
分布式架构
Elasticsearch 将数据分成多个 分片 存储在不同的节点上,查询时,会并行搜索所有分片,最后合并结果返回。
场景还假设在上面文档的内容中,将索引分为3个分片中,分布在三个节点上
- 分片1: 包含文档 1~100
- 分片2: 包含文档 101~200
- 分片3: 包含文档 201~300
当用户查询"鱼",Elasticsearch 会同时向 3 个分片发出请求:
- 分片1 返回包含"鱼"的文档 1
- 分片2 无结果
- 分片3 返回包含"鱼"的文档 3
最后将结果合并返回给用户;就像超时收银的,不会将所有的客户都在一个收银节点,通过设置多个收银节点完成最后的收银工作
缓存机制
Elasticsearch 使用内存缓存和文件系统缓存来存储常用的查询结果,如果相同的查询被多次请求,Elasticsearch 会直接从缓存中返回结果,而无需重新计算
查询优化和分析器
lasticsearch 会对查询请求进行优化,比如避免不必要的计算、合并多个相同的查询条件等
分析器(Analyzer)是对文本数据的处理器,通常会对字段的内容进行分词、去停用词(如 "the"、"is")、小写化等操作;借助分词器和索引机制让全文搜索更加精准和迅速
使用
添加与访问测试
创建索引库
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": "USER14eeea5-442771b9-0262e455-e46631d1", "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-57189c19", "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" }
搜索所有数据
ES客户端接口
参数说明
- **
indexName:
**指定 Elasticsearch 的索引名称 docType
::指定文档的类型(在 Elasticsearch 7.x+ 中已被弃用)- id:文档的唯一标识符,用于获取、索引或删除
- body:Elasticsearch 的请求体,通常为 JSON 格式
- routing:路由参数,如果为空,则使用默认的路由规则
常用接口功能
搜索
- 在 Elasticsearch 集群中搜索指定的索引,直到成功为止
- 如果所有节点都未响应,则抛出
ConnectionException
cpp
cpr::Response search(
const std::string &indexName,
const std::string &docType,
const std::string &body,
const std::string &routing = std::string());
获取文档
- 从集群中获取指定 ID 的文档
- 如果所有节点都未响应,则抛出
ConnectionException
cpp
cpr::Response get(
const std::string &indexName,
const std::string &docType,
const std::string &id = std::string(),
const std::string &routing = std::string());
索引文档
- 在集群中新增或更新文档
- 如果 ID 未提供,Elasticsearch 将自动生成 ID
- 如果所有节点都未响应,则抛出
ConnectionException
cpp
cpr::Response index(
const std::string &indexName,
const std::string &docType,
const std::string &id,
const std::string &body,
const std::string &routing = std::string());
删除文档
- 从集群中删除指定 ID 的文档
- 如果所有节点都未响应,则抛出
ConnectionException
cpp
cpr::Response remove(
const std::string &indexName,
const std::string &docType,
const std::string &id,
const std::string &routing = std::string());
基本操作
预处理
创建索引然后插入数据
搜索
通过客户端对指定内容进行搜索
二次封装
封装思路
索引创建
- 动态设置索引名称、索引类型
- 添加字段、设置字段类型及分词器设置
- 构造的核心逻辑则通过Json::Value构造对应的请求正文
数据新增
- 特定索引中插入文档
- 文档格式以JSON构造,灵活支持动态字段和值
数据查询
- 封装搜索语法 ,生成符合ES查询语法的JSON格式请求体
- 支持复杂的查询条件,例如多条件组合
数据删除
- 封装删除请求接口,可以按索引或文档 ID 进行删除
具体实现
工具函数
Serialize:JSON数据序列化,将Value转换为字符串
cpp
// 序列化
bool Serialize(const Json::Value &val , std::string &dst)
{
Json::StreamWriterBuilder swb;
swb.settings_["emitUTF8"] = true;
std::unique_ptr<Json::StreamWriter> sw(swb.newStreamWriter());
std::stringstream ss;
int ret = sw->write(val, &ss);
if (ret != 0) {
std::cout << "Json反序列化失败!\n";
return false;
}
dst = ss.str();
return true;
}
UnSerialize:JSON数据反序列化,将字符串转换为Value
cpp
// 反序列化
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 == false) {
std::cout << "json反序列化失败: " << err << std::endl;
return false;
}
return true;
}
创建索引
append
- **作用:**给索引字段添加映射配置(字段名称,字段类型,分析器名称,是否启用该字段)
- **实现逻辑:**创建字段的JSON配置,然后将其添加到_properties中,然后字段映射以键值对的形式进行存储
cpp
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"] = enabled;
_properties[key] = fields;
return *this;
}
create
- **作用:**根据settings和mappings配置,通过客户端发送HTTP请求创建Elasticsearch索引
- 逻辑
- 设置索引的mappins配置
- 序列化索引配置为JSON字符串
- 通过_client调用Elasticsearch的接口创建索引
cpp
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);
//2. 发起搜索请求
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(std::exception &e) {
LOG_ERROR("创建ES索引 {} 失败: {}", _name, e.what());
return false;
}
return true;
}
新增数据
append
- **作用:**添加一条记录到_item中
- **逻辑:**将传入的键值对添加到_item对象中
cpp
template<typename T>
ESInsert &append(const std::string &key, const T &val){
_item[key] = val;
return *this;
}
insert
- **作用:**将_item序列化为JSON字符串
- 逻辑
- 通过序列化函数将_item转换为JSON格式的字符串body
- 发起请求,将JSON数据发送到Elasticsearch中,然后检查是否发送成功
cpp
bool insert(const std::string id = "") {
std::string body;
bool ret = Serialize(_item, body);
if (ret == false) {
LOG_ERROR("索引序列化失败!");
return false;
}
LOG_DEBUG("{}", body);
//2. 发起搜索请求
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(std::exception &e) {
LOG_ERROR("新增数据 {} 失败: {}", body, e.what());
return false;
}
return true;
}
删除数据
remove
- **作用:**删除指定ID的文档
- 逻辑:
- 调用客户端的remove方法,向Elasticsearch发送删除请求
- 判断是否成功,同时对错误进行捕捉,防止被其他错误中断
cpp
class ESRemove {
public:
ESRemove(std::shared_ptr<elasticlient::Client> &client,
const std::string &name,
const std::string &type = "_doc"):
_name(name), _type(type), _client(client){}
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::string _name;
std::string _type;
std::shared_ptr<elasticlient::Client> _client;
};
数据查询
- 主要功能
- 提供接口构建多条件的Elasticsearch布尔查询
- 将查询条件序列化为JSON格式的请求体
- 调用Elasticsearch客户端执行查询
- 解析查询结果并返回文档数据
cpp
class ESSearch {
public:
ESSearch(std::shared_ptr<elasticlient::Client> &client,
const std::string &name,
const std::string &type = "_doc"):
_name(name), _type(type), _client(client){}
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;
}
ESSearch& append_must_term(const std::string &key, const std::string &val) {
Json::Value field;
field[key] = val;
Json::Value term;
term["term"] = 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;
}
Json::Value search(){
Json::Value cond;
if (_must_not.empty() == false) cond["must_not"] = _must_not;
if (_should.empty() == false) cond["should"] = _should;
if (_must.empty() == false) cond["must"] = _must;
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);
//2. 发起搜索请求
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(std::exception &e) {
LOG_ERROR("检索数据 {} 失败: {}", body, e.what());
return Json::Value();
}
//3. 需要对响应正文进行反序列化
LOG_DEBUG("检索响应正文: [{}]", rsp.text);
Json::Value json_res;
ret = UnSerialize(rsp.text, json_res);
if (ret == false) {
LOG_ERROR("检索数据 {} 结果反序列化失败", rsp.text);
return Json::Value();
}
return json_res["hits"]["hits"];
}
private:
std::string _name;
std::string _type;
Json::Value _must_not;
Json::Value _should;
Json::Value _must;
std::shared_ptr<elasticlient::Client> _client;
};
cpp
#pragma once
#include <elasticlient/client.h>
#include <cpr/cpr.h>
#include <json/json.h>
#include <iostream>
#include <memory>
#include "logger.hpp"
namespace mag {
/// 工具函数
/**
* @brief 将 Json::Value 数据序列化为 JSON 字符串
* @param val 要序列化的 Json::Value 对象
* @param dst 序列化后的 JSON 字符串
* @return true 序列化成功
* @return false 序列化失败
*/
bool Serialize(const Json::Value &val, std::string &dst) {
Json::StreamWriterBuilder swb;
swb.settings_["emitUTF8"] = true; // 输出为 UTF-8 编码
std::unique_ptr<Json::StreamWriter> sw(swb.newStreamWriter());
std::stringstream ss;
int ret = sw->write(val, &ss); // 执行序列化
if (ret != 0) {
std::cout << "Json序列化失败!\n";
return false;
}
dst = ss.str(); // 将结果写入目标字符串
return true;
}
/**
* @brief 将 JSON 字符串反序列化为 Json::Value 对象
* @param src 待解析的 JSON 字符串
* @param val 解析后的 Json::Value 对象
* @return true 反序列化成功
* @return false 反序列化失败
*/
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;
}
/// 索引创建模块
/**
* @brief 索引创建类:用于定义和创建 Elasticsearch 索引
*/
class ESIndex {
public:
/**
* @brief 构造函数:初始化索引创建模块
* @param client Elasticsearch 客户端实例
* @param name 索引名称
* @param type 文档类型(默认 "_doc")
*/
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_max_word
ik["ik"] = tokenizer;
analyzer["analyzer"] = ik;
analysis["analysis"] = analyzer;
_index["settings"] = analysis; // 设置索引的 settings
}
/**
* @brief 添加字段映射
* @param key 字段名称
* @param type 字段类型(默认 "text")
* @param analyzer 分词器名称(默认 "ik_max_word")
* @param enabled 是否启用字段(默认 true)
* @return ESIndex& 支持链式调用
*/
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) fields["enabled"] = enabled; // 如果禁用字段
_properties[key] = fields; // 添加字段到映射配置中
return *this;
}
/**
* @brief 创建索引
* @param index_id 索引 ID(默认 "default_index_id")
* @return true 创建成功
* @return false 创建失败
*/
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); // 将索引配置序列化为 JSON
if (!ret) {
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 (std::exception &e) {
LOG_ERROR("创建ES索引 {} 失败: {}", _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; // 客户端实例
};
/// 索引插入模块
/**
* @brief 索引插入类:支持动态数据插入到 Elasticsearch 索引
*/
class ESInsert {
public:
/**
* @brief 构造函数:初始化插入模块
* @param client Elasticsearch 客户端实例
* @param name 索引名称
* @param type 文档类型(默认 "_doc")
*/
ESInsert(std::shared_ptr<elasticlient::Client> &client,
const std::string &name,
const std::string &type = "_doc"):
_name(name), _type(type), _client(client) {}
/**
* @brief 添加字段和数据
* @tparam T 数据类型
* @param key 字段名称
* @param val 字段值
* @return ESInsert& 支持链式调用
*/
template<typename T>
ESInsert& append(const std::string &key, const T &val) {
_item[key] = val; // 动态添加字段
return *this;
}
/**
* @brief 插入数据到 Elasticsearch
* @param id 文档 ID(默认为自动生成)
* @return true 插入成功
* @return false 插入失败
*/
bool insert(const std::string id = "") {
std::string body;
bool ret = Serialize(_item, body); // 将数据序列化为 JSON
if (!ret) {
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 (std::exception &e) {
LOG_ERROR("插入数据 {} 失败: {}", body, e.what());
return false;
}
return true;
}
private:
std::string _name; // 索引名称
std::string _type; // 文档类型
Json::Value _item; // 待插入的数据
std::shared_ptr<elasticlient::Client> _client; // 客户端实例
};
/// 数据删除模块
/**
* @brief 删除模块:删除 Elasticsearch 中的文档
*/
class ESRemove {
public:
/**
* @brief 构造函数:初始化删除模块
* @param client Elasticsearch 客户端实例
* @param name 索引名称
* @param type 文档类型(默认 "_doc")
*/
ESRemove(std::shared_ptr<elasticlient::Client> &client,
const std::string &name,
const std::string &type = "_doc"):
_name(name), _type(type), _client(client) {}
/**
* @brief 删除指定文档
* @param id 文档 ID
* @return true 删除成功
* @return false 删除失败
*/
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::string _name; // 索引名称
std::string _type; // 文档类型
std::shared_ptr<elasticlient::Client> _client; // 客户端实例
};
/// 数据查找模块
/**
* @brief 数据查找模块:用于执行复杂查询
*/
class ESSearch {
public:
/**
* @brief 构造函数:初始化查询模块
* @param client Elasticsearch 客户端实例
* @param name 索引名称
* @param type 文档类型(默认 "_doc")
*/
ESSearch(std::shared_ptr<elasticlient::Client> &client,
const std::string &name,
const std::string &type = "_doc"):
_name(name), _type(type), _client(client) {}
/**
* @brief 添加 must_not 条件
* @param key 字段名称
* @param vals 排除的值列表
* @return ESSearch& 支持链式调用
*/
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); // 添加到 must_not 条件
return *this;
}
/**
* @brief 添加 should 条件
* @param key 字段名称
* @param val 匹配的值
* @return ESSearch& 支持链式调用
*/
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); // 添加到 should 条件
return *this;
}
/**
* @brief 添加 must 条件(精确匹配)
* @param key 字段名称
* @param val 精确匹配的值
* @return ESSearch& 支持链式调用
*/
ESSearch& append_must_term(const std::string &key, const std::string &val) {
Json::Value field;
field[key] = val;
Json::Value term;
term["term"] = field;
_must.append(term); // 添加到 must 条件
return *this;
}
/**
* @brief 添加 must 条件(模糊匹配)
* @param key 字段名称
* @param val 模糊匹配的值
* @return ESSearch& 支持链式调用
*/
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); // 添加到 must 条件
return *this;
}
/**
* @brief 执行查询
* @return Json::Value 查询结果
*/
Json::Value search() {
Json::Value cond;
if (!_must_not.empty()) cond["must_not"] = _must_not;
if (!_should.empty()) cond["should"] = _should;
if (!_must.empty()) cond["must"] = _must;
Json::Value query;
query["bool"] = cond;
Json::Value root;
root["query"] = query;
std::string body;
bool ret = Serialize(root, body); // 序列化查询条件
if (!ret) {
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("查询失败,响应状态码异常: {}", rsp.status_code);
return Json::Value();
}
} catch (std::exception &e) {
LOG_ERROR("查询失败: {}", e.what());
return Json::Value();
}
LOG_DEBUG("查询响应: {}", rsp.text);
Json::Value json_res;
ret = UnSerialize(rsp.text, json_res); // 解析响应结果
if (!ret) {
LOG_ERROR("查询结果反序列化失败!");
return Json::Value();
}
return json_res["hits"]["hits"]; // 返回查询结果
}
private:
std::string _name; // 索引名称
std::string _type; // 文档类型
Json::Value _must_not; // must_not 条件
Json::Value _should; // should 条件
Json::Value _must; // must 条件
std::shared_ptr<elasticlient::Client> _client; // 客户端实例
};
}