【微服务即时通讯】消息存储子服务1

目录

一.实现

1.1.base.proto文件的补充

1.1.1.消息的定义

1.1.2.定义聊天会话

1.2.MySQL数据库相关准备

1.2.1.message.hxx编写

1.2.2.message表的辅助操作类编写

1.2.3.测试

1.3.ES相关准备

1.3.1.创建索引

1.3.2.往索引里面添加数据

1.3.3.根据消息ID删除消息

1.3.4.查询数据

1.3.5.测试


一.实现

1.1.base.proto文件的补充

1.1.1.消息的定义

首先我们的消息是分成4大类型的

  • 纯文本消息
  • 图片消息
  • 文件消息
  • 语音消息
javascript 复制代码
// 消息类型枚举,定义不同类型的消息内容
enum MessageType
{
    STRING = 0;   // 纯文本消息
    IMAGE = 1;    // 图片消息
    FILE = 2;     // 文件消息
    SPEECH = 3;   // 语音消息
}

这4种消息的内容是需要不同的数据结构来进行存储的,所以我们就必须需要先将它们定义出来。

cpp 复制代码
// 纯文本消息的具体内容
message StringMessageInfo {
    string content = 1; // 文字聊天内容
}

// 图片消息的具体内容
message ImageMessageInfo {
    // 图片文件ID,客户端发送时不需要设置,由transmit服务器生成后交给storage服务时设置
    optional string file_id = 1;//optional表示该字段是可选的
    // 图片数据,在ES中存储消息时只需保留ID,不需要存储文件数据;服务端转发时需要原样转发
    optional bytes image_content = 2;//optional表示该字段是可选的
}

// 文件消息的具体内容
message FileMessageInfo {
    optional string file_id = 1;      // 文件ID,客户端发送时不用设置,由服务端生成
    optional int64 file_size = 2;     // 文件大小(字节)
    optional string file_name = 3;    // 文件名称
    // 文件数据,在ES中存储消息时只需保留ID和元信息,不需要存储文件数据;服务端转发时也不需要填充
    optional bytes file_contents = 4;
}

// 语音消息的具体内容
message SpeechMessageInfo {
    optional string file_id = 1;      // 语音文件ID,客户端发送时不用设置,由服务端生成
    // 文件数据,在ES中存储消息时只需保留ID,不需要存储文件数据;服务端转发时也不需要填充
    optional bytes file_contents = 2;
}

这样子,我们就类型+内容我们都定义出来了,那么我们现在就需要将这个 消息类型+消息内容 深度绑定,我们需要定义出一种通用的消息类型结构来表示一个消息的内容。

也就是下面这个

cpp 复制代码
// 消息内容的联合体,根据消息类型包含不同的具体消息内容
message MessageContent {
    MessageType message_type = 1; // 消息类型,指明具体是哪种消息
    oneof msg_content {           // 具体消息内容,oneof表明每次只能有一种类型
        StringMessageInfo string_message = 2; // 文字消息
        FileMessageInfo file_message = 3;     // 文件消息
        SpeechMessageInfo speech_message = 4; // 语音消息
        ImageMessageInfo image_message = 5;   // 图片消息
    };
}

消息类型+消息内容 都存放在这个MessageContent里面,但是作为一条消息,它还是缺少了一些其他的信息。

  • 消息ID
  • 这条消息属于哪个聊天会话
  • 这条消息是什么时候发出的
  • 这条消息的发送者信息

那么我们现在就能封装出一个完整的消息

cpp 复制代码
// 消息结构,表示一条完整的聊天消息
message MessageInfo {
    string message_id = 1;          // 消息ID,全局唯一
    string chat_session_id = 2;     // 消息所属聊天会话ID
    int64 timestamp = 3;             // 消息产生的时间戳(Unix时间戳,秒)
    UserInfo sender = 4;             // 消息发送者的用户信息
    MessageContent message = 5;      // 消息内容体
}

有的人可能好奇,为什么消息还需要chat_session_id??这个有啥用啊??为什么我只记录消息的发出者,却不记录消息的接收者??

这个就涉及到一个问题:

  • 1对1聊天
  • 多人聊天

在后台系统中,所有聊天消息都需要被持久化保存,通常通过数据库中的消息存储表来实现。

每条消息都必须明确记录其来源与去向,即消息由谁发出、最终要送达给谁。

1对1聊天场景中,这种设计是可行的,因为每一条消息只涉及发送方和接收方两个明确的用户ID。

然而,在群聊场景中,同一消息需要送达给多个用户,如果仍在单条消息中记录所有接收者,则面临存储结构上的难题------一条消息难以合理承载多个用户ID,这样的设计既不灵活,也不符合数据库规范。

为此,我们引入了"聊天会话"这一中间概念。**每个会话在后台都有独立的元数据,其中记录了参与该会话的所有成员信息。**当一条群消息发送时,系统只需关联到对应的会话ID,即可通过会话成员表确定所有接收者,从而避免在单条消息中冗余存储多个用户ID。这样既保证了数据的规范化,也提升了系统的可扩展性与维护性。

因此,我们就引入了这个chat_session_id来标记每条消息的属性。

回忆消息转发子服务的作用

在之前的消息分发子服务里面,我们实现了会话与用户的关系进行绑定,并且能通过消息转发子服务获取到这条消息属于的应该被转发给哪些用户。

你可以把它想象成一个消息转发中心,当用户发送一条新消息时,它会负责处理这条消息,确保它被正确地记录并送达到该会话中的所有成员。

整个过程大致可以分为以下几个步骤:

1. 接收请求与身份确认

当收到一条新消息的转发请求时,这个服务首先会确认"是谁发的消息"。它不会盲目相信请求里的用户ID ,而是会通过一个专门管理用户信息的服务来核实发送者的身份。它会动态找到一个可用的用户服务节点,然后去查询发送者的详细信息(比如昵称、头像等),确保消息来源是真实有效的。

2. 组装完整的消息信息

在确认发送者身份后,它会将这条消息补充完整。原始的请求里可能只有消息内容和会话ID,而这里会给消息附加上一个全局唯一的消息ID、当前的时间戳,以及刚才查询到的发送者详细信息。这样,一条完整、独立的消息就构建好了。

3. 确定转发目标

接下来,它需要知道这条消息应该发给谁。它会根据请求中的"聊天会话ID",去MySQL数据库中查询这个聊天会话里包含了哪些成员。这个成员列表就是最终的转发目标列表。

4. 消息的可靠存储

在把消息发给其他人之前,它首先会确保这条消息被可靠地保存下来 。它会把刚刚组装好的完整消息发送到消息队列中,交给专门负责存储的服务去处理。这样做的好处是,即使后续的转发环节出现问题,消息也已经落盘了,不会丢失。这一步是异步处理的,保证了消息转发的效率。

5. 返回转发结果

最后,它会向发送者所在的客户端返回一个响应。这个响应里包含了:

  • 操作是否成功的标识。

  • 刚才组装好的那条完整消息(这样发送者那边可以立刻显示自己发出的消息)。

  • 以及从数据库查出来的目标用户列表

这个返回的目标列表,主要是为了让上游服务知道这条消息最终需要推送给哪些在线用户。

1.1.2.定义聊天会话

