C++之基于正倒排索引的Boost搜索引擎项目正倒排索引部分代码及详解

C++之基于正倒排索引的Boost搜索引擎项目正倒排索引部分代码及详解

首先我们通过前面的文章知道正倒排索引在搜索引擎项目中是非常重要的,正排索引与倒排索引协同工作,实现我们对内容的搜索。

这边要说明一点,正排索引是把文档内容放到文档ID里面,同时倒排索引根据文档来映射文档ID。搜索引擎在对文档进行处理和索引构建时,会先创建正排索引,然后基于正排索引进一步生成倒排索引。当用户输入查询关键词时,搜索引擎会利用倒排索引快速定位包含该关键词的文档,再结合正排索引等其他信息进行结果展示。

PS:他们两个是提前创建好的,并不是在外面进行搜索的时候临时创建的。

1.正倒排索引的结构

1.1 正排索引

这个是正排索引所需要的,就是文档的一些内容和他的ID。

//这个是正排索引中需要用到的 typedef struct DocInfo{ std::string title;//文档的标题 std::string content;//文档的内容 std::string url;//文档的url int doc_id;//文档的ID }DocInfo1;

1.2 倒排索引

这个是倒排索引所需要的,文档的ID和他对应的关键字还有他的权重。至于下面那个InvertedList,一般称呼它为倒排拉链,因为一般来说一个关键字不会只有一个对应的文档,比如说关键字A,他可能出现在10个文档里面,那这个A的拉链里面就有这10个文档。

 //倒排索引中需要用到的 struct InvertedElem{ int doc_id;//文档的ID std::string word;//文档的word int weight;//这个文档的权重 }; typedef std::vector<InvertedElem> InvertedList;

2. 正倒排序部分class的private部分

2.1 正倒排序的准备工作

正排索引的forward_index就是通过创建vector,因为vector的下表就是文档的ID,所以使用起来会很方便,同时也减少了我们的代码工作。

然后那个inverted_index就是给倒排索引准备的,因为是倒排拉链的缘故,所以要使用哈希,然后一个关键字映射对个倒排文档。

 private: //正排索引使用vector,因为他的下标就是文档的id std::vector<DocInfo1> forward_index; //使用哈希来进行映射 std::unordered_map<std::string,InvertedList> inverted_index; 

2.2 正倒排序部分的单例模式

在这边的代码中我使用的是单例模式,好处如下:

  1. 减少资源浪费:正排索引相关的排序组件,可能需要加载大量文档元数据到内存,或持有数据库连接、缓存连接等重型资源。单例模式能保证整个程序运行期间,只存在这一个组件实例,避免反复创建或者销毁实例导致的内存占用过高、IO 开销增加问题。
  2. 确保全局逻辑统一:排序逻辑需要全局一致 —— 如果存在多个实例,可能因初始化参数不同,导致对同一批文档输出不同的排序结果,破坏搜索结果的稳定性。单例模式能强制所有搜索请求复用同一套逻辑,避免结果混乱。
  3. 简化资源管理与调用:单例模式的实例全局可访问,搜索引擎中其他模块需要调用排序功能时,无需单独创建实例,直接复用同一个单例即可,降低了模块间的耦合度,也减少了代码冗余。

同时因为是单例模式,所以我们需要把拷贝和运算符=给禁用掉。并且我们还需要对其进行上锁的操作,这样可以防止多线程并发操作创建多个实例。

PS:这边之所以需要两个 if(instance==nullptr),是因为在进入锁前和后度需要判断,主要是因为线程太快了,不这一写容易引发线程安全问题。

private:////////////////////////// Index(){}; Index(const Index&)=delete; Index& operator=(const Index&)=delete; //这边的话就是禁用拷贝构造函数和运算符重载,因为是单例模式 static Index* instance; static std::mutex log; public: ~Index();//这边框起来的这段代码是使用单例模式的方式来进行编程, public: static Index* Getinstance() { if(instance==nullptr) { log.lock();//上锁 if(instance==nullptr) { instance=new Index(); } log.unlock();//解锁 } return instance; }///////////////////////////////////////

3. 去标签后的文档来构建正倒排索引

3.1 创建正排索引代码

这边的话就是通过Split函数来进行分词,(我们后面会讲到一份代码usuallytool,这份代码里面是一些常用函数。)然后创建临时变量doc,先把result里面的结果给到doc,接着doc再给forward_index。

