HDFS
是一个分布式文件系统。在
HDFS
中, 一个数据文件被划分为若干个数据块,分布式存储在集群中的每个节点上。
很多大数据计算引擎是基于
HDFS
,如
MapReduce
、
Spark
等。
为了在
HDFS
上支持
列存储
,出现了很多流行的列存储文件格式,如:
-- RCFile
-- ORC
-- Parquet
-- CarbonData
HDFS
列存储如下图。数据文件首先被水平划分为行组
(row group),
行组内的数据按列存储。
RCFile
是较早的
HDFS
列存储格式。
RCFile
中默认的行组大小只有
4MB
,因此一个数据块中有多个行组。
每个行组中有一个元数据区域,存储行组中各个列的位置偏移。
ORC
对
RCFile
进行了
优化
,将
默认行组
(
ORC
中称为
Stripe)
的大小
提高
到
256MB
(Hive
中的
ORC
默认
64MB
大小
)
。
提高行组大小
增加
了行组中
列
的
大小
,使得查询可以连续读取一个较大的列,以提高
I/O
的连续性。
ORC
还
支持
将
嵌套
数据类型
(
如
Json
、
Map
等
)
。嵌套的数据类型被分解为二维的数据结构,在二维的数据表进行存储。
Parquet
采用与
ORC
类似的技术,用
128MB
的默认行组大小,同样支持列的数据类型和统计信息选择数据的编码压缩算法。
CarbonData
是最近出现的
HDFS
列存储格式,由华为主导的一个
Apache
开源项目。
CarbonData
支持自适应的数据编码压缩和嵌套数据类型。
在数据的存储布局方面进行了更为精细的设计,
更好
地
支持数据压缩和索引
。
CarbonData
中
每列上
的每
32,000
个数据项构成一个页面
(page)
,
页面内
的
数据
可以单独
排序
,排序后的数据压缩效果远好于未排序的数据。
这样的存储组织方式保证了在页面的粒度上数据是按行对齐的。
执行查询时,由于页面的粒度较小,
各个列
上相对应的
一组页面
在查询执行时可以
快速在内存中
进行连接、重组为元组,不会造成很高的元组恢复代价。
CarbonData
还支持将
数据表水平
划分为
segment
,
每个
segment
中的数据可以按照主键进行全局排序并建立索引,以支持在主键上的高性能数据查询。
在编码压缩方面, 支持全局字典,即同一数据表的同一个列上使用同一个字典对数据进行字典编码。
在查询执行时,如果需要执行条件过滤或者
Group By
等操作,可以直接在编码后的数据上进行,无需进行解码。
只
有当返回最终结局时,才对数据进行解码。
文档存储分两类:
(1) 无结构文档存储,例如,文本、HTML等;
(2)半结构化文档,比如XML、JSON等。
无结构文档存储。文档具有非结构化的特点。文档存储通常需要高效实现几个基本操作:读取、写入、删除、更新。
其中,
读取操作
通常需要实现对指定文件、指定位置、指定长度的字符串进行快速读取。
写入:在指定的文件位置写入相关内容,操作相对复杂。
如果
写入
的
位置
在文档
中间
,需要将插入位置之后的字符串全部后移;
如果
写入
的
位置
在文档
末尾
,则只需
插入
需要写的内容。
删除也会遇到同样的问题,如果只删除文档中间的一段字符串,需要移动删除位置之后的字符串。
对于
更新
,可以通过
删除
、
写入操作混合
完成。
文档存储在实现文本的基本操作之后,需要进一步建立文档索引。高效的索引结构有助于快速的信息检索。
文档中的特定关键词可以通过建立
倒排索引
(inverted index)
将单词映射到一个单词出现过的文件列表,如下图所示:
反向索引
用于
大规模文档
信息
检索
中。
Shakespeare’s Collected Works
(
《
莎士比亚全集
》
)这本书。假定想知道其中的哪些剧本包含
Brutus
和
Caesar
但不包含
Calpurnia
。
一种办法就是
从头到尾阅读
这本全集,对
每部剧本
都检测它是否包含
Brutus
和
Caesar
且同时
不包含
Calpurnia
。
这种
线性扫描
就是一种最简单的计算机文档检索方式。
使用现代计算机,对一个规模不大的文档集(
《
莎士比亚全集
》
也就只有不到
100
万个单词)进行线性扫描非常简单,根本不需要做额外的处理。
但是,很多情况下只采用上述扫描方式是远远不够的,需要做更多的处理。这些情况如下所述:
1) 大规模文档集条件下的快速查找。在线数据量的增长并不低于计算机速度的增长,可能需要在几十亿到上万亿单词的数据规模下进行查找;
2) 有时我们需要更灵活的匹配方式。比如,在grep 命令下不能支持诸如 Romans NEAR countrymen 之类的查询,这里的NEAR 操作符的定义可能为“ 5 个词之内” 或者“ 同一句子中” ;
3)需要对结果进行排序。很多情况下,用户希望在多个满足自己需求的文档中得到最佳答案。
此时,不能再采用上面的线性扫描方式了。一种
非线性扫描
的方式是
事先给文档建立索引(
index
)
。
仍然回到上述
《
莎士比亚全集
》
的例子,并通过这个例子来介绍布尔检索模型的基本知识。
给定
词表
(
《
莎士比亚全集
》
中共使用约
32 000
个不同
的词。
假定对每篇文档(这里指每部剧本)都事先记录它
是否
包含词表中的某个词,结果就会得到一个由布尔值构成的词项
—
文档关联矩阵(
incidence matrix
)。
为响应查询
Brutus
AND
Caesar
AND NOT
Calpurnia
,分别取出
Brutus
、
Caesar
及
Calpumia
对应的行向量,并对
Calpumia
对应的向量求反,然后进行基于位的与操作,得到:
110100 AND 110111 AND 101111 = 100100
。
结果向量中的第
1
和第
4
个元素为
1
,这表明该查询对应的剧本是
Antony and Cleopatra
和
Hamlet (
见图
)
。
布尔检索模型接受布尔表达式查询,即通过AND、OR 及NOT 等逻辑操作符将词项连接起来的查询。
在该模型下,每篇文档只被看成是一系列词的集合。
下面考虑一个更真实的场景,同时也引出信息检索中的一些术语和概念。
假定有
N=1 000 000
篇文档。所谓“ 文档”
(document)
指的是检索系统的检索对象,它们可以是一条条单独的记录或者是一本书的各章。
所有的文档组成文档集(
collection
),有时也称为语料库(
corpus
)。
假设每篇文档包含约
1 000
个词(相当于
2
~
3
页书),每个词的平均长度是
6 B
(含空格和标点符号),那么就会得到一个大约
6 GB
大小的文档集。
通常,这些文档中大概会有
M=500 000
个不同的词项。
目标是开发一个能处理
ad hoc
检索(
ad hoc retrieval
)任务(一种常见的信息检索任务)的系统
。
在这个任务中,任一用户的信息需求通过一次性的、由用户提交的查询传递给系统,系统从文档集中返回与之相关的文档。
信息需求(
information need
)指的是用户想查找的信息主题,它和查询(
query
)并不是一回事,后者由用户提交给系统以代表其信息需求。
如果一篇文档包含对用户需求有价值的信息则认为是相关的(
relevant
)。
上面举的例子当中,信息需求采用多个特定的词语组合来表达,从这点来说,人工构造的痕迹明显。
而通常来说,假如用户对
pipeline leaks
感兴趣,在检索时,不管是严格采用这些词还是采用反映这一概念的其他词(如
pipeline rupture
)来表达该需求,都能得到相关的结果。
对检索系统的效果(effectiveness,搜索结果的质量)进行评价,通常需要知道某个查询返回结果的两方面的统计信息:
1.正确率(Precision):返回的结果中真正和信息需求相关的文档所占的百分比。
2.召回率(Recall):所有和信息需求真正相关的文档中被检索系统返回的百分比。
回到刚才的例子,显然不能再采用原来的方式来建立和存储一个词项
—
文档矩阵。
由于词项的个数是
50
万,而文档的篇数为
100
万,所以其对应的词项
—
文档矩阵大概有
5 000
亿(
50
万
×100
万)个取布尔值的元素,这远远大于一台计算机内存的容量。
另外,不难发现,这个庞大的矩阵实际上具有
高度
的
稀疏性
,即大部分元素是
0
,只有极少部分元素为
1
。
可以对上述例子做个粗略计算,由于每篇文档的平均长度是
1 000
个单词,所以
100
万篇文档在词项
—
文档矩阵中最多对应
10
亿(
1 000×1 000 000
)个
1
,也就是在词项
—
文档矩阵中至少有
99.8%
(
1
−
10
亿
/5 000
亿)的
元素为
0
。很显然,只记录原始矩阵中
1
的位置的表示方法比词项
—
文档矩阵更好。
回到刚才的例子,显然不能再采用原来的方式来建立和存储一个词项
—
文档矩阵。上述思路将引出信息检索中的核心概念
——
倒排索引(
inverted index
)。
从名称上看,倒排索引中“ 倒排” 二字似乎有些多余,因为一般提到的索引都是从词项反向映射到文档。
然而,倒排索引(有时也称倒排文件)已经成为信息检索中的一个标准术语。倒排索引的基本思想参见图1-3。
左部称为
词项词典
(
dictionary
,简称词典,有时也称为
vocabulary
或者
lexicon
本书中
dictionary
指图中的数据结构,而
vocabulary
则指词汇表) 。
每个词项都有一个记录出现该词项的所有文档的列表,该表中的每个元素记录的是词项在某文档中的一次出现信息(在后面的讨论中,该信息中往往还包括词项在文档中出现的位置),这个表中的每个元素通常称为倒排记录(
posting
)。
每个词项对应的整个表称为倒排记录表(
posting list
)或倒排表(
inverted list
)。所有词项的倒排记录表一起构成
全体倒排记录表(
postings
)
。
图中的
词典
按照
字母顺序
进行
排序
,而倒排记录表则按照文档
ID
号进行排序。
为获得检索速度的提升,就必须要事先建立索引。建立索引的主要步骤如下:
(1) 收集需要建立索引的文档,如:
(2) 每篇文档转换成一个个词条(token)的列表,这个过程通常称为词条化(tokenization),如:
(3) 进行语言学预处理,产生归一化的词条来作为词项,如:
(4) 对所有文档按照其中出现的词项来建立倒排索引,索引中包括一部词典和一个全体倒排记录表。
现在,假定前3 步已经执行完毕,我们来讲述如何通过基于排序的索引构建方法(sort-based indexing)来建立一个基本的倒排索引。
给定一个文档集,假定每篇文档都有一个唯一的标识符即编号(docID)。
在索引构建过程中,给每篇新出现的文档赋一个连续的整数编号。
在上述的前
3
步处理结束后,对
每篇文档建立索引时
的输入就是一个归一化的词条表,也可以看成二元组(词项,文档
ID
)的一个列表。
建立索引最核心的步骤是将这个列表
按照词项的字母顺序进行排序
,之后得到图中部显示的结果,其中一个词项在同一文档中的多次出现会合并在一起最后整个结果分成词典和倒排记录表两部分
.
由于一个词项通常会在多篇文档中出现,上述组织数据的方法实际上也已经减少了索引的存储空间.
词典中同样可以记录一些统计信息,比如出现某词项的文档的数目,即
文档频率(
document frequency
),
这里也就是每个倒排记录表的长度
.
该信息对于一个基本的布尔搜索引擎来说并不是必需的,但是它可以在处理查询时提高搜索的效率
.
倒排记录表
会按照
docID
进行排序,这为高效的查询处理提供了重要基础。在
ad hoc
文本检索
中,
倒排索引是
其他结构无法抗衡的
高效索引结构
。
在最终得到的倒排索引中,
词典
和
倒排记录表
都有存储开销。前者往往放在
内存
中,而后者由于规模大得多,通常放在
磁盘
上。
由于有些词在很多文档中出现,而另外一些词出现的文档数目却很少,所以,如果采用
定长数组
的方式将会
浪费
很多
空间
。
对于内存中的一个倒排记录表,可以采用两种好的存储方法:一个是
单链表
,另一个是
变长数组
.
单链表
(
singly linked list
)
便于文档的插入和更新
(比如,对更新的网页进行重新采集),因此通过增加指针的方式可以很自然地扩展到更高级的索引策略
.
而
变长数组
(
variable length array
)的存储方式一方面可以节省指针消耗的空间,另一方面由于采用连续的内存存储,可以充分利用现代计算机的缓存(
cache
)技术来提高访问速度
.
如果索引更新不是很频繁的话,变长数组的存储方式在空间上更紧凑,遍历也更快。
另外,也可以采用一种混合的方式,即采用
定长数组
的
链表方式
。当倒排记录表存在磁盘上的时候,它们被连续存放并且没有显式的指针
.