基于boost库的搜索引擎项目

news2024/11/18 6:39:31

文章目录

  • 一、项目背景
  • 二、什么样的搜索引擎
  • 三、搜索引擎的宏观图原理
  • 四、Parse模块
    • 4.1下载boost库源代码
    • 4.2提取boost库中以.html为结尾的文件
      • 4.2.1 boost库的简单使用
    • 4.3数据清洗(去标签化)
      • 4.3.1数据清洗的具体实现
    • 4.4将清洗后的数据写入到raw.txt文件中
  • 五、正排索引 vs 倒排索引——搜索引擎具体原理
  • 六、建立索引
    • 6.1整体框架
    • 6.2建立索引
      • 6.2.1建立正排索引
      • 6.2.2建立倒排索引
    • 6.3根据文档id获得正排
    • 6.4根据关键字获取倒排
    • 6.5单例实现
  • 七、搜索引擎searcher模块
    • 7.1初始化
    • 7.2查找
    • 7.3优化
  • 八、http_server模块
  • 九、日志
  • 十、部署服务到 linux 上

一、项目背景

boost库是指一些为C++标准库提供扩展的程序库总称,但是boost网站中并没有为我们提供站内搜索功能,因此我们要想找到某一个类的用法还要一个个去找,因此我们这次的目的就是实现一个搜索引擎功能,提高我们获取知识的效率

二、什么样的搜索引擎

比如百度,谷歌,360等,这些都是大型的搜索引擎仅靠我自己一个人肯定是无法完成的,因此我打算实现一个小型的。先来看大多数搜索引擎的共同点
在这里插入图片描述
在这里插入图片描述
我们可以发现搜索出来的内容大致由三部分组成,分别是标题,内容和URL链接,因此我们基于boost库实现的搜索引擎的搜索结果也以这三部分构成

三、搜索引擎的宏观图原理

在这里插入图片描述
我们只是去写一个服务器,让用户在我们的服务器上通过关键词搜索获得相关资源,这些资源可以放在我们服务器本地,也可以是在其他网站上的资源,我们本次就从boost官网上将boost库下载到我们服务器为例

四、Parse模块

将下载到我们服务器本地boost库进行数据清洗,我们这里仅把boost库中以.html为结尾的文件放到我们服务器本地中,因为数据量太大会导致我们服务器运行变慢,那么为什么要进行数据清洗和,也就是去标签等操作,因为在文件中包含大量的双标签和单标签,与我们用户搜索关键词无关,因此我们只把有用的信息进行保存即可。
1.递将下载好的boost库放到input文件夹中
2. 读取 html中的对数据进行清洗(去标签化)和提取标题,正文和URL
3.将数据清洗后的每个文档我们以’\3’作为分隔符,写入到我们创建的data/raw_html/raw.txt文件中。

为什么要用’\3’作为文档分隔符?因为它是不可显示的控制字符,使用其它的不可显控制字符也可以

// 源文件目录,下面存放的是所有的HTML网页
const std::string src_path = "data/input/";
// 处理完成的数据的存放位置
const std::string output = "data/raw_html/raw.txt";

typedef struct DocInfo{
    std::string title;   //文档的标题
    std::string content; //文档内容
    std::string url;     //该文档在官网中的url
}DocInfo_t;

//我们自己定的命名规则
//const &:输入
//*: 输出
//&: 输入输出

bool EnumFile(const std::string &src_path, std::vector<std::string> *files_list);
bool ParseHtml(const std::vector<std::string> &files_list, std::vector<DocInfo_t> *results);
bool SaveHtml(const std::vector<DocInfo_t> &results, const std::string &output);

int main()
{
    // 1.递归式的把每个html文件名带路径,到存到files_list容器中,方便后期进行一个一个的文件进行读取
    std::vector<std::string> files_list;
    if (!EnumFile(src_path, &files_list))
    {
        std::cerr << "enum file name error!" << std::endl;
        return 1;
    }
    // 2.对files_list中文件的内容读取与分析
    std::vector<DocInfo_t> results;
    if (!ParseHtml(files_list, &results))
    {
        std::cerr << "parse file name error!" << std::endl;
        return 2;
    }
    // 3.把解析完毕的各个文件内容,写入到output,按照\3作为每个文档的分隔符
    if (!SaveHtml(results, output))
    {
        std::cerr << "Save file name error!" << std::endl;
        return 3;
    }
    return 0;
}

上述三步一句话总结为:把boost库下载到input文件夹下,然后递归式的把以.html结尾的文件进行数据清洗,最后将清洗后的数据写入到/data/raw.txt文件中作为数据源
下面我们来依次实现上述内容

4.1下载boost库源代码

在这里插入图片描述
rz -E放到Linux中,然后解压,将解压后的文件放到/data/input/文件下

4.2提取boost库中以.html为结尾的文件

递归式的把每个后缀为.html的文件,以路径+文件名的方式保存到vector容器files_list中,方便我们后续直接从容器中获取文件路径+文件名,从而读取文件内容

