一、项目前置知识

1、项目背景

我们上网会浏览很多网站,但是有些网站并没有搜索功能。因此就需要一个搜索引擎来改善网站用户体验、提高信息检索效率等。这里已boost库的网站为例,boost库没有搜索功能,可以自己写一个搜索引擎。

2、搜索引擎的相关原理

客户端通过发送http请求的方式进行搜索

服务器将结果构建成html返回

3、正排索引(foward index)

正排索引是文档ID与文档内容的映射。

文档 ---> 单词

基本结构可以理解为:

docID1 -> word1、word2

docID2-> word、word2、word3

4、倒排索引

倒排索引是文档关键字与文档ID的映射。

单词 ---> 文档

根据文档内容,分词,整理不重复的各个关键字,对应联系到文档ID的方案

二、项目正文

1.数据去标签与数据清洗

html的标签对我们来说没有价值,所以去掉这些标签。

只保留标签的内容

代码结构:

#include

#include

#include

#include

#include "util.hpp"

#include "Log.hpp"

// using namespace std;

// html路径

const std::string input = "data/input";

const std::string output = "data/output/raw.bin";

typedef struct HtmlInfo

{

std::string title;

std::string content;

std::string url;

} HtmlInfo_t;

bool EnumFile(const std::string &input, std::vector *files_list);

bool Parse(const std::vector &files_list, std::vector *results);

bool Save(const std::vector &results, const std::string &output);

int main()

{

// 文件带路径存到file_list

std::vector files_list;

if (!EnumFile(input, &files_list))

{

//std::cerr << "enum file name err" << std::endl;

lg(Error, "enum file name error");

return 1;

}

// 解析file_list里面的内容

std::vector results;

if (!Parse(files_list, &results))

{

//std::cerr << "parse err" << std::endl;

lg(Error, "parse error");

return 2;

}

// 解析完把结果存到output里面,使用\3作为分隔符

if (!Save(results, output))

{

//std::cerr << "save err" << std::endl;

lg(Error, "save error");

return 3;

}

return 0;

}

提取title

<开头,>结尾的数据是标签全部去掉。

构建url

根据官网url拼接自己的url

例如:官网url:https://www.boost.org/doc/libs/1_84_0/doc/html/accumulators.html

url_head = "https://www.boost.org/doc/libs/1_84_0/doc/html";

url_tail = /accumulators.html(删除自己目录前缀)

url = url_head + url_tail ; 相当于形成了一个官网链接

将结果写入到文件中

使用'\3'分割数据的模块

使用'\n'分割数据

2.建立索引

代码结构

#include

#include

#include

#include

#include

#include

#include "util.hpp"

#include "Log.hpp"

namespace ns_index

{

struct DocInfo

{

std::string title; // 文档标题

std::string content; // 内容

std::string url; // 官网URL

int doc_id; // 文档ID

};

struct InvertedElem

{

uint64_t doc_id;

std::string keyword;

int weight;

};

typedef std::vector Inverted;

class index

{

public:

static index* GetInstance()

{

if(nullptr == instance)

{

_lock.lock();

if(nullptr == instance)

{

instance = new index();

}

_lock.unlock();

}

return instance;

}

DocInfo *GetForwardIndex(uint64_t doc_id)

{

}

// 倒排拉链

Inverted *GetInvertedList(const std::string &key)

{

}

// 构建倒排正排索引

bool BuildIndex(const std::string &input)

{

}

private:

DocInfo *BuildForwarIndex(const std::string &line)

{

}

public:

~index()

{}

private:

index()

{}

index(const index&) = delete;

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

private:

static index *instance;

static std::mutex _lock;

// 正排用数组,下标表示ID

std::vector forward_index;

// 倒排HASH表,关键字和倒排拉链的映射关系

std::unordered_map inverted_index;

};

index * index::instance = nullptr;

std::mutex index::_lock;

}

建立正排

DocInfo *BuildForwarIndex(const std::string &line)

{

// split string

std::string SEP = "\3";

std::vector results;

util::StringUtil::SplitString(line, &results, SEP);

if (results.size() != 3)

{

return nullptr;

}

// fill DocInfo

DocInfo doc;

doc.title = results[0];

doc.content = results[1];

doc.url = results[2];

doc.doc_id = forward_index.size();

// insert forward_index

forward_index.emplace_back(std::move(doc));

return &forward_index.back();

}

建立倒排

bool BuildInvertedIndex(const DocInfo &doc)