我们的消息转发子服务仅仅只是实现了这个聊天会话与用户的关系查询。

但是我们并没有在消息转发子服务里面将这个聊天会话的这个数据结构给定义出来。

那么我们在这个消息存储子服务里面,我们将把这个聊天会话的数据结构给定义出来。

由此,我们就能写出下面这个了

cpp 复制代码
// 聊天会话信息,表示一个聊天会话(单聊或群聊)
message ChatSessionInfo 
{
    // 群聊会话不需要此字段,单聊会话设置为对方用户ID
    optional string single_chat_friend_id = 1; // 单聊对方的用户ID,optional表示该字段是可选的
    string chat_session_id = 2;                // 会话ID,全局唯一
    string chat_session_name = 3;               // 会话名称,例如群名称或对方昵称
    // 会话中最新的一条消息,新建的会话没有最新消息
    optional MessageInfo prev_message = 4;       // 上一条消息信息
    // 会话头像------群聊会话不需要,直接由前端固定渲染;单聊就是对方的头像
    optional bytes avatar = 5;                   // 会话头像图片数据
}

我们这里采用了两种形态并存的情况

single_chat_friend_id 字段的存在性可以作为一种隐式的会话类型标识:

  • 如果该字段有值,则表明这是一个单聊会话;
  • 如果为空,则可能是群聊或其他类型。

对于这个单聊会话,我们甚至都没有必要去调用我们的消息转发子服务来获取需要发送的对象。

1.2.MySQL数据库相关准备

1.2.1.message.hxx编写

我们的消息最终是需要存储进我们的数据库里面的,那么我们必须先把这个message这个表给定义出来,其实这个表的结构和我们上面在base.proto里面定义的MessageInfo的字段其实是基本相同的。

但是,我们这个时候需要考虑一些问题:

首先我们的消息是分成4大类型的

  • 纯文本消息
  • 图片消息
  • 文件消息
  • 语音消息

难道我们需要将所有类型的消息内容都存储到MySQL数据库里面吗??

实则不然,我们只在数据库里面存储纯文本消息,剩余3种类型的消息,我们将它存储到一个个文件里面去。

  • 对于纯文本消息,文本内容存储在 _content 字段中。
  • 对于图片、文件、语音这三种类型,只存储了文件标识、文件名、文件大小等元信息,文件本身的二进制数据并不存入数据库。

那么当我们想要去获取这些二进制数据的时候就需要去借助我们的文件管理子服务了。

message.hxx

cpp 复制代码
// 消息表映射对象
#pragma once
#include <boost/date_time/posix_time/posix_time.hpp> // Boost日期时间库,用于消息时间戳
#include <cstddef>                                   // 标准尺寸类型头文件
#include <odb/core.hxx>                              // ODB 核心头文件
#include <odb/nullable.hxx>                          // ODB 可空类型支持
#include <string>                                    // 标准字符串头文件

namespace IMS
{
// 使用 ODB 映射到数据库中的 message 表
#pragma db object table("message")
    class Message
    {
    public:
        // 默认构造函数,用于 ODB 反序列化
        Message() {}

        // 构造函数:使用消息ID、会话ID、用户ID、消息类型、创建时间初始化消息对象
        Message(const std::string &mid,
                const std::string &ssid,
                const std::string &uid,
                const unsigned char mtype,
                const boost::posix_time::ptime &ctime) : _message_id(mid), _session_id(ssid),
                                                         _user_id(uid), _message_type(mtype),
                                                         _create_time(ctime) {}

        // 获取消息ID
        std::string message_id() const { return _message_id; }
        // 设置消息ID
        void message_id(const std::string &val) { _message_id = val; }

        // 获取会话ID
        std::string session_id() const { return _session_id; }
        // 设置会话ID
        void session_id(const std::string &val) { _session_id = val; }

        // 获取发送者用户ID
        std::string user_id() const { return _user_id; }
        // 设置发送者用户ID
        void user_id(const std::string &val) { _user_id = val; }

        // 获取消息类型(0-文本;1-图片;2-文件;3-语音)
        unsigned char message_type() const { return _message_type; }
        // 设置消息类型
        void message_type(unsigned char val) { _message_type = val; }

        // 获取消息创建时间
        boost::posix_time::ptime create_time() const { return _create_time; }
        // 设置消息创建时间
        void create_time(const boost::posix_time::ptime &val) { _create_time = val; }

        // 获取文本消息内容(若为空则返回空字符串)
        std::string content() const
        {
            if (!_content)
                return std::string();
            return *_content;
        }
        // 设置文本消息内容
        void content(const std::string &val) { _content = val; }

        // 获取文件消息的文件ID(若为空则返回空字符串)
        std::string file_id() const
        {
            if (!_file_id)
                return std::string();
            return *_file_id;
        }
        // 设置文件消息的文件ID
        void file_id(const std::string &val) { _file_id = val; }

        // 获取文件消息的文件名(若为空则返回空字符串)
        std::string file_name() const
        {
            if (!_file_name)
                return std::string();
            return *_file_name;
        }
        // 设置文件消息的文件名
        void file_name(const std::string &val) { _file_name = val; }

        // 获取文件消息的文件大小(若为空则返回0)
        unsigned int file_size() const
        {
            if (!_file_size)
                return 0;
            return *_file_size;
        }
        // 设置文件消息的文件大小
        void file_size(unsigned int val) { _file_size = val; }

    private:
        // 允许 ODB 访问私有成员
        friend class odb::access;

// 自增主键ID,数据库自动生成
#pragma db id auto
        unsigned long _id;

// 消息ID,类型为 varchar(64),创建唯一索引以保证唯一性
#pragma db type("varchar(64)") index unique
        std::string _message_id;

// 所属会话ID,类型为 varchar(64),创建普通索引以加速按会话查询
#pragma db type("varchar(64)") index
        std::string _session_id;

// 发送者用户ID,类型为 varchar(64)
#pragma db type("varchar(64)")
        std::string _user_id;

        // 消息类型,0-文本;1-图片;2-文件;3-语音
        unsigned char _message_type;

// 消息创建时间,存储为 TIMESTAMP 类型
#pragma db type("TIMESTAMP")
        boost::posix_time::ptime _create_time;

        // 文本消息内容(可空),非文本消息忽略此字段
        odb::nullable<std::string> _content;

// 文件消息的文件ID(可空),文本消息忽略此字段
#pragma db type("varchar(64)")
        odb::nullable<std::string> _file_id;

// 文件消息的文件名(可空),只对文件消息有效
#pragma db type("varchar(128)")
        odb::nullable<std::string> _file_name;

        // 文件消息的文件大小(可空),只对文件消息有效
        odb::nullable<unsigned int> _file_size;
    };

    // ODB 命令行生成代码示例(用于参考):
    // odb -d mysql --std c++11 --generate-query --generate-schema --profile boost/date-time message.hxx

} // namespace IMS

特别注意这里面的

只有消息类型是纯文本消息的时候才会使用到下面这个字段

  • content

只有消息类型是**非纯文本消息时(图片,语音,文件)**才会使用到下面这个字段

  • file_id
  • file_name
  • file_size

1.2.2.message表的辅助操作类编写

