Boost搜索引擎

目录

​编辑

1.项目相关背景

2.搜索引擎的相关宏观原理

3.搜索引擎技术栈和项目环境

4.正倒排索引-搜索引擎具体原理

5.数据去标签与数据清洗模块-Parser

6.建立索引的模块-Index

7.搜索引擎模块-Searcher

8.http-server模块

9.前端模块


gitee源码:新增加boost搜索引擎源码 · cd86251 · XL/gaichihci的学习仓库 - Gitee.com

我们编写完前端网页如下图所示:(只需要服务器连接上,浏览器客户端输入"公网IP:端口号")

1.项目相关背景

(1)当前已有众多企业推出了各类搜索引擎产品:

百度,搜狗,360搜索,头条新闻客户端........

让我们尝试自行研发这种全网搜索引擎吧,实际上我们很难独立完成。

(2)但简单的站内搜索我们还是可以做到的,好像管中窥豹一样。(Boost官网是没有站内搜索的,需要我们自己来实现一个)

站内搜索相较于大型搜索更加垂直(搜索的内容更相关,相关性更强),数据量更小

(3) 像主流搜索引擎(如百度、搜狗)的搜索结果通常包含三个核心要素:

  1. 网页标题
  2. 内容摘要描述
  3. 目标网页链接

接下来我们将实现的也是这三类要素。

2.搜索引擎的相关宏观原理

(1)上述红色方框内就是我们接下来要实现的功能

(2)因为上述全网爬虫在我国有点法律风险,在本文就不实现了,把相关数据下载下来就行

3.搜索引擎技术栈和项目环境

· 技术栈: c/c++,c++11,STL,准标准库Boost,jsoncpp,cppjieba,cpp-httplib

(中间我们还会使用到html5,css,js,jQuery,Ajax)

·项目环境:Centos 7云服务器(目前推荐Ubuntu,因为博主Centos 7下有些配置无法现在目前,看个人情况),vim/gcc(g++)/Makefile,vs2019 or vscode

4.正倒排索引-搜索引擎具体原理

实例:

|----------------|
| 文档1:我买了四斤小米 |
| 文档2:我发布了该吃吃牌手机 |

(1)正排索引:就是从文档ID找到文档内容(文档内的关键字)

|------|------------|
| 文档ID | 文档内容 |
| 1 | 我买了四斤小米 |
| 2 | 我发布了该吃吃牌手机 |

然后我们对目标文档进行分词(目的:方便我们进行倒排索引和查找)

|-------------------|
| 文档1:我/买/了四斤/小米 |
| 文档2:我/发布/了/该吃吃牌手机 |

注意:停止词:了,的,吗,a,the,一般我们在分词的时候可以不考虑

(2)倒排索引:根据文档内容,分词,整理不重复的各个关键字,找到文档ID的方案

|------------|-----------------|
| 关键字(具有唯一性) | 文档ID,weight(权重) |
| 我 | 文档1,文档2 |
| 买 | 文档1 |
| 发布 | 文档2 |
| 四斤 | 文档1 |
| 小米 | 文档1 |
| 该吃吃牌手机 | 文档2 |

上述weight(权重):后面我们会实现一个简单的权重将搜索出来的,哪些文档排在前面,哪些排在后面

(3)模拟一次简单的查找过程:

用户输入:我-->倒排索引中查找-->提取文档(1,2)-->根据正排索引-->找到文档内容-->title

+content(desc)+url 文档结果进行摘要-->构建响应结果

5.数据去标签与数据清洗模块-Parser

注意:下载最新版boost前请先访问官网获取当前最新版本。后续处理时仅需保留HTML文件。

在开始去除标签前,我们需要先明确目标内容:

|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| boost官网:Boosthttps://www.boost.org/ //目前在本文内容需要用的是⽬前只需要 boost_1_89_0/doc/html ⽬录下的 html ⽂件,⽤它来进⾏建⽴索引 |

去标签之前,我们来看看去标签之前的数据

从上图我们可以看到 <> : html 的标签,这个标签对我们进⾏搜索是没有价值的,需要去掉这些标签,⼀般标签都是成对出现的!

去标签:

(1)建立我们需要的文件

首先,我们需要创建一个目录来存放boost搜索引擎项目的所有处理文件。