PS:有人看到这里可能会疑惑为什么在这里results和doc不可以只用一个呢?这是因为这个Split函数在我设计的时候里面的类型是string,因为是通用工具,所以Split的类型不可能是DocInfo1 。

 DocInfo1* BuildForwardIndex(const std::string& line) { std::vector<std::string> results; ns_util::StringUtil::Split(line,&results,"\3");//这边就是进行字符窜切割,把title,content,url分开 if(results.size()!=3) return nullptr; DocInfo1 doc;//创建一个临时变量 doc.title=results[0]; doc.content=results[1]; doc.url=results[2]; doc.doc_id=forward_index.size(); //这上面表示把切分好的内容分给doc //注意,这个id就是下标的个数,所以我们这边就直接所以size()就可以了 //然后我们把处理好的docpush进forward_index里面 forward_index.push_back(doc); return &forward_index.back(); }

3.2 创建倒排序索引代码

这份代码由以下四步组成:

  • 第一步:分词(标题→title_words,正文→content_words)。
  • 第二步:词频统计(区分标题 / 正文,存入 word_map)。
  • 第三步:权重计算(基于预设规则)。
  • 第四步:填充倒排索引(更新 inverted_index)。

首先我们创建一个结构体,用来存放title和content里面的关键字的出现次数,接着使用word_map来统计对应词的出现频率。然后使用usuallytool里面的CutString函数把doc里面的title和content部分取出来,交给title_words和content_words。那个to_lower用来把关键字全部小写化,因为不能说把大小写区分成两个字。

然后创建一个临时变量item,然后把ID,word和对应的权重交给item,接着再交给inverted_index。

PS:inverted_index的本质是倒排拉链,它的value是一个数组,里面存储倒排索引的结构体,所以最后一步可以这么写

bool BuildInvertedIndex(const DocInfo1& doc) { struct word_cnt{ int title_cnt;//title出现的次数 int content_cnt;//content出现的次数 word_cnt() :title_cnt(0) ,content_cnt(0) {} };//这边的话就是先创建一个结构体,然后用这个结构体来存储词频 std::unordered_map<std::string,word_cnt> word_map; //这边就是通过KV的结构来存储 std::vector<std::string> title_words; ns_util::JiebaUsutl::CutString(doc.title,&title_words); for(auto& tw:title_words) { boost::to_lower(tw); word_map[tw].title_cnt++; } //这三行代码就是先创建一个存储分好后的词的容器,接着对其进行计数 std::vector<std::string> content_words; ns_util::JiebaUsutl::CutString(doc.content,&content_words); for(auto& cw:content_words) { boost::to_lower(cw); word_map[cw].content_cnt++; } //这三行代码就是先创建一个存储分好后的词的容器,接着对其进行计数 #define X 10 #define Y 1 for(auto& word_pair:word_map) { InvertedElem item;//InvertedElem这个类是专门为了倒排创建的,里面就只有文档的ID,权重和word item.doc_id=doc.doc_id;//这里就是把分好的文档的ID给item item.word=word_pair.first;//这是是把分好的那个词给item item.weight=X*word_pair.second.title_cnt+Y*word_pair.second.content_cnt; //这边就是模拟进行权重的计算 inverted_index[word_pair.first].push_back(item); //这个inverted_index一开始是空的,然后我们不断的push_back就可以人里面存在key和value } return true; } };

3. 同时调用上面两个函数创建正倒排索引

这边的话就是先以二进制的方式读取input(这边这个input不是用户输入的部分,而是相当于我们的搜索引擎的原始文档)。首先我们使用getline函数,把内容读取到line里面,接着调用BuildInvertedIndex函数和BuildForwardIndex函数来实现构建索引。

至于下面那个count的话是一种简单的日志,这个在后面也会有介绍。

bool BuildIndex(const std::string& input) { std::ifstream in(input,std::ios::in | std::ios::binary); if(!in.is_open())//如果文件打开失败就返回 { std::cout<<input<<"open error"<<std::endl; return false; } int count = 0; std::string line;//设置一个临时变量用来接收去标签后的文档 while(std::getline(in,line)) { //这边先建立正排索引,然后通过if条件判断来确定是否创建成功 DocInfo1* doc=BuildForwardIndex(line); if(doc==nullptr) { std::cout<<"BuildIndex error"<<std::endl; continue; } BuildInvertedIndex(*doc); count++; if(count%50==0) LOG1(NORMAL,"索引建立到:" + std::to_string(count)); } return true; }

4. 获取正倒排索引

4.1 获取正排索引

首先防御性编程,判断一下doc_id与forward_index.size()的大小,没问题的话就返回这个forward_index下标对应的文档内容。

//正排索引,通过id来找到文档内容 DocInfo1* GetForwardIndex(uint64_t doc_id) { if(doc_id>=forward_index.size()) { std::cout<<"doc_id out range, error!"<<std::endl; return nullptr; } return &forward_index[doc_id]; }

4.2 获取倒排索引

倒排比正排稍微麻烦一点点,因为我们要查找这个关键字存在不存在,存在的话就直接返回该关键词对应的倒排拉链的地址。

//通过word(也就是我们的关键字)来获取倒排拉链 //倒排拉链就是说对应的那一串文档的ID InvertedList* GetInvertedList(const std::string& word) { auto iter=inverted_index.find(word); if(iter==inverted_index.end()) { std::cout<<word<<"get error"<<std::endl; return nullptr; } return &(iter->second); }

5. 总结

本文档围绕搜索引擎核心的正倒排索引模块,从设计思路、数据结构、实现流程到核心接口,系统梳理了完整实现逻辑,明确了正倒排索引如何协同支撑 “关键词搜索→结果展示” 的全流程。以下是这一部分的完整代码:

#pragma once #include<iostream> #include<string> #include<vector> #include<unordered_map> #include<fstream> #include<mutex> #include"usuallytool.hpp" #include<boost/algorithm/string.hpp> #include"log.hpp" namespace ns_index{ //这个是正排索引中需要用到的 typedef struct DocInfo{ std::string title;//文档的标题 std::string content;//文档的内容 std::string url;//文档的url int doc_id;//文档的ID }DocInfo1; //倒排索引中需要用到的 struct InvertedElem{ int doc_id;//文档的ID std::string word;//文档的word int weight;//这个文档的权重 }; typedef std::vector<InvertedElem> InvertedList; class Index{ private: //正排索引使用vector,因为他的下标就是文档的id std::vector<DocInfo1> forward_index; //使用哈希来进行映射 std::unordered_map<std::string,InvertedList> inverted_index; private:////////////////////////// Index(){}; Index(const Index&)=delete; Index& operator=(const Index&)=delete; //这边的话就是禁用拷贝构造函数和运算符重载,因为是单例模式 static Index* instance; static std::mutex log; public: ~Index();//这边框起来的这段代码是使用单例模式的方式来进行编程, public: static Index* Getinstance() { if(instance==nullptr) { log.lock();//上锁 if(instance==nullptr) { instance=new Index(); } log.unlock();//解锁 } return instance; }/////////////////////////////////////// public: //正排索引,通过id来找到文档内容 DocInfo1* GetForwardIndex(uint64_t doc_id) { if(doc_id>=forward_index.size()) { std::cout<<"doc_id out range, error!"<<std::endl; return nullptr; } return &forward_index[doc_id]; } //通过word(也就是我们的关键字)来获取倒排拉链 //倒排拉链就是说对应的那一串文档的ID InvertedList* GetInvertedList(const std::string& word) { auto iter=inverted_index.find(word); if(iter==inverted_index.end()) { std::cout<<word<<"get error"<<std::endl; return nullptr; } return &(iter->second); } //根据去标签后的文档来构建正倒排索引 bool BuildIndex(const std::string& input) { std::ifstream in(input,std::ios::in | std::ios::binary); if(!in.is_open())//如果文件打开失败就返回 { std::cout<<input<<"open error"<<std::endl; return false; } int count = 0; std::string line;//设置一个临时变量用来接收去标签后的文档 while(std::getline(in,line)) { //这边先建立正排索引,然后通过if条件判断来确定是否创建成功 DocInfo1* doc=BuildForwardIndex(line); if(doc==nullptr) { std::cout<<"BuildIndex error"<<std::endl; continue; } BuildInvertedIndex(*doc); count++; if(count%50==0) LOG1(NORMAL,"索引建立到:" + std::to_string(count)); } return true; } private: DocInfo1* BuildForwardIndex(const std::string& line) { std::vector<std::string> results; ns_util::StringUtil::Split(line,&results,"\3");//这边就是进行字符窜切割,把title,content,url分开 if(results.size()!=3) return nullptr; DocInfo1 doc;//创建一个临时变量 doc.title=results[0]; doc.content=results[1]; doc.url=results[2]; doc.doc_id=forward_index.size(); //这上面表示把切分好的内容分给doc //注意,这个id就是下标的个数,所以我们这边就直接所以size()就可以了 //然后我们把处理好的docpush进forward_index里面 forward_index.push_back(doc); return &forward_index.back(); } bool BuildInvertedIndex(const DocInfo1& doc) { struct word_cnt{ int title_cnt;//title出现的次数 int content_cnt;//content出现的次数 word_cnt() :title_cnt(0) ,content_cnt(0) {} };//这边的话就是先创建一个结构体,然后用这个结构体来存储词频 std::unordered_map<std::string,word_cnt> word_map; //这边就是通过KV的结构来存储 std::vector<std::string> title_words; ns_util::JiebaUsutl::CutString(doc.title,&title_words); for(auto& tw:title_words) { boost::to_lower(tw); word_map[tw].title_cnt++; } //这三行代码就是先创建一个存储分好后的词的容器,接着对其进行计数 std::vector<std::string> content_words; ns_util::JiebaUsutl::CutString(doc.content,&content_words); for(auto& cw:content_words) { boost::to_lower(cw); word_map[cw].content_cnt++; } //这三行代码就是先创建一个存储分好后的词的容器,接着对其进行计数 #define X 10 #define Y 1 for(auto& word_pair:word_map) { InvertedElem item;//InvertedElem这个类是专门为了倒排创建的,里面就只有文档的ID,权重和word item.doc_id=doc.doc_id;//这里就是把分好的文档的ID给item item.word=word_pair.first;//这是是把分好的那个词给item item.weight=X*word_pair.second.title_cnt+Y*word_pair.second.content_cnt; //这边就是模拟进行权重的计算 inverted_index[word_pair.first].push_back(item); //这个inverted_index一开始是空的,然后我们不断的push_back就可以人里面存在key和value } return true; } }; Index* Index::instance=nullptr; //std::mutex Index::; std::mutex Index::log; } 

愿我的文章对您有所帮助。

Read more

AI时代医疗大健康微服务编程提升路径和具体架构设计

AI时代医疗大健康微服务编程提升路径和具体架构设计

一、引言 1.1 研究背景与意义 随着科技的飞速发展,人工智能(Artificial Intelligence,AI)已逐渐渗透至各个领域,医疗大健康领域亦不例外。人工智能与医疗大健康的融合,正引领着医疗行业迈向智能化、精准化、个性化的新时代,为解决医疗资源分布不均、提升医疗服务效率和质量等问题提供了新的思路与方法。从医疗影像诊断到疾病预测,从智能药物研发到个性化医疗方案制定,人工智能技术的应用使得医疗服务的各个环节都发生了深刻变革。 在医疗影像诊断方面,人工智能算法能够快速、准确地分析 X 光、CT、MRI 等影像数据,帮助医生更及时地发现病变,提高诊断准确率。例如,一些基于深度学习的人工智能系统在识别肺部结节、乳腺癌等疾病方面,已经达到甚至超过了人类专家的水平,大大缩短了诊断时间,为患者赢得了宝贵的治疗时机。在疾病预测领域,通过对大量患者的医疗数据、生活习惯数据以及基因数据等进行分析,人工智能模型可以预测疾病的发生风险,提前为患者提供预防建议,实现疾病的早期干预。 而微服务编程作为一种新兴

By Ne0inhk
在家也能做 AI 导演!本地部署 Wan2.1 视频生成模型全攻略

在家也能做 AI 导演!本地部署 Wan2.1 视频生成模型全攻略

文章目录 * 前言 * 1.软件准备 * 1.1 ComfyUI * 1.2 文本编码器 * 1.3 VAE * 1.4 视频生成模型 * 2.整合配置 * 3. 本地运行测试 * 4. 公网使用Wan2.1模型生成视频 * 4.1 创建远程连接公网地址 * 5. 固定远程访问公网地址 * 总结 前言 Wan2.1 模型搭配 ComfyUI 框架,能实现文本转视频、图片转动画等功能,生成的视频质量可媲美专业工具,普通 PC 就能运行,特别适合自媒体创作者、短视频团队和 AI 爱好者快速制作动态内容,无需复杂技术背景也能上手,且完全开源免费,性价比很高。 使用时发现,选择模型版本要结合显卡配置:

By Ne0inhk
AI智能体|扣子(Coze)工作流搭建【超长文写作】

AI智能体|扣子(Coze)工作流搭建【超长文写作】

超长文章写作AI智能体!Coze输出数万字的长文这到底是怎么做到的?到底谁在用AI小说创作割韭菜啊!? ☀大家好,我是芝麻☀ AI智能体,AI副业搞米,AI实战案例分享 👇点击关注,每篇文章能给你带来一定的收获👇 你好,我是芝麻! 来掀某些割韭菜的桌子了!!一个非常简单的操作他居然收我999?? 传统写小说,动辄数月甚至数年才能完成一部作品。而现在利用AI 工具,一键就能生成长达几百万字的小说内容,彻底告别手动码字的辛苦,大大提高创作效率,让你在长期可以拥有多部可发布的作品,AI小说创作这块我能想到的有以下变现路 * 平台全勤奖:坚持每日更新 4000 字以上,符合平台规则,每月稳定300-800 * 广告分成:你的小说阅读量越高,广告分成收益就越多。平均每 1 万阅读量,就能收获几百元。爆款小说的阅读量轻松破百万,收益还是相当可观的 * 稿费收入:尤其是短篇小说,市场需求大。每 1 万字可获得 200 元左右稿费,只要你持续创作,稿费源源不断 * 读者打赏:当你的小说深受读者喜爱,

By Ne0inhk