那么针对这个message表,我们避免不了了高频率的往这个message表里面查询数据,修改数据等,那么我们就有必要对这些操作进行二次封装。

cpp 复制代码
// 引入MySQL数据库操作相关头文件(可能为自定义封装)
#include "mysql.hpp"
// 引入Message类的定义
#include "message.hxx"
// 引入ODB生成的Message对象关系映射代码
#include "message-odb.hxx"

namespace IMS
{
    // 消息表操作类,封装对消息的数据库增删查操作
    class MessageTable
    {
    public:
        // 定义智能指针类型,便于管理对象生命周期
        using ptr = std::shared_ptr<MessageTable>;

        // 构造函数:接收一个数据库连接对象,保存为成员变量
        // @param db 数据库连接智能指针(ODB数据库对象)
        MessageTable(const std::shared_ptr<odb::core::database> &db) : _db(db) {}

        // 析构函数:默认实现,无需额外资源释放
        ~MessageTable() {}

        // 插入一条消息到数据库
        // @param msg 待插入的消息对象(引用,将持久化到数据库)
        // @return 成功返回true,失败返回false(并记录错误日志)
        bool insert(Message &msg)
        {
            try
            {
                // 开启事务
                odb::transaction trans(_db->begin());
                // 持久化消息对象(插入数据库)
                _db->persist(msg);
                // 提交事务
                trans.commit();
            }
            catch (std::exception &e)
            {
                // 捕获异常,记录错误日志(包括消息ID和异常信息)
                LOG_ERROR("新增消息失败 {}:{}!", msg.message_id(), e.what());
                return false;
            }
            return true;
        }

        // 删除指定聊天会话的所有消息
        // @param chat_ssid 聊天会话ID(chat_session_id),用于定位要删除的消息
        // @return 成功返回true,失败返回false(并记录错误日志)
        bool remove(const std::string &chat_ssid)
        {
            try
            {
                // 开启事务
                odb::transaction trans(_db->begin());
                // 定义查询类型别名,简化代码
                typedef odb::query<Message> query;
                typedef odb::result<Message> result;
                // 执行条件删除:删除所有 chat_session_id 等于 chat_ssid 的消息
                _db->erase_query<Message>(query::chat_session_id == chat_ssid);
                // 提交事务
                trans.commit();
            }
            catch (std::exception &e)
            {
                // 捕获异常,记录错误日志(包括聊天会话ID和异常信息)
                LOG_ERROR("删除聊天会话所有消息失败 {}:{}!", chat_ssid, e.what());
                return false;
            }
            return true;
        }

        // 获取指定聊天会话最近的若干条消息(按时间倒序,但返回顺序为正序)
        // @param chat_ssid 聊天会话ID
        // @param count 需要获取的消息数量(最近count条)
        // @return 返回消息对象的vector,按时间升序排列(最早的在前)
        std::vector<Message> recent(const std::string &chat_ssid, int count)
        {
            std::vector<Message> res;
            try
            {
                // 开启事务
                odb::transaction trans(_db->begin());
                // 定义查询类型别名
                typedef odb::query<Message> query;
                typedef odb::result<Message> result;

                // 构建查询条件字符串:
                // chat_session_id='xx' order by create_time desc limit count;
                std::stringstream cond;
                cond << "chat_session_id='" << chat_ssid << "' ";
                cond << "order by create_time desc limit " << count;

                // 执行查询,返回按时间倒序的结果集(最新的在前)
                result r(_db->query<Message>(cond.str()));

                // 将查询结果依次压入vector(此时顺序为倒序)
                for (result::iterator i(r.begin()); i != r.end(); ++i)
                {
                    res.push_back(*i);
                }

                // 反转vector,使最终结果按时间升序(最早的在前)
                std::reverse(res.begin(), res.end());

                // 提交事务
                trans.commit();
            }
            catch (std::exception &e)
            {
                // 捕获异常,记录错误日志(包括聊天会话ID、数量和异常信息)
                LOG_ERROR("获取最近消息失败:{}-{}-{}!", chat_ssid, count, e.what());
            }
            return res;
        }

        // 获取指定聊天会话在某个时间区间内的所有消息(包含边界)
        // @param chat_ssid 聊天会话ID
        // @param stime 起始时间(包含)
        // @param etime 结束时间(包含)
        // @return 返回消息对象的vector,按时间升序排列(由数据库查询结果决定,通常自然顺序即为时间升序)
        std::vector<Message> range(const std::string &chat_ssid,
                                   boost::posix_time::ptime &stime,
                                   boost::posix_time::ptime &etime)
        {
            std::vector<Message> res;
            try
            {
                // 开启事务
                odb::transaction trans(_db->begin());
                // 定义查询类型别名
                typedef odb::query<Message> query;
                typedef odb::result<Message> result;

                // 执行查询:条件为 chat_session_id 等于 chat_ssid,且 create_time 在 [stime, etime] 之间
                result r(_db->query<Message>(query::chat_session_id == chat_ssid &&
                                             query::create_time >= stime &&
                                             query::create_time <= etime));

                // 将查询结果依次压入vector(数据库返回顺序通常为插入顺序或主键顺序,若create_time为索引则自然升序)
                for (result::iterator i(r.begin()); i != r.end(); ++i)
                {
                    res.push_back(*i);
                }

                // 提交事务
                trans.commit();
            }
            catch (std::exception &e)
            {
                // 捕获异常,记录错误日志(包括聊天会话ID、时间范围和异常信息)
                LOG_ERROR("获取区间消息失败:{}-{}:{}-{}!", chat_ssid,
                          boost::posix_time::to_simple_string(stime),
                          boost::posix_time::to_simple_string(etime), e.what());
            }
            return res;
        }

    private:
        // 数据库连接对象(ODB数据库智能指针),用于执行数据库操作
        std::shared_ptr<odb::core::database> _db;
    };

} // namespace IMS

该类封装了对消息表的四个核心数据库操作:

  1. 插入消息

    将一条完整的消息记录(包括消息 ID、会话 ID、用户 ID、消息类型、时间戳,以及文本消息的内容或文件消息的元信息)写入数据库。

  2. 删除指定聊天会话下所有消息

    根据聊天会话 ID 删除该会话中的所有消息记录,通常用于清空聊天记录或解散会话时调用。

  3. 查询最近消息

    获取指定聊天会话中最近的 N 条消息,内部先按时间倒序查询,再将结果反转为时间正序返回,便于客户端按顺序展示。

  4. 查询时间段内的消息

    根据起始时间和结束时间,获取指定聊天会话在某个时间区间内的所有消息,用于实现"加载更早消息"或"按日期筛选"等功能。

此外,该类在每次操作中都内置了事务管理,确保数据一致性;出现异常时会记录错误日志,并将错误状态返回给调用方。

1.2.3.测试

写了这个辅助类,我们就有必要去进行测试

cpp 复制代码
// 引入消息表操作类头文件
#include "mysql_message.hpp"
// 引入 gflags 命令行参数解析库
#include <gflags/gflags.h>

// 定义命令行参数:运行模式(调试/发布)
DEFINE_bool(run_mode, false, "程序的运行模式,false-调试; true-发布;");
// 定义命令行参数:日志输出文件(发布模式下使用)
DEFINE_string(log_file, "", "发布模式下,用于指定日志的输出文件");
// 定义命令行参数:日志输出等级(发布模式下使用)
DEFINE_int32(log_level, 0, "发布模式下,用于指定日志输出等级");

