第1章 搜索引擎是如何工作的 搜索引擎的基础是应用于信息检索、数据库等领域的信息技术。
1-1 理解搜索引擎的构成
1-2 实现了快速全文搜索的索引结构
利用全扫描进行全文搜索
grep就是从头到尾扫描作为检索对象的文档的。
利用索引进行全文搜索
先建立索引需要花费时间。
倒排索引的结构
例子:印在教程书后面的索引。
所谓倒排索引就是一张列出了“哪个单词出现在了哪一页”的表格。如下图:
倒排索引的构建方法
为了便于浏览,我们交换了上表的行和列,并将单词按字典序排序:
倒排索引中的术语
对于每种作为检索对象的数据,构建索引的单位都是不同的。
1-3 深入理解倒排索引
倒排索引 = 词典 + 倒排文件
从倒排索引中查找单词
如何查找同时包含了多个单词的文档呢?
查找时只 需要先从词典中找出各个单词,然后分别获取这些单词的倒排列表并加 在一起,由此计算出包含在各个倒排列表中的文档编号的交集。
将单词的位置信息加入倒排文件中
- 文档级别的倒排文件。
- 单词级别的倒排文件。这种倒排文件中不仅带有有关单词出现在了 哪个文档中的信息,还带有单词出现在了文档中的什么位置(从开头数 是第几个单词)这一信息。如:
engine: D1;
4
Google: D2;
5
I: D1;
1,D2;
1
从倒排索引中查找短语
查找短语时还需要确认 search 和 engine 是否是相 邻出现的。例如,虽然下面的文档也同样 包含了 search 和 engine,但却与搜索引擎(search engine)无关。
I search for a gas station because my car’s engine doesn’t start.
1-4 制作中文文档的倒排索引 分割中文句子的两种方法
对
全文搜索引擎
这段文本分割将得到不同的结果。 ①词素解析分割法
将句子分割为“词素”序列的方法。 词素是语言中含有意义的最小单位。一般采用的是机器学习的方法。分割结果:
全文 搜索 引擎
②N-gram分割法
N-gram 分割法是一种将句子分割成由 N 个字符组成的片段序列的方法,每个片段称作一个 N-gram。 使用bi-gram分割结果:
全文 文搜 搜索 索引 引擎
权衡分割方法
由 N-gram 构成的倒排索引不会产 生检索遗漏问题。
但是相比于词 素解析,在同一个文档中使用 N-gram 产生的词元通常较多。
1-5 实现倒排索引 实现词典
为了能够快速地获取到对应着单词的倒排列表,通常 都会使用哈希表、树等数据结构。
用二叉查找树实现词典
在内存上实现词典
在二级存储器上实现词典
用B+树实现词典
HDD 或 SSD 等二级存储器 一般被称作“块设备”,由于它们是以块为单位进行输入输出的 A ,所以 即使只是读取块中 1 个字节的数据,也不得不对整个块进行输入输出操 作。当要存储大型词典时,往往要使用适合块设备的 B+ 树等树 形数据结构。
所有的记录都存储在树中的叶结点(Leaf Node)上,内部结点(Internal Node)上只以关键字的顺序存储关键字。
B+ 树通常以文件系统中页尺寸的常数倍为单位管理各结点, 而由这样的结点来构成树,则有助于减少检索时对二级存储的输入输出次数。
实现倒排文件
倒排列表的物理布局
- 文档编号(DocID)
- 文档中的偏移列表(off1、off2…)
- TF词频,用于计算检索结果的排名
对于之前例子中对应着 search 的倒排列表(D1; 3,D2; 2)
,就可 以用如下的整数数列表示。
1,1,3,2,1,2
由于倒排列 表一般都是整数数列,所以通常会采用适合整数数列的压缩方法。
1-6 使用倒排索引进行检索 使用倒排索引的检索处理流程
① 获取查询中每个单词的倒排列表;
② 根据布尔检索,获取符合检索条件的文档编号;
③ ’ 计算符合检索条件的文档和查询的匹配度;
③ ” 获取对检索结果进行排序时使用的属性值;
④ 根据匹配度或用于排序的属性值,获取前 k 个文档。
关联度的计算方法
在计算余弦相似度时,需要把文档和查询映射到以单词(Term)为 维度的向量空间上,文档向量和查询向量的夹角(内积)越小,说明文 档和查询的关联度越高。
信息检索中的检索
在检索处理中,文档是否包含查询无关紧要,重要的是 通过计算查询和整个文档的关联度,把关联度高的文档作为检索结果。
1-7 构建倒排索引 使用内存构建倒排索引
完全可以按照1-2节中的方法构建,先在内存上生成与文档编号对应的单词表(二维数组),然后用相同的方法倒排该表。
但是这样做,生成都是非常稀疏的表,会消耗大量内存。
使用二级存储构建倒排索引
基于排序的索引构建法
基于合并的索引构建法
从后面的代码可以看到就是用来sqlite数据库。
静态索引构建和动态索引构建
动态索引构建不但可以使索引结构时刻处于可供检索的状态,还可以一边实时更新索引,一边构建索引。这种方法多用于信息的时效性非常重要的文档。
1-8 准备要检索的文档 数据规范化
在规范 HTML 文件时, 就要删除标签并提取出作为检索对象的 文章(内容)。
第2章 准备全文搜索引擎的检索样本 2-1 全文搜索引擎wiser
2-2 安装wiser 2-3 运行wiser 先来看下使用说明:
$ ./wiser
usage: ./wiser [options] db_fileoptions:
-c compress_method: compress method for postings list
-x wikipedia_dump_xml: wikipedia dump xml path for indexing
-q search_query: query for search
-m max_index_count: max count for indexing document
-t ii_buffer_update_threshold : inverted index buffer merge threshold
-s: don't use tokens' positions for searchcompress_methods:
none: don't compress.
golomb : Golomb-Rice coding(default).
构建倒排索引
$ ./wiser -x ../../1000.xml -m 1000 wikipedia_1000.db
[time] 2017/02/26 21:57:34.000007
count:1 title: Wikipedia:Upload log
count:2 title: Wikipedia:删除纪录/档案馆/2004年3月
count:3 title: 数学
count:4 title: Help:目录
count:5 title: 哲学
...
生成的数据库表如下:
使用倒排索引查询
$ ./wiser -q "顾城" wikipedia_1000.db
[time] 2017/02/26 22:10:43.000008
document_id: 991 title: 顾城 score: 199.315686
Total 1 documents are found!
[time] 2017/02/26 22:10:43.000008 (diff0.001520)
第3章 构建倒排索引 3-1 复习有关倒排索引的知识 提取词元
考虑UTF-8字符编码特性。
在 UTF-8 中,是用 1 到 4 个字节的长度来表示 1 个字符的。例如, 像数字和拉丁字母等在英文中使用的字符都是用 1 个字节表示的,而在 中文中使用的字符则多半要用 3 个字节才能表示。
例如,对于“Web 检索”这个字符用如下的字节序列表来表示:
0x57 0x65 0x62 0xe6 0xa3 0x80 0xe7 0xb4 0xa2
在 wiser 中,为了避开由使用 UTF-8 带来的处理上的麻烦,我们在 每次获取 N-gram 时,都会先将字符串的编码从 UTF-8 转换成 UTF-32。 UTF-32 是一种以 4 字节(32 bit)的数值为单位表示 Unicode 字符的编 码方式。由于 Unicode 的字符与表示该字符的数值是一一对应的,所以 在 UTF-32 中,由 N-gram 分割而成的词元所含有的字节数就变成固定的 了,这样就简化了程序上的处理过程。
为每个词元创建倒排列表
单词级别的倒排列表:是由文档编号和词元在文档中出现的位置构成的二元组的集合。
3-2 构建倒排索引 在存储器上创建倒排列表
最直接的方法就是不断地 将倒排项(文档编号和位置信息)添加到存储器上的倒排列表的末尾。
倒排列表和倒排文件的数据结构
/* 倒排列表(以文档编号和位置信息为元素的链表结构)*/
typedef struct _postings_list {
int document_id;
/* 文档编号 */
UT_array *positions;
/* 位置信息的数组 */
int positions_count;
/* 位置信息的条数 */
struct _postings_list *next;
/* 指向下一个倒排列表的指针 */
} postings_list;
/* 倒排索引(以词元编号为键,以倒排列表为值的关联数组) */
typedef struct {
int token_id;
/* 词元编号(Token ID)*/
postings_list *postings_list;
/* 指向包含该词元的倒排列表的指针 */
int docs_count;
/* 出现过该词元的文档数 */
int positions_count;
/* 该词元在所有文档中的出现次数之和 */
UT_hash_handle hh;
/* 用于将该结构体转化为哈希表 */
} inverted_index_hash, inverted_index_value;
通过为类型赋予别名使二者有所区别, 用 inverted_index_hash 类型表示整个关联数组,用该类型的别名 inverted_ index_value 表示关联数组中的一个元素。
从源代码级别梳理倒排索引的构建顺序
就用我之前写过的这个方法来看代码,或者用Clion。
add_document() ① 从文档中取出词元。
② 为每个词元创建倒排列表并将该倒排列表添加到小倒排索引中。
③ 每当小倒排索引增长到一定大小,就将其与存储器上的倒排索引 合并到一起。
第4章 开始检索吧 4-1 检索处理的大致流程 ① 将查询分割为词元(如果使用的是 bi-gram, 那么就会分割出“自制”“制搜”“搜索”“索引”“引擎”5 个词元)。
② 将分割出的各个词元,按照出现过该词元的文档数量进行升序排列。
③ 获取各个词元的倒排列表,并从中取出文档编号和该词元在文档中出现位置的列表。
④ 如果所有词元都出现在同一个文档中,并且这些词元的出现位置都是相邻的,那么就将该文档添加到检索结果中。
⑤ 计算已添加到检索结果中的各文档与查询的匹配度(在 wiser中,我们使用 TF-IDF 值作为匹配度)。
⑥ 将检索结果按照匹配度的降序排列。
⑦ 从经过排序的检索结果中取出排在前面的若干个文档作为检索结 果返回。
4-2 使用倒排索引进行检索 从源代码级别梳理检索处理的流程
调用
split_query_to_tokens
函数将查询字符串转换为词元序列inverted_index_hash
。使用具体示例加深对检索处理流程的理解
如果能 够找到一个在所有倒排列表中都出现过的文档编号,那么就将它所指向 的文档加入到候选检索结果中。
- 首先获取了词元 A 的文档编号, 然后检查了其他的词元是否也带有 相同的文档编号
- 如果没有发现带有相同文档编号的词元, 那么接下来就继续向后读 取词元 A 的倒排列表,直到遇到更大的文档编号为止。
相关的实现在
search_docs
中。第5章 压缩倒排索引 5-1 压缩的基础知识 压缩倒排索引的好处
在使用倒排索引进行检索的过程中,总检索时间中的大部分时间往 往花费在了从二级存储读取倒排索引上。于是,就经常可以看到在存储 倒排索引前,对其进行压缩以减少从二级存储读取的时间,进而使检索 处理得以高速运转的对策。
倒排索引的压缩方法
倒排文件的压缩方法
在一般的程序中,大多数情况下都会为整数分配 4 或 8 个字节等定 长的编码,但是在处理倒排文件时,由于经常要处理大量数值较小的整 数,所以为了使用更少的信息量来表示整数,通常都会采用长度可变而 非固定的编码方式。
Golomb编码 压缩的原理
5-2 实现wiser中的压缩功能 了解无需进程压缩时的操作
【算法|《自制搜索引擎》笔记】
encode_postings_none
函数将倒排列表转换成字节序列。 该函数会先从倒排列表的各元素中取出文档编号、位置信息 的数量以及位置信息的数组,然后再将这些数据以二进制的形式写入缓冲区。
/**
* 将倒排列表转换成字节序列
* @param[in] postings 倒排列表
* @param[in] postings_len 倒排列表中的元素数
* @param[out] postings_e 转换后的倒排列表
* @retval 0 成功
*/
static int
encode_postings_none(const postings_list *postings,
const int postings_len,
buffer *postings_e)
{
const postings_list *p;
LL_FOREACH(postings, p) {
int *pos = NULL;
append_buffer(postings_e, (void *)&p->document_id, sizeof(int));
append_buffer(postings_e, (void *)&p->positions_count, sizeof(int));
while ((pos = (int *)utarray_next(p->positions, pos))) {
append_buffer(postings_e, (void *)pos, sizeof(int));
}
}
return 0;
}
介绍几个轮子
- utarry:a dynamic array implementation using macros.
- uthash:处理关联数组。
- buffer:在util.h中定义的支持动态扩容的缓冲区。
推荐阅读
- 人工智能|干货!人体姿态估计与运动预测
- 分析COMP122 The Caesar Cipher
- 技术|为参加2021年蓝桥杯Java软件开发大学B组细心整理常见基础知识、搜索和常用算法解析例题(持续更新...)
- C语言学习(bit)|16.C语言进阶——深度剖析数据在内存中的存储
- Python机器学习基础与进阶|Python机器学习--集成学习算法--XGBoost算法
- 数据结构与算法|【算法】力扣第 266场周赛
- 数据结构和算法|LeetCode 的正确使用方式
- leetcode|今天开始记录自己的力扣之路
- 人工智能|【机器学习】深度盘点(详细介绍 Python 中的 7 种交叉验证方法!)
- 网络|简单聊聊压缩网络