【C++ 基于微服务的即时通讯系统】Elasticsearch 从安装到 C++ 客户端二次封装全落地


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


🎬 博主简介:


文章目录


前言:

做即时通讯系统,有两个功能绕不开全文检索:用户昵称搜索、聊天历史消息搜索。如果直接用 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 IN
  • should:三个字段任意匹配一个就行,对应 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 两个必踩的基础坑

  1. 地址末尾必须加斜杠 比如要写成 "[http://127.0.0.1:9200/](http://127.0.0.1:9200/)",不能漏了最后的 /。不然内部拼接路径会变成 9200user 而不是 9200/user,直接请求失败。
  2. 必须捕获异常 网络不通、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

测试过程中的典型坑

  1. 查询报 400 错误 一开始写查询的时候,很容易少写外层的 query,直接把 bool 放根节点,ES 不认识就会 400。一定要记住:查询条件都要包在 query 里面。
  2. 精确匹配搜不到 用 keyword 类型的字段做匹配,字段名要加 .keyword 后缀,比如 phone.keyword。不然会默认去匹配分词结果,对不上精确值。
  3. 拿到的结果是空的 90% 的情况是少了一层 _source。ES 的结果结构是 hits.hits[]._source,两层 hits 再加 _source,少任何一层都拿不到真实数据。
  4. 中文搜索结果乱七八糟 没装 IK 分词器的时候,默认按单字切,搜"张伟"会把"张三丰""李伟"都搜出来。这不是优化问题,是功能能不能用的问题,中文场景一定要装 IK 分词器,并且在 mapping 里指定好。

八、核心要点总结

  1. 字段类型设计是根基 :text 还是 keyword,要不要建索引,用什么分词器,设计 mapping 的时候就要想清楚。用错了要么性能差,要么结果不准,后期改代价很大。
  2. 二次封装提升工程化效率:屏蔽 DSL 拼接细节,统一异常和日志,业务层只关心数据,不用关心 ES 语法,既能少写重复代码,也能降低出错概率。
  3. 响应结构记牢两层 hits :真实数据在 hits.hits[]._source 里,这是新手最高频的踩坑点,没有之一。
  4. 中文必须配 IK 分词器:默认分词器不适合中文,搜索质量会非常差,这是必做项,不是可选项。
  5. 基础细节不能漏:地址末尾的斜杠、异常捕获,都是很小的点,但漏了要么跑不通要么直接崩。

结尾:

html 复制代码
🍓 我是草莓熊 Lotso!若这篇技术干货帮你打通了学习中的卡点:
👀 【关注】跟我一起深耕技术领域,从基础到进阶,见证每一次成长
❤️ 【点赞】让优质内容被更多人看见,让知识传递更有力量
⭐ 【收藏】把核心知识点、实战技巧存好,需要时直接查、随时用
💬 【评论】分享你的经验或疑问(比如曾踩过的技术坑?),一起交流避坑
🗳️ 【投票】用你的选择助力社区内容方向,告诉大家哪个技术点最该重点拆解
技术之路难免有困惑,但同行的人会让前进更有方向~愿我们都能在自己专注的领域里,一步步靠近心中的技术目标!

结语:到这里,Elasticsearch 从服务端安装到 C++ 客户端二次封装的完整链路就跑通了。在我们的即时通讯系统里,用户搜索、全局消息搜索这两个核心场景,都是基于这套封装实现的,性能和准确性都能满足业务需求。ES 本身的玩法还有很多,比如分页、高亮、聚合分析,后续做消息的多维度统计的时候还会深入。接下来我们会继续把 ES 集成到微服务的各个业务子服务里,完整落地用户搜索和消息搜索功能。后续也会分享更多 C++ 后端、微服务架构的实战内容,欢迎关注交流。

✨把这些内容吃透超牛的!放松下吧✨ ʕ˘ᴥ˘ʔ づきらど

相关推荐
海量计划2 小时前
网络基础
网络
CHPCWWHSU2 小时前
FreeCAD 源码解读:从 Property 到 Command,看一套 CAD 内核的完整闭环
c++·cad
LiaoWL1232 小时前
从零开始学 Linux(二):Shell 脚本入门与文本处理实战
linux·运维·服务器
kyle~2 小时前
DTO --- 数据封装
数据库·oracle·前后端
代钦塔拉2 小时前
【C++/Qt 进阶】设计模式系列(5):适配器模式——给“不兼容的接口“贴一层翻译膜
c++·qt·设计模式
aqiu1111112 小时前
蓝桥杯真题复盘:Fibonacci 数列与黄金分割(C++ 算法详解)
c++·算法·蓝桥杯
.道阻且长.2 小时前
Linux软件包管理器
linux·运维·ubuntu·职场和发展·centos
lojox3 小时前
C++ 手写 STL list(双向循环链表)原理剖析与实现
c++·链表·list
青少儿编程课堂3 小时前
蓝桥杯·全国青少年信息素养大赛·电子学会C/C++考级:拓扑排序课程表排课解析
c++·python·算法·bfs·信息学竞赛