[项目]基于正倒排索引的Boost搜索引擎---编写建立索引的模块Index

一、搭建索引结构代码

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库的安装

①gitcode.com

②搜索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;
}

七、总结

  1. 正排索引以文档 ID 为键,存储每个文档的完整信息(标题、内容、URL、文档 ID),支持通过 ID 快速获取文档详情;
  2. 倒排索引以分词后的单词为键,关联包含该单词的所有文档记录(InvertList),每条记录包含文档 ID 和权重(标题词频 ×10 + 内容词频 ×1,突出标题关键词重要性);
  3. 构建索引时,先读取格式化文档并分割字段生成正排记录,再通过中文分词(JiebaUtil)对标题和内容分词、统计词频,最终生成倒排索引;
  4. 提供两类查询接口:按文档 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;
        }
    };
}

感谢观看

相关推荐
草莓熊Lotso28 分钟前
Git 本地操作进阶:版本回退、撤销修改与文件删除全攻略
java·javascript·c++·人工智能·git·python·网络协议
@卞1 小时前
高阶数据结构 --- 单调队列
数据结构·c++·算法
光算科技2 小时前
网站被谷歌标记“不安全”(Not Secure)怎么处理?
安全·搜索引擎
fpcc2 小时前
并行编程实战——CUDA编程的流的优先级
c++·cuda
勇闯逆流河4 小时前
【C++】C++11(下)
开发语言·c++
胡萝卜3.010 小时前
掌握C++ map:高效键值对操作指南
开发语言·数据结构·c++·人工智能·map
电子_咸鱼10 小时前
【STL string 全解析:接口详解、测试实战与模拟实现】
开发语言·c++·vscode·python·算法·leetcode
月夜的风吹雨12 小时前
【封装红黑树】:深度解析map和set的底层实现
c++·set·map·封装
列逍13 小时前
深入理解 C++ 智能指针:原理、使用与避坑指南
开发语言·c++