[Project] MiniSearch文本检索简介

1. 预处理过程

预处理主要用来事先生成程序在运行过程中可能用到的数据,以便加速处理时间。

预处理的过程主要生成程序所需的三个文件:网页库文件,网页位置信息文件和倒排索引文件。

网页库文件

其中网页库文件ripepage.lib主要是以格式化的数据存储大量的网页信息,每个网页的格式化数据为:

<doc>

<docid>id</docid>

<docurl>url</docurl>

<doctitle>title</doctitle>

<doccontent>content</doccontent>

</doc>

网页位置信息文件

网页位置信息文件offset.lib主要是存放网页在网页库中的偏移位置,以便程序能快速的取出指定的网页,该文件每一行存储一个网页文件在网页库中的位置信息,每一行的格式为:

docid offset size

其中docid为网页的id(此id具有全局唯一性),offset为文档在网页库中距离文件起始位置的字节数,size为文档的大小。

倒排索引文件

倒排索引文件invert.lib为网页库中的所有词(经过分词,去停用词后)与包含这些词的文档的一种关联关系。

每个词的倒排索引在该文件中占一行,每一行的格式为:

word docid1 frequency1 weight1 … docidi frequencyi weighti…

其中word为网页库中的词, 后面接着的是每三个为一组,docidi 为包含该词的网页,frequencyi为该次在该文档中的词频,weighti为该次在该文档中的权重(归一化后的)。

2. 程序运行过程

程序首先从offset.lib中读取网页位置信息,然后根据这些信息从rippage.lib中读取网页信息,然后从invert.lib读取倒排索引信息

程序循环不断地通过socket接受来自客户端的请求,一旦受到请求就fork一个子进程负责处理该请求而主进程则继续监听。子进程接受来自客户端的查询语句,根据查询语句查找结果并将结果返回给客户端。

1. 构建网页库

生成网页库ripepage.lib,生成网页的位置偏移文件offset.lib。

遍历目录读取所需构建网页库的文件,拼接成标准格式,然后写入文件,并同时建立库索引。代码如下:

#include <iostream>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <vector>
#include <fstream>
#include <sys/types.h>
#include <dirent.h>
#include <sys/stat.h>
#include <stdexcept>
#include <unistd.h>
#include <string.h>
#include <map>
#include <utility>
#include <set>
#include <functional>
#include <algorithm>

// 扫描目录类,该类扫描指定目录下的项并将属于普通文件的项的绝对路径保存下来。
class DirScan
{
	public:
		// 带参数的构造函数,传入一个vector容器用于保存文件的绝对路径
		DirScan(std::vector<std::string>& vec):m_vec(vec)
		{

		}
		// 重载函数调用操作符,传入一个路径。
		void operator()(const std::string &dir_name)
		{
			traverse(dir_name);
		}
		// 遍历路径,在遍历的过程中将属于文件类型的项的绝对路径保存到vector容器中。
		// 遍历算法:
		// 打开该目录,进入该目录,依次遍历该目录中的项,判断该项的属性,如果该项的类型是文件则保存该项的绝对路径,如果该项是目录,则递归的遍历该目录。最后遍历完目录后切换到该目录的上一级目录。
		void traverse(const std::string& dir_name)
		{
			// 打开指定的目录
			DIR* pdir = opendir(dir_name.c_str());
			if (pdir == NULL)
			{
				std::cout << "dir open" << std::endl ;
				exit(-1);
			}

			// 进入指定的目录
			chdir(dir_name.c_str());
			struct dirent* mydirent ;
			struct stat mystat ;

			// 依次遍历该目录中的相关项
			while ((mydirent =readdir(pdir) ) !=NULL)
			{
				// 获取目录中项的属性
				stat(mydirent->d_name, &mystat);
				// 判断该项是不是目录。
				if (S_ISDIR(mystat.st_mode))
				{
					// 如果该目录是‘.‘和‘..’(每个目录下都有这两项,如果不排除这两项程序会进入无限循环),则跳过该次循环继续下一次
					if (strncmp(mydirent->d_name, ".", 1)== 0 || strncmp(mydirent->d_name,"..", 2) == 0)
					{
						continue ;
					}
					else// 如果该目录不是前二者,则递归的遍历目录
					{
						traverse(mydirent->d_name);
					}
				}
				else // 如果该项不是目录(是文件),则保存该项的绝对路径
				{
					std::string file_name="";
					file_name = file_name + getcwd(NULL,0)+"/"+ mydirent->d_name ;
					m_vec.push_back(file_name);
				}
			}
			chdir("..");
			closedir(pdir);
		}
	private:
		// 类外一个vector容器的引用,用于保存文件的绝对路径
		std::vector<std::string>& m_vec ;
};