在本文我们将他命名为boost_searcher

其次,我们创建一个data目录,里面存放处理好的(也就是去完标签的)和没处理好的文件(这里的文件指的是html文件)

未处理好的html文件放入input中

处理好的html文件存放处:

然后我们需要建立编写parser模块的文件:

上述util.hpp文件是我们实现一些编写工具的代码

(2)查看makefile文件

boost开发库的安装:

cpp 复制代码
sudo yum install -y boost-devel //是boost 开发库

因为我们在实现parser模块需要使用到boost开发库,所以在编译期间我们需要告知g++我们需要的库文件是什么(注意:-std=c++11中间不能用空格隔开)

cpp 复制代码
cc = g++
parser:parser.cc
	$(cc) -o $@ $^ -lboost_system -lboost_filesystem -std=c++11
.PHONY:clean
clean:
	rm -f parser

(3)查看parser.cc文件

cpp 复制代码
#include <iostream>
#include <string>
#include <vector>
#include <boost/filesystem.hpp>
#include "util.hpp"
//是一个目录,下面存放的是html网页
const std::string src_path = "data/input";
//存放去标签后的html网页的目录
const std::string output = "data/raw_html/raw.txt"; 

typedef struct DocInfo
{
  std::string title;//文档标题
  std::string content;//文档内容
  std::string url;//该文档在官网中的url
}DocInfo_t;

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> *results);
bool SaveHtml(const std::vector<DocInfo> &results,const std::string &output);

int main()
{
  std::vector<std::string> files_list;
  //递归式的把每个html文件名带路径,保存到files_list中,方便后期一个一个文件的读取
  if(!EnumFile(src_path,&files_list))
  {
    std::cout<<"enum file name error"<<std::endl;
    return 1;
  }
  //按照files_list读取每个文件内容,并进行解析
  std::vector<DocInfo_t> results;
  if(!ParseHtml(files_list,&results))
  {
    std::cout<<"Parse html error"<<std::endl;
    return 2;
  }
  //将解析完的各个文件内容,写入到output,按照\3作为每个文档的分隔符
  if(!SaveHtml(results,output))
  {
    std::cout<<"save html error"<<std::endl;
    return 3;
  }
  return 0;
}

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++)
  { //判断文件是否为普通文件,因为html文件都是普通文件
    if(!fs::is_regular_file(*iter))
    {
      continue;
    }
    //判断文件路径名后缀是否满足
    if(iter->path().extension()!=".html")
    {
      continue;
    }
    //当前的路径一定是一个合法的,且以html结尾的普通文件
    //测试: std::cout<<"debug:"<<iter->path().string()<<std::endl;
    files_list->push_back(iter->path().string());//将所有文件带路径的html保留在files_list,以便后续文本分析
  }
 

  return true;
}

bool ParserTitle(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;
}
bool Parsercontent(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 = ' ';//我们这里不保留'\n',因为我们后面要'\n'作为html解析之后文本的分隔符
        content->push_back(c);
      }
      break;
    default:
      break;
  }
  }

  return true;
}
bool ParserUrl(const std::string &file_path,std::string *url)
{
  std::string url_head = "https://www.boost.org/doc/libs/1_89_0/doc/html";
  std::string url_tail = file_path.substr(src_path.size());
  *url = url_head + url_tail;
  return true;
}
//void Showdug(const DocInfo_t &doc)
//{
//std::cout<<"title:"<<doc.title<<std::endl;
//std::cout<<"content:"<<doc.content<<std::endl;
//std::cout<<"url:"<<doc.url<<std::endl;
//}
bool ParseHtml(const std::vector<std::string> &files_list,std::vector<DocInfo> *results)
{
  for(const std::string &file : files_list)
  {
    //读取文件,read();
    std:: string result;
    if(!ns_util::FileUtil::ReadFile(file,&result))
    {
      continue;
    }
    DocInfo_t doc;
    //解析指定文件,提取title
    if(!ParserTitle(result,&doc.title))
    {
      continue;    
    }
    //解析指定文件,提取content
    if(!Parsercontent(result,&doc.content))
    {
      continue;
    }
    //解析指定的文件路径,构建url
    if(!ParserUrl(file,&doc.url))
    {
      continue;
    }
    //完成了文档的解析任务,当前文档的相关结果都保存在了doc中
    results->push_back(std::move(doc));//细节todo:本质会发生拷贝,效率比较低

    //for dubug
    // Showdug(doc);
    
  }
  return true;
}
bool SaveHtml(const std::vector<DocInfo> &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<<"failed"<<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;
}

