LinuxC++项目开发日志——基于正倒排索引的boost搜索引擎(3——通过cppjieba库建立索引模块)
基于正倒排索引的boost搜索引擎
- cppjieba分词库
- cppjieba库简介
- cppjieba的特点和优势
- cppjieba安装
- 索引模块建立
- 概述
- 核心数据结构
- 索引建立流程
- 步骤1:主索引构建入口(BulidIndex)
- 步骤2:正排索引构建(BuildForwordIndex)
- 查询接口
- 技术特点
- 1. 权重计算策略
- 2. 文本预处理
- 3. 内存优化
- 完整代码
- 其它更新模块
- common.h
- util.hpp
- main.cc测试
cppjieba分词库
cppjieba库简介
cppjieba 是 jieba 分词库的 C++ 版本实现,它保留了 Python 版 jieba 的核心功能和分词效果,同时充分利用 C++ 的性能优势,在处理速度上有显著提升,更适合对性能要求较高的生产环境或 C++ 项目中进行中文分词处理。
cppjieba的特点和优势
功能完备:
-
支持精确模式、全模式、搜索引擎模式三种分词模式,与 Python 版 jieba 保持一致。
-
提供词性标注功能,可对分词结果进行词性标记(如名词、动词、形容词等)。
-
支持用户自定义词典,方便添加专业术语、新词等特殊词汇。
支持繁体中文分词。
高性能:
- 基于 C++ 实现,相比 Python 版 jieba 分词速度更快,内存占用更优。
- 采用双数组 trie 树(Double Array Trie)等高效数据结构,提升词典查询和匹配效率。
易用性:
- 提供简洁的 API 接口,便于在 C++ 项目中集成使用。
- 支持跨平台编译,可在 Windows、Linux、macOS 等系统上运行。
应用场景:
-
适用于需要高效中文分词的 C++ 项目,如搜索引擎、文本挖掘、自然语言处理系统等。
-
适合处理大规模中文文本数据,能有效提升系统的处理效率。使用时,通常需要先加载词典(包括主词典、停用词词典等),然后调用相应的分词接口进行处理。其核心分词逻辑与 Python 版 jieba 一致,确保了分词结果的一致性和准确性。
cppjieba安装
cppjieba 是一个开源项目,主要通过源码方式获取和集成到 C++ 项目中。以下是详细的下载安装及使用步骤:
1. 下载源码
cppjieba 的源码托管在 GitHub 上,可通过以下方式获取:
方法 1:使用 git 克隆仓库(推荐)
打开终端(Linux/macOS)或命令提示符(Windows),执行以下命令:
git clone https://github.com/yanyiwu/cppjieba.git
该命令会将源码下载到当前目录的 cppjieba 文件夹中。
方法 2:手动下载源码包
访问 GitHub 仓库地址:https://github.com/yanyiwu/cppjieba
点击右上角的 Code 按钮,选择 Download ZIP,下载后解压到本地目录。
2. 目录结构说明
下载后的 cppjieba 目录包含核心代码和必要资源,主要结构如下:
cppjieba/
├── include/ # 头文件目录(核心算法实现)
│ ├── jieba.hpp # 分词核心类
│ ├── dict_trie.hpp # 词典Trie树结构
│ └── ...
├── src/ # 源文件目录
│ ├── jieba.cpp
│ └── ...
├── dict/ # 内置词典(必须保留,分词依赖)
│ ├── jieba.dict.utf8 # 主词典
│ ├── hmm_model.utf8 # 隐马尔可夫模型词典
│ ├── user.dict.utf8 # 用户自定义词典(可修改)
│ └── stop_words.utf8 # 停用词词典
├── example/ # 示例代码
│ └── demo.cpp # 简单使用示例
└── CMakeLists.txt # CMake 配置文件(用于编译示例)
3. 集成到 C++ 项目中
cppjieba 不需要 “安装” 到系统目录,而是直接将源码和词典集成到你的项目中,步骤如下:
步骤 1:复制必要文件到项目
将 include/ 和 src/ 目录复制到你的项目源码目录(或通过编译选项指定路径)。
将 dict/ 目录复制到你的项目运行目录(确保程序能找到词典,路径可自定义)。
步骤 2:在代码中使用
在你的 C++ 代码中包含头文件,并初始化分词器,示例如下:
运行
#include "jieba.hpp"
#include <iostream>
#include <vector>using namespace cppjieba;
using namespace std;int main() {// 词典路径(根据实际存放位置修改)const string DICT_PATH = "./dict/jieba.dict.utf8";const string HMM_PATH = "./dict/hmm_model.utf8";const string USER_DICT_PATH = "./dict/user.dict.utf8";const string STOP_WORD_PATH = "./dict/stop_words.utf8";// 初始化分词器Jieba jieba(DICT_PATH, HMM_PATH, USER_DICT_PATH, STOP_WORD_PATH);// 待分词文本string text = "我爱自然语言处理";vector<string> words;// 精确模式分词jieba.Cut(text, words, false); // false 表示精确模式// 输出结果for (const auto& word : words) {cout << word << " ";}// 输出:我 爱 自然语言处理return 0;
}
4. 编译项目
编译时需要指定源码文件和头文件路径,以 g++ 为例:
# 假设你的代码文件为 main.cpp,且 cppjieba 的 src/ 和 include/ 在当前目录
g++ -std=c++11 main.cpp ./src/*.cpp -I./include -o jieba_demo
-I./include:指定头文件目录。
./src/*.cpp:编译 cppjieba 的源文件。
-std=c++11:cppjieba 依赖 C++11 标准,需指定。
5. 运行测试
编译成功后,执行生成的可执行文件:
./jieba_demo # Linux/macOS# 或
jieba_demo.exe # Windows
如果输出分词结果,则说明集成成功。
注意事项
- 词典路径:必须保证程序能访问到 dict/ 目录下的词典文件,路径错误会导致分词失败。
- 编码问题:cppjieba 处理的是 UTF-8 编码文本,确保输入文本编码正确。
- 自定义词典:可修改 user.dict.utf8 添加新词(格式:词语 词频 词性,如 cppjieba 1000 n)。
- 跨平台:支持 Linux、macOS、Windows,Windows 下可使用 MinGW 或 MSVC 编译。
通过以上步骤,即可在你的 C++ 项目中使用 cppjieba 进行中文分词。如果需要更复杂的功能(如词性标注),可参考 example/ 目录下的示例代码或者查询AI。
补充:
除了直接从 GitHub 下载源码,cppjieba 还有一些其他获取和集成途径,适用于不同场景,以下是详细说明:
1. 通过国内镜像仓库获取(解决 GitHub 访问慢问题)
如果访问 GitHub 速度较慢,可以使用国内代码托管平台的镜像仓库,例如 Gitee(码云) 上的同步仓库:
方法:克隆 Gitee 镜像
git clone https://gitee.com/mirrors/cppjieba.git
镜像仓库与 GitHub 原仓库内容同步,使用方式和后续集成步骤与 GitHub 源码完全一致。
2. 通过 C++ 包管理器安装(推荐用于现代项目)
对于使用包管理器的 C++ 项目,可以通过以下工具自动获取和集成 cppjieba:
(1)使用 vcpkg(微软开源的 C++ 包管理器)
安装 vcpkg(若未安装):
# 克隆 vcpkg 仓库
git clone https://github.com/microsoft/vcpkg# 编译安装(Windows 用 powershell,Linux/macOS 用 bash)
.\vcpkg\bootstrap-vcpkg.bat # Windows
./vcpkg/bootstrap-vcpkg.sh # Linux/macOS
安装 cppjieba:
# 全局安装(需管理员权限)
vcpkg install cppjieba:x64-windows # Windows
vcpkg install cppjieba:x64-linux # Linux
vcpkg install cppjieba:x64-osx # macOS# 或仅为本项目安装(需在项目中配置 vcpkg)
vcpkg install cppjieba
在项目中使用:安装后,通过 CMake 配置时指定 vcpkg 工具链,即可直接 find_package 引入:
cmake_minimum_required(VERSION 3.10)
project(my_project)
find_package(cppjieba REQUIRED)
add_executable(my_app main.cpp)
target_link_libraries(my_app cppjieba::cppjieba)
索引模块建立
概述
该模块采用正排索引和倒排索引相结合的方式,实现了高效的文档检索功能。
核心数据结构
1. 正排索引结构(ForwordElem)
typedef struct ForwordElem {std::string title_; // 文档标题std::string content_; // 文档内容std::string url_; // 文档URLsize_t doc_id_ = 0; // 文档唯一标识void Set(std::string title, std::string content, std::string url, size_t doc_id);
} Forword_t;
作用:通过文档ID快速定位完整的文档信息,存储原始文档数据。
2. 倒排索引结构(InvertedElem)
typedef struct InvertedElem {size_t doc_id_ = 0; // 文档IDstd::string word_; // 关键词size_t weight_ = 0; // 权重值void Set(size_t doc_id, std::string word, size_t weight);
} Inverted_t;typedef std::vector<Inverted_t> InvertedList; // 倒排拉链
作用:建立关键词到文档的映射,支持快速的关键词检索。
索引建立流程
整体架构
class Inedx : public NonCopyable {
private:std::vector<Forword_t> Forword_Index_; // 正排索引std::unordered_map<std::string, InvertedList> Inverted_Index_; // 倒排索引
};
步骤1:主索引构建入口(BulidIndex)
bool BulidIndex() {// 1. 打开目标文件std::ifstream in(Tragetfile, std::ios::binary | std::ios::in);// 2. 逐行读取文档数据std::string singlefile;while (std::getline(in, singlefile)) {// 3. 构建正排索引bool b = BuildForwordIndex(singlefile);if(!b) continue;// 4. 构建倒排索引(基于刚建立的正排索引项)b = BuildInvertedIndex(Forword_Index_.size() - 1);if(!b) continue;}in.close();return true;
}
步骤2:正排索引构建(BuildForwordIndex)
bool BuildForwordIndex(std::string& singlefile) {// 1. 文档分词处理std::vector<std::string> sepfile;bool b = ns_util::JiebaUtile::CutDoc(singlefile, sepfile);// 2. 验证分词结果(标题、内容、URL三部分)if(sepfile.size() != 3) return false;// 3. 创建正排索引项Forword_t ft;ft.Set(std::move(sepfile[0]), // 标题std::move(sepfile[1]), // 内容 std::move(sepfile[2]), // URLForword_Index_.size()); // 文档ID(当前索引大小)// 4. 加入正排索引表Forword_Index_.push_back(std::move(ft));return true;
}
步骤3:倒排索引构建(BuildInvertedIndex)
cpp
bool BuildInvertedIndex(size_t findex) {// 1. 获取对应的正排索引项Forword_t ft = Forword_Index_[findex];// 2. 词频统计结构std::unordered_map<std::string, DocCount_t> map_s;// 3. 标题分词与词频统计std::vector<std::string> titlesegmentation;ns_util::JiebaUtile::CutPhrase(ft.title_, titlesegmentation);for(auto& s : titlesegmentation) {boost::to_lower(s); // 统一转换为小写map_s[s].title_cnt_++;}// 4. 内容分词与词频统计 std::vector<std::string> contentsegmentation;ns_util::JiebaUtile::CutDoc(ft.content_, contentsegmentation);for(auto& s : contentsegmentation) {boost::to_lower(s); // 统一转换为小写map_s[s].content_cnt_++;}// 5. 权重计算与倒排项创建const int X = 10; // 标题权重系数const int Y = 1; // 内容权重系数for(auto& p : map_s) {Inverted_t it;// 计算权重:标题词频×10 + 内容词频×1it.Set(findex, p.first, p.second.title_cnt_ * X + p.second.content_cnt_);// 加入倒排索引表InvertedList& list = Inverted_Index_[p.first];list.push_back(std::move(it));}return true;
}
查询接口
1. 正排查询(按文档ID)
Forword_t* QueryById(size_t id) {if(id <= 0 || id >= Forword_Index_.size()) return nullptr;return &Forword_Index_[id];
}
2. 倒排查询(按关键词)
InvertedList* QueryByWord(std::string word) {auto it = Inverted_Index_.find(word);if(it == Inverted_Index_.end()) return nullptr;return &it->second;
}
技术特点
1. 权重计算策略
标题权重:词频 × 10(更高优先级)
内容权重:词频 × 1
体现标题关键词比内容关键词更重要
2. 文本预处理
使用结巴分词进行中文分词
统一转换为小写,避免大小写敏感问题
支持中英文混合处理
3. 内存优化
使用std::move避免不必要的拷贝
采用哈希表实现快速查找
向量存储保证内存连续性
完整代码
#pragma once
#include "Log.hpp"
#include "Util.hpp"
#include "common.h"
#include <boost/algorithm/string/case_conv.hpp>
#include <cstddef>
#include <fstream>
#include <string>
#include <unordered_map>
#include <utility>
#include <vector>namespace ns_index
{//正排索引typedef struct ForwordElem{std::string title_;std::string content_;std::string url_;size_t doc_id_ = 0;void Set(std::string title, std::string content, std::string url, size_t doc_id){title_ = title;content_ = content;url_ = url;doc_id_ = doc_id;}}Forword_t;typedef struct InvertedElem{size_t doc_id_ = 0;std::string word_;size_t weight_ = 0;void Set(size_t doc_id, std::string word, size_t weight){doc_id_ = doc_id;word_ = word;weight_ = weight;}}Inverted_t;typedef std::vector<Inverted_t> InvertedList;class Inedx : public NonCopyable{public:Inedx() = default;Forword_t* QueryById(size_t id){if(id <= 0 || id >= Forword_Index_.size()){Log(LogModule::DEBUG) << "id invalid!";return nullptr;}return &Forword_Index_[id];}InvertedList* QueryByWord(std::string word){auto it = Inverted_Index_.find(word);if(it == Inverted_Index_.end()){Log(LogModule::DEBUG) << word << " find fail!";return nullptr;}return &it->second;}bool BulidIndex(){std::ifstream in(Tragetfile, std::ios::binary | std::ios::in);if(!in.is_open()){Log(LogModule::ERROR) << "Targetfile open fail!BulidIndex fail!";return false;}std::string singlefile;while (std::getline(in, singlefile)){bool b = BuildForwordIndex(singlefile);if(!b){Log(LogModule::DEBUG) << "Build Forword Index Error!";continue;}b = BuildInvertedIndex(Forword_Index_.size() - 1);if(!b){Log(LogModule::DEBUG) << "Build Inverted Index Error!";continue;}}in.close();return true;}~Inedx() = default;private:typedef struct DocCount{size_t title_cnt_ = 0;size_t content_cnt_ = 0;}DocCount_t;bool BuildForwordIndex(std::string& singlefile){std::vector<std::string> sepfile;bool b = ns_util::JiebaUtile::CutDoc(singlefile, sepfile);if(!b)return false;if(sepfile.size() != 3){Log(LogModule::DEBUG) << "Segmentation fail!";return false;}Forword_t ft;ft.Set(std::move(sepfile[0]), std::move(sepfile[1]), std::move(sepfile[2]), Forword_Index_.size());Forword_Index_.push_back(std::move(ft));return true;}bool BuildInvertedIndex(size_t findex){Forword_t ft = Forword_Index_[findex];std::unordered_map<std::string, DocCount_t> map_s;std::vector<std::string> titlesegmentation;ns_util::JiebaUtile::CutPhrase(ft.title_, titlesegmentation);for(auto& s : titlesegmentation){boost::to_lower(s);map_s[s].title_cnt_++;}std::vector<std::string> contentsegmentation;ns_util::JiebaUtile::CutDoc(ft.content_, contentsegmentation);for(auto& s : contentsegmentation){boost::to_lower(s);map_s[s].content_cnt_++;}const int X = 10;const int Y = 1;for(auto& p : map_s){Inverted_t it;it.Set(findex, p.first, p.second.title_cnt_ * X + p.second.content_cnt_);InvertedList& list = Inverted_Index_[p.first];list.push_back(std::move(it));}return true;}private:std::vector<Forword_t> Forword_Index_;std::unordered_map<std::string, InvertedList> Inverted_Index_;};
};
其它更新模块
common.h
#pragma once
#include <iostream>
#include <string>
#include <vector>
#include <cstddef>
#include "Log.hpp"
using std::cout;
using std::endl;
using namespace LogModule;const std::string Boost_Url_Head = "https://www.boost.org/doc/libs/1_89_0/doc/html";const std::string Orignaldir = "../Data/html";
const std::string Tragetfile = "../Data/output.txt";const std::string Output_sep = "\3";
const std::string Line_sep = "\n";// 定义词典路径(根据实际路径修改)
const std::string DICT_PATH = "dict/jieba.dict.utf8";
const std::string HMM_PATH = "dict/hmm_model.utf8";
const std::string USER_DICT_PATH = "dict/user.dict.utf8";
const std::string IDF_PATH = "dict/idf.utf8";
const std::string STOP_WORD_PATH = "dict/stop_words.utf8";// 不可复制基类
class NonCopyable {
protected:// 允许派生类构造和析构NonCopyable() = default;~NonCopyable() = default;// 禁止移动操作(可选,根据需求决定)// NonCopyable(NonCopyable&&) = delete;// NonCopyable& operator=(NonCopyable&&) = delete;private:// 禁止拷贝构造和拷贝赋值NonCopyable(const NonCopyable&) = delete;NonCopyable& operator=(const NonCopyable&) = delete;
};
util.hpp
#pragma once
#include "Log.hpp"
#include "common.h"
#include "cppjieba/Jieba.hpp"
#include <boost/algorithm/string/classification.hpp>
#include <boost/algorithm/string/split.hpp>
#include <fstream>
#include <sstream>
#include <string>
#include <vector>
namespace ns_util
{class FileUtil : public NonCopyable{public:static bool ReadFile(std::string path, std::string* out){std::fstream in(path, std::ios::binary | std::ios::in);if(!in.is_open()){Log(LogModule::DEBUG) << "file-" << path << "open fail!";return false;}std::stringstream ss;ss << in.rdbuf();*out = ss.str();in.close();return true;}};class JiebaUtile : public NonCopyable{public:static cppjieba::Jieba* GetInstace(){static cppjieba::Jieba jieba_(DICT_PATH, HMM_PATH, USER_DICT_PATH, IDF_PATH, STOP_WORD_PATH);return &jieba_;}static bool CutPhrase(std::string& src, std::vector<std::string>& out){try{GetInstace()->CutForSearch(src, out, true);}catch (const std::exception& e){Log(LogModule::ERROR) << "CutString Error!" << e.what();return false;}catch (...){Log(ERROR) << "Unknow Error!";return false;}return true;}static bool CutDoc(std::string& filestr, std::vector<std::string>& out){try{boost::split(out, filestr, boost::is_any_of("\3"));}catch (const std::exception& e){Log(LogModule::ERROR) << "std Error-" << e.what();return false;} catch(...){Log(LogModule::ERROR) << "UnKnown Error!";return false;}return true;}private:JiebaUtile() = default;~JiebaUtile() = default;};};
main.cc测试
#include "Log.hpp"
#include "common.h"
#include "Parser.h"
#include "index.hpp"const bool INIT = false;int main()
{if(INIT){Parser parser(Orignaldir, Tragetfile);parser.Init();}ns_index::Inedx index;index.BulidIndex();auto a = index.QueryByWord("split");if(a == nullptr){Log(LogModule::DEBUG) << "find fail!";return 0;}for(auto e : *a){cout << "id:" << e.doc_id_ << " weight: " << e.weight_ << endl;}auto b = index.QueryById(764);cout << "id:764--title-- " << b->title_ << endl;return 0;
}