// 文件处理类,该类将多个文件以某种格式格式化文件并将各个文件统一保存到一个文件形成网页库文件。
// 每个文件被处理成<doc><docid>id</docid><doctitle>title</doctitle><docurl>url</docurl><doccontent>content</doccontent></doc>
class FileProcess
{
	public:
		// 带参数的构造函数,第一个参数为保存着个文件路径的vector容器,第二个参数为传入的字符串用于提取文档中的‘标题’
		FileProcess(std::vector<std::string>& vec, std::string& str):m_vec(vec)
		{
			m_title = str ;
		}
		// 重载函数调用操作符,传入两个文件名用于保存建好的网页库和单个文档在网页库中的偏移位置
		void operator()(const std::string &file_name, const std::string & offset_file)
		{

			do_some(file_name, offset_file) ;
		}
		// 建立网页库,并将其以及文档在库中的偏移保存到文件中。
		void do_some(const std::string& file_name, const std::string& offset_file)
		{
			// 用于保存网页库的文件指针
			FILE* fp = fopen(file_name.c_str(),"w");
			// 用于保存文档在网页库中偏移的文件指针
			FILE* fp_offset = fopen(offset_file.c_str(), "w");
			if (fp == NULL || fp_offset == NULL)
			{
				std::cout << "file open" << std::endl ;
				exit(0);
			}
			int index ;
			// 动态创建一个字符数组,用于保存从文件中读取的全部内容
			char* mytxt = new char[1024*1024]() ;
			int mydocid ;
			char myurl[256] = "" ;
			// 动态创建一个字符数组,用于保存文件内容
			char* mycontent = new char[1024 * 1024]() ;
			// 保存文档标题
			char* mytitle = new char[1024]() ;
			// 依次处理各个文档。处理包括:生成文档id(该id具有全局唯一性),提取文档标题,生成文档url(文档的绝对路径),提取文档内容
			for (index = 0 ; index != m_vec.size(); index ++)
			{
				memset(mytxt, 0, 1024*1024);
				memset(myurl, 0, 256);
				memset(mycontent, 0, 1024* 1024);
				memset(mytitle, 0, 1024);
				// 打开指定的文档
				FILE * fp_file = fopen(m_vec[index].c_str(), "r");
				// 读取文档,并将标题保存到mytitle
				read_file(fp_file, mycontent, mytitle);
				fclose(fp_file);
				mydocid = index + 1 ;
				strncpy(myurl, m_vec[index].c_str(), m_vec[index].size());
				// 将文档格式化成指定格式的串
				sprintf(mytxt, "<doc><docid>%d</docid><docurl>%s</docurl><doctitle>%s</doctitle><doccontent>%s</doccontent></doc>\n", mydocid, myurl, mytitle, mycontent);
				// 算出文档在网页库的起始位置
				int myoffset = ftell(fp); // 函数 ftell 用于得到文件位置指针当前位置相对于文件首的偏移字节数。
				int mysize = strlen(mytxt);
				char offset_buf[128]="";
				// 文档的偏移通过 (文档id  文档在网页库的起始位置  文档的大小)这三个数字来确定, 在文件中占一行
				// 将文档偏移信息写入到偏移文件中去
				fprintf(fp_offset,"%d\t%d\t%d\n",mydocid, myoffset, mysize);
				// 将格式化后的文档写入网页库中
				write_to_file(fp, mytxt);
			}
			fclose(fp);
		}
		// 读取文档的内容,并提取标题,分别把内容和标题保存到mycontent 和 mytitle所指向的空间中去
		void read_file(FILE* fp ,  char* mycontent, char* mytitle )
		{
			int iret ;
			const int size = 1024 * 1024 ;
			char* line = new char[1024]() ;
			int pos = 0 ;
			// 循环读取文档内容
			while(1)
			{
				int iret = fread( mycontent + pos, 1, size - pos, fp);
				if (iret == 0)//如果文档读完,则跳出循环
				{
					break ;
				}
				else //如果没有读完,则接着原来的地方继续读
				{
					pos += iret ;
				}
			}
			// 将文件指针重新回到文档的开头,用于提取标题
			rewind(fp) ;
			// count   记录当前读到的行数,flag记录是否已经找到标题(0代表没有找到,1代表已经找到)。
			int count = 0, flag = 0 ; ;
			// 依次取出文档的前11行 , 看看每行有没有 ‘标题’二字,如果有则将改行作为标题,如果没有则直接将下一行(第12行)作为标题
			// 如果整篇文档没有11行,则直接将第一行作为标题
			while (count <=10 && fgets(line, 512, fp) != NULL)
			{
				std::string str_line(line);
					// 如果改行有‘标题’ 二字
				if ( str_line.find(m_title.c_str(), 0) != std::string::npos)
				{
					// 将该行赋值给mytitle,作为标题
					strncpy(mytitle, str_line.c_str(), str_line.size());
					flag = 1 ;
					break ;
				}
				count ++ ;

			}
			if (count < 11 && flag == 0)// 如果文档没有12行,将第一行作为标题
			{
				rewind(fp);
				fgets(mytitle,1024, fp );
			}
			else if (count == 11 && flag == 0)// 如果有12行,则将12行作为标题
			{

				fgets(mytitle,1024, fp );
			}

		}
		// 将格式化后的文档写到网页库文件中
		void write_to_file(FILE* fp, char* mytxt)
		{
			int iret , pos = 0 ;
			int len = strlen(mytxt);
			// 循环写到网页库文件中,直到写完
			while (1)
			{
				iret = fwrite(mytxt + pos, 1, len - pos, fp);
				len = len - iret ;
				if (len == 0)
				{
					break ;
				}
			}
		}
	private:
		// 保存文件路径的容器的引用。
		std::vector<std::string>& m_vec ;
		// 用于保存提取标题
		std::string m_title ;
		std::map<int, std::pair<int, int> > m_offset ;
};