// 测试插入功能:向数据库插入多条测试消息
void insert_test(IMS::MessageTable &tb)
{
    // 创建消息对象并插入到会话ID1
    IMS::Message m1("消息ID1", "会话ID1", "用户ID1", 0, boost::posix_time::time_from_string("2002-01-20 23:59:59.000"));
    tb.insert(m1);
    IMS::Message m2("消息ID2", "会话ID1", "用户ID2", 0, boost::posix_time::time_from_string("2002-01-21 23:59:59.000"));
    tb.insert(m2);
    IMS::Message m3("消息ID3", "会话ID1", "用户ID3", 0, boost::posix_time::time_from_string("2002-01-22 23:59:59.000"));
    tb.insert(m3);

    // 向会话ID2插入两条消息
    IMS::Message m4("消息ID4", "会话ID2", "用户ID4", 0, boost::posix_time::time_from_string("2002-01-20 23:59:59.000"));
    tb.insert(m4);
    IMS::Message m5("消息ID5", "会话ID2", "用户ID5", 0, boost::posix_time::time_from_string("2002-01-21 23:59:59.000"));
    tb.insert(m5);
}

// 测试删除功能:删除指定会话的所有消息
void remove_test(IMS::MessageTable &tb)
{
    tb.remove("会话ID2");
}

// 测试获取最近消息功能:获取指定会话最近 N 条消息
void recent_test(IMS::MessageTable &tb)
{
    // 获取会话ID1最近2条消息(按时间倒序查询后反转,返回升序)
    auto res = tb.recent("会话ID1", 2);
    // 使用反向迭代器遍历,输出每条消息的信息
    auto begin = res.rbegin();
    auto end = res.rend();
    for (; begin != end; ++begin)
    {
        std::cout << begin->message_id() << std::endl;
        std::cout << begin->chat_session_id() << std::endl;
        std::cout << begin->user_id() << std::endl;
        std::cout << boost::posix_time::to_simple_string(begin->create_time()) << std::endl;
    }
}

// 测试时间范围查询:获取指定会话在指定时间段内的消息
void range_test(IMS::MessageTable &tb)
{
    // 定义起始时间和结束时间(使用字符串解析)
    boost::posix_time::ptime stime(boost::posix_time::time_from_string("2002-01-20 23:59:59.000"));
    boost::posix_time::ptime etime(boost::posix_time::time_from_string("2002-01-21 23:59:59.000"));
    // 执行范围查询
    auto res = tb.range("会话ID1", stime, etime);
    // 遍历并打印结果
    for (const auto &m : res)
    {
        std::cout << m.message_id() << std::endl;
        std::cout << m.chat_session_id() << std::endl;
        std::cout << m.user_id() << std::endl;
        std::cout << boost::posix_time::to_simple_string(m.create_time()) << std::endl;
    }
}

int main(int argc, char *argv[])
{
    // 解析命令行参数(gflags)
    google::ParseCommandLineFlags(&argc, &argv, true);
    // 初始化日志系统(根据运行模式)
    IMS::init_logger(FLAGS_run_mode, FLAGS_log_file, FLAGS_log_level);

    // 创建数据库连接(使用ODB工厂,参数:用户名、密码、主机、数据库名、字符集、端口、连接池大小)
    auto db = IMS::ODBFactory::create("root", "123456", "127.0.0.1", "IMS", "utf8", 0, 1);
    // 创建消息表操作对象
    IMS::MessageTable tb(db);
    // 执行测试
    insert_test(tb); // 插入测试数据
    remove_test(tb); // 删除会话ID2的消息
    recent_test(tb); // 查询最近消息
    range_test(tb);  // 查询时间段消息
    return 0;
}

注意:我们编译完成之后会在当前目录生成一个message.sql,如下图所示

我们在进行测试之前,需要先将这个message.sql文件导入进MySQL数据库里面去

cpp 复制代码
mysql -u root -p -D IMS <message.sql

有了这个表之后我们才能去进行测试

我们去数据库看看

cpp 复制代码
mysql> select * from message;
+----+------------+-----------------+-----------+--------------+---------------------+---------+---------+-----------+-----------+
| id | message_id | chat_session_id | user_id   | message_type | create_time         | content | file_id | file_name | file_size |
+----+------------+-----------------+-----------+--------------+---------------------+---------+---------+-----------+-----------+
|  1 | 消息ID1    | 会话ID1         | 用户ID1   |            0 | 2002-01-20 23:59:59 | NULL    | NULL    | NULL      |      NULL |
|  2 | 消息ID2    | 会话ID1         | 用户ID2   |            0 | 2002-01-21 23:59:59 | NULL    | NULL    | NULL      |      NULL |
|  3 | 消息ID3    | 会话ID1         | 用户ID3   |            0 | 2002-01-22 23:59:59 | NULL    | NULL    | NULL      |      NULL |
+----+------------+-----------------+-----------+--------------+---------------------+---------+---------+-----------+-----------+
3 rows in set (0.00 sec)

我们来分析一下这个测试的过程啊!!!

  1. insert_test(tb) 插入的数据

会话ID1:

  • 消息ID1:2002-01-20 23:59:59
  • 消息ID2:2002-01-21 23:59:59
  • 消息ID3:2002-01-22 23:59:59

会话ID2:

  • 消息ID4:2002-01-20 23:59:59
  • 消息ID5:2002-01-21 23:59:59
  1. remove_test(tb) 删除会话ID2的所有消息
  • 会话ID2的两条消息被删除,后续查询只涉及会话ID1。
  1. recent_test(tb) 获取会话ID1最近2条消息
  • tb.recent("会话ID1", 2) 内部查询按时间倒序获取最新的2条(ID3和ID2),再反转成正序,返回 [消息ID2, 消息ID3]。
  • 测试代码中使用反向迭代器 res.rbegin() 遍历,因此打印顺序为:
  • 先打印 消息ID3(最新)
  • 再打印 消息ID2
  1. range_test(tb) 查询会话ID1在 [2002-01-20 23:59:59, 2002-01-21 23:59:59] 内的消息
  • 时间范围包含消息ID1和消息ID2(ID1恰为起始时间,ID2恰为结束时间)。
  • 查询结果按时间升序返回,因此顺序为 [消息ID1, 消息ID2]。
  • 遍历打印

完全对上了。

注意:测试完之后需要在数据库里面删除对应数据

cpp 复制代码
TRUNCATE TABLE message;

1.3.ES相关准备

我们的消息数量是非常庞大的,我们是需要借助ES来进行消息的检索的。

还记得我们在用户管理子服务那里我们也进行过ES的使用吗?

首先我们肯定需要一个工厂类来快速搭建我们的ES客户端句柄

cpp 复制代码
// ESClientFactory类:用于创建elasticlient::Client对象的工厂类
    class ESClientFactory
    {
    public:
        // 静态方法:根据主机列表创建并返回elasticlient::Client的共享指针
        // 参数:
        //   host_list - Elasticsearch节点地址列表(例如:{"http://localhost:9200"})
        static std::shared_ptr<elasticlient::Client> create(const std::vector<std::string> host_list)
        {
            // 使用主机列表构造Client对象并封装为共享指针返回
            return std::make_shared<elasticlient::Client>(host_list);
        }
    };

