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

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

目录


文章末有源码链接。

项目背景

目前我们常用的搜索引擎有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文件名和路径保存起来,方便后续文件读取:

cpp 复制代码
#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时别忘了链接指定库哦:

cpp 复制代码
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读取每个文件的内容,并进行解析:

cpp 复制代码
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:

cpp 复制代码
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 ,也就是去标签,我们只需要像下面这种白色的内容:

cpp 复制代码
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; // 如果遇到'>',假设接下来是content
		case CONTENT : 
			if (c == '<') s = LABLE; // 如果假设错误,状态重新转为lable
			else
			{
				// 后面我们想用\n作为html解析后文本的分隔符
				if (c == '\n') c = ' ';
				content->push_back(c);
			}
			break;
		default : break;
		}
	}
	return true;
}

构建URL:

cpp 复制代码
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;
}

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

cpp 复制代码
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 的基本结构:

cpp 复制代码
namespace yjz_index
{
    struct DocInfo
    {
        std::string title;
        std::string content;
        std::string url;
        uint64_t doc_id;
    };

    struct InvertedElem
    {
        uint64_t doc_id;     // 文档ID
        std::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:
        // 将数组下标作为文档ID
        std::vector<DocInfo> _forward_index;  // 正排索引
        std::unordered_map<std::string, InvertedList> _inverted_index;
    };
    Index* Index::_instance= nullptr;
    std::mutex Index::_mutex;
}

构建正排索引

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

cpp 复制代码
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;
    }
    // 将切分好的字符串构建DocInfo
    DocInfo 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();
}

构建倒排索引

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

cpp 复制代码
// 构建倒排索引
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库切分字符串:

cpp 复制代码
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 基本框架:

cpp 复制代码
namespace yjz_searcher
{
    class Searcher
    {
    public:
        Searcher(){}
        ~Searcher(){}

        struct InvertedElemPrint
        {
            uint64_t doc_id;
            int weight;
            std::vector<std::string> words; // 多个词对应同一个doc_id
            InvertedElemPrint() : 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函数:

cpp 复制代码
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> 头文件中,用于在一个序列中查找另一个序列首次出现的位置,并支持自定义查找规则。
cpp 复制代码
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 库,然后直接参照给的示例编写我们想要的服务,非常简单。

cpp 复制代码
#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服务。

cpp 复制代码

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

cpp 复制代码
<!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 相同的关键词统计到一起,权重累加。

cpp 复制代码
struct InvertedElemPrint
{
    uint64_t doc_id;
    int weight;
    std::vector<std::string> words; // 多个词对应同一个doc_id
    InvertedElemPrint() : 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);
}


完成去重结果。

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

cpp 复制代码
nohup ./http_server &

Boost搜索引擎源码


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

相关推荐
在未来等你2 天前
Elasticsearch面试精讲 Day 9:复合查询与过滤器优化
大数据·分布式·elasticsearch·搜索引擎·面试
LQ深蹲不写BUG2 天前
ElasticSearch 基础内容深度解析
大数据·elasticsearch·搜索引擎
AIbase20243 天前
AI时代企业获取精准流量与实现增长的GEO新引擎
人工智能·搜索引擎·百度
小小菜鸟,可笑可笑3 天前
Bing 搜索引擎检索语法
搜索引擎
和科比合砍81分3 天前
ES模块(ESM)、CommonJS(CJS)和UMD三种格式
大数据·elasticsearch·搜索引擎
程序员小潘3 天前
Dubbo3.3 Triple协议处理东西向流量
dubbo
敲上瘾4 天前
Docker 容器核心指令与数据库容器化实践
linux·运维·服务器·docker·容器·eureka·dubbo
Elastic 中国社区官方博客4 天前
Elasticsearch 的 JVM 基础知识:指标、内存和监控
java·大数据·elasticsearch·搜索引擎·全文检索
Arthurmoo4 天前
Git常用命令大全:高效开发必备
大数据·elasticsearch·搜索引擎
SEO_juper4 天前
E-E-A-T与现代SEO:赢得搜索引擎信任的完整策略
前端·搜索引擎·seo·数字营销·seo优化·谷歌seo