void show(std::vector<std::string>::value_type& val)
{
	std::cout << val << std::endl ;
}

int main(int argc, char* argv[]) //exe  src_txt_dir  ripepage_filename  offset_file_name
{
	// 初始化一个容器,用于保存文档的路径
	std::vector<std::string> str_vec ;

	// 定义一个扫描目录的对象
	DirScan mydirscan(str_vec);
	mydirscan(argv[1]);

	// 定义一个文件处理对象
	FileProcess myfileprocess(str_vec, title);
	myfileprocess(argv[2], argv[3]);

	std::cout << "Over" << std::endl ;
	return 0 ;
}

2. 网页去重

网页去重生成新的位置偏移文件newOffset.lib。

1. 根据网页的位置偏移文件offset.lib,从网页库文件ripepage.lib中依次将每一个网页的内容读入内存中。

内存中以vector<page>来存储网页

其中page为自定义的class,该类将硬盘网页库中的每一个网页文件抽取出docid,doctitle,docurl,doccontent(这4项每一项均用string存储),网页中每个单词出现的词频(使用unordered_map<string, int> mapWordFreq来进行存储),并且封装了计算每一个网页的哈希指纹等方法

2. 使用分词程序对每个网页的content进行分词,分词结果存入一个临时的vector<string>,并且完成去除停用词的步骤,大致代码如下:

std::vector<std::string>  Split::wordSplit(const char*  pageContent) {

    size_t pageContentSize = strlen(pageContent);
    char* contentAfterSplit = new char[6 * src_len]() ;

    // 中科院分词处理程序,分词后的内容以字符串形式存放在contentAfterSplit字符数组中
    ICTCLAS_ParagraphProcess(pageContent, pageContentSize, contentAfterSplit, CODE_TYPE_GB, 0);

    std::istringstream sin(contentAfterSplit);
    std::string word ;
    // 存放分词结果
    std::vector<std::string> vecWord;
    while(sin >> word) {
        if(!conf.setStoplist.count(word) && word[0] != ‘\r‘) {
            vecWord.push_back(word);
        }
    }

    delete [] contentAfterSplit ;

    return vecWord ;
}

3. 统计每个网页单词出现的词频

使用unordered_map<string, int> mapWordFreq来进行存储

void Page::getWordFreq(std::vector<std::string>& vecWord) // 参数vecWord为网页经分词其除去停用词后的返回结果
{
    //  std::unordered_map<std::string , int> mapWordFreq 为网页类page的数据成员;
    std::vector<std::string>::iterator iter  ;
    for (iter = vecWord.begin(); iter != vecWord.end(); iter ++) {
        mapWordFreq[*iter] ++ ;
    }
}

4. 根据vector<page>中每一个网页的词频词典mapWordFreq,可以得到在所有网页中出现过的单词

将每一个网页中的每一个单词放入一个hashset中即可,此处定义为unordered_set<string> setAllWords

统计setAllWords中的每一个单词在所有网页中出现过的次数

遍历setAllWords中的每一个单词,看其是否在每一个网页的mapWordFreq中即可,

此处用unordered_map<string, int> mapWordFreqInAllPage来存储

5. 计算每一个网页中每个单词的TF-IDF值

使用unordered_map<string, double> mapTFIDFOfWord来存储

遍历由3获取的词频词典unordered_map<string, int> mapWordFreq,结合unordered_map<string, int> mapWordFreqInAllPage,很容易就可以获得每一个单词的TF-IDF值,计算公式如下:

tfdoc即为单词在本网页中出现的次数(词频),N为网页总数,dfword为该单词所出现过的网页数。

TF-IDF值表明了一个单词在网页中的重要性,一个词在网页中预测主题的能力越强,那么它的权重(TF-IDF值)越大。

对网页中每一个词的TF-IDF值进行归一化,公式如下:

6. 计算每一个网页的哈希指纹(simhash方法)

simhash方法,先将单词使用MD5算法映射成64位的二进制向量,然后将权重融入向量中,形成一个实数向量。假设某个词的权值(TF-IDF)为w,则对二进制向量做如下改写:如果二进制的某个比特位是数值1,则实数向量中对应位置改写为w;如果比特位数值为0,则实数向量中对应位置改写为-w,即权值的负数。通过以上规则,就将二进制向量改写为体现了单词权重的实数向量。

当网页中的每一个单词都进行了上述改写后,对所有单词的实数向量累加获得一个代表文档整体的实数向量。

最后一步,再次将实数向量转换为二进制向量,转换规则如下:如果对应位置的数值大于0,则设置为二进制数字1;如果小于0,则设置为二进制数字0。

哈希指纹存放在unordered_map<string(docid), string(指纹)> fingerPrint中。

7. 利用哈希指纹对网页进行去重

如果两个网页的哈希指纹的海明距离小于3,我们则判断这两个网页为相同(相似)的网页。

8. 在网页去重的过程中,更新vector<page>

