当前位置: 首页 > article >正文

基于Boost库、Jsoncpp、cppjieba、cpp-httplib等构建Boost搜索引擎

头像
⭐️个人主页:@小羊
⭐️所属专栏:项目
很荣幸您能阅读我的文章,诚请评论指点,欢迎欢迎 ~

动图描述

目录

    • 项目背景
    • 技术栈和项目环境
    • 正排索引和倒排索引
    • 数据去标签与清洗
      • 下载数据源
      • 去标签
    • 建立索引
      • 构建正排索引
      • 构建倒排索引
    • 建立搜索引擎
    • http_server 服务
    • 搜索到的内容有重复


文章末有源码链接。

项目背景

目前我们常用的搜索引擎有Google、百度、360等,这些搜索引擎都是超大型超完善的全网搜索,而本项目Boost搜索引擎只是一个非常简单的站内搜索。

比较维度全网搜索站内搜索
搜索范围与数据来源覆盖整个互联网,数据来源广泛,需搜索引擎爬虫抓取收录网页限定在特定网站内部,数据仅来源于该网站自身内容
搜索效率范围广、数据量大,检索复杂,速度相对较慢,结果筛选耗时搜索范围小,速度更快,可快速定位信息
可控性用户和网站管理者无法干涉搜索引擎算法,搜索结果不可控网站管理者可优化搜索功能,根据需求调整搜索算法等,具有可控性
索引构建需构建庞大复杂的索引系统处理海量数据,技术难度高针对特定网站内容和数据结构优化,索引构建相对简单且更具针对性

为什么选做Boost的搜索引擎呢?
作为C++选手,相信大家都浏览过Boost官网,而我们在2023年之前浏览Boost官网时是没有搜索功能的,虽然自从2023年起新增了搜索功能,但这之前给我们的不太好的浏览体验可能还是耿耿于怀,所以本项目选做Boost搜索引擎,算是弥补之前没有的遗憾吧(虽然肯定没有现在官网提供的好用🤡)。

我们最熟悉最常用的站内搜索cplusplus官网,当我们想查看 vector 的官方文档时可以直接在搜索框中搜索,就能得到我们想要的信息。

虽然我们无法独立实现向百度这样大型的搜索引擎,但是通过本项目Boost搜索引擎这个站内搜索小项目,可以做到管中窥豹的效果,大致了解像他们这样大型的工程整体框架是什么样的,是怎么运作的。

首先我们来看看百度搜索是以什么样的方式展示的:
在这里插入图片描述
可以看到基本有这三部分内容,(当然还有图片,为了简单我们就不展示图片了😜)那本项目也就模仿这样的格式展示搜索到的结果。

另外,当我们的搜索语句中有多个搜索关键词的时候,它是不严格匹配的,因此我们需要有一个切分搜索关键字的过程。这个任务可以借助 cppjieba 这个库来帮我们完成。

在这里插入图片描述
在这里插入图片描述

在这里插入图片描述

搜索引擎的宏观原理:
在这里插入图片描述

本项目实现的是红色框框中的内容。


技术栈和项目环境

  • 技术栈:C/C++、C++11、STL、Boost库、JsonCpp、cppjieba、cpp-httplib;
  • 项目环境:Ubuntu-22.04、vscode、gcc/g++、makefile。
  • cppjieba 是一个用 C++ 实现的中文分词库,它具有高效、准确、易用等特点;
  • cpp-httplib 是一个轻量级、跨平台的 C++ HTTP 库,它以单头文件的形式存在,使用起来非常便捷。

正排索引和倒排索引

首先我们通过一个例子来了解下什么是正排和倒排索引:

  • 文档1:小帅是安徽理工大学的三好学生
  • 文档2:小帅是安徽理工大学电信院的学生会主席

正排索引:从文档ID找到文档内容(文档中的关键字)。

文档ID文档内容
1小帅是安徽理工大学的三好学生
2小帅是安徽理工大学电信院的学生会主席

目标文档进行分词(方便建立倒排索引和查找):

  • 文档1:小帅、安徽理工大学、三好学生、学生
  • 文档2:小帅、安徽理工大学、电信院、学生、学生会、主席

倒排索引:根据文档内容分词,整理不重复的关键字,找到对应文档ID的方案。

关键字文档ID
小帅文档1、文档2
安徽理工大学文档1、文档2
三好学生文档1
学生文档1、文档2
电信院文档2
学生会文档2
主席文档2

当用户输入学生:倒排索引中查找 -> 提取文档ID -> 根据正排索引 -> 找到文档内容 ->
title+desc+url -> 构建响应结果。