我们的ESMessage类就是基于这个客户端句柄来进行操作的

cpp 复制代码
// ESMessage类:管理Elasticsearch中消息索引的增删改查操作
    class ESMessage
    {
    public:
        // 类型别名:指向ESMessage的共享指针
        using ptr = std::shared_ptr<ESMessage>;

        // 构造函数:接收elasticlient::Client共享指针
        // 参数:
        //   es_client - Elasticsearch客户端共享指针
        ESMessage(const std::shared_ptr<elasticlient::Client> &es_client) : _es_client(es_client) {}

......

    private:
        // 私有成员:Elasticsearch客户端共享指针
        std::shared_ptr<elasticlient::Client> _es_client;
    };

有了客户端,我们就需要提供下面这3步核心操作

  • 创建索引
  • 向索引内容添加一条新数据
  • 在索引内部查询数据

1.3.1.创建索引

注意这个ESIndex这个接口其实是我们的common/icsearch.hpp里面的,如果你有点忘记了,我这里直接给你看common/icsearch.hpp源代码

cpp 复制代码
// 类 ESIndex:用于定义和创建 Elasticsearch 索引的映射(mapping)和设置(settings)
class ESIndex 
{
public:
    // 构造函数:初始化索引名称、文档类型,并设置默认的 IK 分词器配置
    // 参数 client:指向 elasticlient::Client 的共享指针,用于发送 HTTP 请求
    // 参数 name:索引名称
    // 参数 type:文档类型,默认为 "_doc"(Elasticsearch 7.x 后推荐)
    ESIndex(std::shared_ptr<elasticlient::Client> &client, 
        const std::string &name, 
        const std::string &type = "_doc"):
        _name(name), _type(type), _client(client) 
    {
        // 构建索引设置(settings)部分,配置 IK 分词器
        Json::Value analysis;   // analysis 对象
        Json::Value analyzer;   // analyzer 对象
        Json::Value ik;         // ik 分词器对象
        Json::Value tokenizer;  // tokenizer 对象
        // 设置分词器为 ik_max_word(IK 分词器的最大粒度分词模式)
        tokenizer["tokenizer"] = "ik_max_word";
        // 将 tokenizer 赋值给 ik 对象下的 "ik" 字段
        ik["ik"] = tokenizer;
        // 将 ik 对象赋值给 analyzer 对象下的 "analyzer" 字段
        analyzer["analyzer"] = ik;
        // 将 analyzer 对象赋值给 analysis 对象下的 "analysis" 字段
        analysis["analysis"] = analyzer;
        // 将 analysis 对象放入 _index 的 "settings" 字段中
        _index["settings"] = analysis;
    }

