一、搭建索引结构代码
1.正倒排索引
①正排索引
功能:根据文档id查找文档
②倒排索引
功能:根据关键字,查找文档id
2.正倒排索引结构体
①正排索引结构体
以一篇文档为单位,有多少个文档就有多少个DocInfo
cpp
struct DocInfo
{
std::string title;//标题
std::string content;//内容
std::string url;//官网文档url
uint64_t doc_id;//文档id
};
②倒排索引结构体
以一篇文档中的单词数为单位,doc_id存的是文档id,word存的是一篇文档中的一种单词,weight存的是单词权重
cpp
struct InvertElem
{
std::string word;
uint64_t doc_id;
int weight;//权重
};
比如文档1中存在10个quit单词,该结构体中
word:quit
doc_id:1
weight:10
3.倒排拉链
存储所有文档中的同一个单词的InvertElem,里边记录着这个单词所处id和权重,方便根据id找到正排索引并对文档进行展示
cpp
typedef std::vector<InvertElem> InvertList;
4.index类
①成员
Ⅰ正排索引:存储所有正排索引内容
cpp
std::vector<DocInfo> forward_index;
Ⅱ倒排索引:按单词分类,使用unordered_map,first存单词,second存倒排拉链
cpp
std::unordered_map<std::string, InvertList> inverted_index;
②必要功能
Ⅰ获取正派索引
cpp
DocInfo* GetForwordIndex(const uint64_t id)
{
return nullptr;
}
Ⅱ获取倒排索引
根据搜索的单词进行获取
cpp
InvertList* GetInvertList(std::string& word)
{
return nullptr;
}
Ⅲ创建正倒排索引
根据去标签后的文档进行创建
cpp
bool BuildIndex(std::string& input)
{
return true;
}
二、获取正倒排索引
1.获取正排索引
cpp
DocInfo* GetForwordIndex(const uint64_t id)
{
//forward里有多少元素,id最大就是多少
if(id >= forward_index.size())
{
std::cerr << "id error" << std::endl;
return nullptr;
}
return &forward_index[id];
}
2.获取倒排拉链
倒排拉链里的元素才是我们需要的,所以返回倒排拉链
思路:先在inverted_index中找到单词,找不到返回空,找到返回倒排拉来你
cpp
InvertList* GetInvertList(std::string& word)
{
auto pos = inverted_index.find(word);
if(pos == inverted_index.end())
{
std::cerr << word << "not found !" << std::endl;
return nullptr;
}
return &(pos->second);
}
三、创建索引
1.思路
①读取文档内容
②按行读取文档(一行就是一个文档)
③建立正派索引
④建立倒排索引
2.代码编写
cpp
bool BuildIndex(std::string& input)
{
//1.读取文档内容
std::ifstream in(input, std::ios::in | std::ios::binary);
if(!in.is_open())
{
std::cerr << input << " open error" << std::endl;
return false;
}
//2.按行读取文档
std::string line;
while(std::getline(in,line))
{
//3.建立正派索引
DocInfo *doc = BuildForwardIndex(line);
if(doc == nullptr)
{
continue;
}
4.根据正派索引建立倒排索引
BuildInvertedIndex(*doc);
}
return true;
}
四、建立正排索引
1.思路
①切割字符串
②将分割后的结果写入doc
③将doc推入forward_index
2.分割字符串
分割字符串有很多方法,我们可以按照c++的方法先进行find分隔符,然后再用substr进行切割;也可以使用c语言中的strtok,但是这些都太麻烦了,今天我们引入一个boost库中的分割字符串的接口,他就是split()
①头文件
cpp
#include<boost/algorithm/string.hpp>
②使用示例
cpp
#include<boost/algorithm/string.hpp>
#include<iostream>
#include<vector>
int main()
{
std::string tmp = "aaa,bbb,ccc";
std::vector<std::string> r;
boost::split(r,tmp,boost::is_any_of(","));
for(auto e : r)
{
std::cout<<e::std::endl;
}
}
将tmp以','为分隔符进行切割保存至r中,非常的简单