文档1和文档2中都有学生这个关键字,那先显示谁呢?我们后面在搭建的时候会给每个文档设置权重。


数据去标签与清洗

下载数据源

首先从Boost官网下载数据源:
在这里插入图片描述

下载好后通过 rz -E 拉取到Ubuntu服务器上,然后 tar xzf 解压,我们只需要 boost_1_88_0\doc\html 中的内容,将所有内容拷贝到新目录中,其他的部分就可以删除掉了,得到下面这些文件:

在这里插入图片描述

把每个html文件名和路径保存起来,方便后续文件读取:

#include <iostream>
#include <string>
#include <vector>
#include <boost/filesystem.hpp>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;
}DocInfo_t;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) == false) // 判断路径是否存在{std::cerr << src_path << " not exists" << std::endl;return false;}fs::recursive_directory_iterator end;for (fs::recursive_directory_iterator it(root_path); it != end; it++){if (fs::is_regular_file(*it) == false) // 判断是否是普通文件{continue;}if (it->path().extension() != ".html") // 判断文件路径名是否符合要求{continue;}std::cout << "debug: " << it->path().string() << std::endl;files_list->push_back(it->path().string());}return true;
}bool ParseHtml(const std::vector<std::string> &files, std::vector<DocInfo_t> *results)
{return true;
}bool SaveHtml(const std::vector<DocInfo_t> &results, const std::string &output)
{return true;
}int main()
{// 1.把每个html文件名和路径保存起来,方便后续文件读取std::vector<std::string> files_list;if (EnumFile(src_path, &files_list) == false){std::cerr << "enum file name fail!" << std::endl;return 1;}// 2.按照files_list读取每个文件的内容,并进行解析std::vector<DocInfo_t> results;if (ParseHtml(files_list, &results) == false){std::cerr << "parse html fail!" << std::endl;return 2;}// 3.把解析完毕的各个文件内容写入到output中,按照 \3 作为每个文档的分隔符if (SaveHtml(results, output) == false){std::cerr << "save html fail!" << std::endl;return 3;}return 0;
}

Boost库不是C++标准库,因此在编写makefile时别忘了链接指定库哦:

cc=g++parser : parser.cc$(cc) -o $@ $^ -lboost_system -lboost_filesystem -std=c++11.PHONY:clean
clean:rm -f parser

通过打印调式,我们就能得到下面这些信息:
在这里插入图片描述


去标签

什么是标签?我们随便打开一个上面的文件:
在这里插入图片描述

  • 标签对我们搜索是没有价值的,因此需要去掉这些标签,剩下的内容就是我们需要的;
  • 我们的目标是把每个文档都去标签,然后把内容写入到同一个文件中,每个文档内容不需要任何换行,文档和文档之间用 \3 区分,这样做是为了读取的时候更方便;
  • 比如:title\3content\3url \n title\3content\3url...,用 getline(ifstream, line) 直接读取一个文档的全部内容,然后再根据 \3 获取各个部分。

按照files_list读取每个文件的内容,并进行解析:

bool ParseHtml(const std::vector<std::string> &files_list, std::vector<DocInfo_t> *results)
{for (const std::string &file : files_list){std::string result;if (yjz_util::FileUtil::ReadFile(file, &result) == false) continue;DocInfo_t doc;if (ParseTitle(result, &doc.title) == false) continue;if (ParseContent(result, &doc.content) == false) continue;if (ParseUrl(file, &doc.url) == false) continue;results->push_back(std::move(doc));}return true;
}

提取title:
在这里插入图片描述

static bool ParseTitle(const std::string &file, std::string *title)
{size_t begin = file.find("<title>");if (begin == std::string::npos) return false;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;
}

提取content,也就是去标签,我们只需要像下面这种白色的内容:
在这里插入图片描述

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; // 如果遇到'>',假设接下来是contentcase CONTENT : if (c == '<') s = LABLE; // 如果假设错误,状态重新转为lableelse{// 后面我们想用\n作为html解析后文本的分隔符if (c == '\n') c = ' ';content->push_back(c);}break;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_88_0/doc/html";std::string url_tail = file_path.substr(src_path.size());*url = url_head + url_tail;return true;
}

将解析的内容写入到指定的文件中:

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() == false){std::cerr << "open " << output << " fail!" << std::endl;return false;}for (auto &it : results){std::string out_string;out_string = it.title;out_string += SEP;out_string += it.content;out_string += SEP;out_string += it.url;out_string += '\n';out.write(out_string.c_str(), out_string.size());}out.close();return true;
}

建立索引

index.hpp 的基本结构:

namespace yjz_index
{struct DocInfo{std::string title;std::string content;std::string url;uint64_t doc_id;};struct InvertedElem{uint64_t doc_id;     // 文档IDstd::string word;    // 关键字int weight;          // 权重};class Index{private:Index(){}Index(const Index&) = delete;Index& operator=(const Index&) = delete;static Index *_instance;static std::mutex _mutex;public:using InvertedList = std::vector<InvertedElem>;~Index(){}// 获取单例static Index* GetInstance(){}// 根据文档ID找到文档内容DocInfo* GetForwardIndex(uint64_t doc_id){}// 根据关键字找到倒排拉链InvertedList* GetInvertedList(const std::string &word){}// 根据格式化后的文档,构建正排、倒排索引bool BuildIndex(const std::string &input){}private:// 构建正排索引DocInfo* BuildForwardIndex(const std::string &line){}// 构建倒排索引bool BuildInvertedIndex(const DocInfo &doc){}private:// 将数组下标作为文档IDstd::vector<DocInfo> _forward_index;  // 正排索引std::unordered_map<std::string, InvertedList> _inverted_index;};Index* Index::_instance= nullptr;std::mutex Index::_mutex;
}

构建正排索引

将符合特定格式的字符串解析并转化为结构化的文档信息对象,进而添加到正排索引数据结构(_forward_index 容器 )中,为后续基于文档信息的检索、分析等操作提供基础。

DocInfo* BuildForwardIndex(const std::string &line)
{// 解析line,字符串切分std::vector<std::string> results;const std::string sep = "\3";yjz_util::StringUtil::Split(line, &results, sep);if (results.size() != 3){return nullptr;}// 将切分好的字符串构建DocInfoDocInfo doc;doc.title = results[0];doc.content = results[1];doc.url = results[2];doc.doc_id = _forward_index.size(); // 先更新文档ID再插入_forward_index.push_back(std::move(doc));return &_forward_index.back();
}

构建倒排索引

对给定的文档进行分词处理,统计每个单词在标题和内容中的出现次数,计算每个单词的权重,然后将这些信息添加到倒排索引中。通过这种方式,可以快速查找包含特定单词的文档,并根据单词的权重对文档进行排序。

// 构建倒排索引
bool BuildInvertedIndex(const DocInfo &doc)
{// doc -> 倒排拉链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;yjz_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;yjz_util::JiebaUtil::CutString(doc.content, &content_words);for (auto &s : content_words){boost::to_lower(s); word_map[s].content_cnt++;}for (auto &word_pair : word_map){InvertedElem item;item.doc_id = doc.doc_id;item.word = word_pair.first;item.weight = 10 * word_pair.second.title_cnt + word_pair.second.content_cnt;InvertedList &inverted_list = _inverted_index[word_pair.first];inverted_list.push_back(std::move(item));}return true;
}

Boost库切分字符串:

static void CutString(const 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);
}
  • boost::split 函数:这是 Boost 库中的一个函数,用于将字符串按照指定的分隔符进行分割;
  • *result:通过解引用指针 result,将分割后的子字符串存储到该向量中;
  • line:待分割的输入字符串;
  • boost::is_any_of(sep):用于指定分割字符串时使用的分隔符;
  • boost::token_compress_on:这是一个分割标志,设置为 boost::token_compress_on 表示如果连续出现多个分隔符,会将它们视为一个分隔符进行处理,避免产生空的子字符串

建立搜索引擎

searcher.hpp 基本框架:

namespace yjz_searcher
{class Searcher{public:Searcher(){}~Searcher(){}struct InvertedElemPrint{uint64_t doc_id;int weight;std::vector<std::string> words; // 多个词对应同一个doc_idInvertedElemPrint() : doc_id(0), weight(0) {}};void InitSearcher(const std::string &input){// 获取或创建index对象_index = yjz_index::Index::GetInstance();std::cout << "获取或创建index单例成功!" << std::endl;_index->BuildIndex(input);std::cout << "建立正排和倒排索引成功!" << std::endl;}void Search(const std::string &query, std::string *json_string){// 1.分词,对query(搜索关键字)按要求进行分词// 2.触发,根据分好的词进行索引查找,关键字需要忽略大小写// 3.合并排序,汇总查找结果,按照权重排降序// 4.根据查找出来的结果,构建Json串}// 获取摘要std::string GetDesc(const std::string &html_content, const std::string &word){}private:yjz_index::Index *_index; };
}

编写Search函数:

void Search(const std::string &query, std::string *json_string)
{// 1.分词,对query(搜索关键字)按要求进行分词std::vector<std::string> words;yjz_util::JiebaUtil::CutString(query, &words);// 2.触发,根据分好的词进行索引查找,关键字需要忽略大小写yjz_index::Index::InvertedList inverted_list_all;for (auto word : words){boost::to_lower(word);yjz_index::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.合并排序,汇总查找结果,按照权重排降序std::sort(inverted_list_all.begin(), inverted_list_all.end(), [](const yjz_index::InvertedElem& e1, const yjz_index::InvertedElem& e2){return e1.weight > e2.weight;});// 4.根据查找出来的结果,构建Json串Json::Value root;for (auto &it : inverted_list_all){// 根据文档ID进行正排索引yjz_index::DocInfo *doc = _index->GetForwardIndex(it.doc_id); if (doc == nullptr) continue;Json::Value elem;elem["title"] = doc->title;elem["desc"] = GetDesc(doc->content, it.word);elem["url"] = doc->url;// for Debug// elem["id"] = it.doc_id;// elem["weight"] = it.weight;root.append(elem);}Json::StyledWriter writer;*json_string = writer.write(root);
}

获取摘要:找到word关键字在html_content中首次出现的位置,规定往前找50字节,往后找100字节,截取这部分内容。

因为我们在构建倒排索引和索引查找时将关键字统一转换为了小写,因此在原始数据中查找时也应该统一按小写字母查找。

  • search 函数定义在 <algorithm> 头文件中,用于在一个序列中查找另一个序列首次出现的位置,并支持自定义查找规则。
std::string GetDesc(const std::string &html_content, const std::string &word)
{const int pre_step = 50;const int next_step = 100;// 找到首次出现auto it = std::search(html_content.begin(), html_content.end(), word.begin(), word.end(), [](char x, char y){return std::tolower(x) == std::tolower(y);});if (it == html_content.end()) return "None1";int pos = std::distance(html_content.begin(), it);int start = 0;int end = html_content.size() - 1;start = std::max(start, pos - pre_step);end = std::min(end, pos + next_step);if (start >= end) return "None2";return html_content.substr(start, end - start);
}

我们想知道现在的搜索结果是不是按照我们预想的按照权重 weight 进行顺序呈现的呢?
search 函数中构建Json串时,我们把文档ID和权重加上进行测试:
在这里插入图片描述

下面是搜索结果:
在这里插入图片描述

在这里插入图片描述
可以看到是没有问题的。


http_server 服务

下载 cpp-httplib 库,然后直接参照给的示例编写我们想要的服务,非常简单。

#include "cpp-httplib/httplib.h"
#include "searcher.hpp"const std::string input = "data/raw_html/raw.txt";
const std::string root_path = "./wwwroot";int main()
{yjz_searcher::Searcher search;search.InitSearcher(input);httplib::Server svr;svr.set_base_dir(root_path);svr.Get("/s", [&search](const httplib::Request &req, httplib::Response &rsp){if (req.has_param("word") == false) {rsp.set_content("必须要有搜索关键字!", "text/plain: charset=utf-8");return;}std::string word = req.get_param_value("word");std::cout << "用户在搜索: " << word << std::endl;std::string json_string;search.Search(word, &json_string);rsp.set_content(json_string, "application/json");});svr.listen("0.0.0.0", 8081);return 0;
}

当然我们也可以自己搭建http服务。


到这里后端的工作基本已经完成了,那前端代码怎么办呢?我这里就直接让Deepseek帮我写了,如下:

<!DOCTYPE html>
<html lang="zh-CN">
<head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no"><script src="https://code.jquery.com/jquery-3.6.0.min.js"></script><link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css"><title>Boost 智能搜索引擎</title><style>:root {--primary-color: #4e6ef2;--hover-color: #3b5bdb;--background: #f8f9fa;--text-dark: #2d3436;--text-light: #636e72;}* {margin: 0;padding: 0;box-sizing: border-box;font-family: 'Segoe UI', system-ui, sans-serif;}body {background: var(--background);min-height: 100vh;padding: 2rem 1rem;}.container {max-width: 800px;margin: 0 auto;animation: fadeIn 0.5s ease;}.search-box {display: flex;gap: 10px;margin-bottom: 2rem;box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);border-radius: 30px;background: white;padding: 5px;}.search-input {flex: 1;padding: 1rem 1.5rem;border: none;border-radius: 30px;font-size: 1.1rem;color: var(--text-dark);transition: all 0.3s ease;}.search-input:focus {outline: none;box-shadow: 0 0 0 3px rgba(78, 110, 242, 0.2);}.search-btn {padding: 0 2rem;border: none;border-radius: 30px;background: linear-gradient(135deg, var(--primary-color), var(--hover-color));color: white;font-size: 1rem;font-weight: 600;cursor: pointer;transition: all 0.3s ease;display: flex;align-items: center;gap: 8px;}.search-btn:hover {background: var(--hover-color);transform: translateY(-1px);}.result-item {background: white;border-radius: 12px;padding: 1.5rem;margin-bottom: 1rem;box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);transition: transform 0.2s ease;}.result-item:hover {transform: translateX(5px);}.result-title {color: var(--primary-color);font-size: 1.2rem;font-weight: 600;margin-bottom: 0.5rem;text-decoration: none;display: flex;align-items: center;gap: 8px;}.result-title:hover {text-decoration: underline;}.result-desc {color: var(--text-dark);line-height: 1.6;margin-bottom: 0.5rem;display: -webkit-box;-webkit-line-clamp: 3;-webkit-box-orient: vertical;overflow: hidden;}.result-url {color: var(--text-light);font-size: 0.9rem;font-family: monospace;}.loading {text-align: center;padding: 2rem;color: var(--text-light);}@keyframes fadeIn {from { opacity: 0; transform: translateY(20px); }to { opacity: 1; transform: translateY(0); }}@media (max-width: 768px) {.search-box {flex-direction: column;border-radius: 15px;}.search-btn {padding: 1rem;justify-content: center;}}</style>
</head>
<body><div class="container"><div class="search-box"><input type="text" class="search-input" placeholder="请输入搜索关键词..." autofocus><button class="search-btn" onclick="search()"><i class="fas fa-search"></i>搜索</button></div><div class="result-container"></div></div><script>// 增强功能$(document).ready(() => {// 回车键搜索$('.search-input').keypress(e => e.which === 13 && search())// 输入框交互$('.search-input').focus(function() {if (this.value === "请输入搜索关键词...") this.value = ""}).blur(function() {if (this.value === "") this.value = "请输入搜索关键词..."})})function search() {const query = $('.search-input').val().trim()if (!query) return// 显示加载状态$('.result-container').html(`<div class="loading"><i class="fas fa-spinner fa-spin"></i>正在搜索中...</div>`)$.ajax({url: `/s?word=${encodeURIComponent(query)}`,method: 'GET',success: buildResults,error: () => {$('.result-container').html(`<div class="result-item" style="color: #dc3545;"><i class="fas fa-exclamation-triangle"></i>请求失败,请稍后重试</div>`)}})}function buildResults(data) {const container = $('.result-container').empty()if (data.length === 0) {container.html(`<div class="result-item"><div style="color: var(--text-light); text-align: center;"><i class="fas fa-search-minus"></i>没有找到相关结果</div></div>`)return}data.forEach(item => {const elem = $(`<div class="result-item"><a href="${item.url}" class="result-title" target="_blank"><i class="fas fa-link"></i>${item.title}</a><p class="result-desc">${item.desc}</p><div class="result-url">${item.url}</div></div>`)container.append(elem)})}</script>
</body>
</html>

最后结果展示:
在这里插入图片描述
可以看到非常完美,Deepseek写的页面还是非常好看的。

但是目前的代码还有一个不易察觉的问题,当我们输入搜索内容,通过 cppjieba 分词得到多个关键词,这些关键词可能都来自同一个文档,根据目前的代码每个关键词都会通过索引查找到这个文档,也就是说这个文档会给我们呈现多份,而我们希望得到的只是一个文档就行,因此接下来还需要优化一下去重的问题。


搜索到的内容有重复

下面是一个测试文件:
在这里插入图片描述

在这里插入图片描述
可以看到通过 cppjieba 分词然后通过每个关键词都索引到了这个文档,给我们重复呈现了四次。

接下来考虑如何去重。我们可以根据一个文档只有一个 doc_id 的特点,将所有 doc_id 相同的关键词统计到一起,权重累加。

struct InvertedElemPrint
{uint64_t doc_id;int weight;std::vector<std::string> words; // 多个词对应同一个doc_idInvertedElemPrint() : doc_id(0), weight(0) {}
};
//... void Search(const std::string &query, std::string *json_string)
{// 1.分词,对query(搜索关键字)按要求进行分词std::vector<std::string> words;yjz_util::JiebaUtil::CutString(query, &words);// 2.触发,根据分好的词进行索引查找,关键字需要忽略大小写// yjz_index::Index::InvertedList inverted_list_all;std::unordered_map<uint64_t, InvertedElemPrint> tokens_map; // 通过doc_id去重for (auto word : words){boost::to_lower(word);yjz_index::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]; // 根据doc_id找到相同的索引节点item.doc_id = elem.doc_id;item.weight += elem.weight; // 权重累加item.words.push_back(elem.word); // 将相同doc_id的关键词管理到一起}}std::vector<InvertedElemPrint> inverted_list_all; // 保存不重复的倒排拉链节点for (const auto &item : tokens_map){inverted_list_all.push_back(std::move(item.second));}// 3.合并排序,汇总查找结果,按照权重排降序// std::sort(inverted_list_all.begin(), inverted_list_all.end(), //     [](const yjz_index::InvertedElem& e1, const yjz_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串Json::Value root;for (auto &it : inverted_list_all){// 根据文档ID进行正排索引yjz_index::DocInfo *doc = _index->GetForwardIndex(it.doc_id); if (doc == nullptr) continue;Json::Value elem;elem["title"] = doc->title;elem["desc"] = GetDesc(doc->content, it.words[0]);elem["url"] = doc->url;// for Debug// elem["id"] = it.doc_id;// elem["weight"] = it.weight;root.append(elem);}// Json::StyledWriter writer;Json::FastWriter writer;*json_string = writer.write(root);
}

在这里插入图片描述
在这里插入图片描述
完成去重结果。

最后我们可以通过下面的指令将服务放到后台运行,方便我们随时搜索。

nohup ./http_server &

Boost搜索引擎源码


本篇文章的分享就到这里了,如果您觉得在本文有所收获,还请留下您的三连支持哦~

头像

相关文章:

基于Boost库、Jsoncpp、cppjieba、cpp-httplib等构建Boost搜索引擎

⭐️个人主页&#xff1a;小羊 ⭐️所属专栏&#xff1a;项目 很荣幸您能阅读我的文章&#xff0c;诚请评论指点&#xff0c;欢迎欢迎 ~ 目录 项目背景技术栈和项目环境正排索引和倒排索引数据去标签与清洗下载数据源去标签 建立索引构建正排索引构建倒排索引 建立搜索引擎h…...

Qt 通过控件按钮实现hello world + 命名规范(7)

文章目录 使用编辑框来完成 hello world通过编辑图形化界面方式通过纯代码方式 通过按钮的方式来创建 hello world通过编辑图形化界面方式通过纯代码方式 总结Qt Creator中的快捷键如何使用文档命名规范 简介&#xff1a;这篇文章着重点并不在于创建hello world程序&#xff0c…...

JAVA继承中变量和方法的存储和方法中访问变量的顺序

一、变量归属与内存位置 static 变量&#xff1a;属于类&#xff0c;只存在一份&#xff0c;保存在方法区&#xff08;或元空间&#xff09;。 实例变量&#xff08;非static&#xff09;&#xff1a;属于对象&#xff0c;每个对象单独一份&#xff0c;保存在堆内存中。 二、…...

关于大数据的基础知识(二)——国内大数据产业链分布结构

成长路上不孤单&#x1f60a;&#x1f60a;&#x1f60a;&#x1f60a;&#x1f60a;&#x1f60a; 【14后&#x1f60a;///计算机爱好者&#x1f60a;///持续分享所学&#x1f60a;///如有需要欢迎收藏转发///&#x1f60a;】 今日分享关于大数据的基础知识&#xff08;二&a…...

【Python 字典(Dictionary)】

Python 中的字典&#xff08;Dictionary&#xff09;是最强大的键值对&#xff08;key-value&#xff09;数据结构&#xff0c;用于高效存储和访问数据。以下是字典的核心知识点&#xff1a; 一、基础特性 键值对存储&#xff1a;通过唯一键&#xff08;Key&#xff09;快速访…...

Linux 内核中的 security_sk_free:安全模块与 Socket 释放机制解析

引言 在 Linux 内核中,网络通信和进程间交互(IPC)的核心数据结构之一是 struct sock(即 socket)。其生命周期管理涉及复杂的资源分配与释放逻辑。本文聚焦于 security_sk_free 这一函数,探讨其作用、调用场景以及与安全模块的交互机制,并解答一个常见疑问:在单机间 TC…...

VBA -- 学习Day4

数组 创建数组&#xff1a; Dim 数组名&#xff08;数组元素上下角标&#xff09;[As 元素类型] eg. Dim MyArray (1 To 3) As Integer 注意&#xff1a;1.如果不指定元素类型&#xff0c;则是Variant类型 向数组赋值&#xff1a; eg. MyArray(1) 100 MyArray(2) 200…...

Winform(11.案例讲解1)

今天写两个案例,用于更好的理解控件的使用 在写之前先写一个类 using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; namespace _1.案例讲解 { internal class Student { public string …...

电机密集型工厂环境下的无线通信技术选型与优化策略

点击下面图片带您领略全新的嵌入式学习路线 &#x1f525;爆款热榜 88万阅读 1.6万收藏 在电机、变频器、电焊机等强电磁干扰源遍布的工业环境中&#xff0c;无线通信系统的可靠性面临严峻挑战。本文从抗干扰能力、传输稳定性、实时性需求三大核心维度出发&#xff0c;结合工…...

Web 自动化之 HTML JavaScript 详解

文章目录 一、HTML 常用标签二、javascript 脚本1、什么是 javascript(js)2、 js变量和函数3、js 弹窗处理4、js 流程控制语句和 switch 结构语句应用 一、HTML 常用标签 HTML&#xff1a;超文本标记语言 超文本&#xff1a;不仅只包含文字&#xff0c;还有超链接、视频…这些…...

一文了解氨基酸的分类、代谢和应用

氨基酸&#xff08;Amino acids&#xff09;是在分子中含有氨基和羧基的一类化合物。氨基酸是生命的基石&#xff0c;人类所有的疾病与健康状况都与氨基酸有直接或间接的关系。氨基酸失衡可引起肝硬化、神经系统感染性疾病、糖尿病、免疫性疾病、心血管疾病、肾病、肿瘤等各类疾…...

系统学习算法:动态规划(斐波那契+路径问题)

题目一&#xff1a; 思路&#xff1a; 作为动态规划的第一道题&#xff0c;这个题很有代表性且很简单&#xff0c;适合入门 先理解题意&#xff0c;很简单&#xff0c;就是斐波那契数列的加强版&#xff0c;从前两个数变为前三个数 算法原理&#xff1a; 这五步可以说是所有…...

jquery实现文字点选验证码

文字点选验证码是一种有效的防止自动化攻击的手段。用户需要按照提示顺序点击特定的文字&#xff0c;验证通过后才能进行下一步操作。本文将详细介绍如何使用jQuery实现这种验证码。 一、实现思路 生成验证码&#xff1a;随机生成一组文字&#xff0c;并随机排列在验证码区域…...

VTK|加载ply文件数据进行平移+高程渲染

文章目录 将 .ply 点云或模型数据进行 Elevation 着色并可视化渲染的完整流程&#x1f7e6; 1. **使用 ElevationFilter 给模型上色&#xff08;根据 Z 值&#xff09;**&#x1f7e9; 2. **构造 Jet 风格的 Lookup Table&#xff08;颜色映射表&#xff09;**&#x1f537; 3.…...

JAVA房屋租售管理系统房屋出租出售平台房屋销售房屋租赁房屋交易信息管理源码

一、源码描述 这是一套房屋租售管理源码&#xff0c;基于SpringBootVue框架&#xff0c;后端采用JAVA开发&#xff0c;源码功能完善&#xff0c;涵盖了房屋租赁、房屋销售、房屋交易等业务。 二、源码截图...

掌握Multi-Agent实践(三):ReAct Agent集成Bing和Google搜索功能,采用推理与执行交替策略,增强处理复杂任务能力

一个普遍的现象是,大模型通常会根据给定的提示直接生成回复。对于一些简单的任务,大模型或许能够较好地应对。然而,当我们面对更加复杂的任务时,往往希望大模型能够表现得更加“智能”,具备适应多样场景和解决复杂问题的能力。为此,AgentScope 提供了内置的 ReAct 智能体…...

AOP实现原理

AOP实现原理 背景实现常用注解 背景 感觉需要掌握, 对理解其他知识点有好处. 实现 动态代理实现. JDK 实现 InvacationHander CGLib Enhancer 轻量级的基于ASM字节码框架. 常用注解 Before After AfterRetruning AfterThrowing Around...

Bearer Token的神秘面纱:深入解析HTTP认证头的设计哲学

为何有些Token会带Bearer&#xff1f; 在接口测试与开发中&#xff0c;我们经常会遇到这样的请求头&#xff1a; Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9... 这个神秘的"Bearer"前缀从何而来&#xff1f;为何不直接使用Authorization: Token..…...

【国产化】在银河麒麟ARM环境下离线安装docker

1、前言 采用离线安装的方式。 关于离线安装的方式官网有介绍&#xff0c;但是说的很简单&#xff0c;网址&#xff1a;Binaries | Docker Docs 官网介绍的有几种主流linux系统的安装方式&#xff0c;但是没有kylin的&#xff0c;所以在此记录一下。 在安装过程中也遇到了些…...

K8S - Harbor 镜像仓库部署与 GitLab CI 集成实战

引言 在 Kubernetes 环境中&#xff0c;容器镜像的存储与管理至关重要。企业级镜像仓库&#xff08;如 Harbor&#xff09;为团队提供了安全、稳定、可扩展的镜像管理解决方案。 一、Harbor 安装与配置 Harbor 是由 VMware 开源的企业级云原生镜像仓库&#xff0c;它不仅支持…...

ASCII码的快速记忆方法

当然&#xff01;记住ASCII码的关键是找到规律和分组记忆。以下是一些快速记忆的方法&#xff1a; 1. 记住关键分界点 0~31&#xff1a;控制字符&#xff08;不可打印&#xff0c;如换行、制表符等&#xff09;&#xff0c;不需要全记&#xff0c;知道0是NULL&#xff0c;10是…...

java volatile关键字

volatile 是 Java 中用于保证多线程环境下变量可见性和禁止指令重排序的关键字。 普通变量不加volatile修饰有可见性问题&#xff0c;即有线程修改该变量值&#xff0c;其他线程无法立即感知该变量值修改了。代码&#xff1a; private static int intVal 0; // 普通变量未加 …...

解决SQL Server SQL语句性能问题(9)——正确使用索引

前述章节中,我们介绍和讲解了SQL调优所需要的基本知识和分析方法,那么,通过前述这些知识和方法定位到问题后,接下来,我们该怎么做呢?那就是本章的内容,给出解决SQL语句性能问题的、科学而合理的方案和方法。 本章主要对解决SQL语句性能问题的几种常用方法进行说明和讲解…...

Vibe Coding: 优点与缺点

如果你最近在开发圈子里,你很可能听说过这个新趋势"vibe coding"(氛围编程)。 我只能说我对此感受复杂。以下是原因。 优势 在构建新项目时,靠着氛围编程达到成功感觉很自由!但对于遗留代码来说情况就不同了,尽管也不是不可能。 实时反馈和快速迭代 Cursor(…...

技术分享 | 如何在2k0300(LoongArch架构)处理器上跑通qt开发流程

近期迅为售后团队反馈&#xff0c;许多用户咨询&#xff1a;2K0300处理器采用了LA264处理器核&#xff0c;若要在该处理器上运行Qt程序&#xff0c;由于架构发生了变化&#xff0c;其使用方法是否仍与ARM平台保持一致&#xff1f; 单纯回答‘一致’或‘不一致’缺乏说服力&…...

产品经理如何借助 DeepSeek 提升工作效能

在数字化时代的浪潮中&#xff0c;产品经理肩负着推动产品从概念到成功落地的重任&#xff0c;面临着复杂多变的市场环境、层出不穷的用户需求以及紧锣密鼓的项目周期。而 DeepSeek 这一先进的人工智能工具&#xff0c;宛如一把 “瑞士军刀”&#xff0c;为产品经理在各个工作环…...

基于卷积神经网络和Pyqt5的猫狗识别小程序

任务描述 猫狗分类任务&#xff08;Dogs vs Cats&#xff09;是Kaggle平台在2013年举办的一个经典计算机视觉竞赛。官方给出的Kaggle Dogs vs Cats 数据集中包括由12500张猫咪图片和12500张狗狗图片组成的训练集&#xff0c;12500张未标记照片组成的测试集。选手需要在规定时间…...

Hadoop 和 Spark 生态系统中的核心组件

以下是 Hadoop 和 Spark 生态系统的核心组件及其功能&#xff1a; Hadoop 生态核心组件 1. HDFS&#xff08;Hadoop 分布式文件系统&#xff09; - 命令/工具&#xff1a; hdfs 命令&#xff08;如 hdfs dfs -put 等&#xff09;。 - 作用&#xff1a;分布式存储海量数据&a…...

解锁健康养生新境界

在追求高品质生活的当下&#xff0c;健康养生早已超越 “治未病” 的传统认知&#xff0c;成为贯穿全生命周期的生活艺术。它如同精密的交响乐&#xff0c;需饮食、运动、心理与生活习惯多维度协奏&#xff0c;方能奏响生命的强音。 饮食养生讲究 “顺时、适性”。遵循二十四节…...

MQTT:轻量级物联网通信协议详解

引言 在物联网&#xff08;IoT&#xff09;迅速发展的今天&#xff0c;设备之间的高效通信变得至关重要。MQTT&#xff08;Message Queuing Telemetry Transport&#xff09;作为一种轻量级的发布/订阅消息传输协议&#xff0c;因其低带宽、低延迟和易于实现的特性&#xff0c…...