{

struct word_cnt

{

word_cnt()

: title_cnt(0), content_cnt(0)

{

}

int title_cnt;

int content_cnt;

};

std::unordered_map word_map;

//标题分词

std::vector title_words;

util::JiebaUtil::CutString(doc.title, &title_words);

for(auto s : title_words)

{

boost::to_lower(s);

word_map[s].title_cnt++;

}

//内容分词

std::vector content_cnt;

util::JiebaUtil::CutString(doc.content, &content_cnt);

for(auto s : content_cnt)

{

boost::to_lower(s);

word_map[s].content_cnt++;

}

#define W1 2

#define W2 1

//填充

for(auto &pair : word_map)

{

InvertedElem elem;

elem.doc_id = doc.doc_id;

elem.keyword = pair.first;

elem.weight = pair.second.title_cnt * W1 + pair.second.content_cnt * W2; //权重

inverted_index[pair.first].push_back(std::move(elem));

}

return true;

}

3.编写搜索模块

主要完成:

1.把用户搜索的关键字分词

2.根据关键字查找索引

3.按照权重排序查找结果

4.把查找结果构建成json

#pragma once

#include "index.hpp"

#include "util.hpp"

#include

#include

namespace searcher

{

struct UltInvertedElem

{

uint64_t doc_id;

int weight;

std::vector keys;

UltInvertedElem()

: doc_id(0), weight(0)

{

}

};

class Searcher

{

public:

Searcher() {}

~Searcher() {}

public:

void InitSearcher(const std::string &input)

{

}

// seek用户搜索关键字,json_string给用户返回的结果

void Search(const std::string &seek, std::string *json_string)

{

}

//摘要

std::string getdesc(const std::string &html_content, const std::string &key)

{

//可以自定义摘要

}

private:

ns_index::index *index;

};

};

4.编写http搜索服务器模块

使用cpp-httplib库

代码示例:

const std::string input = "data/output/raw.bin";

const std::string root_dir = "./rootdir";

int main()

{

searcher::Searcher search;

search.InitSearcher(input);

httplib::Server svr;

svr.set_base_dir(root_dir.c_str());

svr.Get("/s", [&search](const httplib::Request &req, httplib::Response &res)

{

if(!req.has_param("word"))

{

res.set_content("请输入关键字!", "text/plain;charset=utf-8");

return;

}

std::string word = req.get_param_value("word");

// std::cout << "搜索: " << word <

std::string json_string;

search.Search(word, &json_string);

res.set_content(json_string, "application/json");

});

svr.listen("0.0.0.0", 8080);

return 0;

}

5.编写前端模块

这里前端代码使用chat-gpt 3.5编写完成。

html css效果

代码示例

HTML

搜索

CSS

@charset "utf-8";

* {

padding: 0;

margin: 0;

}

html,

body {

font-family: Arial, sans-serif;

background-color: #f2f2f2;

height: 100%;

}

.container {

max-width: 800px;

margin: 50px auto;

padding: 20px;

background-color: #fff;

border-radius: 8px;

box-shadow: 0px 0px 10px rgba(0, 0, 0, 0.1);

}

.container .search {

position: relative;

margin-bottom: 20px;

}

.container .search input {

width: 780px;

padding: 10px;

border: 1px solid #ddd;

border-radius: 5px;

font-size: 16px;

}

.container .search button {

position: absolute;

right: 0;

top: 0;

padding: 10px 20px;

background-color: #1342c3;

border: none;

border-radius: 0 5px 5px 0;

color: #fff;

font-size: 16px;

cursor: pointer;

}

.container .result {

list-style-type: none;

padding: 0;

margin: 0;

}

.container .result .item {

margin-bottom: 10px;

padding: 10px;

background-color: #f9f9f9;

border-radius: 5px;

border: 1px solid #ddd;

}

.container .result .item a {

display: block;

text-decoration: none;

font-size: 20px;

color: #4e6ef2;

margin-bottom: 10px;

padding: 10px;

background-color: #f9f9f9;

border-radius: 5px;

border: 1px solid #ddd;

}

.container .result .item a:hover {

text-decoration: underline;

background-color: #e9e9e9;

}

.container .result .item p {

display: inline-block;

margin-top: 5px;

font-size: 16px;

font-family: 'Lucida Sans', 'Lucida Sans Regular', 'Lucida Grande', 'Lucida Sans Unicode', Geneva, Verdana, sans-serif;

}

.container .result .item i {

display: block;

font-style: normal;

color: green;

}

JS

function Search(){

let query = $(".container .search input").val();

console.log("query = " + query);

$.ajax({

type: "GET",

url: "/s?word=" + query,

success: function(data){

console.log(data);

BuildHtml(data);

}

});

}

function BuildHtml(data){

let result_lable = $(".container .result");

result_lable.empty();

for( let elem of data){

let a_lable = $("", {

text: elem.title,

href: elem.url,

target: "_blank"

});

let p_lable = $("

", {

text: elem.desc

});

let i_lable = $("", {

text: elem.url

});

let div_lable = $("