文章目录
项目整体展示
一、项目的背景
- 现在的大型互联网公司:百度、搜狗、360等等所做的是全网搜索,所做的大型搜索引擎,功能很全面,门槛比较高,我们自己想要实现几乎是不可能的
- 我们所实现的:站内搜索引擎,例如cplusplus.com这样的网站,在里面搜索
string
。站内搜索的特点就是,搜索的内容更加垂直,内容的范围也比较有限,数据量比较小。
- boost官网是没有站内搜索功能的,所以需要我们自己做一个!
二、搜索引擎的宏观原理
介绍一下数据在搜索引擎上是怎样流动的
三、此项目所需要的技术栈和环境
- 技术栈:C/C++、STL、boost准标准库、jsoncpp、cppjieba、cpp-httplib
- 环境:Centos 7、g++、makefile、vim、VS2019、vscode
- 其他:html5、css、js、jQuery、Ajax
四、正排索引和倒排索引原理
目标文档:
文档1:雷军买了四斤小米
文档2:雷军发布了小米手机
目标文档进行分词
目的:方便建立倒排索引和查找
雷军买了四斤小米:雷军/买/四斤/小米
停止词:了、的、吗、啊,这样的词,我们在分词的时候一般不考虑进去。
1、正排索引
从文档ID找到文档的内容(关键字)
文档ID | 文档内容 |
---|---|
1 | 雷军买了四斤小米 |
2 | 雷军发布了小米手机 |
2、倒排索引
根据文档内容,分词、整理不重复的各个关键字,找到对应联系的文档ID的方法
关键字(具有唯一性) | 文档ID,weight(权重排序) |
---|---|
雷军 | 文档1、文档2 |
买 | 文档1 |
四斤 | 文档1 |
小米 | 文档1、文档2 |
发布 | 文档2 |
小米手机 | 文档2 |
模拟一次查找的过程:
用户输入#
小米
小米→倒排索引中查找→提取出文档ID(1,2)→根据正排索引→找到文档的内容→title+conent (desc) +url文档结果进行摘要→构建响应结果
五、编写数据去标签与数据清理模块 Parser
数据源:boost官网,目前项目只需要boost_1_78_0/doc/html
下的网页文件,用它来建立索引
https://www.boost.org/
将此文件下载下来过后,拖入到Linux云服务器中(中间可能会因为服务器配置太低而自动挂掉,多次重复即可),然后解压该文件,将该文件中的html文件拷贝到我们的data目录下,作为数据源
[sjj@VM-20-15-centos boost_searcher]$ cp -rf boost_1_78_0/doc/html/* data/input/
[sjj@VM-20-15-centos input]$ ls -Rl | grep -E '*.html' | wc -l
8141
5.1 去标签和数据清洗
将inpute下的文件进行去标签和数据清洗
[sjj@VM-20-15-centos boost_searcher]$ touch parser.cc
[sjj@VM-20-15-centos boost_searcher]$ ll
total 4
drwxrwxr-x 3 sjj sjj 4096 Jul 25 22:36 data
-rw-rw-r-- 1 sjj sjj 0 Jul 25 22:46 parser.cc
这个标签是对我们的搜索是没有任何意义的,我们需要将其过滤出去,而且一般标签都是成对出现的
[sjj@VM-20-15-centos data]$ mkdir raw_html
[sjj@VM-20-15-centos data]$ ll
total 20
drwxrwxr-x 60 sjj sjj 16384 Jul 25 22:38 input
drwxrwxr-x 2 sjj sjj 4096 Jul 25 22:53 raw_html
[sjj@VM-20-15-centos input]$ ls -Rl | grep -E '*.html' | wc -l
8141
input
:存放原始数据
raw_html
:存放去标签后的干净数据
最后目标:把每个文档都去标签,然后写入到同一个文件中,每个文档内容不需要换行,文档和文档之间用\3
(经验规律)来划分,\3
是属于控制字符,不能显式出来的,所以不会污染我们的文档内容。
eg
:XXXXXXX\3YYYYYY\3ZZZZZZ\3
5.2 parser代码编写
第一步:将所有的文件名全部提取出来放到vector files_lis
t中
递归式的把每个html文件名带路径,保存到files_list
中,方便后期进行一个一个的文件进行读取
第二步:按照files_list
读取每个文件内容,并进行解析
每个文件路径解析成为如下所示统一的格式:
typedef struct DocInfo
{
std::string title; //文章的标题
std::string content; //文章的摘要内容
std::string url; //此网页的url
}DocInfo_t;
第三步:把解析完毕的各个文件内容,写入到output中,按照\3
作为每个文档的分隔符
小小的规则:
const &:输入型参数
*(指针):输出型参数
&(引用):输入输出型参数
parser.cc主体代码
#include<iostream>
#include<string>
#include<vector>
const std::string src_path="data/raw_html";
const std::string output="data/raw_html/raw.txt";
typedef struct DocInfo{
std::string title;
std::string content;
std::string url;
}DocInfo_t;
bool EnumList(const std::string &src_path,std::vector<std::string> *files_list);
bool ParseHtml(const std::string &files_list,std::string *std::vector<DocInfo_t> results);
bool SaveHtml(const std::vector<DocInfo_t> &results,const std::string *output);
int main()
{
std::vector<std::string> files_list;
if(!EnumList(src_path,&files_list))
{
std::cerr<<"enum file name error!"<<std::endl;
return 1;
}
std::vector<DocInfo_t> results;
if(!ParseHtml(files_list,&results))
{
std::cerr<<"parse html error!"<<std::endl;
return 2;
}
if(!SaveHtml(results,&output))
{
std::cerr<<"save html error!"<<std::endl;
return 3;
}
return 0;
}
三个函数的实现
1、EnumList
枚举所有网页文件写入到std::vector<std::string> files_list
中
由于C++和STL对于文件读写操作支持的不是特别的好,所以我们这里采用的是boost库中的filesystem
这样的文件操作
安装boost库:sudo yum install -y boost-devel
这是一个boost开发库,里面包含了此项目所需的头文件
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))
{
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;
// 判断是否是以 html 结尾的文件
if(iter->path().extension()!=".html") continue;
//现在的路径就是一个合法的,以html结尾的我们所需要的路径
//我们字符串的格式插入到files_list中
//std::cout<<"debug :"<<iter->path().string()<<std::endl;
files_list->push_back(iter->path().string());
}
return true;
}
结果展示:
2、ParseHtml
每个文件路径解析成为DocInfo_t
类型的结构体
bool ParseHtml(const std::vector<std::string> &files_list, std::vector<DocInfo_t> *results)
{
// file代表每一个文件的路径
for (const std::string &file : files_list)
{
// 1 ReadFile
std::string result;
if (!ns_util::FileUtil::ReadFile(file, &result))
{
continue;
}
// 即将要填充的doc对象
DocInfo_t doc;
// 2 提取title
if (!ParseTitle(result, &doc.title))
{
continue;
}
// 3 提取content
if (!ParseContent(result, &doc.content))
{
continue;
}
// 4 提取url
if (!ParseUrl(file, &doc.url))
{
continue;
}
// 到目前这行,一定是完成了解析的任务,将当前文档的相关内容保存到了doc结构体中
// results->push_back(doc); // push_back本质会发生拷贝
results->push_back(std::move(doc));
// ShowDoc(doc);
// break;
}
return true;
}
这里又可以分为四个小步骤
①读取文件,ReadFile()函数读取文件的内容
再建立一个工具集,存放工具
getline的返回值是一个对象的引用,while(bool),本质是因为对象中重载了强制类型转换
#pragma once
#include <iostream>
#include <string>
#include <fstream>
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<<file_path<<"open error"<<std::endl;
return false;
}
std::string line;
while(std::getline(in,line))
{
*out+=line;
}
in.close();
return true;
}
};
}
②解析指定的文件,提取title
就是在文档中搜索<title
>和</title>
,然后再提取这中间的,便是标题了
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;
}
③解析指定的文件,提取content
本质就是去标签,只保留网页的内容部分
在遍历的时候,只要遇到了右尖括号>
,就意味着当前标签处理完毕了
只要遇到了新的左尖括号<
,意味着新的标签开始了
static bool ParseContent(const std::string &file, std::string *content)
{
// 去标签,基于一个简单的状态机
enum status{
LABLE,
CONTENT
};
enum status s = LABLE;
for (char c : file){
switch (c)
{
case LABLE:
if (c == '>')
s = CONTENT;
break;
case CONTENT:
if (c == '<')
s = LABLE;
else{
// 我们不想保留原始文档中的\n
if(c=='\n') c=' ';
content->push_back(c);
}
break;
default:
break;
}
}
return true;
}
④解析指定的文件路径,构建url
boost库的官方文档和我们做项目下载下来的文档,是有路径的对应关系的
官网URL样例:
https://www.boost.org/doc/libs/1_78_0/doc/html
/accumulators.html
我们下载下来的URL样例:
boost_1_78_0/doc/html/accumulators.html
我们拷贝到我们项目中的样例:
data/input/accumulators.html
我们把下载下来的boost库中doc/html/* 拷贝到 data/input/
url_head = “https://www.boost.org/doc/libs/1_78_0/doc/html”;
url_tail = data/input(删除) /accumulators.html -> url_tail = /accumulators.html
最后拼接相当于形成了一个官网链接:
url = url_head + url_tail
static bool ParseUrl(const std::string &file_path, std::string *url)
{
// 官网中的URL
std::string url_head = "https://www.boost.org/doc/libs/1_78_0/doc/html";
// 忽略src_path,一直截取到文件尾
std::string url_tail = file_path.substr(src_path.size());
*url = url_head + url_tail;
return true;
}
3、SaveHtml
把解析完毕的各个文件内容,写入到output(output=“data/raw_html/raw.txt”)中,按照\3
作为每个文档的分隔符
vesion1: eg:XXXXXXX\3YYYYYY\3ZZZZZZ\3
现在简化一下: 采用下面的方案:
version2: 写入文件中,一定要考虑下一次在读取的时候,也要方便操作!
类似:title \3content \3 url\n
title \3 content \3 url\n
title \3 content \3 url\n
…
方便我们getline(ifsream, line)
,直接获取文档的全部内容:title\3content\3url
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())
{
std::cerr << "open " << output << "error!" << std::endl;
return false;
}
// 按照我们的约定规则写入到output文件中
for (auto &item : results)
{
std::string out_string;
out_string = item.title;
out_string += SEP;
out_string += item.content;
out_string += SEP;
out_string += item.url;
out_string += '\n';
//将内容写入到文件中
out.write(out_string.c_str(), out_string.size());
}
out.close();
return true;
}
六、建立索引
所需的数据结构
// 正排单个数据
struct DocInfo
{
std::string title; // 文档的标题
std::string content; // 文档对应的去标签之后的内容
std::string url; // 文档的url
uint64_t doc_id; // 文档的ID
};
// 倒排单个数据
struct InvertedElem
{
uint64_t doc_id; // 文档的ID
std::string word; // 搜索的关键字
int weight; // 权重
};
// 倒排拉链
typedef std::vector<InvertedElem> InvertedList;
正排索引
正排索引的数据结构用数组,数组下标就是天然的文档ID
// 根据文档ID获得索引值
DocInfo *GetForwardIndex(uint64_t doc_id)
{
if (doc_id >= forward_index.size())
{
std::cerr << "doc_id has out of range!" << std::endl;
return nullptr;
}
return &forward_index[doc_id];
}
构建正排索引
首先就是切分字符串
使用C++的字符串切分比较麻烦,所以采用现成的boost库中的split接口即可完成。
class StringUtil
{
public:
static void CutString(const std::string& target, std::vector<std::string>* out, const std::string sep)
{
boost::split(*out, target, boost::is_any_of(sep), boost::token_compress_on);
}
};
DocInfo* BulidForwardIndex(const std::string& line)
{
// 1、切分字符串
const std::string sep = "\3";
std::vector<std::string> results; // 将切分的line ,放入vector中
ns_util::StringUtil::CutString(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();// 对应的ID就是当前doc在当前vector中的下标
// 3、插入到正排索引的vector中
forward_index.push_back(std::move(doc));
return &forward_index.back();
}
倒排索引
倒排索引一定是一个唯一的关键字和一个(组)InvertedElem
对应,即关键字和倒排拉链的映射关系,所以选用unordered_map
作为存储的数据结构。
// 根据关键字,获得倒排拉链
InvertedList *GetInvertedList(const std::string &word)
{
auto iter = inverted_index.find(word);
if (iter == inverted_index.end())
{
std::cerr << word << "has no InvertedList!" << std::endl;
return nullptr;
}
return &(iter->second);
}
构建倒排索引
原理:
我们拿到的文档内容格式:
struct DocInfo
{
std::string title; // 文档的标题
std::string content; // 文档对应的去标签之后的内容
std::string url; // 文档的url
uint64_t doc_id; // 文档的ID
};
文档:
title : 吃葡萄
content: 吃葡萄不吐葡萄皮
url: http://XXXX
doc_id: 123
根据文档内容,形成一个或者多个InvertedElem(倒排拉链) 因为当前我们是一个一个文档进行处理的,一个文档会包含多个”词“,都应当对应到当前的doc_id
1、需要对title 和content都要先分词——使用jieba分词
title:吃/葡萄/吃葡萄(title_word)
content:吃/葡萄/不吐/葡萄皮(content_word)
词和文档的相关性(词频:在标题中出现的词,可以认为相关性更高一些,在内容中出现相关性低一些)
cppjieba的安装和测试使用
安装:git clone https://gitcode.net/mirrors/yanyiwu/cppjieba.git
使用:
1、建立词库的软连接
[sjj@VM-20-15-centos test]$ ln -s cppjieba/dict dict
[sjj@VM-20-15-centos test]$ ll
total 108
-rwxrwxr-x 1 sjj sjj 96608 Jul 29 15:42 a.out
drwxrwxr-x 8 sjj sjj 4096 Mar 3 15:07 cppjieba
-rw-rw-r-- 1 sjj sjj 2797 Jul 29 22:27 demo.cpp
lrwxrwxrwx 1 sjj sjj 13 Jul 29 22:29 dict -> cppjieba/dict
-rw-rw-r-- 1 sjj sjj 389 Jul 29 15:43 test.cc
[sjj@VM-20-15-centos test]$ ls cppjieba/dict/
hmm_model.utf8 idf.utf8 jieba.dict.utf8 pos_dict README.md stop_words.utf8 user.dict.utf8
2、建立头文件(Jieba.hpp)的软连接
[sjj@VM-20-15-centos test]$ ln -s cppjieba/include/ inc
[sjj@VM-20-15-centos test]$ ll
total 108
-rwxrwxr-x 1 sjj sjj 96608 Jul 29 15:42 a.out
drwxrwxr-x 8 sjj sjj 4096 Mar 3 15:07 cppjieba
-rw-rw-r-- 1 sjj sjj 2797 Jul 29 22:27 demo.cpp
lrwxrwxrwx 1 sjj sjj 13 Jul 29 22:29 dict -> cppjieba/dict
lrwxrwxrwx 1 sjj sjj 17 Jul 29 22:32 inc -> cppjieba/include/
-rw-rw-r-- 1 sjj sjj 389 Jul 29 15:43 test.cc
[sjj@VM-20-15-centos test]$ ls cppjieba/include/
cppjieba
[sjj@VM-20-15-centos test]$ ls cppjieba/include/cppjieba/
DictTrie.hpp HMMSegment.hpp limonp PosTagger.hpp SegmentBase.hpp Trie.hpp
FullSegment.hpp Jieba.hpp MixSegment.hpp PreFilter.hpp SegmentTagged.hpp Unicode.hpp
HMMModel.hpp KeywordExtractor.hpp MPSegment.hpp QuerySegment.hpp TextRankExtractor.hpp
注意细节:需要手动拷贝文件deps/limonp,否则可能编译不通过
[sjj@VM-20-15-centos cppjieba]$ `cp deps/limonp include/cppjieba/ -rf`
[sjj@VM-20-15-centos cppjieba]$ ls include/cppjieba/
DictTrie.hpp HMMSegment.hpp `limonp` PosTagger.hpp SegmentBase.hpp Trie.hpp
FullSegment.hpp Jieba.hpp MixSegment.hpp PreFilter.hpp SegmentTagged.hpp Unicode.hpp
HMMModel.hpp KeywordExtractor.hpp MPSegment.hpp QuerySegment.hpp TextRankExtractor.hpp
[sjj@VM-20-15-centos cppjieba]$ ls include/cppjieba/limonp/
ArgvContext.hpp Closure.hpp FileLock.hpp Md5.hpp StringUtil.hpp
BlockingQueue.hpp Colors.hpp ForcePublic.hpp MutexLock.hpp Thread.hpp
BoundedBlockingQueue.hpp Condition.hpp LocalVector.hpp NonCopyable.hpp ThreadPool.hpp
BoundedQueue.hpp Config.hpp `Logging.hpp` StdExtension.hpp
测试样例:demo.cc
#include "inc/cppjieba/Jieba.hpp"
#include <string>
#include <iostream>
#include <vector>
using namespace std;
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";
int main(int argc, char **argv)
{
cppjieba::Jieba jieba(DICT_PATH,
HMM_PATH,
USER_DICT_PATH,
IDF_PATH,
STOP_WORD_PATH);
vector<string> words;
string s;
s = "小明硕士毕业于中国科学院计算所,后在日本京都大学深造";
cout << s << endl;
cout << "[demo] CutForSearch" << endl;
jieba.CutForSearch(s, words);
cout << limonp::Join(words.begin(), words.end(), "/") << endl;
return EXIT_SUCCESS;
}
结果展示:
[sjj@VM-20-15-centos test]$ g++ demo.cpp -std=c++11
[sjj@VM-20-15-centos test]$ ./a.out
小明硕士毕业于中国科学院计算所,后在日本京都大学深造
[demo] CutForSearch
小明/硕士/毕业/于/中国/科学/学院/科学院/中国科学院/计算/计算所/,/后/在/日本/京都/大学/日本京都大学/深造
将jieba库引入到项目中
建立两个软连接,方便使用
[sjj@VM-20-15-centos boost_searcher]$ ln -s ~/thirdpart/cppjieba/include/cppjieba/ cppjieba
[sjj@VM-20-15-centos boost_searcher]$ ln -s ~/thirdpart/cppjieba/dict/ dict
#include "cppjieba/Jieba.hpp" //结巴分词
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:
static cppjieba::Jieba jieba;
public:
static void CutString(const std::string &src, std::vector<std::string> *out)
{
jieba.CutForSearch(src, *out);
}
};
// 静态成员需要在类外初始化
cppjieba::Jieba JiebaUtil::jieba(DICT_PATH, HMM_PATH, USER_DICT_PATH, IDF_PATH, STOP_WORD_PATH);
2、词频统计——词和文档的相关性
我们约定:在标题中出现的词相关性更高一些,在内容中出现的词,相关性就低一些
// 统计词频的结构体
struct word_cnt {
title_cnt;
content_cnt;
}
// 词和词频的映射关系
unordered_map<std::string, word_cnt> word_cnt;
// 遍历统计
for(&word : title_word){
word_cnt[word].title_cnt++; //吃(1)/葡萄(1)/吃葡萄(1)
}
for(&word : content_word){
word_cnt[word].content_cnt++; //吃(1)/葡萄(1)/不吐(1)/葡萄皮(1)
}
知道了在文档中,标题和内容每个词出现的次数
3.、自定义相关性——按照权值排序
for (&word : word_cnt) {
//具体一个词和123文档的对应关系,当有多个不同的词,指向同一个文档的时候,此时该优先显示谁?
//由相关性决定!
struct InvertedElem elem;// 构建结点
elem.doc_id = 123;
elem.word = word.first;
elem.weight = 10 * word.second.title_cnt + word.second.content_cnt;
// 结点插入到map中,以key方括号的形式进行索引
inverted_index[word.first].push_back(elem);
}
完整的建立索引的代码:
DocInfo *BulidForwardIndex(const std::string &line)
{
// 1、切分字符串
const std::string sep="\3";
std::vector<std::string> results; // 将切分的line ,放入vector中
ns_util::StringUtil::CutString(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();// 对应的ID就是当前doc在当前vector中的下标
// 3、插入到正排索引的vector中
forward_index.push_back(std::move(doc));
return &forward_index.back();
}
bool BulidInvertedIndex(const DocInfo &doc)
{
// DocInfo doc{title,content,url,doc_id}
// 利用正排索引得到的文档,建立倒排
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(std::string s: title_words)
{
// 统一为小写
boost::to_lower(s);
word_map[s].title_cnt++;// 如果key值存在就统计,如果不存在就新建
}
// 对内容进行分词并且统计词频
std::vector<std::string> content_words;
ns_util::JiebaUtil::CutString(doc.content,&content_words);
for(auto s: content_words)
{
// 统一为小写
boost::to_lower(s);
word_map[s].content_cnt++;
}
#define X 10
#define Y 1
// 构建倒排拉链
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(item);
}
return true;
}
七、编写searcher
我们的索引已经建立好了,我们接下来的任务就是根据索引去搜索内容
1、分词
首先进行分词操作,才能够进行搜索!对于我们输入的关键字query,按照searcher的要求进行分词
2、触发
就是根据分词的各个“词”,进行index查找
不完美的地方:
可能会存在搜索到重复文档的情况,这种情况并不是错误的,而是因为分词的原因,多个词可能对应的都是同一篇文档,这就给用户的体验带来了不便
3、合并排序
汇总查找结果,按照相关性weight进行降序排序
4、构建
根据查找出来的结果,构建json字符串——第三方库jsoncpp
通过jsoncpp完成序列化和反序列化的过程
安装jsoncpp:
[sjj@VM-20-15-centos boost_searcher]$ sudo yum install -y jsoncpp-devel
使用jsoncpp:
[sjj@VM-20-15-centos test]$ cat test.cc
#include<iostream>
#include<string>
#include<jsoncpp/json/json.h>
int main()
{
Json::Value root;
Json::Value item1;
item1["key1"]="value111";
item1["key2"]="value222";
Json::Value item2;
item2["key1"]="value1";
item2["key2"]="value1";
// 相当于把item插入到root数组中
root.append(item1);
root.append(item2);
//Json::StyledWriter writer;
Json::FastWriter writer;
std::string s=writer.write(root);
std::cout<<s<<std::endl;
return 0;
}
[sjj@VM-20-15-centos test]$ g++ test.cc -std=c++11 -ljsoncpp
[sjj@VM-20-15-centos test]$ ./a.out
[
{
"key1" : "value111",
"key2" : "value222"
},
{
"key1" : "value1",
"key2" : "value1"
}
]
[sjj@VM-20-15-centos test]$ g++ test.cc -std=c++11 -ljsoncpp
[sjj@VM-20-15-centos test]$ ./a.out
[{"key1":"value111","key2":"value222"},{"key1":"value1","key2":"value1"}]
截取摘要
找到word
在html_content
中的首次出现,然后往前找50字节(如果不足50字节,那就从开头开始begin找),往后找100字节(如果不足100字节,那就一直到end就可以了),最后截取这部分内容就是我们平常在网页上面看到的摘要了。
// 获取摘要
std::string GetDesc(const std::string &html_content, const std::string &word)
{
const std::size_t prev_step = 50;
const std::size_t next_step = 100;
// 1、找到首次出现的词
std::size_t pos = html_content.find(word);
if (pos == std::string::npos)
{
return "None";
}
// 2、获取start、end
std::size_t start = 0;
std::size_t end = html_content.size()-1;
// 如果前面有50+个字符,需要更新start位置
if(pos-prev_step>start) start=pos - prev_step;
if(pos+next_step<end) end=pos +next_step;
// 3、截取子串
if(start>=end) return "None";
return html_content.substr(start,end-start);
}
八、综合调试
在search.hpp中
bug1
// 2、获取start、end
std::size_t start = 0;
std::size_t end = html_content.size() - 1;
// 如果前面有50+个字符,需要更新start位置
if (pos - prev_step > start)
start = pos - prev_step;
if (pos + next_step < end)
end = pos + next_step;
size_t
是无符号整数,pos-prev_step
可能是一个负数,但是由于是无符号整数,所以会被转换称为很大的正数,if条件始终满足,所以这里有个bug
修改如下:
// 2、获取start、end
// size_t是无符号整数
std::size_t start = 0;
std::size_t end = html_content.size() - 1;
// 如果前面有50+个字符,需要更新start位置
if (pos > start + prev_step)
start = pos - prev_step;
if ((int)pos < (int)(end - next_step))
end = pos + next_step;
// 3、截取子串
if (start >= end)
return "None2";
return html_content.substr(start, end - start);
bug2
// 1、找到首次出现的词
std::size_t pos = html_content.find(word);
if (pos == std::string::npos)
{
return "None1";
}
我们在搜索文档是,将其转化为小写,但是我们搜索的数据源本就是大小写都有的,所以我们要利用搜索时,忽略大小写的方法
// 1、找到首次出现的词
auto iter = std::search(html_content.begin(), html_content.end(), word.begin(), word.end(), [](int x, int y)
{ return (std::tolower(x) == std::tolower(y)); });
if(iter==html_content.end())
{
return "None1";
}
int pos = std::distance(html_content.begin(),iter);
bug3
我最想知道,我们的文档是否是按照权值来排倒序的
// for debug
elem["id"]=(int)item.doc_id;
elem["weight"]=item.weight;
root.append(elem);
bug4
可能会存在搜索到重复文档的情况,这种情况并不是错误的,而是因为分词的原因,多个词可能对应的都是同一篇文档,这就给用户的体验带来了不便
eg:
搜索关键字为->你是一个好人
分词过后:你/是/一个/好人
这个分词结果在倒排当中就可能对应了4个key-Value关系,我们实际上只想要一个kv关系,所以接下来要进行去重操作
思路:文档id相同的全部合并起来,合并的这些文档的权值全部累加起来
// 新增加一个结点:用于去重的打印倒排拉链的结点
struct InvertedElemPrint{
uint64_t doc_id;
int weight;
std::vector<std::string> words;
InvertedElemPrint():doc_id(0), weight(0){}
};
void Search(const std::string &query, std::string *json_string)
{
// 1、分词
std::vector<std::string> words;
ns_util::JiebaUtil::CutString(query, &words);
// 2、触发
// ns_index::InvertedList inverted_list_all; // typedef std::vector<InvertedElem> InvertedList;
std::vector<InvertedElemPrint> inverted_list_all;
std::unordered_map<uint64_t, InvertedElemPrint> tokens_map;
for (std::string word : words)
{
boost::to_lower(word);
// 首先需要获取倒排拉链
ns_index::InvertedList *inverted_list = index->GetInvertedList(word);
if (nullptr == inverted_list)
{
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]; //[]:如果存在直接获取,如果不存在新建
// item一定是doc_id相同的print节点
item.doc_id = elem.doc_id;
item.weight += elem.weight;
item.words.push_back(elem.word);
}
}
for (const auto &item : tokens_map)
{
inverted_list_all.push_back(std::move(item.second));
}
// 3、合并排序 根据weight降序排序
// std::sort(inverted_list_all.begin(), inverted_list_all.end(),
// [](const ns_index::InvertedElem &e1, const ns_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 &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]); //我们需要的是内容摘要的一部分,0号就是摘要
elem["url"] = doc->url;
// for debug
elem["id"] = (int)item.doc_id;
elem["weight"] = item.weight;
root.append(elem);
}
// Json::StyledWriter writer;
Json::FastWriter writer;
*json_string = writer.write(root);
}
修改之后:
九、编写http_server
#include "searcher.hpp"
#include "cpp-httplib/httplib.h"
const std::string root_path="./wwwroot";
const std::string input="data/raw_html/raw.txt";
int main()
{
ns_searcher::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 &rsp){
if(!req.has_param("word"))
{
rsp.set_content("必须要有搜索关键字!", "text/plain; charset=utf-8");
return;
}
// rsp.set_content("hhh","text/plain; charset=utf-8");
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;
}
十、前端显示
<!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">
<script src="http://code.jquery.com/jquery-2.1.1.min.js"></script>
<title>boost 搜索引擎</title>
<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: #CCC;
font-size: 14px;
}
/* 先选中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 {
width: 100%;
}
.container .result .item {
margin-top: 15px;
}
.container .result .item a {
/* 设置为块级元素,单独站一行 */
display: block;
/* a标签的下划线去掉 */
text-decoration: none;
/* 设置a标签中的文字的字体大小 */
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;
}
</style>
</head>
<body>
<div class="container">
<div class="search">
<input type="text" value="请输入搜索关键字">
<button onclick="Search()">搜索一下</button>
</div>
<div class="result">
<!-- 动态生成网页内容 -->
<!-- <div class="item">
<a href="#">这是标题</a>
<p>这是摘要这是摘要这是摘要这是摘要这是摘要这是摘要这是摘要这是摘要这是摘要这是摘要这是摘要这是摘要这是摘要这是摘要这是摘要</p>
<i>https://search.gitee.com/?skin=rec&type=repository&q=cpp-httplib</i>
</div>
<div class="item">
<a href="#">这是标题</a>
<p>这是摘要这是摘要这是摘要这是摘要这是摘要这是摘要这是摘要这是摘要这是摘要这是摘要这是摘要这是摘要这是摘要这是摘要这是摘要</p>
<i>https://search.gitee.com/?skin=rec&type=repository&q=cpp-httplib</i>
</div>
<div class="item">
<a href="#">这是标题</a>
<p>这是摘要这是摘要这是摘要这是摘要这是摘要这是摘要这是摘要这是摘要这是摘要这是摘要这是摘要这是摘要这是摘要这是摘要这是摘要</p>
<i>https://search.gitee.com/?skin=rec&type=repository&q=cpp-httplib</i>
</div>
<div class="item">
<a href="#">这是标题</a>
<p>这是摘要这是摘要这是摘要这是摘要这是摘要这是摘要这是摘要这是摘要这是摘要这是摘要这是摘要这是摘要这是摘要这是摘要这是摘要</p>
<i>https://search.gitee.com/?skin=rec&type=repository&q=cpp-httplib</i>
</div>
<div class="item">
<a href="#">这是标题</a>
<p>这是摘要这是摘要这是摘要这是摘要这是摘要这是摘要这是摘要这是摘要这是摘要这是摘要这是摘要这是摘要这是摘要这是摘要这是摘要</p>
<i>https://search.gitee.com/?skin=rec&type=repository&q=cpp-httplib</i>
</div> -->
</div>
</div>
<script>
function Search(){
// 是浏览器的一个弹出框
// alert("hello js!");
// 1. 提取数据, $可以理解成就是JQuery的别称
let query = $(".container .search input").val();
console.log("query = " + query); //console是浏览器的对话框,可以用来进行查看js数据
//2. 发起http请求,ajax: 属于一个和后端进行数据交互的函数,JQuery中的
$.ajax({
type: "GET",
url: "/s?word=" + query,
success: function(data){
console.log(data);
BuildHtml(data);
}
});
}
function BuildHtml(data){
// 获取html中的result标签
let result_lable = $(".container .result");
// 清空历史搜索结果
result_lable.empty();
for( let elem of data){
// console.log(elem.title);
// console.log(elem.url);
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);
}
}
</script>
</body>
</html>
十一、添加日志信息
#pragma once
#include <iostream>
#include <string>
#include <ctime>
#define NORMAL 1
#define WARNING 2
#define DEBUG 3
#define FATAL 4
#define LOG(LEVEL, MESSAGE) log(#LEVEL, MESSAGE, __FILE__, __LINE__)
void log(std::string level, std::string message, std::string file, int line)
{
std::cout << "[" << level << "]" << "[" << time(nullptr) << "]" << "[" << message << "]" << "[" << file << " : " << line << "]" << std::endl;
}
十二、部署到Linux服务器上
nohup
命令
启动程序在后台调用,默认将日志信息打印打nohup.out文件中,方便查看
nohup ./http_server > log/log.txt 2>&1 &
十三、总结与改善
1. 建立整站搜索
2. 设计一个在线更新的方案,信号,爬虫,完成整个服务器的设计
3. 不使用组件,而是自己设计一下对应的各种方案(有时间,有精力)
4. 在我们的搜索引擎中,添加竞价排名(强烈推荐)
5. 热次统计,智能显示搜索关键词(字典树,优先级队列)(比较推荐)
6. 设置登陆注册,引入对mysql的使用(比较推荐的)