基于boost库的搜索引擎项目

文章目录

  • 一、项目背景
  • 二、什么样的搜索引擎
  • 三、搜索引擎的宏观图原理
  • 四、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结束进程即可。

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

最新android icon和splashScreen适配兼容至2024android

android在12做了splashScreen的变动&#xff0c;即&#xff0c;android12有自带的screenSplash过渡&#xff0c;不论你是否自己有变化&#xff0c;都会插入该动画。 android8做了icon的巨大变动。13做了图标的主题兼容。 一、icon制作 制作 使用android自带的工具&#xff0…

甜甜圈和贪吃蛇的后续

代码复现-项目复现 代码复现 云课五分钟-02第一个代码复现-终端甜甜圈C-CSDN博客 项目复现 云课五分钟-03第一个开源游戏复现-贪吃蛇-CSDN博客 不同的地图 加入班级和标识 循序渐进 这些案例都是来源网络&#xff0c;只是方便熟悉一下云课使用过程。 此部分学生掌握情况非…

阿里云数据湖存储加速套件JindoData

计算存储分离已经成为云计算的一种发展趋势。在计算存储分离之前&#xff0c;普遍采用的是传统的计算存储相互融合的架构&#xff0c;但是这种架构存在一定的问题&#xff0c;比如在集群扩容的时候会面临计算能力和存储能力相互不匹配的问题。用户在某些情况下只需要扩容计算能…

颜色检测python项目

注意&#xff1a;本文引用自专业人工智能社区Venus AI 更多AI知识请参考原站 &#xff08;[www.aideeplearning.cn]&#xff09; 什么是颜色检测&#xff1f; 颜色检测是检测任何颜色名称的过程。很简单不是吗&#xff1f;嗯&#xff0c;对于人类来说&#xff0c;这是一项极…

Raspberry Pi树莓派CODESYS PLC控制器解决方案,提供license和实时系统

我们提供正版codesys license和实时系统集成服务。 使用树莓派制作一个CODESYS的PLC控制器 树莓派上实现控制器 1. 安装CodeSys编程环境 1.1 下载CODESYS 1.2 安装CODESYS开发环境 1.3 运行CODESYS开发环境 2. 运行CODESYS开发环境 2.1 创建项目 2.2 编辑程序 2.3 编译程序 2.4…

如何避免MYSQL主从延迟带来的读写问题?

在MYSQL 部署架构选型上&#xff0c;许多公司都会用到主从读写分离的架构&#xff0c;如下是一个一主一从的架构&#xff0c;主库master负责写入&#xff0c;从库slave进行读取。 但是既然是读写分离&#xff0c;必然会面临这样一个问题&#xff0c;当在主库上进行更新后&#…

跨平台大小端判断与主机节序转网络字节序使用

1.macOS : 默认使用小端 ,高位使用高地址,转换为网络字节序成大端 #include <iostream> #include <arpa/inet.h> int main() {//大小端判断union{short s;char c[sizeof(short)];}un;un.s = 0x0102;printf("低地址:%d,高地址:%d\n",un.c[0],un.c[1]);if …

排序类算法

目录 一、交换类排序 1.冒泡排序 2.快速排序 二、 插入排序 1.直接插入排序 2.折半插入排序 3.希尔排序 三、选择排序 1.简单选择排序 2.堆排序 完整代码 四、归并排序 完整代码 五、汇总 六、OJ练习 1.冒泡排序&#xff1a;正确表示前一个数和后一个数 2.选…

线下活动线上同步直播?媒体同步直播的好处

传媒如春雨&#xff0c;润物细无声&#xff0c;大家好&#xff0c;我是51媒体网胡老师。 线下活动线上同步直播&#xff0c;即利用互联网技术将线下活动的实时内容传输到线上平台&#xff0c;供无法到场的观众在线观看。这种直播方式的好处主要体现在以下几个方面&#xff1a;…

010Editor汉化版+下载+注册码+模板bug

