文章目录
- boost搜索引擎的意义
- 技术栈与项目环境
- 搜索引擎的原理
-
- 暂停词
- [正排索引(Forward Index)](#正排索引(Forward Index))
- [倒排索引(Inverted Index)](#倒排索引(Inverted Index))
- 数据去标签化
- 索引的建立
- 搜索引擎的构成
- 前端网页
- HTTP服务
- 项目展示
boost搜索引擎的意义
boost库是为C++语言标准库提供扩展的一些C++程序库的总称,由Boost社区组织开发、维护。Boost库可以与C++标准库完美共同工作,并且为其提供扩展功能。boost网站提供了大量的文档,实现一个搜索引擎可以帮助我们在大量的文档中准确快速找到自己所需求的文档。
技术栈与项目环境
- 技术栈
后端:C/C++、C++11、STL、准标准库Boost、Jsoncpp、cppjieba、cpp-httplib
前端:html5、css、js、jQuery、Ajax
- 项目环境
Centos7、vim、gcc(g++)/Makefile、vscode
搜索引擎的原理
搜索大量的文档以及文档中包含的内容,显然是非常耗时耗力的一种行为。如果直接去一个个去遍历访问,基本会导致服务长时间得不到响应。为此需要更见快速以及便捷的方式去规划和管理大量的数据,实现快速查找,建立索引是解决这个问题的核心。
所谓的索引,就是将文档贴上一个标签,根据标签去快速查找。管理标签相对于管理文档的压力要小很多,这是建立索引的本质原因。
暂停词
暂停词(Stop Words)是指在自然语言处理中(NLP)和信息检索中,为了提高处理效率和效果而被忽略或移除的常见词语。这些词通常是高频率出现但对文档内容或语义贡献不大的词汇。
特点
- 高频率出现:暂停词在文档中出现频率非常高,例如英语中的 "the", "is", "in", "and",以及中文中的 "的", "了", "在" 等。
- 低语义贡献:暂停词对文档内容的实际语义贡献较小,通常不能用来区分不同的文档。
作用
- 减少数据处理量:通过移除暂停词,可以显著减少要处理的词汇数量,从而提高处理速度和效率。
- 提升检索效果:暂停词的移除可以减少噪声,提升信息检索系统的查询结果的相关性和精确度。
正排索引(Forward Index)
正排索引,也称为正向索引,是将文档与其包含的词汇进行关联的索引结构。每个文档都包含一个词汇列表及其在文档中的位置信息。
原理
文档级别:每个文档都有一个唯一的标识符(如DocID)。
词汇表:每个文档包含一个词汇表,记录了该文档中的所有词汇及其位置。
例如,有两个文档如下:
文档ID | 文档内容 |
---|---|
1 | 苹果的电脑 |
2 | 苹果的手机 |
去掉暂停词之后,正排索引可以表示为:
文档ID | 文档内容 |
---|---|
1 | 苹果 电脑 |
2 | 苹果 手机 |
优点
- 易于理解和构建。
- 适合小规模数据的全文检索。
缺点
- 查询效率低。要查找某个词语出现在哪些文档中,需要遍历所有文档的词汇表。
倒排索引(Inverted Index)
倒排索引也称为反向索引,是将词汇与其所在的文档进行关联的索引结构。每个词汇都关联一个包含该词汇的文档列表及其位置。
原理
词汇级别:每个词汇都有一个唯一的标识符。
文档列表:每个词汇包含一个文档列表,记录了该词汇出现的所有文档及其位置。
例如,有两个文档如下:
文档ID | 文档内容 |
---|---|
1 | 苹果的电脑 |
2 | 苹果的手机 |
去掉暂停词之后,倒排索引可以表示为:
关键字(具有唯一性) | 文档ID |
---|---|
苹果 | 1、2 |
电脑 | 1 |
手机 | 2 |
使用倒排索引,此时用户搜索苹果,则会通过索引找到文档1和2。用户搜索电脑,则会找到文档1。
优点
- 查询效率高。可以快速查找到包含某个词汇的所有文档。
- 适合大规模数据的全文检索。
缺点
- 构建和维护复杂度高。需要处理大量数据和存储空间。(用户友好型)
数据去标签化
去标签
本项目的文档搜索范围均为html文件,均在https://www.boost.org/doc/libs/1_85_0/doc/html路径下,其中1_85_0是boost库的版本。
如下是该路径下的一个文件内容https://www.boost.org/doc/libs/1_85_0/doc/html/tools.html:
html
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title>Chapter 1. Boost.Accumulators - 1.85.0</title>
<link rel="stylesheet" href="../../doc/src/boostbook.css" type="text/css">
<meta name="generator" content="DocBook XSL Stylesheets V1.79.1">
<link rel="home" href="index.html" title="The Boost C++ Libraries BoostBook Documentation Subset">
<link rel="up" href="libraries.html" title="Part I. The Boost C++ Libraries (BoostBook Subset)">
<link rel="prev" href="libraries.html" title="Part I. The Boost C++ Libraries (BoostBook Subset)">
<link rel="next" href="accumulators/user_s_guide.html" title="User's Guide">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="icon" href="/favicon.ico" type="image/ico"/><link rel="stylesheet" type="text/css" href="/style-v2/section-basic.css"/></head>
<body bgcolor="white" text="black" link="#0000FF" vlink="#840084" alink="#0000FF"> <div id="boost-common-heading-doc">
<div class="heading-inner">
<div class="heading-placard"></div>
<h1 class="heading-title">
<a href="/">
<img src="/gfx/space.png" alt= "Boost C++ Libraries" class="heading-logo" />
<span class="heading-boost">Boost</span>
<span class="heading-cpplibraries">C++ Libraries</span>
</a></h1>
......
</body>
</html>
文件中的形如、
、 等处于两个小尖括号之间的是html的语法,本就是不需要的内容。这些标签一般成对出现,形如......。去标签化就是删除这些没有用处的信息,只保留下该html文件在网页所呈现的文字内容。
最终的效果:将每一个文档的标签删除,得到的内容放到一个网页正文文件的一行。
- 步骤1 读取html文件内容
- 步骤2 获取文档的标题、正文、URL
- 步骤3 将获取的信息连接成一行数据,写入网页正文文件。
代码如下
C++
#include<iostream>
#include <string.h>
#include <vector>
#include <boost/filesystem.hpp>
#include "util.hpp"
#include "log.hpp"
//所有的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;//文档网页的链接
}DocInfo_t;
//遍历文档
bool EnumFile(const std::string &src_path, std::vector<std::string>* file_path_list);
//去标签化
bool ParseHtml(const std::vector<std::string> &file_path_list, std::vector<DocInfo_t>* results);
//保存文档
bool SaveHtml(const std::vector<DocInfo_t> &results, const std::string &output);
int main()
{
std::vector<std::string> file_path_list;
//第一步,递归式的把每个html文件名和路径保存到file_path_list
if(!EnumFile(src_path,&file_path_list))
{
std::cerr<<"enum file error!"<<std::endl;
return 1;
}
//第二步,读取每个文件的内容,并进行解析
std::vector<DocInfo_t> results;
if(!ParseHtml(file_path_list,&results))
{
std::cerr<<"parse html error"<<std::endl;
return 2;
}
//第三步,把解析完毕的各个文件内容,输入到output
if(!SaveHtml(results,output))
{
std::cerr<< "save html error"<<std::endl;
return 3;
}
return 0;
}
bool EnumFile(const std::string &src_path, std::vector<std::string>* file_path_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++)
{
//判断是否是普通文件
if(!fs::is_regular_file(*iter))
{
continue;
}
//判断文件名的后缀是否符合要求
if(iter->path().extension()!=".html")
{
continue;
}
file_path_list->push_back(iter->path().string());
}
return true;
}
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{
if(c=='\n')
{
c = ' ';
}
content->push_back(c);
}
break;
default:
break;
}
}
return true;
}
static bool PaserUrl(const std::string& file_path, std::string* url)
{
std::string url_head = "https://www.boost.org/doc/libs/1_85_0/doc/html";
std::string url_tail = file_path.substr(src_path.size());
*url=url_head+url_tail;
return true;
}
bool ParseHtml(const std::vector<std::string> &file_path_list, std::vector<DocInfo_t>* results)
{
for(const std::string &file : file_path_list)
{
std::string result;
//1.读取文件。Read()
if(!ns_util::FileUtil::ReadFile(file,&result))
{
continue;
}
//2.解析指定的文件,提取title
DocInfo_t doc;
if(!ParseTitle(result,&doc.title))
{
continue;
}
//3.解析指定的文件,提取content
if(!ParseContent(result,&doc.content))
{
continue;
}
//4.解析指定的文件路径,构建URL
if(!PaserUrl(file,&doc.url))
{
continue;
}
results->push_back(std::move(doc));
}
return true;
}
bool SaveHtml(const std::vector<DocInfo_t> &results, const std::string &output_file_path)
{
#define SEP '\3'
//保存形式title\3content\3url\n
std::ofstream out(output_file_path,std::ios::out | std::ios::binary);
if(!out.is_open())
{
std::cerr << "oean "<<output_file_path <<" failed"<<std::endl;
}
//文件内容写入
for(auto &item : results)
{
std::string line=item.title;
line+=SEP;
line+=item.content;
line+=SEP;
line+=item.url;
line+='\n';
out.write(line.c_str(),line.size());
}
out.close();
return true;
}
其中涉及到的util.hpp以及log.hpp分别是常用分词工具以及日志功能。
常用工具类
util.hpp:
c++
#pragma once
#include <string>
#include <iostream>
#include <fstream>
#include <vector>
#include <unordered_map>
#include <mutex>
#include <boost/algorithm/string.hpp>
#include "cppjieba/Jieba.hpp"
#include "log.hpp"
namespace ns_util
{
class FileUtil
{
public:
static bool ReadFile(const std::string& file_path, std::string* out)
{
std::ifstream in(file_path,std::ios::in);
if(!in.is_open())
{
std::cerr<<"open file "<<file_path<<"error"<<std::endl;
return false;
}
std::string line;
while(std::getline(in,line))
{
*out+=line;
}
in.close();
return true;
}
};
class StringUtil
{
public:
static void Split(const std::string& target, std::vector<std::string> *out,std::string sep)
{
boost::split(*out,target,boost::is_any_of(sep),boost::token_compress_on);
}
};
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";
class JiebaUtil
{
private:
JiebaUtil():jieba(DICT_PATH,HMM_PATH,USER_DICT_PATH,IDF_PATH,STOP_WORD_PATH){}
JiebaUtil(const JiebaUtil&)=delete;
JiebaUtil& operator=(const JiebaUtil&)=delete;
static JiebaUtil *instance;
public:
static JiebaUtil* GetInstance()
{
static std::mutex mtx;
if(nullptr==instance)
{
mtx.lock();
if(nullptr==instance)
{
instance = new JiebaUtil();
instance->InitJiabaUtil();
}
mtx.unlock();
}
return instance;
}
void InitJiabaUtil()
{
std::ifstream in(STOP_WORD_PATH);
if(!in.is_open())
{
LOG(FATAL,"加载暂停词文件失败");
return;
}
std::string line;
while(std::getline(in,line))
{
stop_words.insert({line,true});
}
in.close();
}
void CutStringHelper(const std::string &src,std::vector<std::string>*out)
{
jieba.CutForSearch(src,*out);
for(auto iter=out->begin();iter!=out->end();){
auto it = stop_words.find(*iter);
if(it!=stop_words.end())
{
iter=out->erase(iter);
}else{
iter++;
}
}
}
public:
static void CutString(const std::string &src,std::vector<std::string>*out)
{
ns_util::JiebaUtil::GetInstance()->CutStringHelper(src,out);
}
private:
cppjieba::Jieba jieba;
std::unordered_map<std::string,bool> stop_words;
};
JiebaUtil *JiebaUtil::instance=nullptr;
}
FileUtil
类
ReadFile
方法:读取文件内容,并将其存储到out
字符串中。如果文件无法打开,则返回false
并输出错误信息。
StringUtil
类
Split
方法:使用 Boost 库的split
函数,根据指定的分隔符sep
将目标字符串target
分割成多个子字符串,并存储在out
向量中。
JiebaUtil
类
-
单例模式 :通过私有构造函数和静态指针
instance
实现单例模式,确保JiebaUtil
类只有一个实例。 -
初始化:
JiebaUtil()
构造函数:初始化 Jieba 分词器,加载相关词典。InitJiebaUtil
方法:加载暂停词(Stop Words)文件,将暂停词存储在stop_words
哈希表中。
-
分词功能:
CutStringHelper
方法:使用 Jieba 的CutForSearch
方法进行分词,并移除暂停词。
-
CutString
方法:对外提供分词接口,调用CutStringHelper
。
添加日志
log.hpp:
c++
#pragma once
#include <iostream>
#include <string>
#include <ctime>
#include <iomanip>
#include <sstream>
#include <fstream>
#include <mutex>
// 日志级别枚举
enum LogLevel {
NORMAL,
WARNING,
DEBUG,
FATAL
};
// 日志宏定义
#define LOG(LEVEL, MESSAGE) log(LEVEL, MESSAGE, __FILE__, __LINE__)
std::mutex logMutex;
// 获取当前时间的格式化字符串
std::string getCurrentTime() {
std::time_t now = std::time(nullptr);
std::tm* localTime = std::localtime(&now);
std::ostringstream timeStream;
timeStream << std::put_time(localTime, "%Y-%m-%d %H:%M:%S");
return timeStream.str();
}
// 将日志级别枚举转换为字符串
std::string logLevelToString(LogLevel level) {
switch(level) {
case NORMAL: return "NORMAL";
case WARNING: return "WARNING";
case DEBUG: return "DEBUG";
case FATAL: return "FATAL";
default: return "UNKNOWN";
}
}
// 日志记录函数
void log(LogLevel level, const std::string& message, const std::string& file, int line) {
std::lock_guard<std::mutex> guard(logMutex); // 保证线程安全
std::string currentTime = getCurrentTime();
std::cout << "[" << currentTime << "]"
<< "[" << logLevelToString(level) << "]"
<< "[" << message << "]"
<< "[" << file << " : " << line << "]" << std::endl;
}
索引的建立
根据已经去标签之后的数据,逐行获取每个文档的信息,分别建立正排索引以及倒排索引。正排索引是用来根据文档的id获取文档的内容,而倒排索引是用来进行快速搜索,在搜索模块会用到。
正排索引的建立
正排索引是文档id与文档内容的一对一映射关系,因此可以使用vector的数据结构来表示。
c++
struct DocInfo
{
std::string title;//文档的标题
std::string content;//文档的主体
std::string url;//文档网页的链接
std::uint64_t doc_id; //文档的ID
};
std::vector<DocInfo> forward_index;//正排索引
可以遍历网页正文文件的每一行,文档ID就是行号,也可以是加入正排索引前正排索引的大小。
c++
DocInfo* BuildForwardIndex(const std::string &line)
{
//1.解析line,进行字符串切分
std::vector<std::string> results;
const 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();
//3.插入到正排索引的vector
forward_index.push_back(std::move(doc));
return &forward_index.back();
}
倒排索引的建立
倒排索引用于搜索功能的实现,根据关键字,获取对应的文档ID,再通过文档ID结合正排索引,获得文档的内容。此时关键字对应的文档ID的个数很有可能有多个,形成一个拉链(顾名思义就是一串的文档ID)。但是光有文档ID是不够的,搜索所呈现出来的结果要有排列的顺序,因此需要对每一ID附加一个权重值,方便对查询结果进行排序。
c++
struct InvertedElem
{
std::uint64_t doc_id;
std::string word;
int weight;//权重
};
倒排拉链:
c++
typedef std::vector<InvertedElem> InvertedList;
倒排索引:
c++
std::unordered_map<std::string, InvertedList> inverted_index;
倒排索引紧跟在正排索引之后,当网页正文文件的某一行被加入到正排索引之后,会返回一个带有文档ID的DocInfo,此时可以统计该文档的词频,根据词频计算每个词的权重。
c++
//用于权重计算
#define X 10
#define Y 1
bool BuildInvertedIndex(const DocInfo &doc)
{
//1.词频统计
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 it : title_words)
{
boost::to_lower(it);//将分词全部转换成小写
word_map[it].title_cnt++;
}
//对文档内容进行分词并进行统计
std::vector<std::string> content_words;
ns_util::JiebaUtil::CutString(doc.content,&content_words);
for(auto it : content_words)
{
boost::to_lower(it);//将分词全部转换成小写
word_map[it].content_cnt++;
}
//进行权重计算
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;
}
正排索引和倒排索引结合,将网页正文文件的每一行(每一个文档)进行处理,完成所有索引的建立任务。
index.hpp:
c++
#pragma once
#include <iostream>
#include <string>
#include <vector>
#include <unordered_map>
#include <fstream>
#include <mutex>
#include "util.hpp"
#include "log.hpp"
#define X 10
#define Y 1
namespace ns_index
{
struct DocInfo
{
std::string title;//文档的标题
std::string content;//文档的主体
std::string url;//文档网页的链接
std::uint64_t doc_id; //文档的ID
};
struct InvertedElem
{
std::uint64_t doc_id;
std::string word;
int weight;//权重
};
typedef std::vector<InvertedElem> InvertedList; //倒排拉链
class Index
{
private:
Index(){}
Index(const Index&) = delete;
Index& operator=(const Index&) = delete;
static Index* instance;
static std::mutex locker;
public:
~Index(){}
public:
static Index* GetInstance()
{
if(nullptr == instance)
{
locker.lock();
if(nullptr == instance)
{
instance=new Index();
}
locker.unlock();
}
return instance;
}
//根据doc_id找出文档内容
DocInfo* GetForwardIndex(std::uint64_t doc_id)
{
if(doc_id>=forward_index.size())
{
std::cerr<<"doc_id:"<<doc_id<<" out of range"<<std::endl;
}
return &forward_index[doc_id];
}
//根据关键字string,获得倒排拉链
InvertedList*GetInvertedList(const std::string &word)
{
auto iter = inverted_index.find(word);
if(iter==inverted_index.end())
{
LOG(FATAL,word+" has no InvertedList");
return nullptr;
}
return &(iter->second);
}
//根据去标签之后的文档,构建正排和倒排索引
bool BuildIndex(const std::string &input)
{
std::ifstream in(input, std::ios::in | std::ios::binary);
if(!in.is_open())
{
std::cerr<<input<<" open error"<<std::endl;
return false;
}
std::string line;
int count=0;
while(std::getline(in,line))
{
DocInfo* doc = BuildForwardIndex(line);
if(nullptr == doc)
{
std::cerr<<"build "<<line<<" error"<<std::endl;
continue;
}
BuildInvertedIndex(*doc);
count++;
if(count%100==0)
{
LOG(NORMAL,"当前已经建立的索引数量:"+std::to_string(count));
}
}
LOG(NORMAL,"当前已经建立的索引数量:"+std::to_string(count));
return true;
}
private:
DocInfo* BuildForwardIndex(const std::string &line)
{
//...
}
bool BuildInvertedIndex(const DocInfo &doc)
{
//...
}
private:
std::vector<DocInfo> forward_index;//正排索引
std::unordered_map<std::string, InvertedList> inverted_index;//倒排索引
};
Index* Index::instance=nullptr;
std::mutex Index::locker;
}
搜索引擎的构成
搜索引擎作为服务器的核心模块,完成用户给出的搜索词的搜索和排序功能。其结构如下:
初始化
在 InitSearcher
方法中,初始化 Searcher
类并加载索引数据:
- 获取索引实例 :使用单例模式获取
Index
类的唯一实例。 - 构建索引 :调用
BuildIndex
方法,从指定输入文件中构建正排索引和倒排索引。
查询处理
Search
方法是模块的核心,用于处理查询请求:
- 分词 :利用
JiebaUtil
对输入查询进行中文分词,生成关键词列表。 - 索引查找 :根据关键词在倒排索引中查找相关文档,将结果存储在
tokens_map
中,并累加每个文档的权重。 - 结果排序 :将查找到的文档按相关性(权重)降序排序,存储在
inverted_list_all
中。 - JSON构建:将排序后的结果转化为 JSON 格式的字符串,包含文档标题、简介和链接等信息。
c++
//根据查询语句,搜索文档
void Search(std::string &query, std::string *json_string)
{
//1.对查询的语句进行分词
std::vector<std::string> words;
ns_util::JiebaUtil::CutString(query,&words);
//2.根据各个分词,使用index索引进行查找
std::vector<InvertedListPrint> inverted_list_all;
std::unordered_map<uint64_t,InvertedListPrint> tokens_map;
//由于多个word可能映射到同一个文档,为了避免查询结果中文档重复,应将重复的文档合并成一个文档。
//(这里的合并指的是权重累加,文档内容不变。)
for(std::string word : words)
{
boost::to_lower(word);
ns_index::InvertedList *inverted_list = index->GetInvertedList(word);
if(nullptr==inverted_list)
{
continue;
}
for(auto &elem: *inverted_list)
{
auto &item = tokens_map[elem.doc_id];
item.doc_id=elem.doc_id;
//关键词对应的文档相同,则累加该文档的权重值
item.weight+=elem.weight;
item.words.push_back(elem.word);
}
}
for(const auto& elem : tokens_map)
{
inverted_list_all.push_back(std::move(elem.second));
}
//3.汇总查找结果,按照相关性进行降序排序
std::sort(inverted_list_all.begin(),inverted_list_all.end(),\
[](const InvertedListPrint &e1, const InvertedListPrint &e2){return e1.weight>e2.weight;});
//4.根据查找结果,构建json串
Json::Value root;
for(auto &item:inverted_list_all)
{
ns_index::DocInfo* doc = index->GetForwardIndex(item.doc_id);
if(nullptr==doc)
{
continue;
}
Json::Value elem;
elem["title"]=doc->title;
elem["desc"]=GetDesc(doc->content,item.words[0]);//获取关键词的相关简介
elem["url"]=doc->url;
elem["weight"]=item.weight;
root.append(elem);
}
Json::FastWriter writer;
*json_string=writer.write(root);
}
简介生成
GetDesc
方法用于从文档内容中提取包含查询词的片段,生成简介:
- 查找关键词:在文档内容中查找查询词的位置。
- 提取片段:从关键词位置前后一定范围内提取内容片段,作为简介。
seacher.hpp:
c++
#pragma once
#include <iostream>
#include <algorithm>
#include <jsoncpp/json/json.h>
#include "index.hpp"
#include "util.hpp"
#include "log.hpp"
namespace ns_seacher
{
struct InvertedListPrint
{
uint64_t doc_id;
int weight;
std::vector<std::string> words;
};
class Searcher
{
public:
Searcher(){}
~Searcher(){}
public:
void InitSearcher(const std::string &input)
{
//1.获取Index对象
index=ns_index::Index::GetInstance();
LOG(NORMAL,"获取单例索引成功");
//2.建立索引
index->BuildIndex(input);
LOG(NORMAL,"建立正排和倒排索引成功");
}
//根据查询语句,搜索文档
void Search(std::string &query, std::string *json_string)
{
//...
}
//获取搜索词的简介
std::string GetDesc(const std::string &content, const std::string &word)
{
//根据
auto it = std::search(content.begin(),content.end(),word.begin(),word.end(),\
[](int x,int y){return std::tolower(x)==std::tolower(y);});
if(it==content.end())
{
return "None1";
}
//正文[...]
//简介截取范围 [...[<- prev_step ->"key_word"<- next_step ->]...]
int pos = std::distance(content.begin(),it);
const int prev_step=50;
const int next_step=100;
int begin=0;
int end = content.size()-1;
if(pos-prev_step>begin)
{
begin=pos-prev_step;
}
if(pos+next_step<end)
{
end=pos+next_step;
}
if(begin>=end)
{
return "None2";
}
return content.substr(begin,end-begin) + "...";
}
private:
ns_index::Index *index;
};
}
前端网页
实现目的:基本的搜索功能,包括查询输入、搜索结果显示和分页控制。
查询输入
html
<body>
<div class="container">
<div class="search">
<input type="text" value="请输入关键字...">
<button onclick="Search()">搜索一下</button>
</div>
<div class="result"></div>
<div class="pagination">
<button onclick="PrevPage()">上一页</button>
<span id="pageCounter">第 1 页</span>
<button onclick="NextPage()">下一页</button>
</div>
</div>
<script>
// JavaScript 代码在此定义
</script>
</body>
搜索以及结果展示
javascript
let currentPage = 1;
let pageSize = 15;
let totalPages = 1;
let searchData = [];
function Search() {
let query = $(".container .search input").val();
if(query == '' || query == null) {
return;
}
$.ajax({
type: "GET",
url: "/s?word=" + query,
success: function(data) {
searchData = data;
if(searchData == '' || searchData == null) {
document.write("没有找到要搜索的内容");
return;
}
totalPages = Math.ceil(searchData.length / pageSize);
DisplayResults();
UpdatePagination();
}
});
}
function DisplayResults() {
if(searchData == '' || searchData == null) {
document.write("没有找到要搜索的内容");
return;
}
let result_lable = $(".container .result");
result_lable.empty();
let startIndex = (currentPage - 1) * pageSize;
let endIndex = Math.min(startIndex + pageSize, searchData.length);
for (let i = startIndex; i < endIndex; i++) {
let elem = searchData[i];
let a_label = $("<a>", {
text: elem.title,
href: elem.url,
target: "_blank"
});
let p_label = $("<p>", {
text: elem.desc
});
let i_label = $("<i>", {
text: elem.url
});
let div_label = $("<div>", {
class: "item"
});
a_label.appendTo(div_label);
p_label.appendTo(div_label);
i_label.appendTo(div_label);
div_label.appendTo(result_lable);
}
}
分页控制
javascript
function UpdatePagination() {
$("#pageCounter").text("第 " + currentPage + " 页,共 " + totalPages + " 页");
}
function NextPage() {
if (currentPage < totalPages) {
currentPage++;
DisplayResults();
UpdatePagination();
}
}
function PrevPage() {
if (currentPage > 1) {
currentPage--;
DisplayResults();
UpdatePagination();
}
}
CSS样式
css
<style>
/* 去掉网页中的所有的默认内外边距,html的盒子模型 */
* {
/* 设置外边距 */
margin: 0;
/* 设置内边距 */
padding: 0;
}
/* 将我们的body内的内容100%和html的呈现吻合 */
html,
body {
height: 100%;
}
/* 类选择器.container */
.container {
/* 设置div的宽度 */
width: 800px;
/* 通过设置外边距达到居中对齐的目的 */
margin: 0px auto;
/* 设置外边距的上边距,保持元素和网页的上部距离 */
margin-top: 15px;
}
/* 复合选择器,选中container 下的 search */
.container .search {
/* 宽度与父标签保持一致 */
width: 100%;
/* 高度设置为52px */
height: 52px;
}
/* 先选中input标签, 直接设置标签的属性,先要选中, input:标签选择器*/
/* input在进行高度设置的时候,没有考虑边框的问题 */
.container .search input {
/* 设置left浮动 */
float: left;
width: 600px;
height: 50px;
/* 设置边框属性:边框的宽度,样式,颜色 */
border: 1px solid black;
/* 去掉input输入框的有边框 */
border-right: none;
/* 设置内边距,默认文字不要和左侧边框紧挨着 */
padding-left: 10px;
/* 设置input内部的字体的颜色和样式 */
color: black;
font-size: 18px;
}
/* 先选中button标签, 直接设置标签的属性,先要选中, button:标签选择器*/
.container .search button {
/* 设置left浮动 */
float: left;
width: 150px;
height: 52px;
/* 设置button的背景颜色,#4e6ef2 */
background-color: #4e6ef2;
/* 设置button中的字体颜色 */
color: #FFF;
/* 设置字体的大小 */
font-size: 19px;
font-family:Georgia, 'Times New Roman', Times, serif;
}
.container .result .item{
margin-top: 15px;
}
.container .result .item a{
display: block;
text-decoration: none;
font-size: 20px;
color: #4e6ef2;
}
.container .result .item a:hover{
text-decoration: underline;
}
.container .result .item p{
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;
}
.pagination {
margin-top: 20px;
text-align: center;
}
.pagination button {
margin: 0 5px;
padding: 5px 10px;
background-color: #4e6ef2;
color: white;
border: none;
border-radius: 5px;
cursor: pointer;
}
#pageCounter {
font-size: 18px;
font-weight: bold;
}
</style>
HTTP服务
用于和前端交互的模块,使得用户可以在浏览器上访问服务。
工作流程概述
- 启动阶段 :
- 初始化搜索器,建立索引。
- 配置HTTP服务器的根目录。
- 定义处理搜索请求的逻辑。
- 运行阶段 :
- 服务器启动并监听端口8081。
- 当客户端发送搜索请求时,服务器检查请求参数,处理搜索请求,生成JSON格式的结果,并返回给客户端。
- 记录用户的搜索关键词和相关日志信息。
http_server.cc:
c++
#include <iostream>
#include "seacher.hpp"
#include "cpp-httplib/httplib.h"
#include "log.hpp"
const std::string root_path="./wwwroot";//网页资源
const std::string input="data/raw_html/raw.txt";
int main()
{
ns_seacher::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& resp){
if(!req.has_param("word"))
{
resp.set_content("必须要有搜索关键字!","text/plain;charset=utf-8");
return;
}
std::string word = req.get_param_value("word");
std::string json_string;
LOG(NORMAL,"用户在搜索:"+word);
search.Search(word,&json_string);
resp.set_content(json_string,"application/json");
});
LOG(NORMAL,"服务器启动成功...");
svr.listen("0.0.0.0",8081);
return 0;
}
部署服务器
启动http_server为后台进程,并开启日志。
./http_server > log/log.txt 2>&1 &
查询进程状态
ps -ef | awk 'NR==1 || /http_server/' | grep -v awk
部署之后的日志
[2024-06-09 11:19:05][NORMAL][获取单例索引成功][seacher.hpp : 26]
[2024-06-09 11:19:10][NORMAL][当前已经建立的索引数量:100][index.hpp : 266]
[2024-06-09 11:21:57][NORMAL][当前已经建立的索引数量:200][index.hpp : 266]
[2024-06-09 11:22:22][NORMAL][当前已经建立的索引数量:300][index.hpp : 266]
[2024-06-09 11:24:34][NORMAL][当前已经建立的索引数量:400][index.hpp : 266]
[2024-06-09 11:24:35][NORMAL][当前已经建立的索引数量:500][index.hpp : 266]
[2024-06-09 11:24:35][NORMAL][当前已经建立的索引数量:600][index.hpp : 266]
...
[2024-06-09 11:27:03][NORMAL][当前已经建立的索引数量:8400][index.hpp : 266]
[2024-06-09 11:28:45][NORMAL][当前已经建立的索引数量:8500][index.hpp : 266]
[2024-06-09 11:30:21][NORMAL][当前已经建立的索引数量:8592][index.hpp : 270]
[2024-06-09 11:30:21][NORMAL][建立正排和倒排索引成功][seacher.hpp : 29]
[2024-06-09 11:30:21][NORMAL][服务器启动成功...][http_server.cc : 26]
项目展示
启动页面
搜索结果页面
日志
[2024-06-09 11:53:23][NORMAL][用户在搜索:linux system][http_server.cc : 22]
点击标题访问boost网站资源
例如:点击Revision History
,则跳转到如下网页。