bool EnumFile(const std::string &src_path, std::vector<std::string> *files_list)
{
    namespace fs = boost::filesystem;
    fs::path root_path(src_path);

    //判断路径是否存在,如果不存在就没有必要向后走了
    if(!fs::exists(root_path))
    {
        std::cerr << src_path << "not exists" << std::endl;
        return false;
    }

    //定义一个空的迭代器,用来进行判断递归结束
    fs::recursive_directory_iterator end;
    for(fs::recursive_directory_iterator iter(root_path); iter != end; iter++)
    {
        //1.判断是否是普通文件
        if(!fs::is_regular_file(*iter))
        {
            continue;//不是break
        }
        //2.判断文件后缀是否是.html
        if(iter->path().extension() != ".html")
        {
            continue;
        }
        //std::cout << "debug: " << iter->path().string() << std::endl;
        //当前的路径一定是一个合法的,以.html结束的普通网页文件
        files_list->push_back(iter->path().string());//将符合要求的文件放到files_list容器中
    }
    return true;
}

4.2.1 boost库的简单使用

boost库的文件操作函数全在filesystem命名空间下,因此我们赋给一个命名空间fs,方便我们使用,以后我们直接使用fs就行了

namespace fs = boost::filesystem;

4.3数据清洗(去标签化)

遍历files_list容器中的每一个文档,提取该文档的标题,正文,和URL,并将这些数据作为成员保存到一个对象中,然后再将这些依次对象放到保存当前对象的vector容器results中。我们提取后的标题,正文和URL中没有标签存在的

typedef struct DocInfo{
    std::string title;   //文档的标题
    std::string content; //文档内容
    std::string url;     //该文档在官网中的url
}DocInfo_t;

ParseHtml整体框架

bool ParseHtml(const std::vector<std::string> &files_list, std::vector<DocInfo_t> *results)
{
    //int cnt = 3;
    for(const std::string &file : files_list)
    {
        //1.读取文件,Read()
        std::string result;
        if(!ns_util::FileUtil::ReadFile(file, &result))
        {
            continue;
        }
        DocInfo_t doc;
        //2.解析指定的文件,提取title
        if(!ParseTitle(result, &doc.title))
        {
            continue;
        }
        //3.解析指定的文件,提取content
        if(!ParseContent(result, &doc.content))
        {
            continue;
        }
        //4.解析指定的文件路径,构建URL
        if(!ParseUrl(file, &doc.url))
        {
            continue;
        }

        //完成解析任务,当前文档的相关结果都保存在了doc里面
        //results->push_back(doc);//这样写会发生拷贝,效率比较低
        results->push_back(std::move(doc));
        //ShowDoc(doc);
        // cnt--;
        // if(cnt == 0)
        // {
        //     break;
        // }
    }
    return true;
}

4.3.1数据清洗的具体实现

##############获取标题
static bool ParseTitle(const std::string &file, std::string *title)
{
    std::size_t begin = file.find("<title>");
    if(begin == std::string::npos)
    {
        return false;
    }
    std::size_t end = file.find("</title>");
    if(end == std::string::npos)
    {
        return false;
    }

    begin += std::string("<title>").size();

    if(begin > end)
    {
        return false;
    }
    *title = file.substr(begin, end - begin);
    return true;
}
##############获取正文
static bool ParseContent(const std::string &file, std::string *content)
{
    //去标签,基于一个简易的状态机
    enum status
    {
        LABLE,
        CONTENT
    };

    enum status s = LABLE;
    for(char c : file)
    {
        switch (s)
        {
        case LABLE:
            if(c == '>') s = CONTENT;
            break;
        case CONTENT:
            if(c == '<') s = LABLE;
            else{
                //我么不想保留原始文件中的\n,因为我们想用\n作为html解析之后的文本分割符
                if(c == '\n') c = ' ';
                content->push_back(c);
            }
        default:
            break;
        }
    }
    return true;
}
################获取URL
static bool ParseUrl(const std::string &file_path, std::string *url)
{
    std::string url_head = "https://www.boost.org/doc/libs/1_78_0/doc/html/";
    std::string url_tail = file_path.substr(src_path.size());
    *url = url_head + url_tail;
    return true;
}

4.4将清洗后的数据写入到raw.txt文件中

清洗后的文件内容已经以对象的方式被我们保存到results容器中了,因此此时每个文档去标签后的内容,就为容器中其中一个对象的各成员相加之后得到的字符串

我们将文档中的标题,正文,URL以’\3’作为分隔符进行分隔,文档与文档之间以’\n’作为分隔符进行分隔

将每个文件以二进制的方式写入到raw.txt文件中

bool SaveHtml(const std::vector<DocInfo_t> &results, const std::string &output)
{
    #define SEP '\3'

    //按照二进制方式进行写入
    std::ofstream out(output, std::ios::out | std::ios::binary);
    if(!out.is_open())
    {
        std::cerr << "open " << output << " failed!" <<std::endl;
        return false;
    }

    //进行文件内容的写入
    for(auto &item : results)
    {
        std::string out_string;
        out_string = item.title;
        out_string += SEP;
        out_string += item.content;
        out_string += SEP;
        out_string += item.url;
        out_string += '\n';

        out.write(out_string.c_str(), out_string.size());
    }
    return true;
}

五、正排索引 vs 倒排索引——搜索引擎具体原理