项目场景&#xff1a; 这天我想使用我的不知名的一个破解版本的010Edit来查看一个EXE程序&#xff0c;并想使用模板功能&#xff0c;但是发现没有该模板还无法下载最新模板 问题描述 010Edit联网后需要注册码&#xff1a; 010 Editor 激活码生成器 使用方法 参照教程使用0…

AI新晋王者Claude3完胜ChatGPT4?

1 引言 新王即位&#xff1f;ChatGPT 4跌下神坛? 3月4日&#xff0c;OpenAI 的主要竞争对手之一Anthropic发布了最新的Claude 3系列模型&#xff0c;并宣称claude3系列在推理、数学、编程、多语言理解和视觉方面都树立了新的行业基准。Claude家族包括了Claude 3 Haiku, Clau…

pycharm中连接远程服务器

文章目录 概要文件进行映射将本地的project和远程的project进行映射&#xff0c;一定要使用sftp本地文件和远程的位置配置不想将远程的文件同步时候&#xff0c;可以进行下面设置 配置远程服务器的python解释器需要setting-->python interpreter-->add---> on ssh选择…

车规芯片为什么需要信息安全(1)

目录 1.汽车出现过被黑客攻击事件吗&#xff1f; 2.汽车信息安全标准汇总 2.1 国际标准 2.2 国内标准 3.车规芯片的信息安全应该从什么地方考虑 3.1 芯片硬件安全防护能力 3.2 车规芯片的信息安全服务 3.3 芯片厂如何证明芯片的信息安全能力 4.小结 这个来自家里人的灵…

Spring Cloud Gateway自定义断言

问题&#xff1a;Spring Cloud Gateway自带的断言&#xff08;Predicate&#xff09;不满足业务怎么办&#xff1f;可以自定义断言&#xff01; 先看Spring Cloud Gateway是如何实现断言的 Gateway中断言的整体架构如下&#xff1a; public abstract class AbstractRoutePred…

【pycharm使用ssh连接服务器】

2、pycharm使用ssh连接服务器 1、具体流程2、一些需要注意的小问题2.1 更改代码地址2.2 本地代码上传到服务器2.3 在服务器的环境中上新安装库&#xff0c;但是pycharm检测不到 1、具体流程 打开pycharm – File – Setting 输入服务器的IP地址&#xff0c;端口号、登录账号名…

指令调用模板

也就是这边指令通过id和map会定位到一个结构体&#xff0c;然后这个结构再赋值两个成员&#xff0c;一个是函数一个是指令类型&#xff0c;然后这个函数是模板的实例化 使用的时候就传进去&#xff0c;这只是参数&#xff0c;最开始初始化的时候模板就已经实例化了。然后关于模…

嵌入式软件开发工程师如何提高C语言编码技能?

嵌入式软件开发工程师如何提高C语言编码技能&#xff1f; 在开始前我分享下我的经历&#xff0c;我刚入行时遇到一个好公司和师父&#xff0c;给了我机会&#xff0c;一年时间从3k薪资涨到18k的&#xff0c; 我师父给了一些 电气工程师学习方法和资料&#xff0c;让我不断提升…

[Unity3D]--更换天空盒子

我们原来的天空盒子是这样的。 感觉不是特别满意&#xff0c;想换一个更好看的。 去资源商店找个好看的 外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传 例如这个 然后在Window>Rendering>Lighting里的环境选项里更换材质 更换&#xff1a; ​ …

【Echarts】柱状图上方显示数字以及自定义值,标题和副标题居中,鼠标上显示信息以及自定义信息

欢迎来到《小5讲堂》 大家好&#xff0c;我是全栈小5。 这是《前端》系列文章&#xff0c;每篇文章将以博主理解的角度展开讲解&#xff0c; 特别是针对知识点的概念进行叙说&#xff0c;大部分文章将会对这些概念进行实际例子验证&#xff0c;以此达到加深对知识点的理解和掌握…