一、项目前置知识
1、项目背景
我们上网会浏览很多网站,但是有些网站并没有搜索功能。因此就需要一个搜索引擎来改善网站用户体验、提高信息检索效率等。这里已boost库的网站为例,boost库没有搜索功能,可以自己写一个搜索引擎。
2、搜索引擎的相关原理
客户端通过发送http请求的方式进行搜索
服务器将结果构建成html返回
3、正排索引(foward index)
正排索引是文档ID与文档内容的映射。
文档 ---> 单词
基本结构可以理解为:
docID1 -> word1、word2
docID2-> word、word2、word3
4、倒排索引
倒排索引是文档关键字与文档ID的映射。
单词 ---> 文档
根据文档内容,分词,整理不重复的各个关键字,对应联系到文档ID的方案
二、项目正文
1.数据去标签与数据清洗
html的标签对我们来说没有价值,所以去掉这些标签。
只保留标签的内容
代码结构:
cpp
#include <iostream>
#include <string>
#include <vector>
#include <boost/filesystem.hpp>
#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<std::string> *files_list);
bool Parse(const std::vector<std::string> &files_list, std::vector<HtmlInfo_t> *results);
bool Save(const std::vector<HtmlInfo_t> &results, const std::string &output);
int main()
{
// 文件带路径存到file_list
std::vector<std::string> 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<HtmlInfo> 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.建立索引
代码结构
cpp
#include <iostream>
#include <string>
#include <vector>
#include <fstream>
#include <mutex>
#include <unordered_map>
#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<InvertedElem> 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<DocInfo> forward_index;
// 倒排HASH表,关键字和倒排拉链的映射关系
std::unordered_map<std::string, Inverted> inverted_index;
};
index * index::instance = nullptr;
std::mutex index::_lock;
}
建立正排
cpp
DocInfo *BuildForwarIndex(const std::string &line)
{
// split string
std::string SEP = "\3";
std::vector<std::string> 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();
}
建立倒排
cpp
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<std::string, word_cnt> word_map;
//标题分词
std::vector<std::string> 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<std::string> 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
cpp
#pragma once
#include "index.hpp"
#include "util.hpp"
#include <algorithm>
#include <jsoncpp/json/json.h>
namespace searcher
{
struct UltInvertedElem
{
uint64_t doc_id;
int weight;
std::vector<std::string> 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库
代码示例:
cpp
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::endl;
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
html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="stylesheet" href="index.css">
<script src="http://code.jquery.com/jquery-2.1.1.min.js"></script>
<script src="index.js"></script>
<title>搜索</title>
</head>
<body>
<div class="container">
<div class="search">
<input type="text" placeholder="请输入搜索关键字">
<button onclick="Search()">搜索一下</button>
</div>
<div class="result">
</div>
</div>
<script>
</script>
</body>
</html>
CSS
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
javascript
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 = $("<a>", {
text: elem.title,
href: elem.url,
target: "_blank"
});
let p_lable = $("<p>", {
text: elem.desc
});
let i_lable = $("<i>", {
text: elem.url
});
let div_lable = $("<div>", {
class: "item"
});
a_lable.appendTo(div_lable);
p_lable.appendTo(div_lable);
i_lable.appendTo(div_lable);
div_lable.appendTo(result_lable);
}
}