正排索引: 根据文档ID映射文档内容,只要文档ID正确,就一定能找到对应的文档
在这里插入图片描述
对目标文档进行分词,目的是为了方便建立倒排索引和查找

重庆今年特别热:重庆/今年/特别/热/特别热/不愧/中国/火炉
重庆是中国的四大火炉之首:重庆/中国/四大火炉/火炉/之首
像"的",“呀"之类的词,几乎在所有的文章都有大量出现,不应该作为搜索的关键词,这类词称之为**“暂停词”**,类似的还有"了”、"吗"等等,对此,不给予考虑。

倒排索引: 根据文档内容,分词,整理不重复的各个关键字,对应联系到文档ID的方案
在这里插入图片描述
模拟一次查找的过程: 输入关键词火炉,通过倒排索引提取对应的文档ID,在正排索引中,通过文档ID获取文档内容,对文档内容中的title、conent(desc)、url文档结果进行摘要,最后构成响应结果

此时有两个细节:
细节一:需要用到权值(weight)对搜索到的文档结果进行排序,而权值根据关键词在标题中是否出现以及在内容中出现的次数决定
细节二:搜索的内容可能不是一个关键词,而是一条语句,语句经过分词处理,被处理成多个关键词,多个关键词可能对应着同一个文档,此时需要进行去重处理

六、建立索引

为什么要建立索引?
因为我们的目标是文档既可以通过文档中关键字进行搜索到,也可以通过文档的文件ID被搜索到,
因此我们要分别建立正排索引和倒排索引

6.1整体框架

自定义类型

struct DocInfo
    {
        std::string title;   // 文档的标题
        std::string content; // 文档内容
        std::string url;     // 该文档在官网中的url
        uint64_t doc_id;     // 文档的ID
    };

struct InvertedElem
{
    uint64_t doc_id;
    std::string word;
    int weight;
};

// 倒排拉链
typedef std::vector<InvertedElem> InvertedList;

class Index
{
private:
    // 正派索引的数据结构用数组,数组的下标天然是文档的ID
    std::vector<DocInfo> forward_index; // 正排索引
    // 倒排索引一定是一个关键字和一组(个)InvertedElem对应[关键字和倒排拉链的映射关系]
    std::unordered_map<std::string, InvertedList> inverted_index;
}

为什么要定义出上述两种类型?
正排索引是要通过文档id获取对应的文档内容,一个id只会对应一个文档,因此我们可以利用vector数组来存放文档对象DocInfo,以下标作为文档id。
而倒排索引是要通过关键字来获取对应文档,与正排索引不同的是,通过关键字得到的文档可能是多个,因为不同的文档可能会具有相同的关键字,因此我们选择用map容器,通过实体的std::string关键字去映射一个vector数组,数组中存放的是多个文档

// 倒排拉链
typedef std::vector<InvertedElem> InvertedList;
std::unordered_map<std::string, InvertedList> inverted_index;

并倒排拉链对象中还要保存一个叫权重的值,因为不同的文件包含该关键字的数量不同,包含的多我们认为该文件占有的权重大,权重我们以关键字在标题中出现的次数*10 + 在正文中出现的次数

struct InvertedElem
{
    uint64_t doc_id;
    std::string word;
    int weight;
};
class Index
{
private:
    // 正派索引的数据结构用数组,数组的下标天然是文档的ID
    std::vector<DocInfo> forward_index; // 正排索引
    // 倒排索引一定是一个关键字和一组(个)InvertedElem对应[关键字和倒排拉链的映射关系]
    std::unordered_map<std::string, InvertedList> inverted_index;
public:
//获取单例
    static Index *GetInstance(){}
    
// 根据去标签,格式化之后的文档,构建正排和倒排索引 --建立索引
	 bool BuildIndex(const std::string &input) {}// parse处理完毕的数据交给我
        
// 根据doc_id找到文档内容 --获得正排
     DocInfo *GetForwordIndex(uint16_t doc_id) {}
        
// 根据关键字string,获得倒排拉链 --获得倒排
     InvertedList *GetInvertedList(const std::string &word){}
}

在Index类中我们对外部提供的接口有四个(不包含析构),分别是
1.建立索引(建立正排索引和倒排索引)
2.根据文档关键字获取倒排索引
3.根据文档倒排索引id获取正排索引中的文件
4.获取单例

6.2建立索引

以输入模式或二进制的方式打开数据清理后的文件

bool BuildIndex(const std::string &input) // parse处理完毕的数据交给我
{
   std::ifstream in(input, std::ios::in | std::ios::binary);
   if (!in.is_open())
   {
       std::cerr << "sorry, " << input << "open error" << std::endl;
       return false;
   }

   std::string line;
   int count = 0;
   while (std::getline(in, line))
   {
       DocInfo *doc = BuildForwordIndex(line);
       if (doc == nullptr)
       {
           std::cerr << "build " << line << " error" << std::endl;
           continue;
       }
       BuildInvertedIndex(*doc);
       count++;
       if(count % 50 == 0)
       {
           //std::cout << "当前已经建立的索引文档:" << count <<std::endl;
           LOG(NORMAL, "当前的已经建立的索引文档: " + std::to_string(count));
       }
   }
   return true;
}