//网页去重
void removeDupPage(std::vector<Page>& vecPage) {
    int i , j ;

    for (i = 0 ; i!= vecPage.size() - 1; i ++) {
        for (j = i + 1 ; j != vecPage.size(); j ++) {
            if (vecPage[i] == vecPage[j]) { // 重载了Page类的operator==,利用哈希指纹判断两篇文章是否相似

                MyPage tmp = vecPage[j] ;
                vecPage[j] = vecPage[vecPage.size() - 1] ;
                vecPage[vecPage.size() - 1] = tmp ;

                vecPage.pop_back() ;

                j -- ;
            }
        }
    }

网页去重后,生成新的位置偏移文件newOffset.lib

注意:在配置类conf中有将原来的位置偏移文件offset.lib加载到内存的方法

存储offset.lib的格式为:std::unordered_map<int, std::pair<int, int> > m_offset

void updateOffset(const std::vector<Page> &vecPage) {
    std::ofstream of(conf.m_conf["mynewoffset"].c_str());
    if (!of) {
        std::cout << "open mynewoffset fail " << std::endl ;
        exit(0);
    }

    //将去重之后的文档的偏移信息重新保存到一个新的偏移文件中去
    for (page_index = 0 ; page_index != vecPage.size(); page_index ++ ) {
        of << atoi(vecPagevecPage[page_index].m_docid.c_str()) <<"   "
           <<conf.m_offset[atoi(vecPage[page_index].m_docid.c_str()) ].first <<"   "
           <<conf.m_offset[atoi(page_vec[page_index].m_docid.c_str()) ].second << std::endl;
    }

    of.close();
}

3. 建立倒排索引文件

建立 词-文章 的倒排索引文件invert.lib

格式为:word1 <doc1, weight> <doc2, weight> … <docn, weight>

           word2 <doc1, weight> <doc2, weight> … <docn, weight>

           ……

           wordm <doc1, weight> <doc2, weight> … <docn, weight>

倒排索引存储格式为:std::unordered_map<std::string, std::vector<std::pair<int,int> > > mapReverseIndex

// 生成倒排索引
void invert_index(std::vector<MyPage> &vecPage,
                  std::unordered_map<std::string, std::vector<std::pair<int,int> > > &mapReverseIndex)
{
    int index ;
    //遍历每一个Page对象
    for (index = 0 ; index != vecPage.size(); index ++) {

        std::map<std::string, int >::iterator iter ;
        // unordered_map<string, double> mapTFIDFOfWord
        for (iter = (vecPage[index].mapTFIDFOfWord).begin() ;
             iter != vecPage[index].mapTFIDFOfWord.end();
             iter ++ )
        {
            mapReverseIndex[iter->first].push_back
            ( std::make_pair(atoi(vecPage[index].m_docid.c_str()),iter->second) );
        }
    }
}

注意,需要从内存写回文件做好备份

4. 程序查询逻辑

首先将查询语句进行分词去停用词,获得一个词组,计算该词组的每个词的权重(通过 TF*IDF),然后根据网页库的倒排索引(已经提前加载到内存),找出包含查询词组的各个文档,然后通过计算找到的每个文档与查询语句(将查询语句当成一篇文档)的余弦相似度,根据这个余弦相似度给找到的文档集合按照从大到小排个序(余弦值越大,相似性越高),最后将结果封装成json格式的数据返回 。

(待续)

 

 

时间: 2024-10-16 08:05:07

[Project] MiniSearch文本检索简介的相关文章

POM (Project Object Model)简介

1  概念介绍 一个项目所有的配置都放置在 POM 文件中:定义项目的类型.名字,管理依赖关系,定制插件的行为等等.比如说,你可以配置 compiler 插件让它使用 java1.5 来编译. [html] view plain copy print? <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"

Video Object Detection with an Aligned Spatial-Temporal Memory

摘要: 本文针对视频目标检测问题提出时空记忆网络(STMN).它的核心是时空记忆模块,作为一种递归计算单元去建模长时间目标外观和运动信息.STMN可以用一个预训练的CNN backbone进行初始化,这对提高检测精度非常重要.本文为了建模目标运动提出匹配变换去对齐帧到帧的特征.本文的方法在VID数据集上获得了state-of-the-art的结果,我们的消融学习清楚地证明了本文设计架构的有效性,代码和模型都公开在:http://fanyix.cs.ucdavis.edu/project/stmn

Team Foundation Server 2013 with Update 3 Install LOG

[Info   @10:14:58.155] ====================================================================[Info   @10:14:58.163] Team Foundation Server Administration Log[Info   @10:14:58.175] Version  : 12.0.30723.0[Info   @10:14:58.175] DateTime : 10/03/2014 18:1

.NET Core项目从xproj+project.json向csproj迁移简介

3月7日,微软发布了Visual Studio 2017 RTM,与之一起发布的还有.NET Core Runtime 1.1.0以及.NET Core SDK 1.0.0,尽管这些并不是最新版,但也已经从preview版本升级到了正式版.所以,在安装Visual Studio 2017时如果启用了.NET Core开发的相关功能,那么在安装完成后,你可以在命令行直接执行dotnet.exe,此时你可以看到.NET Core版本是1.1.0,而如果是执行dotnet.exe --version命

pintos Project (1) 简介

一 实验描述 关于pintos的介绍,官方给出的introduction上是这样描述的: Welcome to Pintos. Pintos is a simple operating system framework for the 80x86 architecture. It supports kernel threads, loading and running user programs, and a file system, but it implements all of these

ASP.NET Core 1.1 静态文件、路由、自定义中间件、身份验证简介

概述 之前写过一篇关于<ASP.NET Core 1.0 静态文件.路由.自定义中间件.身份验证简介>的文章,主要介绍了ASP.NET Core中StaticFile.Middleware.CustomizeMiddleware和Asp.NetCore Identity.但是由于所有的ASP.NET Core的版本有些老,所以,此次重写一次.使用最新的ASP.NET Core 1.1版本.对于ASP.NET Core 1.1 Preview 1会在以后的文章中介绍 目录 使用静态文件 使用路由

开源GIS简介

原文 开源GIS C++开源GIS中间件类库: GDAL(栅格)/OGR(矢量)提供了类型丰富的读写支持 GEOS(Geometry Engine Open Source)是基于C++的空间拓扑分析实现类库,遵循LGPL协议发布.GEOS类库提供了丰富的空间拓扑操作函数,用以判断几何对象间的相互关系,以及空间分析操作之后形成新的几何对象.点.线.面要素的两两相互关系,包括相合.分离.相交.重合.包含.相邻等不同位置关系,都可以通过GEOS类库中提供的函数进行分析和判断.并且GEOS类库提供了缓冲

OSGi原理与最佳实践:第一章 OSGi框架简介(5)Spring-DM

OSGi原理与最佳实践:第一章 OSGi框架简介(5)Spring-DM 由  ValRay 发布 已被浏览8409次 共有3条评论 已被2个人收藏 2013-08-16 21:29 顶(1) 踩(0) osgi原理与最佳实践 1.3 Spring-DM 1.3.1 简介 Spring-DM 指的是 Spring Dynamic Modules.Spring-DM 的主要目的是能够方便地将 Spring 框架 和OSGi框架结合在一起,使得使用Spring的应用程序可以方便简单地部署在OSGi环

Github上优秀的Objective-C项目简介

Github上优秀的Objective-C项目简介 主要对当前Github排名靠前的项目做一个简单的简介,方便自己快速了解 Objective-C的一些优秀的开源框架. 项目名称 项目信息 AFNetworking 作者是 NSHipster 的博主, iOS 开发界的大神级人物, 毕业于卡内基·梅隆大学, 开源了许多牛逼的项目, 这个便是其中之一, AFNetworking 采用 NSURLConnection + NSOperation, 主要方便与服务端 API 进行数据交换, 操作简单,