③补充
cpp
boost::split(r,tmp,boost::is_any_of(","),boost::token_compress_on)
token_compress_on的功能是把多个分隔符归为一个,不写默认是关闭的
例如 tmp = "aaa,,,bbb"

3.编写切割字符串代码
在Util.hpp中实现,公用
cpp
class StringUtil
{
public:
static void CutString(std::string line, std::vector<std::string>* result, const std::string sep)
{
boost::split(*result,line,boost::is_any_of("sep"),boost::token_compress_on);
}
};
4.建立正派索引代码实现
cpp
DocInfo* BuildForwardIndex(const std::string &line)
{
//1.分割字符串
const std::string sep = "\3";
std::vector<std::string> result;
Util::StringUtil::CutString(line,&result,sep);
if(result.size()!=3)
return nullptr;
//2.将分割后的结果写入doc
DocInfo doc;
doc.title = result[0];
doc.content = result[1];
doc.url = result[2];
doc.doc_id = forward_index.size();
//3.将doc存入forward_index
forward_index.push_back(std::move(doc));
//返回最新插入的元素
return &forward_index.back();
}
五、引入jieba库
我们都知道构建倒排索引必不可少的一步就是对字符串进行分词,所以在构建倒排索引之前,我们要引入一个jieba库,jieba库的功能是对字符串进行分词
1.jieba库的安装
②搜索cppjieba
③克隆复制
④打开云服务器,粘贴,自动下载完成
2.jieba库的使用
①jieba库的内容
cpp
zmz@hcss-ecs-201c:~/study/Boost/test/cppjieba$ ls
CHANGELOG.md CMakeLists.txt deps dict include LICENSE README.md test
其中dict存词库 include存头文件
bash
cppjieba
├── deps
│ └── limonp
├── dict
│ ├── hmm_model.utf8
│ ├── idf.utf8
│ ├── jieba.dict.utf8
│ ├── pos_dict
│ │ ├── char_state_tab.utf8
│ │ ├── prob_emit.utf8
│ │ ├── prob_start.utf8
│ │ └── prob_trans.utf8
├── include
└── cppjieba
├── Jieba.hpp
└── 其他库
还有好多不重要的东西
②要让代码看到词库和头文件,所以要建立软连接
bash
zmz@hcss-ecs-201c:~/study/Boost/test$ ln -s cppjieba/dict dict
zmz@hcss-ecs-201c:~/study/Boost/test$ ln -s cppjieba/include inc
zmz@hcss-ecs-201c:~/study/Boost/test$ ll
drwxrwxr-x 8 zmz zmz 4096 Nov 23 10:11 cppjieba/
lrwxrwxrwx 1 zmz zmz 13 Nov 23 10:15 dict -> cppjieba/dict/
lrwxrwxrwx 1 zmz zmz 16 Nov 23 10:19 inc -> cppjieba/include/
-rw-rw-r-- 1 zmz zmz 9146 Nov 23 10:14 jieba_test.cpp
③重要细节:将deps中的limonp拷贝到include中,否则无法运行
bash
zmz@hcss-ecs-201c:~/study/Boost/test/cppjieba$ cp deps/limonp include/cppjieba/ -rf
3.使用案例
步骤:
①cpp::jieba jieba(...)传入路径,进行初始化,传入的路径就是词库的路径,方便使用过程中进行分词
②使用jieba.CutForSerch继续分词
③使用limonp::Join进行打印
limonp::Join的功能:将容器(如 vector<string>)中的所有元素,用指定的分隔符连接成一个完整的字符串。
cpp
#include "inc/cppjieba/Jieba.hpp"
#include <iostream>
#include <string>
#include <vector>
using namespace std;
const char* const DICT_PATH = "./dict/jieba.dict.utf8";
const char* const HMM_PATH = "./dict/hmm_model.utf8";
const char* const USER_DICT_PATH = "./dict/user.dict.utf8";
const char* const IDF_PATH = "./dict/idf.utf8";
const char* const STOP_WORD_PATH = "./dict/stop_words.utf8";
int main()
{
//传入路径,进行初始化
cppjieba::Jieba jieba(DICT_PATH,
HMM_PATH,
USER_DICT_PATH,
IDF_PATH,
STOP_WORD_PATH);
vector<string> words;
string s;
s = "小明同学毕业于中国科学院计算所,后在日本京都大学深造";
cout << s << endl;
cout << "[demo] CutForSerch" << endl;
jieba.CutForSearch(s, words);
cout << limonp::Join(words.begin(), words.end(), "/") << endl;
return EXIT_SUCCESS;
}
结果
bash
zmz@hcss-ecs-201c:~/study/Boost/test$ ./a.out
小明同学毕业于中国科学院计算所,后在日本京都大学深造
[demo] CutForSerch
小明/同学/毕业/于/中国/科学/学院/科学院/中国科学院/计算/计算所/,/后/在/日本/京都/大学/日本京都大学/深造
六、建立倒排索引
1.思路
①建立一个map表来保存单词和词频的映射关系
②对标题和内容进行分词,并统计分词后的每个单词出现的次数
③遍历map表,间内容保存到一个临时的InvertElem表,然后推入倒排索引表中
2.计数
对一个单词在标题和内容中出现的次数进行存储
cpp
struct word_cnt{
int title_count;
int content_count;
word_cnt():title_count(0),content_count(0)
{}
};
3.分词模块
在Util.hpp中实现,公用
cpp
const char* const DICT_PATH = "./dict/jieba.dict.utf8";
const char* const HMM_PATH = "./dict/hmm_model.utf8";
const char* const USER_DICT_PATH = "./dict/user.dict.utf8";
const char* const IDF_PATH = "./dict/idf.utf8";
const char* const STOP_WORD_PATH = "./dict/stop_words.utf8";
class JiebaUtil
{
private:
static cppjieba::Jieba jieba;
public:
static void CutString(const std::string& src, std::vector<std::string> *out)
{
jieba.CutForSearch(src, *out);
}
};
cppjieba::Jieba JiebaUtil::jieba(DICT_PATH,
HMM_PATH,
USER_DICT_PATH,
IDF_PATH,
STOP_WORD_PATH);
4.函数实现
cpp
bool BuildInvertedIndex(const DocInfo& doc)
{
//计数
struct word_cnt{
int title_count;
int content_count;
word_cnt():title_count(0),content_count(0)
{}
};
//保存单词的映射
std::unordered_map<std::string, word_cnt> word_map;
//分词
std::vector<std::string> title_words;
Util::JiebaUtil::CutString(doc.title, &title_words);
std::vector<std::string> content_words;
Util::JiebaUtil::CutString(doc.content, &content_words);
//进行词频统计
for(auto s : title_words)
{
boost::to_lower(s);
word_map[s].title_count++;
}
for(auto s : content_words)
{
boost::to_lower(s);
word_map[s].content_count++;
}
#define X 10
#define Y 1
for(auto &word_pair : word_map)
{
InvertElem item;
item.doc_id = doc.doc_id;
item.word = word_pair.first;
item.weight = word_pair.second.title_count*X + word_pair.second.content_count*Y;
InvertList &inverted_list = inverted_index[word_pair.first];
inverted_list.push_back(item);
}
return true;
}
七、总结
- 正排索引以文档 ID 为键,存储每个文档的完整信息(标题、内容、URL、文档 ID),支持通过 ID 快速获取文档详情;
- 倒排索引以分词后的单词为键,关联包含该单词的所有文档记录(
InvertList),每条记录包含文档 ID 和权重(标题词频 ×10 + 内容词频 ×1,突出标题关键词重要性); - 构建索引时,先读取格式化文档并分割字段生成正排记录,再通过中文分词(
JiebaUtil)对标题和内容分词、统计词频,最终生成倒排索引; - 提供两类查询接口:按文档 ID 查询正排信息,按关键词查询相关文档的倒排拉链(便于按权重排序展示结果)。
八、源代码展示
cpp
#pragma once
#include <vector>
#include <string>
#include <iostream>
#include <unordered_map>
#include "Util.hpp"
namespace ns_index{
//正排索引结构体
struct DocInfo
{
std::string title;//标题
std::string content;//内容
std::string url;//官网文档url
uint64_t doc_id;//文档id
};
//倒排索引
struct InvertElem
{
std::string word;
uint64_t doc_id;
int weight;//权重
};
//倒排拉链:存储所有文档中的同一个单词的InvertElem,里边记录这这个单词所处id和权重,方便根据权重展示
typedef std::vector<InvertElem> InvertList;
class Index
{
private:
//正排索引内容
std::vector<DocInfo> forward_index;
//倒排索引内容:first存单词,second存有这个单词的文档的InvertElem结构体
std::unordered_map<std::string, InvertList> inverted_index;
public:
Index(){}
~Index(){}
public:
//根据doc_id获取正排索引
DocInfo* GetForwordIndex(const uint64_t id)
{
//forward里有多少元素,id最大就是多少
if(id >= forward_index.size())
{
std::cerr << "id error" << std::endl;
return nullptr;
}
return &forward_index[id];
}
//根据单词获取倒排拉链
InvertList* GetInvertList(std::string& word)
{
auto pos = inverted_index.find(word);
if(pos == inverted_index.end())
{
std::cerr << word << "not found !" << std::endl;
return nullptr;
}
return &(pos->second);
}
//根据格式化,去标签后的文档构建索引
bool BuildIndex(std::string& input)
{
//1.读取文档内容
std::ifstream in(input, std::ios::in | std::ios::binary);
if(!in.is_open())
{
std::cerr << input << " open error" << std::endl;
return false;
}
std::string line;
while(std::getline(in,line))
{
DocInfo *doc = BuildForwardIndex(line);
if(doc == nullptr)
{
continue;
}
BuildInvertedIndex(*doc);
}
return true;
}
private:
DocInfo* BuildForwardIndex(const std::string &line)
{
//1.分割字符串
const std::string sep = "\3";
std::vector<std::string> result;
Util::StringUtil::CutString(line,&result,sep);
if(result.size()!=3)
return nullptr;
//2.将分割后的结果写入doc
DocInfo doc;
doc.title = result[0];
doc.content = result[1];
doc.url = result[2];
doc.doc_id = forward_index.size();
//3.将doc存入forward_index
forward_index.push_back(std::move(doc));
//返回最新插入的元素
return &forward_index.back();
}
bool BuildInvertedIndex(const DocInfo& doc)
{
//计数
struct word_cnt{
int title_count;
int content_count;
word_cnt():title_count(0),content_count(0)
{}
};
//保存单词的映射
std::unordered_map<std::string, word_cnt> word_map;
//分词
std::vector<std::string> title_words;
Util::JiebaUtil::CutString(doc.title, &title_words);
std::vector<std::string> content_words;
Util::JiebaUtil::CutString(doc.content, &content_words);
//进行词频统计
for(auto s : title_words)
{
boost::to_lower(s);
word_map[s].title_count++;
}
for(auto s : content_words)
{
boost::to_lower(s);
word_map[s].content_count++;
}
#define X 10
#define Y 1
for(auto &word_pair : word_map)
{
InvertElem item;
item.doc_id = doc.doc_id;
item.word = word_pair.first;
item.weight = word_pair.second.title_count*X + word_pair.second.content_count*Y;
InvertList &inverted_list = inverted_index[word_pair.first];
inverted_list.push_back(item);
}
return true;
}
};
}