    // 成员函数 append:向索引的 properties 中添加一个字段定义(链式调用)
    // 参数 key:字段名称
    // 参数 type:字段类型,默认为 "text"
    // 参数 analyzer:分词器名称,默认为 "ik_max_word"
    // 参数 enabled:是否启用该字段(若为 false,则该字段不会被索引和存储)
    // 返回值:返回当前对象的引用,支持链式调用
    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;  // 设置分词器(仅对 text 类型有效)
        if (enabled == false) 
        {
            // 如果 enabled 为 false,则添加 "enabled": false 字段,表示该字段不参与索引和存储
            fields["enabled"] = enabled;
        }
        // 将该字段定义添加到 _properties 对象中,键为字段名
        _properties[key] = fields;
        // 返回当前对象引用,以便链式调用
        return *this;
    }

    // 成员函数 create:根据已添加的字段定义,创建索引(实际发送请求到 Elasticsearch)
    // 参数 index_id:可选的索引文档 ID,但这里可能用于某些特殊场景,默认为 "default_index_id"
    // 注意:在 elasticlient 的 index 方法中,如果指定了 ID,会创建或更新一个文档,而不是创建索引本身。
    // 但是如果索引不存在,那么就会自动触发索引的自动创建(同时设置 mapping),
    bool create(const std::string &index_id = "default_index_id") 
    {
        // 构建 mappings 部分
        Json::Value mappings;
        mappings["dynamic"] = true;                 // 允许动态添加字段(未在 mapping 中定义的字段也会被索引)
        mappings["properties"] = _properties;       // 将字段定义赋值给 properties
        _index["mappings"] = mappings;               // 将 mappings 对象放入 _index 的 "mappings" 字段

        // 将整个 _index 对象序列化为字符串
        std::string body;
        bool ret = Serialize(_index, body);
        if (ret == false) {
            LOG_ERROR("索引序列化失败!");
            return false;
        }
        LOG_DEBUG("{}", body);  // 输出序列化后的请求体,便于调试

        // 发起索引创建请求(实际上调用的是 index 方法,可能期望创建索引并插入一个文档)
        try 
        {
            // 调用 client->index 方法,传入索引名称、类型、文档 ID 和请求体
            // 注意:这实际上是在创建或更新一个文档,而不是纯粹的创建索引。
            // 如果索引不存在,Elasticsearch 会自动创建索引并使用请求体中的 mapping 作为索引的 mapping。
            auto rsp = _client->index(_name, _type, index_id, body);
            // 检查响应状态码是否为 2xx(成功)
            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;                       // 文档类型(通常为 "_doc")
    Json::Value _properties;                  // 存储字段定义的 JSON 对象(mapping 的 properties 部分)
    Json::Value _index;                       // 完整的索引定义(包含 settings 和 mappings)
    std::shared_ptr<elasticlient::Client> _client; // 共享的 elasticlient 客户端指针
};

回忆过我们的icsearch.hpp之后,我们就来创建我们的索引吧

cpp 复制代码
// 创建消息索引(如果不存在)
        // 返回值:成功返回true,失败返回false
        bool createIndex()
        {
            // 构建消息索引结构
            bool ret = ESIndex(_es_client, "message")                              // 指定索引名称为"message"
                           .append("user_id", "keyword", "standard", false)        // 发送者用户ID,keyword类型,不存储
                           .append("message_id", "keyword", "standard", false)     // 消息ID,keyword类型,不存储
                           .append("create_time", "long", "standard", false)       // 创建时间(时间戳),long类型,不存储
                           .append("chat_session_id", "keyword", "standard", true) // 会话ID,keyword类型,存储
                           .append("content")                                      // 消息内容,默认情况是append("content","text","ik_max_word",true)
                           .create();                                              // 执行创建索引
            if (ret == false)
            {
                LOG_INFO("消息信息索引创建失败!");
                return false;
            }
            LOG_INFO("消息信息索引创建成功!");
            return true;
        }

我知道大家这样子看其实是有一点小困难的。

那么我们就将它转换成JSON格式来看看

cpp 复制代码
// 整个 JSON 对象定义了 Elasticsearch 索引的配置
// 包含 settings(索引设置)和 mappings(映射)两部分
{
  // settings 部分:定义索引的全局设置,如分词器、副本数等
  "settings": {
    // analysis 对象:定义文本分析相关配置
    "analysis": {
      // analyzer 对象:定义分词器配置
      "analyzer": {
        // 自定义分词器名为 "ik"
        "ik": {
          // 指定分词器使用的 tokenizer(分词器)为 ik_max_word,即 IK 分词器的最大粒度分词模式
          "tokenizer": "ik_max_word"
        }
      }
    }
  },
  // mappings 部分:定义索引的字段映射
  "mappings": {
    // dynamic 为 true 表示允许动态添加新字段(未在 mapping 中定义的字段也会被自动索引)
    "dynamic": true,
    // properties 对象:定义每个字段的具体映射
    "properties": {
      // user_id 字段:发送者用户 ID
      "user_id": {
        "type": "keyword",        // 字段类型为 keyword(不分词,精确匹配)
        "analyzer": "standard",   // 指定分词器(但 keyword 类型实际不使用,此处仅为代码逻辑体现)
        "enabled": false          // 该字段不参与索引和存储(仅用于记录元数据)
      },
      // message_id 字段:消息唯一标识
      "message_id": {
        "type": "keyword",
        "analyzer": "standard",
        "enabled": false
      },
      // create_time 字段:消息创建时间(Unix 时间戳)
      "create_time": {
        "type": "long",           // 字段类型为 long(64位整数)
        "analyzer": "standard",
        "enabled": false
      },
      // chat_session_id 字段:所属聊天会话 ID
      "chat_session_id": {
        "type": "keyword",
        "analyzer": "standard",
        "enabled": true           // 该字段参与索引和存储,可用于过滤查询
      },
      // content 字段:消息文本内容
      "content": {
        "type": "text",           // 字段类型为 text(支持全文搜索)
        "analyzer": "ik_max_word" // 使用 IK 最大粒度分词器,便于中文搜索
        "enabled": true           // 该字段参与索引和存储,可用于过滤查询
      }
    }
  }
}

这个创建索引还算是明白吧。

1.3.2.往索引里面添加数据

这个还是借助了icsearch.hpp的ESInsert来实现

cpp 复制代码
// 类 ESInsert:用于向 Elasticsearch 索引中插入或更新文档
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){}

    // 模板成员函数 append:向待插入的文档中添加字段(链式调用)
    // 参数 key:字段名
    // 参数 val:字段值,可以是任意类型(Json::Value 支持的类型)
    // 返回值:返回当前对象的引用,支持链式调用
    template<typename T>
    ESInsert &append(const std::string &key, const T &val){
        _item[key] = val;   // 将键值对存入 _item 对象
        return *this;
    }

    // 成员函数 insert:将构建的文档插入到 Elasticsearch 中
    // 参数 id:可选,文档的 ID。如果为空字符串,Elasticsearch 会自动生成 ID。
    // 返回值:成功返回 true,失败返回 false
    bool insert(const std::string id = "") 
    {
        // 将 _item 序列化为 JSON 字符串
        std::string body;
        bool ret = Serialize(_item, body);
        if (ret == false) 
        {
            LOG_ERROR("索引序列化失败!");
            return false;
        }
        LOG_DEBUG("{}", body);  // 输出请求体,便于调试

        // 发起索引文档请求
        try 
        {
            // 调用 client->index 方法,传入索引名、类型、文档 ID 和请求体
            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;          // 存储待插入文档的 JSON 对象
    std::shared_ptr<elasticlient::Client> _client; // 共享的客户端指针
};

现在我们就能比较轻松的写出下面这个代码了

cpp 复制代码
// 向消息索引中插入或更新一条消息数据
        // 参数:
        //   user_id         - 发送者用户ID
        //   message_id      - 消息ID(作为文档ID)
        //   create_time     - 创建时间(Unix时间戳,长整型)
        //   chat_session_id - 所属会话ID
        //   content         - 消息内容
        // 返回值:成功返回true,失败返回false
        bool appendData(const std::string &user_id,
                        const std::string &message_id,
                        const long create_time,
                        const std::string &chat_session_id,
                        const std::string &content)
        {
            // 构建插入数据
            bool ret = ESInsert(_es_client, "message")                 // 指定索引"message"
                           .append("message_id", message_id)           // 消息ID
                           .append("create_time", create_time)         // 创建时间
                           .append("user_id", user_id)                 // 用户ID
                           .append("chat_session_id", chat_session_id) // 会话ID
                           .append("content", content)                 // 内容
                           .insert(message_id);                        // 以message_id作为文档ID插入
            if (ret == false)
            {
                LOG_ERROR("消息数据插入/更新失败!");
                return false;
            }
            LOG_INFO("消息数据新增/更新成功!");
            return true;
        }

这个也没什么好说的,就是存储的添加而已。

1.3.3.根据消息ID删除消息

这个也是借助了这个我们自己写的ESRemove类来实现

cpp 复制代码
// 类 ESRemove:用于从 Elasticsearch 索引中删除文档
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){}

    // 成员函数 remove:根据文档 ID 删除文档
    // 参数 id:要删除的文档 ID
    // 返回值:成功返回 true,失败返回 false
    bool remove(const std::string &id) 
    {
        try 
        {
            // 调用 client->remove 方法,传入索引名、类型和文档 ID
            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; // 共享的客户端指针
};

我们很快就能写出

cpp 复制代码
// 根据消息ID删除消息
        // 参数:
        //   mid - 消息ID
        // 返回值:成功返回true,失败返回false
        bool remove(const std::string &mid)
        {
            bool ret = ESRemove(_es_client, "message").remove(mid); // 执行删除
            if (ret == false)
            {
                LOG_ERROR("消息数据删除失败!");
                return false;
            }
            LOG_INFO("消息数据删除成功!");
            return true;
        }

1.3.4.查询数据

这个还是借助了icsearch.hpp的ESSearch类来进行实现

这里我就不列出来了,你们自己去代码网站里面找

cpp 复制代码
// 在指定会话中搜索包含关键词的消息
        // 参数:
        //   key  - 搜索关键词(在消息内容中匹配)
        //   chat_ssid - 聊天会话ID
        // 返回值:IMS::Message对象的vector
        std::vector<IMS::Message> search(const std::string &key, const std::string &chat_ssid)
        {
            std::vector<IMS::Message> res; // 存储结果
            // 构建搜索查询:必须满足聊天会话ID匹配,并且内容包含关键词
            Json::Value json_user = ESSearch(_es_client, "message")                        // 指定索引"message"
                                        .append_must_term("chat_session_id.keyword", chat_ssid) // 必须匹配聊天会话ID(精确)
                                        .append_must_match("content", key)                 // 必须匹配消息内容(分词匹配)
                                        .search();                                         // 执行搜索
            if (json_user.isArray() == false)
            { // 检查结果是否为数组
                LOG_ERROR("用户搜索结果为空,或者结果不是数组类型");
                return res; // 返回空vector
            }
            int sz = json_user.size(); // 结果数量
            LOG_DEBUG("检索结果条目数量:{}", sz);
            // 遍历搜索结果,转换为Message对象
            for (int i = 0; i < sz; i++)
            {
                IMS::Message message; // 创建Message对象
                // 设置用户ID
                message.user_id(json_user[i]["_source"]["user_id"].asString());
                // 设置消息ID
                message.message_id(json_user[i]["_source"]["message_id"].asString());
                // 从时间戳构造boost::posix_time::ptime对象(需要先转为time_t)
                boost::posix_time::ptime ctime(boost::posix_time::from_time_t(
                    json_user[i]["_source"]["create_time"].asInt64()));
                message.create_time(ctime); // 设置创建时间
                // 设置聊天会话ID
                message.chat_session_id(json_user[i]["_source"]["chat_session_id"].asString());
                // 设置消息内容
                message.content(json_user[i]["_source"]["content"].asString());
                res.push_back(message); // 添加到结果vector
            }
            return res;
        }

还是比较容易懂的

1.3.5.测试

针对ES接口,我们需要进行测试

cpp 复制代码
#include "../../../../common/data_es.hpp"
#include <gflags/gflags.h>

DEFINE_bool(run_mode, false, "程序的运行模式,false-调试; true-发布;");
DEFINE_string(log_file, "", "发布模式下,用于指定日志的输出文件");
DEFINE_int32(log_level, 0, "发布模式下,用于指定日志输出等级");

DEFINE_string(es_host, "http://127.0.0.1:9200/", "es服务器URL");

int main(int argc, char *argv[])
{
    google::ParseCommandLineFlags(&argc, &argv, true);
    IMS::init_logger(FLAGS_run_mode, FLAGS_log_file, FLAGS_log_level);

    auto es_client = IMS::ESClientFactory::create({FLAGS_es_host});

    auto es_msg = std::make_shared<IMS::ESMessage>(es_client);
    es_msg->createIndex();
    es_msg->appendData("用户ID1", "消息ID1", 1723025035, "会话ID1", "吃饭了吗?");
    es_msg->appendData("用户ID2", "消息ID2", 1723025035 - 100, "会话ID1", "吃的盖浇饭!");
    es_msg->appendData("用户ID3", "消息ID3", 1723025035, "会话ID1", "你吃盖浇饭吗?");
    es_msg->appendData("用户ID4", "消息ID4", 1723025035 - 100, "会话ID2", "吃的盖浇饭!");
    std::this_thread::sleep_for(std::chrono::seconds(2));
    auto res1 = es_msg->search("盖浇", "会话ID1");
    for (auto &u : res1)
    {
        std::cout << "-----------------" << std::endl;
        std::cout << u.user_id() << std::endl;
        std::cout << u.message_id() << std::endl;
        std::cout << u.chat_session_id() << std::endl;
        std::cout << boost::posix_time::to_simple_string(u.create_time()) << std::endl;
        std::cout << u.content() << std::endl;
    }
    std::cout<<"删除消息ID1之后:"<<std::endl;
    es_msg->remove("消息ID2");
    std::this_thread::sleep_for(std::chrono::seconds(2));
    auto res2 = es_msg->search("盖浇", "会话ID1");
    for (auto &u : res2)
    {
        std::cout << "-----------------" << std::endl;
        std::cout << u.user_id() << std::endl;
        std::cout << u.message_id() << std::endl;
        std::cout << u.chat_session_id() << std::endl;
        std::cout << boost::posix_time::to_simple_string(u.create_time()) << std::endl;
        std::cout << u.content() << std::endl;
    }
    return 0;
}

注意:插入数据之后需要进行延时,才能查询的到,我们看看测试结果来

cpp 复制代码
ubuntu@10-13-52-255:~/cpp-chatsystem/server/Service/message/test/es_test$ ./main
[default-logger][21:13:22][1232725][debug   ][../../../../common/icsearch.hpp:137] {
        "mappings" : 
        {
                "dynamic" : true,
                "properties" : 
                {
                        "chat_session_id" : 
                        {
                                "analyzer" : "standard",
                                "type" : "keyword"
                        },
                        "content" : 
                        {
                                "analyzer" : "ik_max_word",
                                "type" : "text"
                        },
                        "create_time" : 
                        {
                                "analyzer" : "standard",
                                "enabled" : false,
                                "type" : "long"
                        },
                        "message_id" : 
                        {
                                "analyzer" : "standard",
                                "enabled" : false,
                                "type" : "keyword"
                        },
                        "user_id" : 
                        {
                                "analyzer" : "standard",
                                "enabled" : false,
                                "type" : "keyword"
                        }
                }
        },
        "settings" : 
        {
                "analysis" : 
                {
                        "analyzer" : 
                        {
                                "ik" : 
                                {
                                        "tokenizer" : "ik_max_word"
                                }
                        }
                }
        }
}
[default-logger][21:13:22][1232725][info    ][../../../../common/data_es.hpp:162] 消息信息索引创建成功!
[default-logger][21:13:22][1232725][debug   ][../../../../common/icsearch.hpp:204] {
        "chat_session_id" : "\u4f1a\u8bddID1",
        "content" : "\u5403\u996d\u4e86\u5417\uff1f",
        "create_time" : 1723025035,
        "message_id" : "\u6d88\u606fID1",
        "user_id" : "\u7528\u6237ID1"
}
[default-logger][21:13:22][1232725][info    ][../../../../common/data_es.hpp:193] 消息数据新增/更新成功!
[default-logger][21:13:22][1232725][debug   ][../../../../common/icsearch.hpp:204] {
        "chat_session_id" : "\u4f1a\u8bddID1",
        "content" : "\u5403\u7684\u76d6\u6d47\u996d\uff01",
        "create_time" : 1723024935,
        "message_id" : "\u6d88\u606fID2",
        "user_id" : "\u7528\u6237ID2"
}
[default-logger][21:13:22][1232725][info    ][../../../../common/data_es.hpp:193] 消息数据新增/更新成功!
[default-logger][21:13:22][1232725][debug   ][../../../../common/icsearch.hpp:204] {
        "chat_session_id" : "\u4f1a\u8bddID1",
        "content" : "\u4f60\u5403\u76d6\u6d47\u996d\u5417\uff1f",
        "create_time" : 1723025035,
        "message_id" : "\u6d88\u606fID3",
        "user_id" : "\u7528\u6237ID3"
}
[default-logger][21:13:22][1232725][info    ][../../../../common/data_es.hpp:193] 消息数据新增/更新成功!
[default-logger][21:13:22][1232725][debug   ][../../../../common/icsearch.hpp:204] {
        "chat_session_id" : "\u4f1a\u8bddID2",
        "content" : "\u5403\u7684\u76d6\u6d47\u996d\uff01",
        "create_time" : 1723024935,
        "message_id" : "\u6d88\u606fID4",
        "user_id" : "\u7528\u6237ID4"
}
[default-logger][21:13:22][1232725][info    ][../../../../common/data_es.hpp:193] 消息数据新增/更新成功!
[default-logger][21:13:24][1232725][debug   ][../../../../common/icsearch.hpp:361] {
        "query" : 
        {
                "bool" : 
                {
                        "must" : 
                        [
                                {
                                        "term" : 
                                        {
                                                "chat_session_id.keyword" : "\u4f1a\u8bddID1"
                                        }
                                },
                                {
                                        "match" : 
                                        {
                                                "content" : "\u76d6\u6d47"
                                        }
                                }
                        ]
                }
        }
}
[default-logger][21:13:24][1232725][debug   ][../../../../common/icsearch.hpp:379] 检索响应正文: [{"took":1,"timed_out":false,"_shards":{"total":1,"successful":1,"skipped":0,"failed":0},"hits":{"total":{"value":2,"relation":"eq"},"max_score":1.0700248,"hits":[{"_index":"message","_type":"_doc","_id":"æ¶æ¯ID2","_score":1.0700248,"_source":{
        "chat_session_id" : "\u4f1a\u8bddID1",
        "content" : "\u5403\u7684\u76d6\u6d47\u996d\uff01",
        "create_time" : 1723024935,
        "message_id" : "\u6d88\u606fID2",
        "user_id" : "\u7528\u6237ID2"
}},{"_index":"message","_type":"_doc","_id":"æ¶æ¯ID3","_score":1.0160741,"_source":{
        "chat_session_id" : "\u4f1a\u8bddID1",
        "content" : "\u4f60\u5403\u76d6\u6d47\u996d\u5417\uff1f",
        "create_time" : 1723025035,
        "message_id" : "\u6d88\u606fID3",
        "user_id" : "\u7528\u6237ID3"
}}]}}]
[default-logger][21:13:24][1232725][debug   ][../../../../common/data_es.hpp:232] 检索结果条目数量:2
-----------------
用户ID2
消息ID2
会话ID1
2024-Aug-07 10:02:15
吃的盖浇饭!
-----------------
用户ID3
消息ID3
会话ID1
2024-Aug-07 10:03:55
你吃盖浇饭吗?
删除消息ID1之后:
[default-logger][21:13:24][1232725][info    ][../../../../common/data_es.hpp:209] 消息数据删除成功!
[default-logger][21:13:26][1232725][debug   ][../../../../common/icsearch.hpp:361] {
        "query" : 
        {
                "bool" : 
                {
                        "must" : 
                        [
                                {
                                        "term" : 
                                        {
                                               "chat_session_id.keyword" : "\u4f1a\u8bddID1"
                                        }
                                },
                                {
                                        "match" : 
                                        {
                                               "content" : "\u76d6\u6d47"
                                        }
                                }
                        ]
                }
        }
}
[default-logger][21:13:26][1232725][debug   ][../../../../common/icsearch.hpp:379] 检索响应正文: [{"took":1,"timed_out":false,"_shards":{"total":1,"successful":1,"skipped":0,"failed":0},"hits":{"total":{"value":1,"relation":"eq"},"max_score":1.0160741,"hits":[{"_index":"message","_type":"_doc","_id":"æ¶æ¯ID3","_score":1.0160741,"_source":{
        "chat_session_id" : "\u4f1a\u8bddID1",
        "content" : "\u4f60\u5403\u76d6\u6d47\u996d\u5417\uff1f",
        "create_time" : 1723025035,
        "message_id" : "\u6d88\u606fID3",
        "user_id" : "\u7528\u6237ID3"
}}]}}]
[default-logger][21:13:26][1232725][debug   ][../../../../common/data_es.hpp:232] 检索结果条目数量:1
-----------------
用户ID3
消息ID3
会话ID1
2024-Aug-07 10:03:55
你吃盖浇饭吗?

完美符合。

现在我们也可以去ES里面查询一下

cpp 复制代码
GET /message/_search
{
  "query": {
    "match_all": {}
  }
}

我们看看结果

cpp 复制代码
#! Elasticsearch built-in security features are not enabled. Without authentication, your cluster could be accessible to anyone. See https://www.elastic.co/guide/en/elasticsearch/reference/7.17/security-minimal-setup.html to enable security.
{
  "took" : 0,
  "timed_out" : false,
  "_shards" : {
    "total" : 1,
    "successful" : 1,
    "skipped" : 0,
    "failed" : 0
  },
  "hits" : {
    "total" : {
      "value" : 4,
      "relation" : "eq"
    },
    "max_score" : 1.0,
    "hits" : [
      {
        "_index" : "message",
        "_type" : "_doc",
        "_id" : "default_index_id",
        "_score" : 1.0,
        "_source" : {
          "mappings" : {
            "dynamic" : true,
            "properties" : {
              "chat_session_id" : {
                "analyzer" : "standard",
                "type" : "keyword"
              },
              "content" : {
                "analyzer" : "ik_max_word",
                "type" : "text"
              },
              "create_time" : {
                "analyzer" : "standard",
                "enabled" : false,
                "type" : "long"
              },
              "message_id" : {
                "analyzer" : "standard",
                "enabled" : false,
                "type" : "keyword"
              },
              "user_id" : {
                "analyzer" : "standard",
                "enabled" : false,
                "type" : "keyword"
              }
            }
          },
          "settings" : {
            "analysis" : {
              "analyzer" : {
                "ik" : {
                  "tokenizer" : "ik_max_word"
                }
              }
            }
          }
        }
      },
      {
        "_index" : "message",
        "_type" : "_doc",
        "_id" : "消息ID1",
        "_score" : 1.0,
        "_source" : {
          "chat_session_id" : "会话ID1",
          "content" : "吃饭了吗?",
          "create_time" : 1723025035,
          "message_id" : "消息ID1",
          "user_id" : "用户ID1"
        }
      },
      {
        "_index" : "message",
        "_type" : "_doc",
        "_id" : "消息ID3",
        "_score" : 1.0,
        "_source" : {
          "chat_session_id" : "会话ID1",
          "content" : "你吃盖浇饭吗?",
          "create_time" : 1723025035,
          "message_id" : "消息ID3",
          "user_id" : "用户ID3"
        }
      },
      {
        "_index" : "message",
        "_type" : "_doc",
        "_id" : "消息ID4",
        "_score" : 1.0,
        "_source" : {
          "chat_session_id" : "会话ID2",
          "content" : "吃的盖浇饭!",
          "create_time" : 1723024935,
          "message_id" : "消息ID4",
          "user_id" : "用户ID4"
        }
      }
    ]
  }
}

完美符合啊!!!

现在我们就去ES里面执行下面这个把这个索引给删掉

cpp 复制代码
DELETE /message

测试完成

相关推荐
indexsunny2 小时前
互联网大厂Java面试:从Spring Boot到微服务的逐步挑战
java·数据库·spring boot·redis·微服务·面试·电商
Are_You_Okkk_2 小时前
研发运维一体化:开源知识库落地案例与价值探析
运维·人工智能·架构·开源
JackieZhengChina2 小时前
BMAD-METHOD 筑梦架构:AI 驱动的开源敏捷开发方法
人工智能·架构·开源
LCG元2 小时前
STM32嵌入式开发:基于STM32F103的智能语音识别系统
stm32·嵌入式硬件·语音识别
剑飞的编程思维2 小时前
技术评审方法与流程全解析-如何做好技术评审
git·架构·个人开发·学习方法·技术美术·代码复审
爱吃萝卜的美羊羊3 小时前
ruoyi-cloud微服务跨服务调用实例接口
微服务·架构·ruoyi-cloud
Swift社区3 小时前
分布式能力不是功能,而是一种架构约束
分布式·架构
人间打气筒(Ada)4 小时前
go实战案例:如何基于 Conul 给微服务添加服务注册与发现?
开发语言·微服务·zookeeper·golang·kubernetes·etcd·consul
jerwey4 小时前
app-unavailable-in-region
架构