std::getline(in, line)每次读取一行,也就是读到以\n,而遇到一个\n分隔符就意味着一个文件的结束,因此我们是每读取一个文件就去对它建立正排和倒排索引

6.2.1建立正排索引

namespace ns_util{
class StringUtil
	{
	public:
	    static void Split(const std::string &target, std::vector<std::string> *out, const std::string &sep)
	    {
	        // boost split
	        boost::split(*out, target, boost::is_any_of(sep), boost::token_compress_on);
	    }
	};
}

boost::token_compress_on :意味将连续的分隔符视为一个分隔符

private:
   DocInfo *BuildForwordIndex(const std::string &line)
   {
       // 1.解析line,字符串切分
       // line -> 3 string, title, content, url
       std::vector<std::string> results;
       std::string sep = "\3";
       ns_util::StringUtil::Split(line, &results, sep);
       if (results.size() != 3)
       {
           return nullptr;
       }
       // 2.字符串进行填充到DocInfo
       DocInfo doc;
       doc.title = results[0];
       doc.content = results[1];
       doc.url = results[2];
       doc.doc_id = forward_index.size(); // 先进行保存id,再插入,对应的id就是当前doc在vector中的下标
       // 3.插入到正排索引的vector
       forward_index.push_back(std::move(doc));
       return &forward_index.back();
   }

DocInfo *BuildForwordIndex(const std::string &line)参数line是读取的一行,也就是一个文件
先通过boost库中的split函数将读取的字符串(这个文件)进行拆分,拆分后的字符串自然的就放到了一个vector容器中,然后vector容器中的每个元素依次填充到DocInfo对象中,最后将DocInfo对象放到正排索引的vector容器中

6.2.2建立倒排索引

private:
	bool BuildInvertedIndex(const DocInfo &doc)
       {
           // DocInfo{title, content, url, doc_id}
           // word -> 倒排拉链
           struct word_cnt
           {
               int title_cnt;
               int content_cnt;
               word_cnt() : title_cnt(0), content_cnt(0) {}
           };

           std::unordered_map<std::string, word_cnt> word_map; // 用来暂存词频的映射表

           // 对标题进行分词
           std::vector<std::string> title_words;
           ns_util::JiebaUtil::CutString(doc.title, &title_words);

           // 对标题进行词频统计
           for (auto &s : title_words)
           {
               boost::to_lower(s); //需要统一转化成为小写
               word_map[s].title_cnt++; // 如果存在就获取,如果不存在就新建
           }

           // 对文档内容进行分词
           std::vector<std::string> content_words;
           ns_util::JiebaUtil::CutString(doc.content, &content_words);

           // 对内容进行词频统计
           for (auto &s : content_words)
           {
               boost::to_lower(s);
               word_map[s].content_cnt++;
           }

#define X 10
#define Y 1

           for (auto &word_pair : word_map)
           {
               InvertedElem item;
               item.doc_id = doc.doc_id;
               item.word = word_pair.first;
               item.weight = X * word_pair.second.title_cnt + Y * word_pair.second.content_cnt; // 相关性
               InvertedList &inverted_list = inverted_index[word_pair.first];
               inverted_list.push_back(std::move(item));
           }
           return true;
       }
   };

建立倒排索引是在基于建立完成正排索引之后的,因为在建立完正排索引之后我们就可以直接使用正排索引的文档id作为倒排索引的文档id,即提高了效率,又保证了同一个文档对应的文档id的唯一性
在词频统计的时候不区分大小写,以便我们在搜索时也可以不区分大小写
1.我们每次读取的是正排索引中的一个文档对象,然后对这个对象的标题和正文进行分词和词频统计,再将词和词频映射到一个临时map容器中
2.遍历这个临时map容器,构建当前文档的文档id,关键词和权重放到倒排拉链元素对象InvertedElem 中
根据关键词找到它在倒排索引inverted_index中的映射值,这个映射值为一个倒排拉链(vector容器),然后我们再把当前关键字对应的元素对象InvertedElem插入到倒排拉链中
注意:索引是索引,拉链是拉链,倒排索引中有拉链,因为它不是的元素一一映射的关系,一个关键字可能对应多个值(多个文档),而在正排索引中,一个文档id只会对应一个文档

我们在对文档进行分词,从而获取关键字的时候,使用了第三方库cppjieba分词

#include "cppjieba/Jieba.hpp"
class JiebaUtil
    {
    private:
        static cppjieba::Jieba jieba;

    public:
        static void CutString(const std::string &src, std::vector<std::string> *out)
        {
            jieba.CutForSearch(src, *out);
        }
    };
    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";
    //静态成员类外初始化
    cppjieba::Jieba JiebaUtil::jieba(DICT_PATH, HMM_PATH, USER_DICT_PATH, IDF_PATH, STOP_WORD_PATH);

6.3根据文档id获得正排

// 根据doc_id找到文档内容 1.获得正排
DocInfo *GetForwordIndex(uint16_t doc_id)
{
    if (doc_id >= forward_index.size())
    {
        std::cerr << "doc_id out range, error!" << std::endl;
        return nullptr;
    }
    return &forward_index[doc_id];
}

