• 首页 首页 icon
  • 工具库 工具库 icon
    • IP查询 IP查询 icon
  • 内容库 内容库 icon
    • 快讯库 快讯库 icon
    • 精品库 精品库 icon
    • 问答库 问答库 icon
  • 更多 更多 icon
    • 服务条款 服务条款 icon

项目Boost搜索引擎

武飞扬头像
sakeww
帮助1

1. 项目的相关背景与目标

针对boost网站没有搜索导航功能,为boost网站文档的查找提供搜索功能

站内搜索:搜索的数据更垂直,数据量小
类似于cplusplus.com的搜索
学新通

2.相关宏观原理

学新通

3.技术栈和项目环境

技术栈:C/C ,C 11,STL,准标准库Boost(相关文件操作),jsoncpp(客户端和数据端数据交互),cppjieba(将搜索关键字进行切分),cpp-httplib(构建http服务器)
其他技术栈(前端):html5,css,js,jQuery,Ajax
项目环境:Centos 7云服务器,vim/gcc(g )/Makefile,vs2019/vs code(网页)

4. 正排索引、倒排索引

正排索引:从文档ID找到文档内容(文档内的关键字)
正排索引类似于书的目录,我们可以根据页数查找到对应的内容

目标文档进行分词:目的:方便建立倒排索引和查找
停止词:了,吗,的,the,a,一般情况我们在分词的时候可以不考虑

倒排索引:根据文档内容,分词,整理不重复的各个关键字,对应联系到文档ID的方案
文档ID中,各个文档ID的排序按照权重进行排序
倒排索引和正排索引是相反的概念,我们可以根据文档内容查询到这部分内容在哪些文件中出现,从而找到对应的文件

模拟查找过程
用户输入:
关键字->倒排索引中查找->提取出是文档ID(x,y,z,,,)->根据正排索引->找到文档的内容->将文档内容中的title conent(desc) url 文档结果进行摘要->构建响应结果

5. 数据去标签与数据清洗 Parser

5.1下载文件&&准备工作

学新通

经过测试:
edge,和360浏览器不能下载,每次的速度都是极慢(不到30KB),
并且在下载到一定时间后,会因为权限不足等情况直接不能下载
所以这个下载只能使用chrome下载,
虽然下载的速度还是很慢,但是不会因为其他问题而导致不能下载