(4)编写util.hpp文件

cpp 复制代码
#pragma once
#include <iostream>
#include <string>
#include<fstream>
namespace ns_util
{
  class FileUtil
  { public:
    static bool ReadFile(const std::string &files_path,std::string *out)
    {
      std::ifstream in(files_path,std::ios::in);
      if(!in.is_open())
      {
        std::cerr<<files_path<<"open error"<<std::endl;
        return false;
      }
      std::string line;
      while(std::getline(in,line))//有一个关键点就是getline返回是一个流的引用,这里是因为本质进行了重载强制类型转换,才得以可以用作循环判断
      {
        *out+=line;
      }
      in.close();
      return true;
    }
  };
}

写入文件时,务必考虑后续读取操作的便捷性。

建议采用以下格式: title\3content\3url\n title\3content\3url\n title\3content\3url\n...

这种结构化格式既方便使用getline(ifstream, line)一次性读取整个文档,又能保持数据字段的清晰分隔。

6.建立索引的模块-Index

展示一下index.hpp所有基本代码(后面再进行逐一讲解):

cpp 复制代码
#pragma once
#include <iostream>
#include "log.hpp"
#include <fstream>
#include <string>
#include <vector>
#include <mutex>
#include <unordered_map>
#include "util.hpp"
namespace ns_index
{
  struct DocInfo{
    std::string title;//文档标题
    std::string content;//文档去标签后的内容
    std::string url;//官网文档url
    uint64_t doc_id;//文档ID
  };
  struct InvertedElem{
    std::string word;
    uint64_t doc_id;
    int weight;
  };
  typedef std::vector<InvertedElem> InvertedList;//倒排拉链

  class Index
  {
    private:
      //正排索引的数据结构用数组,数组的下标就是文档ID
      std::vector<DocInfo> forward_index; 
      //倒排索引是一个关键字和一个(一组)倒排拉链对应(关键字和倒排拉链建立映射关系)
      std::unordered_map<std::string,InvertedList> inverted_index;

    private:
      Index(){}//一定要有函数体,不能delete
      Index(const Index&) = delete;
      Index& operator=(const Index&) = delete;


      static Index *instance;
      static std::mutex mtx;
    public:
      ~Index(){

      }
    public:
      static Index * GetInstance()
      {
        if(instance == nullptr)
        {
          mtx.lock();
          if(instance ==nullptr)
          {
            instance = new Index();
          }
          mtx.unlock();
        }
        return instance;
      }
      //根据doc_id找到文档内容
      DocInfo * GetForwardIndex(uint64_t doc_id)
      {
        if(doc_id >= forward_index.size())
        {
          std::cerr<<"doc_id out range err"<<std::endl;
          return nullptr;
        }
        return &forward_index[doc_id];
      }
      //根据关键字string,获得倒排拉链
      InvertedList * GetInvertedList(std::string word)
      {
        auto iter = inverted_index.find(word);
        if(iter == inverted_index.end())
        {
          std::cerr<<word<<"have no invertedlist"<<std::endl;
          return nullptr;
        }

        return &(iter->second);
      }
      //根据去标签后格式化的文档,构建正排索引和倒排索引
      bool BuildIndex(const std::string &input)//parser处理完毕后的数据
      {
        std::ifstream in(input,std::ios::in | std::ios::binary);
        if(!in.is_open())
        {
          std::cerr<<input<<"open err"<<std::endl;
          return false;
        }
        std::string line;
        int count = 0;
        while(std::getline(in,line))
        {
          DocInfo *doc = BuildFwardIndex(line);
          if(doc == nullptr)
          {
            std::cerr<<"build"<<line<<"err"<<std::endl;
            continue;
          }
          BuildInvertedIndex(*doc);
          count++;
          if(count%50 == 0)
          {
           // std::cout<<"当前建立索引文档"<<count<<std::endl;
		   LOG(NORMAL, "当前的已经建立的索引文档: " + std::to_string(count));
          }
        }
        return true;
      }
    private:
      DocInfo * BuildFwardIndex(const std::string &line)
      {
        //解析line,字符串切分
        std::vector<std::string> result;
        const std::string sep = "\3";
        ns_util::StringUtil::Split(line,&result,sep);
        if(result.size()!=3)
        {
          std::cerr<<"cut"<<line<<"err"<<std::endl;
          return nullptr;
        }
        //将字符串进行填充
        DocInfo doc;
        doc.title = result[0];
        doc.content = result[1];
        doc.url = result[2];
        doc.doc_id = forward_index.size();
        //插入正排索引的vector
        forward_index.push_back(doc);
        return &forward_index.back();
      }
      bool BuildInvertedIndex(const DocInfo &doc)
      {
        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_word;
        ns_util::JiebaUtil::CutString(doc.title,&title_word);
        for(auto s : title_word)
        {
          boost::to_lower(s);
          word_map[s].title_cnt++;
        }
        //对文档内容进行分词
        std::vector<std::string> content_word;
        ns_util::JiebaUtil::CutString(doc.content,&content_word);
        for(auto s : content_word)
        {
          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(std::move(item));
          
        }
        return true;
      }
  };
    Index *Index::instance = nullptr;
    std::mutex Index::mtx;
}