6.4根据关键字获取倒排

// 根据关键字string,获得倒排拉链 2.获得倒排
InvertedList *GetInvertedList(const std::string &word)
{
    auto iter = inverted_index.find(word);
    if (iter == inverted_index.end())
    {
        std::cerr << word << " have no InvertedList" << std::endl;
        return nullptr;
    }
    return &(iter->second);
}

在倒排索引中,找是否存在该关键字如果存在,就把他所映射的vector容器返回,vector容器包含所有具有该关键字的文件

6.5单例实现

private:
Index()
{}

Index(const Index &) = delete;
Index &operator=(const Index &) = delete;

static Index *instance;
static std::mutex mtx;

public:
~Index()
{}

static Index *GetInstance()
{

    if (instance == nullptr)
    {
        mtx.lock();
        if (instance == nullptr)
        {
            instance = new Index();
        }
        mtx.unlock();
    }
    return instance;
}

因为我们只需服务器上保存一份已经被清洗过且建立完索引的数据就可以,如果每访问一次都要去重新创建一次,就会导致服务器数据冗余,包含多份相同的数据

七、搜索引擎searcher模块

在这里插入图片描述
我们想要的搜索结果是关键词不区分大小写,权重越大(与关键字相关性越强)的文件越靠前,显示的结果没有重复文档,且只需要显示该文档中的部分信息即可。
我们在搜索的时候用户搜索关键字,通过关键字获取倒排索引,再通过倒排索引中的对象id获取正排索引中保存的文件,注意:在搜索栏搜索的是关键字信息或一个语句,不是文件索引id号

7.1初始化

namespace ns_searcher
{
    class Searcher
    {
    private:
        ns_index::Index *index; // 供系统进行查找的索引
    public:
        Searcher() {}
        ~Searcher() {}

    public:
        void InitSearcher(const std::string &input)
        {
            // 1.获取或者建立index对象
            index = ns_index::Index::GetInstance();
            //std::cout << "获取index单例成功..." << std::endl;
            LOG(NORMAL, "获取index单例成功...");
            // 2.根据index对象建立索引
            index->BuildIndex(input);
            //std::cout << "建立正排和倒排索引成功..." << std::endl;
            LOG(NORMAL, "建立正排和倒排索引成功...");
        }
}

7.2查找

查找过程分为四个模块
1.对搜索关键词或关键句进行分词
2.将分完的关键词在倒排索引中进行搜索
3.根据搜索结果的权重,将搜出来的文件进行排序
4.再通过排序后的文件id,在正排索引中获取对应的文件内容,返回给用户

// query:搜索关键字
// json_string:返回给用户浏览器的搜索结果
void Search(const std::string &query, std::string *json_string)
{
    // 1.[分词]:对我们的query进行按照searcher的要求进行分词
    std::vector<std::string> words;
    ns_util::JiebaUtil::CutString(query, &words);
    // 2.[触发]:就是根据分词的各个"词", 进行index查找
    ns_index::InvertedList inverted_list_all; // 内部InverterElem

    
    for (std::string word : words)
    {
        boost::to_lower(word);

        ns_index::InvertedList *inverted_list = index->GetInvertedList(word);
        if (inverted_list == nullptr)
        {
            continue;
        }
        inverted_list_all.insert(inverted_list_all.end(), inverted_list->begin(), inverted_list->end());
    }
    // 3.[合并排序]:汇总查找结果,按照相关性(weight)降序排序
    std::sort(inverted_list_all.begin(), inverted_list_all.end(),
              [](const ns_index::InvertedElem &e1, const ns_index::InvertedElem &e2)
              {
                  return e1.weight > e2.weight;
              });
    // 4.[构建]:根据查找出来的结果,构建json串--jsoncpp
    Json::Value root;
    for (auto &item : inverted_list_all)
    {
        ns_index::DocInfo *doc = index->GetForwordIndex(item.doc_id);
        if (doc == nullptr)
        {
            continue;
        }
        Json::Value elem;
        elem["title"] = doc->title;
        elem["desc"] = GetDesc(doc->content, item.word);
        elem["url"] = doc->url;
        elem["id"] = (int)item.doc_id;
        elem["weight"] = item.weight;//int -> string
        root.append(elem);
    }

    Json::StyledWriter writer;
    //Json::FastWriter writer;
    *json_string = writer.write(root);
}
inverted_list_all.insert(inverted_list_all.end(), inverted_list->begin(), inverted_list->end());

将所有关键词对应的倒多个排拉链拉链都汇总插入到同一个倒排拉链中

std::sort(inverted_list_all.begin(), inverted_list_all.end(),
         [](const ns_index::InvertedElem &e1, const ns_index::InvertedElem &e2)
          {
              return e1.weight > e2.weight;
          });

对汇总后的倒排拉链,按照权重将文件进行排序

7.3优化

1.呈现给用户的信息不是把该文件的所有内容都呈现,而是只呈现部分关键信息
因此我们选择在找到word在html_content中的首次出现,然后往前找50字节(如果没有,从begin考试),往后找100字节(如果没有,到end就可以),最后截取出这部分内容

//获取摘要
std::string GetDesc(const std::string &html_content, const std::string &word)
{
    //找到word在html_content中的首次出现,然后往前找50字节(如果没有,从begin考试),往后找100字节(如果没有,到end就可以的)
    //截取出这部分内容
    const int prev_step = 50;
    const int next_step = 100;
    //1.找到首次出现
    //find去找是有坑的
    //std::size_t pos = html_content.find(word);
    // if(pos == std::string::npos)
    // {
    //     return "None";//这种情况是不可能存在的,因为一定会找到
    // }
    //使用忽略大小写查找
    auto iter = std::search(html_content.begin(), html_content.end(), word.begin(), word.end(), [](int x, int y){
        return std::tolower(x) == std::tolower(y);
    });
    if(iter == html_content.end())
    {
        return "None1";
    }
    int pos = std::distance(html_content.begin(), iter);//获取html_content.begin()和iter之间的距离
    
    //2.获取start,end
    int start = 0;
    int end = html_content.size() - 1;
    //如果之前有50+字符,就更新开始位置
    if(pos - prev_step > start) start = pos - prev_step;//否则start值仍然为0
    if(pos + next_step < end) end = pos + next_step;

    //3.截取子串,return
    if(start >= end) return "None2";
    return html_content.substr(start, end - start);
}

标准库中的search函数
在这里插入图片描述
查找条件