下载好之后,
创建目录
然后将文件拖进Linux中
然后解压
学新通
我们只需要boost_1_79_0/doc/html/* 内的所有网址即可:
学新通
然后删除boost_1_79_0即可:
学新通
我们只需要:boost_1_79_0/doc/html 目录下的html文件,用它来进行建立索引


原始数据->去标签之后的数据
学新通

查看一共多少html文件
学新通
目标:
把每个文档都去标签,然后写入到同一个文件中,每个文档内容只占一行!文档和文档之间‘\3’区分。

为什么是\3?
学新通

5.2编写parser解析文档

5.2.1整体架构


#include<iostream>                                                                                                                                                                        
#include<string>    
#include<vector>

//目录   
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;//该文档在官网中的url    
}DocInfo_t;


//格式规范:const &:输入;*:输出;&:输入输出
// 运用boost库进行读取
bool EnumFile(const std::string& src_path, std::vector<std::string>* files_list);
//解析标签, 
bool ParseHtml(const std::vector<std::string>& files_list, 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;
    //(1)将每个html文件名带路径保存到file_list中
    if (!EnumFile(src_path, &files_list))
    {
        std::cerr << "枚举文件名失败!" << std::endl;
        return 1;
    }

    //(2)对files_list中的每个文件内容进行解析
    std::vector<DocInfo_t> results;
    if (!ParseHtml(files_list, &results))
    {
        std::cerr << "解析html失败" << std::endl;
        return 2;
    }

    //(3)解析完后,将文件内容写入ouput,用'\3'进行分割
    if (!SaveHtml(results, output))
    {
        std::cerr << "保存文件失败!" << std::endl;
        return 3;
    }
    return 0;
}
学新通

学新通

5.2.2EnumFile

我们需要使用filesystem函数(boost库中的)

boost库安装

sudo yum install -y boost-devel
//devel是boost开发库

我们用的是1.53,我们搜索的是1.79,这两个不冲突。

boost网页查看函数使用方法:
学新通

#include<boost/filesystem.hpp>
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 << "不存在" << std::endl;
        return false;
    }

    //对文件进行递归遍历    
    //定义一个空的迭代器,用来进行判断递归结束    
    fs::recursive_directory_iterator end;
    for (fs::recursive_directory_iterator iter(root_path); iter != end; iter  )
    {
        //判断文件是否为普通文件(html)都是普通文件    
        if (!(fs::is_regular_file(*iter)))  continue;


        //判断文件后缀是否为 .html     
        //path()迭代器的一种方法,用来提取路径的字符串;extension()提取带路径的文件名后缀    
        if (iter->path().extension() != ".html") continue;

        //std::cout<<"插入路径:"<<iter->path().string()<<std::endl;//测试代码,测试其是否能正常插入    

        //当前的路径一定是一个合法的,以.html结束的普通网页文件                                       
        //string()可以将其转化为字符串的形式                                                          
        files_list->push_back(iter->path().string());
    }
    return true;
}
学新通

5.2.3ParseHtml

5.2.3.1框架
bool ParseHtml(const std::vector<std::string>& files_list, std::vector<DocInfo_t>* results)
{
	for (const std::string& file : files_list)
	{
		//1.读取文件 Read()    
		std::string result;//读取文件放到这里    
		if (!(ns_util::FileUtil::ReadFile(file, &result))) {
			continue;//读取失败,继续处理下一个文件    
		}

		//2.解析指定文件,提取title    
		DocInfo_t doc;
		//解析title    
		if (!(ParseTitle(result, &doc.title))) {
			continue;//提取报头失败    
		}

		//3.解析指令的文件,提取content(内容)(去标签)    
		if (!(ParseContent(result, &doc.content))) {
			continue;
		}

		//4.解析指定的文件路径,构建url    
		if (!(ParseUrl(file,&doc.url))){
			continue;
		}

		//push_back()本质是拷贝,效率低    
		results->push_back(doc);
	}
	return true;
}
学新通
5.2.3.2ns_util::FileUtil::ReadFile(); 读取文件

延伸知识点:
如何理解getline读取到文件结束呢?
getline返回的是一个&,
本质是因为重载了强制类型转化

#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 << "打开失败!" << std::endl;
                return false;
            }

            std::string line;
            while (std::getline(in, line))
            {
                *out  = line;
            }
            in.close();
            return true;

        }
    };
}
学新通
5.2.3.3ParseTitle();提取标签
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)//判断begin和end位置关系    
        return false;
    *title = file.substr(begin, end - begin);
    return true;
}
学新通
5.2.3.4ParseContent();获取文档的Content(解析去标签)

在进行遍历的时候,
只要碰到了 >,就意味着,当前的标签被处理完毕
只要碰到了 <,就意味着,新的标签开始了

static bool ParseContent(const std::string& file, std::string* content)
{
    //去标签,基于一个简易的状态机
    enum status {
        LABLE,
        CONTEN
    };

    enum status s = LABLE;
    for (char c : file)
    {
        switch (s)
        {
        case LABLE:
            if (c == '>') s = CONTEN;
            break;
        case CONTEN:
            if (c == '<') s = LABLE;
            else
            {
                //我们不想保留原始文件的'\n',因为我们想用'\n'作为html解析文本之后的分隔符
                if (c == '\n') c = ' ';
                content->push_back(c);
            }
            break;
        default:
            break;
        }
    }
    return true;
}
学新通
5.2.3.5 ParseUrl();

构建URL
boost库的官方文档,和我们下载下来的文档,是有路径对应关系的

官网URL样例: https 😕/www.boost.org/doc/libs/1_79_0/doc/html/accumulators.html
我们下载下来的url样例:boost_1_79_0 / doc / html / accumulators.html
我们拷贝到我们项目中的样例:data / input / accumulators.html
url_head = “https://www.boost.org/doc/libs/1_79_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)
{
    std::string url_head = "https ://www.boost.org/doc/libs/1_79_0/doc/html";
    std::string url_tail = file_path.substr(src_path.size());

    *url = url_head   url_tail;
    return true;
}
5.2.3.6测试
bool ParseHtml(const std::vector<std::string>& files_list, std::vector<DocInfo_t>* results)
{
	for (const std::string& file : files_list)
	{
		//1.读取文件 Read()    
		std::string result;//读取文件放到这里    
		if (!(ns_util::FileUtil::ReadFile(file, &result))) {
			continue;//读取失败,继续处理下一个文件    
		}

		//2.解析指定文件,提取title    
		DocInfo_t doc;
		//解析title    
		if (!(ParseTitle(result, &doc.title))) {
			continue;//提取报头失败    
		}

		//3.解析指令的文件,提取content(内容)(去标签)    
		if (!(ParseContent(result, &doc.content))) {
			continue;
		}

		//4.解析指定的文件路径,构建url    
		if (!(ParseUrl(result, &doc.url)))
		{
			continue;
		}

		//push_back()本质是拷贝,效率低    
		results->push_back(doc);

        //测试
        std::cout << "title:" << doc.title << std::endl;
        std::cout << "content:" << doc.content << std::endl;
        std::cout << "url:" << doc.url << std::endl;
        break;//打印一份就行
	}
	return true;
}
学新通

学新通

5.3SaveHtml();将解析结果写入到文件当中

目标:将results,写入到output中
采用方案:

title\3content\3url \n title\3content\3url \n title\3content\3url \n ...
//保存文件
//将results写入到output中    
bool SaveHtml(const std::vector<DocInfo_t>& results, const std::string& output)
{
#define SEP '\3'    
    //将output对应的文件打开    
    //按照二进制方式写入    
    std::ofstream out(output, std::ios::out | std::ios::binary);
    if (!out.is_open())
    {
        std::cerr << "打开" << "output" << "失败" << std::endl;
        return false;
    }

    //文件内容写入    
    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;
}
学新通

5.4锦上添花

```cpp
bool ParseHtml(const std::vector<std::string>& files_list, std::vector<DocInfo_t>* results)
{
	for (const std::string& file : files_list)
	{
		//1.读取文件 Read()    
		std::string result;//读取文件放到这里    
		if (!(ns_util::FileUtil::ReadFile(file, &result))) {
			continue;//读取失败,继续处理下一个文件    
		}

		//2.解析指定文件,提取title    
		DocInfo_t doc;
		//解析title    
		if (!(ParseTitle(result, &doc.title))) {
			continue;//提取报头失败    
		}

		//3.解析指令的文件,提取content(内容)(去标签)    
		if (!(ParseContent(result, &doc.content))) {
			continue;
		}

		//4.解析指定的文件路径,构建url    
		if (!(ParseUrl(file,&doc.url))){
			continue;
		}

		//push_back()本质是拷贝,效率低    
		results->push_back(doc);
	}
	return true;
}
学新通

每次循环之后,我们都要push_back(doc),其本质是拷贝,效率低下,影响性能

move:作为右值移动
将我们所要拷贝的对象,在地址空间层面上,让我们对象直接和容器当中的成员相关联,也就是说不会发生太多的拷贝


//push_back()本质是拷贝,效率低     
//results->push_back(doc);      
results->push_back(std::move(doc));

5.5结果

温馨提醒:
记得把 5.2.3.6 中的测试代码删除

学新通

vim raw.txt

学新通学新通
学新通

6. 建立索引模块 Index

6.1 index.hpp整体框架

#pragma once
#include<iostream>    
#include<string>    
#include<vector>    
#include<unordered_map>    

namespace ns_index
{
	//文档    
	struct DocInfo
	{
		std::string title;
		std::string content;
		std::string url;
		uint64_t doc_id; // 文档id    
	};

	//倒排对应的节点    
	struct InvertedElem
	{
		std::string word;//关键字    
		uint64_t doc_id;//文档id    
		int weight;//权重    
	};

	//倒排拉链    
	typedef std::vector<InvertedElem> InvertedList;

	class Index
	{
	private:
		//使用数组原因:数组下标可以当作文档id    
		std::vector<DocInfo> for_ward_index;//正排索引 

	 	//倒排索引:关键字与文档id = 1:n
		std::unordered_map<std::string, InvertedList> inverted_index;

	public:
		Index() {}
		~Index() {}

		//doc_id找到文档内容
		DocInfo* GetForwardIndex(const uint64_t doc_id) {
			return nullptr;
		}

		//关键字查找倒排拉链
		InvertedList* GetInvertedList(const std::string& word)
		{
			return nullptr;
		}

		//构建索引:整理raw.txt
		bool BuildIndex(const std::string& input)
		{
			return true;
		}
	};
}
学新通

6.2GetForwardIndex();GetInvertedList();BuildIndex()框架

		DocInfo* GetForwardIndex(const uint64_t doc_id)
		{
			if (doc_id >= for_ward_index.size())
			{
				std::cerr << "索引id 越界" << std::endl;
				return nullptr;
			}
			return &for_ward_index[doc_id];
		}

		//关键字查找倒排拉链
		InvertedList* GetInvertedList(const std::string& word)
		{
			auto iter = inverted_index.find(word);
			if (iter == inverted_index.end())
			{
				std::cerr << "没有找到【" << word << "】对应的倒排拉链" << std::endl;
				return nullptr;
			}
			return &(iter->second);
		}


		//构建索引:整理data/raw_html/raw.txt
		bool BuildIndex(const std::string& input)
		{
			std::ifstream in(input, std::ios::in | std::ios::binary);
			if (!in.is_open())
			{
				std::cerr << "给定路径【" << input << "】不能打开!" << std::endl;
				return false;
			}

			std::string line;
			while (std::getline(in, line))//按行读取内容
			{
				//构建正排索引
				DocInfo* doc = BuildForwardIndex(line);
				if (nullptr == doc)
				{
					std::cerr << "构建【" << line << "】失败!" << std::endl;
					continue;
				}

				//构建倒排索引
				BuildInvertedIndex(*doc);

			}
			return true;
		}
	private:
		DocInfo* BuildForwardIndex(const std::string& line)
		{
			return nullptr;
		}

		bool BuildInvertedIndex(const DocInfo& doc)
		{
			return false;
		}
学新通

6.2.1BuildForwardIndex();构建正排索引

		DocInfo* BuildForwardIndex(const std::string& line)
		{
			//1.解析line,对齐字符串按照"\3"切分
			std::vector<std::string> result;
			const std::string sep = "\3";//分隔符
			ns_util::StringUtil::Split(line, &result, sep);
			if (result.size() != 3)//切分字符串出错!
			{
				return nullptr;
			}

			//2.字符串填充DocInfo
			DocInfo doc;
			doc.title = result[0];
			doc.content = result[1];
			doc.url = result[2];
			doc.doc_id = for_ward_index.size();

			//3.插入到正排索引的vector中
			for_ward_index.push_back(std::move(doc));

			return &for_ward_index.back();
		}
学新通

util.hpp

    class StringUtil
    {
    public:
        //target:切分目标;out:目的地;sep:分隔符
        //这个方法需要被外部直接使用,所以要加static
        static void Split(const std::string& target, std::vector<std::string>* out, const std::string sep)
        {
            //boost/split:切分字符串
            //out目的地;target:切分目标;boost::is_any_of("分隔符");boost::token_compress_on:切分是否间隔
            boost::split(*out, target, boost::is_any_of(sep), boost::token_compress_on);
        }
    };

6.2.2BuildInvertedIndex();构建倒排索引

6.2.2.1思路

学新通

6.2.2.2 分词工具 cppjieba 介绍:

cppjieba的使用方法路径:
学新通
学新通
我们将放到cppjieba的同一目录下进行测试:

准备:
学新通

开始测试:
学新通

需要部分:
学新通

6.2.2.3将cppjieba 写入util.hpp中

准备工作:
学新通

#include <test/cppjieba/include/cppjieba/Jieba.hpp>//注意位置

//下面代码在ns_util中
    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 Split(const std::string& src, std::vector<std::string>* out)
        {
            jieba.CutForSearch(src, *word);
        }
    };
    //静态成员变量的初始化需要在类外面进行
    static cppjieba::Jieba JiebaUtil::jieba(DICT_PATH, HMM_PATH, USER_DICT_PATH, IDF_PATH, STOP_WORD_PATH);
学新通
6.2.2.4问题:暂停词

这个问题建议最后解决,此时可以忽略这块的问题
原因:
1.这里相当于只是优化,不影响整体效果
2.如果这里修改了,那么以后的测试中,将会非常的慢,浪费时间

在进行分词的过程中,“暂停词”也会被建立倒排索引,,,
util.hpp


  class StringUtil
  {
  public:
    // target:切分目标;out:目的地;sep:分隔符
    //这个方法需要被外部直接使用,所以要加static
    static void Split(const std::string &target, std::vector<std::string> *out, const std::string sep)
    {
      // boost/split:切分字符串
      // out:结果;target:切分目标;boost::is_any_of('分隔符');boost::token_compress_on:切分是否间隔
      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:
    //static cppjieba::Jieba jieba;
    cppjieba::Jieba jieba;
    
    //暂停词
    std::unordered_map<std::string,bool> stop_words;
    
    //设置成为单例模式
    JiebaUtil()
      :jieba(DICT_PATH,HMM_PATH,USER_DICT_PATH,IDF_PATH,STOP_WORD_PATH)
    {}
    JiebaUtil(const JiebaUtil&)=delete;
    static JiebaUtil *instance;
  public:
    static JiebaUtil* get_instance()
    {
      static std::mutex mtx;
      if(nullptr == instance)
      {
        mtx.lock();
        if(nullptr==instance)
        {
          instance = new JiebaUtil();
          instance->InitJiebaUtil();
        }
        mtx.unlock();
      }
      return instance;
    }

    void InitJiebaUtil()
    {
      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  ;
        }
      }

    }
    static void CutString(const std::string & src,std::vector<std::string> *out)
    {
      //jieba.CutForSearch(src,*out);
      ns_util::JiebaUtil::get_instance()->CutstringHelper(src,out);
    }
  };
  //静态成员变量的初始化需要在类外面进行
  //cppjieba::Jieba JiebaUtil::jieba(DICT_PATH,HMM_PATH,USER_DICT_PATH,IDF_PATH,STOP_WORD_PATH);
  JiebaUtil *JiebaUtil::instance =nullptr;
学新通

testtest/index.html

function Search(){
            // 是浏览器的一个弹出框
            // alert("hello js!");
            // 1. 提取数据, $可以理解成就是JQuery的别称
            let query = $(".container .search input").val();
            if(query=='' || query==null)
            {
                return;
            }
            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){
            if(data=='' || data ==null)
            {
                document.write("搜索的内容没有、或搜索的内容是暂停词!");
                return;
            }
学新通
6.2.2.5编写倒排索引的代码
    bool BuildInvertedIndex(const DocInfo &doc)
    {
      //字符串->倒排拉链
      struct word_cnt
      {
        int title_cnt;
        int content_cnt;

        word_cnt() :title_cnt(0) ,content_cnt(0)
        {}
      };

      //umpword_cnt 用来暂时存储词频
      std::unordered_map<std::string,word_cnt> umpword_cnt;
      
      //标题分词
      std::vector<std::string> title_words;
      ns_util::JiebaUtil::CutString(doc.title,&title_words);
      //标题词频统计
      for(auto s:title_words)
      {
        boost::to_lower(s);//将s里面的字母全部转为小写
        umpword_cnt[s].title_cnt  ;
      }

      //内容分词
      std::vector<std::string> content_words;
      ns_util::JiebaUtil::CutString(doc.content,&content_words);
      //内容词频统计
      for(auto s:content_words)
      {
        boost::to_lower(s);
        umpword_cnt[s].content_cnt  ;
      }

      for(auto &word_pair:umpword_cnt)
      {
        InvertedElem item;
        item.doc_id = doc.doc_id;
        item.word = word_pair.first;
        item.weight = word_pair.second.title_cnt*10 word_pair.second.content_cnt*1;//权重:10:1

        //<字符串,数组>,,,,插入数组元素
        InvertedList &Inverted_List = inverted_index[word_pair.first];
        Inverted_List.push_back(std::move(item));
      }
学新通

7.搜索引擎模块 Searcher.hpp

7.1整体框架

#pragma once
#include"index.hpp"

namespace ns_searcher
{
    class Searcher
    {
    private:
        ns_index::Index *index;//系统查找的索引

    public:
        Searcher(){}
        ~Searcher(){}

        //初始化
        void InitSearcher(std::string &input)
        {
            //1.获取/创建index对象
            //2.根据index对象建立索引
        }

        //提供搜索服务
        //query:搜索关键字;json_string:搜索结果
        void Search(const std::string &query,std::string *json_string)
        {
            //1.query分词
            //2.根据分“词”进行index查找
            //3.根据查找结果,根据权重进行降序排序
            //4.构建json_string
        }
    };
}
学新通

7.2将Index设置为单例模式

单例模式是存在线程安全的,所以我们需要对其进行加锁

  private:
    Index() {}
    Index(const Index &) = delete;
    Index &operator=(const Index &) = delete;

    static Index *instance;
    static std::mutex mtx;

  public:
    ~Index() {}

    static Index *GetInstance()
    {
      if (nullptr == instance)
      {
        mtx.lock();
        if (nullptr == instance)
          instance = new Index();
      }
      mtx.unlock();
      return instance;
    }
学新通
#include <mutex>

  Index *Index::instance = nullptr;
  std::mutex Index::mtx;

7.3 InitSearcher();

        //初始化
        void InitSearcher(std::string &input)
        {
            //1.获取/创建index对象(单例模式)
            index = ns_index::Index::GetInstance();

            //2.根据index对象建立索引
            index->BuildIndex(input);
        }

7.4Search();

7.4.1安装josncpp

sudo yum install -y jsoncpp-devel

7.4.2josncpp测试:

#include <iostream>
#include <string>
#include <vector>
#include <jsoncpp/json/json.h>

int main()
{
  Json::Value root;
  Json::Value item1;
  item1["key1"]="Value1";
  item1["key2"]="Value2";

  Json::Value item2;
  item2["key1"]="Value1";
  item2["key2"]="Value2";

  root.append(item1);
  root.append(item2);

  Json::StyledWriter writer;
  std::string s = writer.write(root);

  std::cout<<s<<std::endl;
  return 0;
}
学新通

输出:

[sakeww@VM-24-4-centos test]$ ./a.out 
[
   {
      "key1" : "Value1",
      "key2" : "Value2"
   },
   {
      "key1" : "Value1",
      "key2" : "Value2"
   }
]

[sakeww@VM-24-4-centos test]$ ll

7.4.3Search()编写

        //提供搜索服务
        //query:搜索关键字;json_string:搜索结果
        void Search(const std::string &query,std::string *json_string)
        {
            //1.query分词
            std::vector<std::string> words;
            ns_util::JiebaUtil::CutString(query,&words);

            //2.根据分“词”进行index查找
            ns_index::InvertedList inverted_list_all;//所有词的倒排拉链
            for(auto &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());
            }

            //3.根据查找结果,根据权重进行降序排序
            sort(inverted_list_all.begin(),inverted_list_all.end(),\
                [](const ns_index::InvertedElem &a,const ns_index::InvertedElem &b){
                    return a.weight>b.weight;//降序
            });


            //4.构建json_string
            //根据3,构建json串-jsoncpp-通过jsoncpp完成序列化和反序列化
            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;
                //doc->content是文档去标签之后的内容,我们要的是截取其中一部分
                elem["desc"]=doc->content;
                elem["url"]=doc->url; 

                root.append(elem);
            }

            Json::StyledWriter writer;
            *json_string = writer.write(root);
        }
学新通

7.4.4测试

server.cc

#include"searcher.hpp"
#include<iostream>
#include<string>

const std::string input="data/raw_html/raw.txt";

int main()
{
  //测试:
  ns_searcher::Searcher *search = new ns_searcher::Searcher();
  search->InitSearcher(input);//初始化

  std::string query;//搜索内容
  std::string json_string;//最终结果
  while(true)
  {
    std::cout<<"输入搜索内容:";

    //当出现空格的时候,我们输入aa bb cc相当于我们搜索了三个内容
    //cin>>query
    getline(std::cin,query);
    search->Search(query,&json_string);

    std::cout<<json_string<<std::endl;
  }
  return 0;
}
学新通

Makefile

.PHONY:all
all:Parser Server

Parser:parser.cc 
	g   -o $@ $^ -std=c  11 -lboost_system -lboost_filesystem

Server:server.cc
	g   -o $@ $^ -std=c  11 -lboost_system -lboost_filesystem -ljsoncpp

.PHONY:clean
clean:
	rm -f Parser Server

测试指令:

//数据去标签,对网页内容进行数据清理
./Parser
//结果在data/raw_html/raw.txt形成8000多行数据

//对数据进行处理,构建索引
./Server

输入搜索内容
结果:出现大量数据

7.4.5建立摘要

为什么需要摘要?
因为经过我们的测试,搜索出来的数据量很大,不是很美观。

在构建json_string中

 Json::Value elem;
 elem["title"]=doc->title;
 //doc->content是文档去标签之后的内容,我们要的是截取其中一部分
 //elem["desc"]=doc->content;
 //item.word关键字内容
 elem["desc"]=GetDesc(doc->content,item.word);
 elem["url"]=doc->url; 

7.4.6GetDesc();

        std::string GetDesc(const std::string &h_content,const std::string &word)
        {
            //1.找到word在h_content首次出现位置,
            //我们的准备过程中,是将所有的内容转为小写,然后加入到搜索内容中的,word可能包含大写!
            auto iter = std::search(h_content.begin(),h_content.end(),word.begin(),word.end(),[](int a,int b){
                return (std::tolower(a)==std::tolower(b));
            });
            if(iter == h_content.end())//判断“内容中”是否含有"word“
            {
                return "h_content中找不到word!";
            }
            //获取迭代器距离
            std::size_t pos = std::distance(h_content.begin(),iter);

            //2.截取前五十(prev_cut)到word到后一百个(next_cut)的内容,
            const std::size_t prev_cut = 50;
            const std::size_t next_cut = 100;

            //默认我们全部截取
            std::size_t begin = 0;
            std::size_t end = h_content.size()-1;
            if(pos > begin prev_cut) begin = pos-prev_cut;
            if(pos next_cut<end) end = pos next_cut;

            //3.截取返回
            //substr(开始位置,截取多少);
            if(begin>=end) return "截取返回出现问题!";
            return h_content.substr(begin,end-begin);
        }
学新通

7.4.7问题:

7.4.7.1问题介绍:

在Search();的第二步:2.根据分“词”进行index查找中
我们第一步会对一段话进行分词
例如:
搜索内容:吃葡萄不吐葡萄皮
文档内容:吃葡萄不吐葡萄皮吃葡萄不吐葡萄皮吃葡萄不吐葡萄皮
在根据分词进行查找时:
吃,葡萄,吐,等等很多词语都会指向这个文档内容
从而导致:输出结果会出现很多重复内容

7.4.7.2问题测试:

测试:
data/input目录下更改一个比较小的文件

<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">
<html>
  <head>
  <!-- Copyright (C) 2002 Douglas Gregor <doug.gregor -at- gmail.com>

      Distributed under the Boost Software License, Version 1.0.
      (See accompanying file LICENSE_1_0.txt or copy at
      http://www.boost.org/LICENSE_1_0.txt) -->
    <title>Redirect to generated documentation 测试搜索重复问题test test test test test test </title>
    <meta http-equiv="refresh" content="0; URL=http://www.boost.org/doc/libs/master/doc/html/unordered.html">
  </head>
  <body>
    测试内容:test test test test test test
    Automatic redirection failed, please go to
    <a href="http://www.boost.org/doc/libs/master/doc/html/unordered.html">http://www.boost.org/doc/libs/master/doc/html/unordered.html</a>
  </body>
</html>
学新通

学新通

7.4.7.3问题解决
    struct InvertedElemPrint
    {
        uint64_t doc_id;
        int weight;
        std::vector<std::string> word;
        InvertedElemPrint() : doc_id(0), weight(0) {}
    };

    class Searcher
    {
    private:
        ns_index::Index *index; //系统查找的索引

    public:
        Searcher() {}
        ~Searcher() {}

        //初始化
        void InitSearcher(std::string input)
        {
            // 1.获取/创建index对象(单例模式)
            index = ns_index::Index::GetInstance();
            std::cout << "获取index单例成功" << std::endl;

            // 2.根据index对象建立索引
            index->BuildIndex(input);
            std::cout << "建立正排倒排索引成功" << std::endl;
        }

        //提供搜索服务
        // query:搜索关键字;json_string:搜索结果
        void Search(const std::string &query, std::string *json_string)
        {
            // 1.query分词
            std::vector<std::string> words;
            ns_util::JiebaUtil::CutString(query, &words);

            // 2.根据分“词”进行index查找
            //ns_index::InvertedList inverted_list_all;                   //所有词的倒排拉链
            std::vector<InvertedElemPrint> inverted_list_all;
            std::unordered_map<uint64_t, InvertedElemPrint> tokens_map; // map
            for (auto &word : words)
            {
                boost::to_lower(word); //转小写

                ns_index::InvertedList *inverted_list = index->GetInvertedList(word);
                if (nullptr == inverted_list)
                {
                    //这个词没有倒排节点->没有正排节点->检测下一个词
                    continue;
                }

                //含有输出重复内容bug的地方:
                // 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.word.push_back(elem.word);
                }
            }
            for (const auto &item : tokens_map)
            {
                inverted_list_all.push_back(std::move(item.second));
            }

            // 3.根据查找结果,根据权重进行降序排序
            // sort(inverted_list_all.begin(), inverted_list_all.end(),
            //      [](const ns_index::InvertedElem &a, const ns_index::InvertedElem &b)
            //      {
            //          return a.weight > b.weight; //降序
            //      });
            std::sort(inverted_list_all.begin(), inverted_list_all.end(),
                    [](const InvertedElemPrint &a, const InvertedElemPrint &b){
                        return a.weight > b.weight;
        });

            // 4.构建json_string
            //根据3,构建json串-jsoncpp-通过jsoncpp完成序列化和反序列化
            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;
                // doc->content是文档去标签之后的内容,我们要的是截取其中一部分
                // elem["desc"]=doc->content;
                // item.word关键字内容
                elem["desc"] = GetDesc(doc->content, item.word[0]);
                elem["url"] = doc->url;

                root.append(elem);
            }

            Json::StyledWriter writer;
            *json_string = writer.write(root);
        }
学新通

学新通

8.http_server模块

8.1准备工作,

8.1.1安装较新版本的gcc

cpp_httplib在使用的时候需要使用较新版本的gcc(大约是7.0以上的版本)
centos 7默认版本是4.8.5

//查看gcc版本
gcc -v

安装gcc

//安装scl源
sudo yum install centos-release-scl scl-utils-build
//安装新版本的gcc
sudo yum install -y devtoolset-7-gcc devtoolset-7-gcc-c  
//启动新版本
[whb@VM-0-3-centos boost_searcher]$ scl enable devtoolset-7 bash

8.1.2安装cpp-httplib

最新的cpp-httplib在使用的时候,如果gcc不是特别新的话有可能会有运行时错误的问题
建议:cpp-httplib 0.7.15
下载zip安装包,上传到服务器即可

cpp-httplib安装路径:
https://gitee.com/zhangkt1995/cpp-httplib?_from=gitee_search

8.1.3测试

http_server.cc

#include "cpp-httplib/httplib.h"
int main()
{
    httplib::Server svr;
    svr.Get("/test", [](const httplib::Request &req, httplib::Response &rsp{
        rsp.set_content("你好,这是一个测试!", "text/plain; charset=utf-8"); 
    });
    svr.listen("0.0.0.0", 8081);
    return 0;
}

在浏览器上:
服务器号:8081/test

8.2代码:

#include "cpp-httplib/httplib.h"
#include "searcher.hpp"
const std::string input = "data/raw_html/raw.txt";
const std::string root_path = "./testtest";
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;
        }
        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"); });


    //测试代码
    // httplib::Server svr;
    // svr.set_base_dir(root_path.c_str());
    // svr.Get("/test", [](const httplib::Request &req, httplib::Response &rsp)
    //         { rsp.set_content("你好,这是一个测试!", "text/plain; charset=utf-8"); });
    svr.listen("0.0.0.0", 8081);
    return 0;
}
学新通
服务器号/s?word=split

9.前端

testtest/index.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">
    <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>

学新通

10.添加日志log.hpp

10.1代码:

#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__)

// level:日志等级;message:日志内容;file:错误文件;line:第几行
void log(std::string level, std::string message, std::string file, int line)
{
    std::cout << "[" << level << "]";
    std::cout << "[" << time(nullptr) << "]";
    std::cout << "[" << message << "]";
    std::cout << "[" << file << "]";
    std::cout << "[" << line << "]"<<std::endl;
}

学新通

10.2测试:

在lindex.hpp中
我们在查看构建索引进度的地方

        if(countP0==0) 
          LOG(NORMAL,"当前进度:" std::to_string(count));
          //std::cout<<"当前进度:"<<count<<std::endl;

学新通
我们将启动过程中生成的日志放进一个文件内

10.3将程序部署到服务器上

nohup ./http_server > log/log.txt 2>&1 &

查看进程

ps axj | grep Http_server

删除

kill -9 进程

11.整体理解

11.1各个文件简介

学新通

整体思路

学新通

12.个人遇见问题整理

12.1点击链接自动添加ip和端口号

1.一切准备就绪,在搜索框内输入关键字,然后点击搜索,此时也是可以正常出现内容的,但是在点击出现内容的标题(正常情况下会进入一个新的网页),但是此时进入的是:
学新通

只有将蓝色部分,删除了,才能进入这个我们想要的网页(除了蓝色部分以外的灰色部分)
检查步骤:
./Parser
这个不需要执行,我们只需要看data/raw_html/raw.txt里面的内容即可
./Server
执行这个看后端整体是否有问题
./Http_server
就是我们目前的状态

延伸检查:
在edge中ctrl shift i(开发人员工具)
点击元素
当我们正常输入关键字,然后搜索,
打开我们要点击的网页蓝色字体部分,

问题所在:
https:这里有个空格//:www.boo…
即:https://这部分格式不对,导致其自动添加

解决思路起源点:
学新通

12.2关于12.1的补充

不知道什么原因:12.1中的那个空格还是会拥有,虽然不知道什么情况,但是以下几点是我猜测和乱想,仅供参考
1.长时间不使用这个项目,然后在加载的时候,打开了vscode,可能会自动优化,导致这里加了一个空格

12.3关于网络端口号

现象:长时间不使用这个服务,或者不常用网络端口号,我以前打开网络端口的时候,是数字-数字的形式,今天访问范围内的网络端口的时候不行
解决:仅供参考
在内部打开指定网络端口号

firewall-cmd --permanent --zone=public --add-port=8080/tcp

这篇好文章是转载于:学新通技术网

  • 版权申明: 本站部分内容来自互联网,仅供学习及演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系,请提供相关证据及您的身份证明,我们将在收到邮件后48小时内删除。
  • 本站站名: 学新通技术网
  • 本文地址: /boutique/detail/tanhgebkkf
系列文章
更多 icon
同类精品
更多 icon
继续加载