工具util:(这里就先全部展示出来,因为本博主没有边做项目,边编写博客,只能麻烦一下xd,到时候查看用目录跳转一下或者其他方法)

cpp 复制代码
#pragma once
#include <iostream>
#include <string>
#include <fstream>
#include <vector>
#include <boost/algorithm/string.hpp>
#include "cppjieba/Jieba.hpp"
namespace ns_util
{
  class FileUtil
  { public:
    static bool ReadFile(const std::string &files_path,std::string *out)
    {
      std::ifstream in(files_path,std::ios::in);
      if(!in.is_open())
      {
        std::cerr<<files_path<<"open error"<<std::endl;
        return false;
      }
      std::string line;
      while(std::getline(in,line))//有一个关键点就是getline返回是一个流的引用,这里是因为本质进行了重载强制类型转换,才得以可以用作循环判断
      {
        *out+=line;
      }
      in.close();
      return true;
    }
  };

  class StringUtil
  {
    public:
      static void Split(const std::string &line,std::vector<std::string> *result,const std::string &sep)
      {
        boost::split(*result,line,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;
    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);
}

1.让我们首先构建索引数据的基本框架。

2.第二步当然是建立我们的正排索引和倒排索引

(1)这里我们将一组数据(InvertedElem)用vector存储起来就像拉链一样,我们就简称倒排拉链,最后构成倒排索引的一部分

(2)为便于后续使用,我们改用typedef定义

3.正常的析构和构造函数是对象生命周期管理的基本机制。在C++中,构造函数在对象创建时自动调用,而析构函数在对象销毁时自动调用。然而在某些情况下,比如处理频繁访问的索引文件时,这种自动机制会导致性能问题。

以index.hpp为例,如果每次访问都自动构造索引,会导致以下问题:

  1. 重复初始化开销
  2. 内存资源浪费
  3. 潜在的性能瓶颈

为解决这个问题,我们可以实现单例模式,具体步骤如下:

  1. 将构造函数设为私有,防止外部直接实例化
  2. 提供静态的GetInstance方法作为唯一访问点
  3. 在该方法中实现懒加载逻辑:
    • 首次调用时构造实例
    • 后续调用返回已存在的实例
  4. 同时要注意线程安全,所以我们加入了入互斥锁保护数据。

示例实现代码框架:

注意:这里静态成员初始化要在类外初始化,否则会报错!!!

4. 在代码中间部分,我已经添加了详细的注释,这里就不再逐行解释了。下面重点说明一下关键实现:我们使用to_lower函数将所有搜索代码转为小写,这样做的目的是为了统一统计权重,并实现搜索关键词的大小写不敏感。需要注意的是,后续在searcher.hpp文件中,我们也需要将搜索到的关键词转为小写形式进行匹配。

最后定义的X和Y是权重(在标题出现就*X,在内容出现就*Y)

注意:这里我们引入了cppjieba库,安装包需要自己去gitee去搜索这里就不提供了

这个cppjieba库我们只用了下面两个文件一个是dict,一个是cppjieba,这里我们用了软连接将其链接在了本目录下,专门用了threadpart目录来保存本项目使用的各种库

7.搜索引擎模块-Searcher

依旧老样子先展示正文代码:

cpp 复制代码
#pragma once
#include "index.hpp"
#include "util.hpp"
#include <algorithm>
#include "log.hpp"
#include <jsoncpp/json/json.h>
namespace ns_searcher
{
	struct InvertedElemPrint{
		uint64_t doc_id;
		int weight;
		std::vector<std::string> words;
		InvertedElemPrint():doc_id(0),weight(0){}
	};
  class Searcher{
    private:
      ns_index::Index *index;//共系统查找的索引
    public:
      Searcher(){}
      ~Searcher(){}
    public:
      void InitSearcher(const std::string &input)
      {
        //获取或者创建index对象
        index = ns_index::Index::GetInstance();
        //std::cout<<"获取单例索引成功"<<std::endl;
		LOG(NORMAL, "获取index单例成功...");
        //根据index对象建立索引
        index->BuildIndex(input);
        // std::cout<<"建立索引成功"<<std::endl;
		LOG(NORMAL, "建立正排和倒排索引成功...");
      }
      //query:搜索关键字
      //json_string:返回给用户浏览器的搜索结果
      void Search(const std::string &query,std::string *json_string)
      {
        std::vector<std::string> words;
        //1.[分词]:对我们query进行按照searcher的要求进行分词
        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;
        for(std::string word : words)
        {
          boost::to_lower(word);
          ns_index::InvertedList *inverted_list = index->GetInvertedList(word);
          if(nullptr == inverted_list)
          {
            continue;
          }
		  for(const 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);
		  }
          //这里将我们所有的去重后的倒排拉链放在inverted_list_all中,方便进行后续排序
		  for(const auto &item : tokens_map)
		  {
		  inverted_list_all.push_back(std::move(item.second));
		  }

         // inverted_list_all.insert(inverted_list_all.end(),inverted_list->begin(),inverted_list->end());
        }
        //3.[合并排序]:汇总查找结果,按照相关性(weight)降序排序
        std::sort(inverted_list_all.begin(),inverted_list_all.end(),[](const InvertedElemPrint &e1,const InvertedElemPrint &e2){
                return e1.weight>e2.weight;
            });

        //4.[构建]:根据查找出来的结果,构建json串--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;
          elem["desc"] = GetDesc(doc->content,item.words[0]);
          elem["url"] = doc->url;
          root.append(elem);
        }
        Json::FastWriter writer;
        *json_string = writer.write(root); 
      }
      std::string GetDesc(const std::string &html_content,const std::string &word)
      {
        int prev_step = 50;
        int next_step = 100;
        //找到首次出现word
        //因为find搜索是不全部按照小写进行搜索的,所以这里我们用search来进行搜索
        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);
        //获取start,end
        
        int start = 0;
        int end = html_content.size()-1;
        //size_t是一个无符号整形所以在加减的时候一定要注意!!!!(重点)
        //为了更加简便我们将其全部从size_t用int来替换
        if(pos > start+prev_step) start = pos-prev_step;
        if(pos <end-next_step) end = pos+next_step;
       
       //截取字符串 
        if(start>=end)
        {
          return "NONE2";
        }
		std::string desc = html_content.substr(start,end - start);
        desc+="...";
		return desc;
      }
  };

}

1.采用这种 words 结构的设计是为了解决实际问题。如果仅用 word 而非 vector 存储,我们在实践中发现检索结果会出现大量重复文档的情况,即在倒排拉链上存在多个相同文档的问题。

具体应用在search函数:

我们定义了一个tokens_map,通过遍历倒排索引链(其中可能包含重复文档)来重新计算权重。对于相同文档,我们累加其权重值,同时保持文档ID不变。重复文档的关键字会被收集到一个vector中存储,这样既实现了去重目的,又保留了完整的索引信息。

这里就体现了我们上述结构体的关键!!!

2.下图的构建desc的内容是正文的一部分(具体我们可以随便搜索一下浏览器都是标题,正文的一部分...)