  // using predicate comparison:
bool mypredicate (int i, int j) {
return (i==j);
int needle2[] = {20,30,50};
it = std::search (haystack.begin(), haystack.end(), needle2, needle2+3, mypredicate);

if (it!=haystack.end())
  std::cout << "needle2 found at position " << (it-haystack.begin()) << '\n';
else
  std::cout << "needle2 not found\n";
}

标准库中的distance函数
在这里插入图片描述
计算两个迭代器之间的距离

2.因为同一个关键句中可能包含多个关键词信息,而这多个关键词就会映射多个倒排索引,那么在这些索引对应的倒排拉链中的文件很有可能有一些是相同的,因此我们还要对搜索出来的文件进行去重操作
通过map容器去重

struct InvertedElemPrint{
	uint64_t doc_id;
	 int weight;
	 std::vector<std::string> words;
	 InvertedElemPrint():doc_id(0), weight(0){}
};
// query:搜索关键字
// json_string:返回给用户浏览器的搜索结果
void Search(const std::string &query, std::string *json_string)
{
   // 1.[分词]:对我们的query进行按照searcher的要求进行分词
   std::vector<std::string> words;
   ns_util::JiebaUtil::CutString(query, &words);
   // 2.[触发]:就是根据分词的各个"词", 进行index查找
   //ns_index::InvertedList inverted_list_all; // 内部InverterElem

   std::vector<InvertedElemPrint> inverted_list_all;

   std::unordered_map<uint64_t, InvertedElemPrint> tokens_map;


   for (std::string word : words)
   {
       boost::to_lower(word);

       ns_index::InvertedList *inverted_list = index->GetInvertedList(word);
       if (inverted_list == nullptr)
       {
           continue;
       }
       //inverted_list_all.insert(inverted_list_all.end(), inverted_list->begin(), inverted_list->end());
       for(const auto &elem : *inverted_list){
               auto &item = tokens_map[elem.doc_id]; //[]:如果存在直接获取,如果不存在新建
               //item一定是doc_id相同的print节点
               item.doc_id = elem.doc_id;
               item.weight += elem.weight;
               item.words.push_back(elem.word);
       }
   }

   for(const auto &item : tokens_map){
           inverted_list_all.push_back(std::move(item.second));
       }
   // 3.[合并排序]:汇总查找结果,按照相关性(weight)降序排序
   // std::sort(inverted_list_all.begin(), inverted_list_all.end(),
   //           [](const ns_index::InvertedElem &e1, const ns_index::InvertedElem &e2)
   //           {
   //               return e1.weight > e2.weight;
   //           });
   std::sort(inverted_list_all.begin(), inverted_list_all.end(),
                 [](const InvertedElemPrint &e1, const InvertedElemPrint &e2){
                 return e1.weight > e2.weight;
                 });
   // 4.[构建]:根据查找出来的结果,构建json串--jsoncpp
   Json::Value root;
   for (auto &item : inverted_list_all)
   {
       ns_index::DocInfo *doc = index->GetForwordIndex(item.doc_id);
       if (doc == nullptr)
       {
           continue;
       }
       Json::Value elem;
       elem["title"] = doc->title;
       elem["desc"] = GetDesc(doc->content, item.words[0]);
       elem["url"] = doc->url;
       elem["id"] = (int)item.doc_id;
       elem["weight"] = item.weight;//int -> string
       root.append(elem);
   }

   Json::StyledWriter writer;
   //Json::FastWriter writer;
   *json_string = writer.write(root);
}

八、http_server模块

利用第三方网络库

#include "searcher.hpp"
#include "cpp-httplib/httplib.h"

const std::string input = "data/raw_html/raw.txt";
const std::string root_path = "./wwwroot";

int main()
{
    ns_searcher::Searcher search;
    search.InitSearcher(input);

    httplib::Server svr;
    svr.set_base_dir(root_path.c_str());
    svr.Get("/s", [&search](const httplib::Request &req, httplib::Response &rsp){
        if(!req.has_param("word"))
        {
            rsp.set_content("必须要有搜索关键字!", "text/plain; charset=utf-8");
            return;
        }
        std::string word = req.get_param_value("word");
        //std::cout << "用户正在搜索:" << word <<std::endl;
        LOG(NORMAL, "用户搜索的: " + word);
        std::string json_string;
        search.Search(word, &json_string);
        rsp.set_content(json_string, "application/json");
    });
    LOG(NORMAL, "服务器启动成功...");
    svr.listen("0.0.0.0",8080 );
    return 0;
}

九、日志


#pragma once

#include <iostream>
#include <string>
#include <ctime>

#define NORMAL  1
#define WARNING 2
#define DEBUG   3
#define FATAL   4

#define LOG(LEVEL, MESSAGE) log(#LEVEL, MESSAGE, __FILE__, __LINE__)

void log(std::string level, std::string message, std::string file, int line)
{
    std::cout << "[" << level << "]" << "[" << time(nullptr) << "]" << "[" << message << "]" << "[" << file << " : " << line << "]" << std::endl;
}

十、部署服务到 linux 上

nohup命令 ,英文全称no hang up (不挂起),用于在系统后台不挂断地运行命令,退出终端不会影响程序的运行。nohup命令,在默认情况下(非重定向时),会输出一个名叫nohup.out的文件到当前目录下,如果当前目录的nohup out文件不可写,输出重定向到$HOME/nohup.out文件中。

使用命令:nohup ./http_server > log/log.txt 2>&1 &将服务部署到linux上
2>&1 解释:将标准错误2重定向到标准输出1,标准输出1再重定向输入到 log/log.txt 文件中
最后一个&解释:表示后台运行

部署成功后,如果需要关闭服务,则使用kill -9 命令,根据进程pid结束进程即可。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1516547.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

【数据结构七】堆与PriorityQueue详解

堆 在Java中有一种数据结构基于队列&#xff0c;并保证操作的数据带有优先级&#xff0c;该数据结构应该提供了两个最基本的操作&#xff0c;一个是返回最高优先级对象&#xff0c;一个是添加新的对象。这种数据结构就是优先级队列(Priority Queue)。它的底层使用了堆这种数据结…

day-19 合并后数组中的最大元素

思路&#xff1a;从后向前遍历数组&#xff0c;用tans记录每一种可能的最大值&#xff0c;ans为实际最大值。 注意&#xff1a;若ans0,返回nums[0] 要用long code class Solution {public long maxArrayValue(int[] nums) {long ans0;long tans0;boolean flagtrue;for(int in…

SpringBoot接口添加IP白名单限制

实现流程&#xff1a; 自定义拦截器——注入拦截器——获取请求IP——对比IP是否一致——请求返回 文章背景&#xff1a; 接口添加IP白名单限制&#xff0c;只有规定的IP可以访问项目。 实现思路&#xff1a; 添加拦截器&#xff0c;拦截项目所有的请求&#xff0c;获取请求的…

MySQL 篇-深入了解事务四大特性及原理

&#x1f525;博客主页&#xff1a; 【小扳_-CSDN博客】 ❤感谢大家点赞&#x1f44d;收藏⭐评论✍ 文章目录 1.0 事务的概述 2.0 事务的特性 2.1 原子性 2.2 一致性 2.3 持久性 2.4 隔离性 2.4.1 脏读问题 2.4.2 不可重复读问题 2.4.3 幻读问题 3.0 事务的四个隔离级别 3.1…

#QT(一种朴素的计算器实现方法)

1.IDE&#xff1a;QTCreator 2.实验&#xff1a;这是全靠自己想法写的计算器&#xff0c;没有参考任何教程。 &#xff08;1&#xff09;这个计算器只要有运算符敲入就会进行一次运算&#xff0c;所以他没有先后之后&#xff0c;无法满足运算优先级。 &#xff08;2&#xff…

服务器集群 -- nginx配置tcp负载均衡

当面临高流量、高可用性、水平扩展、会话保持或跨地域流量分发等需求时&#xff0c;单台服务器受限于硬件资源、性能有限不能满足应用场景的并发需求量时&#xff0c;引入负载均衡器部署多个服务器共同处理客户端的并发请求&#xff0c;可以帮助优化系统架构&#xff0c;提高系…

【CSP试题回顾】201712-2-游戏

CSP-201712-2-游戏 解题思路 代码实现了一个模拟游戏过程的算法&#xff0c;其中n个小朋友围成一圈&#xff0c;按照顺时针方向依次编号从1到n&#xff0c;然后按顺时针方向依次报数。每当报的数是k的倍数或者个位数是k时&#xff0c;报数的小朋友会被淘汰。游戏继续进行&…

关于使用elementUI中select和el-checkbox-group的回显问题

网上看到很关于这个的问题回显&#xff0c;各式各样&#xff0c;没有绝句自己的问题&#xff0c;总重问题出在数据格式上 select和el-checkbox-group el-checkbox 都是字符串数组格式&#xff1a;[12,13,....]; 我写的格式是id是选中的id组成的回显数据格式&#xff1b; 如…

【爬虫】requests.post请求中的data和json使用区别

请求体是键值对形式&#xff08;无花括号&#xff09;&#xff0c;请求时需要使用data参数处理。 代码&#xff1a; data {...} ret requests.post(url, headersheaders, datadata)请求体是字典形式&#xff08;有花括号&#xff09;&#xff0c;请求时需要使用json参数处理。…

第16届大广赛XPPen都有哪些参赛命题

截至到发文时间&#xff0c;2024年3月14日&#xff0c;第16届大广赛已经累计公布了6个品牌命题&#xff0c;本文就给大家介绍一下XPPen命题的详细细节。 XPPen为汉王友基旗下全球知名数字艺术创新品牌&#xff0c;专注消费级用户创作需求&#xff0c;品牌产品覆盖全球160多个国…

C/C++中strcpy,strcat,strstr以及strncpy,strncat,strncmp的使用

1、首先我们来介绍strcpy的使用从这个函数的名字来看它应该是属于字符串的拷贝&#xff0c;string copy。 那么这个函数是怎么用的呢&#xff1f;下面我么来介绍她的基本结构。 char * strcpy ( char * destination, const char * source );由上述可知它需要两部分 一个是目标字…

如何在CentOS7搭建DashDot服务器仪表盘并实现远程监控

文章目录 1. 本地环境检查1.1 安装docker1.2 下载Dashdot镜像 2. 部署DashDot应用3. 本地访问DashDot服务4. 安装cpolar内网穿透5. 固定DashDot公网地址 本篇文章我们将使用Docker在本地部署DashDot服务器仪表盘&#xff0c;并且结合cpolar内网穿透工具可以实现公网实时监测服务…

【Python】新手入门学习:详细介绍里氏替换原则(LSP)及其作用、代码示例

【Python】新手入门学习&#xff1a;详细介绍里氏替换原则&#xff08;LSP&#xff09;及其作用、代码示例 &#x1f308; 个人主页&#xff1a;高斯小哥 &#x1f525; 高质量专栏&#xff1a;Matplotlib之旅&#xff1a;零基础精通数据可视化、Python基础【高质量合集】、PyT…

ArcGIS学习(十五)用地适宜性评价

ArcGIS学习(十五)用地适宜性评价 本任务给大家带来的内容是用地适宜性评价。 用地适宜性评价是大家在平时工作中最常接触到的分析场景之一。尤其是在国土空间规划的大背景下,用地适宜性评价变得越来越重要。 此外,我们之前的任务主要是使用矢量数据进行分析。本案例是主讲…

vs2022 错误(活动) E1696 无法打开 源 文件 “bits/stdc++.h“解决办法

一、创建stdc.h的头文件 随便找一个项目->添加->新建项->头文件->命名为stdc.h->添加 二、复制下述代码->保存 ​ // C includes used for precompiling -*- C -*-// Copyright (C) 2003-2017 Free Software Foundation, Inc. // // This file is part of …

镭雕机:如何利用激光技术实现高质量的产品标记

镭雕机是一种利用激光技术实现高质量产品标记的设备。它通过激光束在各种不同的物质表面进行精确的打标&#xff0c;可以产生永久性的标记效果&#xff0c;这些标记不仅精美&#xff0c;而且具有高度的精度和清晰度。以下是镭雕机如何利用激光技术实现高质量产品标记的详细过程…

java枚举与模拟方法

枚举 枚举的定义 枚举算法&#xff08;穷举算法&#xff09;&#xff0c;这种算法就是在解决实际问题的时候去使用所有的方式去解决这个问题&#xff0c;会通过推理去考虑事件发生的每一种可能性&#xff0c;最后推导出结果 优点 简单粗暴&#xff0c;他暴力的枚举所有可能&…

Unity之PUN实现多人联机射击游戏的优化

目录 &#x1f3ae;一、 跳跃&#xff0c;加速跑 &#x1f3ae;二、玩家自定义输入昵称 &#x1f345;2.1 给昵称赋值 &#x1f345;2.2 实现 &#x1f3ae;三、玩家昵称同步到房间列表 &#x1f345;3.1 获取全部玩家 &#x1f345;3.2 自定义Player中的字段 &#…

【代码随想录 | 数组 03】有序数组的平方

文章目录 3.有序数组的平方3.1题目3.2思路3.2.1暴力解法3.2.2双指针法 3.有序数组的平方 3.1题目 977.有序数组的平方——力扣题目链接 给你一个按 非递减顺序 排序的整数数组 nums&#xff0c;返回 每个数字的平方 组成的新数组&#xff0c;要求也按 非递减顺序 排序。 示…

Python 3.6.6安装方法(保留环境中python2不受影响)

前言&#xff1a;因为Linux系统下自带了python2的版本&#xff0c;所以我们要用Python3的话需要自己构建安装。并保证某些已经存在的服务可以正常使用python2。 具体步骤如下&#xff1a; 一、python3.6.6 安装 1.安装依赖包&#xff1a; yum -y install zlib zlib-devel yu…