关键在于我们采用了Json库实现文档的序列化和反序列化处理。具体安装时,只需在Gitee平台搜索Jsoncpp即可下载相应的库文件。

用来测试searcher:debug的测试代码:

cpp 复制代码
#include "searcher.hpp"
#include <iostream>
#include <cstring>
#include <string>
#include <cstdio>

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

int main()
{
  //for text
  ns_searcher::Searcher *search = new ns_searcher::Searcher();
  search->InitSearcher(input);
  std::string query;
  std::string json_string;
  while(true)
  {
    //在这里我们使用cin会以空格为分隔符一个一个输出,我们想要的字符串整体一起输出
    std::cout<<"Please Enter You Search Query#";
    char buffer[1024];
    fgets(buffer,sizeof(buffer)-1,stdin);
    buffer[strlen(buffer)-1] = 0;
    query = buffer;
    search->Search(query,&json_string);
    std::cout<<json_string<<std::endl;
  }
  return 0;
}

8.http-server模块

正文展示:

cpp 复制代码
#include "searcher.hpp"
#include "cpp-httplib/httplib.h"

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

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,"text/plain;charset=utf-8");
			});
        	svr.listen("0.0.0.0",8081);
  return 0;
}

makefile:

cpp 复制代码
PARSER=parser
DUG=debug
HTTP_SERVER=http_server
cc = g++

.PHONY:all
all:$(PARSER) $(DUG) $(HTTP_SERVER)

$(PARSER):parser.cc
	$(cc) -o $@ $^ -lboost_system -lboost_filesystem -std=c++11
$(DUG):debug.cc
	$(cc) -o $@ $^ -ljsoncpp -std=c++11
$(HTTP_SERVER):http_server.cc                                              
	$(cc) -o $@ $^ -ljsoncpp -lpthread -std=c++11
	
.PHONY:clean
clean:
	rm -f $(PARSER) $(DUG) $(HTTP_SERVER)

(1)建议使用cpp-httplib库时注意:该库需要较新版本的GCC编译器,而CentOS 7系统默认安装的GCC 4.8.5版本可能无法满足要求。(原因:⽤⽼的编译器,要么编译不通过,要么直接运⾏报错)

(2)访问网页时,请在地址栏输入"公网IP:端口号(本示例使用8081)"。此时页面将仅显示由svr.set_base_dir(root_path.c_str())函数提供的网页内容,这些网页文件存储在我们的wwwroot目录下。从这里开始,我们需要使用前端语言来完善网页功能,而后端部分的开发到这里就基本完成了。

日志相关代码如下图:(本文就简单展示一下日志内容)

cpp 复制代码
#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;
}

9.前端模块

本文前端语言仅展示代码,具体就不讲解了:(这里我们是用vscode编写的)

cpp 复制代码
<!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="https://code.jquery.com/jquery-3.7.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>
相关推荐
Elastic 中国社区官方博客3 小时前
Elasticsearch:Microsoft Azure AI Foundry Agent Service 中用于提供可靠信息和编排的上下文引擎
大数据·人工智能·elasticsearch·microsoft·搜索引擎·全文检索·azure
Elastic 中国社区官方博客1 天前
Elasticsearch:如何创建知识库并使用 AI Assistant 来配置 slack 连接器
大数据·人工智能·elasticsearch·搜索引擎·全文检索·信息与通信
小园子的小菜1 天前
深度剖析Elasticsearch数据写入与读取:从分片同步到核心组件协同
大数据·elasticsearch·搜索引擎
怀璧其罪2 天前
aleph-node Node upgrade instructions 节点升级说明
大数据·elasticsearch·搜索引擎
邮专薛之谦2 天前
Git复习(查询版本)
大数据·elasticsearch·搜索引擎
KANGBboy2 天前
ES 生产排查
大数据·elasticsearch·搜索引擎
JavaBoy_XJ3 天前
电商系统中ES检索技术设计和运用
大数据·elasticsearch·搜索引擎
小园子的小菜3 天前
Elasticsearch高阶用法实战:从数据建模到集群管控的极致优化
大数据·elasticsearch·搜索引擎
老陈头聊SEO3 天前
AI与SEO策略结合下的关键词优化新发现
其他·搜索引擎·seo优化