您的位置:首页 > 其它

一个信息可视化Demo的设计(二):Index & Search

2011-11-02 10:12 246 查看
一个信息可视化Demo的设计(二):Index & Search
作者:愤怒的小狐狸 撰写日期:2011-10-29 ~ 2011-10-29
博客链接: http://blog.csdn.net/MONKEY_D_MENG

此为系列博文,前续请看-->第一部分:《一个信息可视化Demo的设计(一):架构设计》

一、信息检索

愈演愈烈的云计算来势汹汹,铺天盖地般席卷全球,逢人言必称云。仿佛只是在一夜间,海量信息、大数据即充斥着我们的世界。信息量极度膨胀的现在,云存储显得极为重要,然而大量的、海量的信息我们存下来究竟寓意为何?答曰:将此数据转换成有用的信息和知识,并将之广泛用于各种应用,包括商务管理、生产控制、市场分析、工程设计和科学探索等。

数据丰富,即会需要有强有力的数据分析工具的支撑去寻求和发现知识。快速增长的海量数据存在于大型和大量的数据库中,没有强有力的工具,理解它们已经远远超出了人的能力,决策者难以直观地从海量数据中提取有价值的知识。结果,数据虽丰富,但信息却贫乏,收集在大型数据库中的数据信息变成了“数据坟墓”。

要对海量数据进行分析和处理,先决条件即需要将信息按照一定的方式组织起来,使得数据分析员能够迅速地找出相关的信息,这一过程即为信息检索。至于数据找到后如何分析,一般是基于数据挖掘和机器学习理论,本文暂不讨论。

如果直接对信息资源内容做检索,顺序匹配检索请求,对于小数据量的环境,这种方法是非常直接、简单和易于实现的,而且效果也不会太差,但在海量数据环境下,这种扫描过程将是非常耗时的,也是绝对不可取的。

对于非结构化数据顺序扫描很慢,但对于结构化数据的搜索相对较快。原因在于:结构化数据都是有一定结构的,我们可以采取一些技巧:使用搜索优化算法来加快搜索的速度。所以,到此我们认识到问题的关键在于:我们应该想办法把非结构化的数据处理成结构化数据。这种想法是如此的自然而然,却构成了信息检索的基本思路:即将非结构化数据中的一部分信息提取出来,使其变得有一定结构,然后设计出相应的高效的数据搜索算法和机制,从而签到搜索相对较快的目的。而这部分从非结构化数据中提取出来的,然后重新组织,变得有结构的信息,在信息检索领域,我们便称之为索引。举例说明一下:我们都用过字典,如果没有拼音和部首检索表,要你在那么大一坨字典里找一个词组,那真不是一件Happy的事情,但如果按照拼音或部首进行查询,我们可以很快地定位并找到这个词组,拼音和部首的检索表对于字典而言就是所谓的索引。

二、检索需求

在软件开发过程中,需求这种东西是最悬乎的,飘忽不定,很容易说变就变。笔者在MSRA/STC做过两个月的Dev,期间需求并未形成统一的文档,只是口口相传。误传和误解是常有的事情,不是说MSRA/STC开发不够规范,而是笔者作为暑期实习生,做的只是一个Demo,所受的重视程度不够高导致的。这种情况下,交流和沟通显得无比之重要。再到后来,有些需求是我们两个Dev结合应用场景想出来的,最终也得到了大家的认可,其中之一即是提供表达式级别的信息检索。

为便于说明,精简的需求如下:有数千棵Decision Tree,每棵Tree包含数百个节点,每个节点的内容是一个特征Feature序列,现需要检索以下内容:

(1)某一Feature被哪些Tree包含

(2)某一Feature被哪些Node包含

(3)某一Tree包含了哪些Feature

(4)Feature的模糊检索,如检索“PerStream%”所对应的Tree或Node

(5)表达式级别检索,如([FeatureName] = PerStreamBM25F_Body || [FeatureID] = 18) && ([TreeID] = 1 || [TreeID] = 2)

三、Index设计

如果你了解过搜索引擎,学习或使用过开源全文检索框架Lucene的话,不难会理解为什么要创建索引,以及倒排索引的基本结构。基于笔者信息检索理论与实践的积累,给出了下列索引方案:

(1)倒排索引:Feature-->Tree

(2)倒排索引:Feature-->Node

(3)正排索引:Tree-->Feature

(4)字典树索引(键树):Feature Fuzzy Search

在这个Demo中,我们输入Feature,然后检索其对应的所有Tree或Node。而原始的信息是,某棵Tree中包含了若干节点,每个节点又包含了Feature的序列。如果没有索引,我们就需要遍历所有的Tree,然后遍历每棵Tree的所有节点,并匹配每个节点的Feature序列是否包含了该Feature,直到扫描完所有的Tree为止。这种低效的检索实现方式是如此让人无法忍受,以至于你即使没学习过信息检索都会想到要设计一个高效的算法来解决这个尴尬。

那么,我们来分析一下为什么顺序扫描的速度会慢。其实是我们想要搜索的信息和原始数据中所存储的信息不一致造成的。原始数据中所存储的信息是每个Tree包含哪些Node,每个Node又包含哪些Feature,即从Tree到Node的映射,以及从Node到Feature的映射。然而我们想搜索的信息是哪些Tree或Node包含此Feature,即已知Feature,求相应的Tree或Node,即从Feature到Tree或Node的映射。两者恰恰是相反的,因此顺序扫描的速度会很慢。这样一来,如果索引中存储的是从Feature到Tree或Node的映射,则会大大提高搜索速度。

于是乎倒排索引的方式就形成了,具体的实现就不用细谈了吧,每个Feature对应于一个Tree List或Node List即可。如果要求同时包含两个Feature的Tree或Node,只需要把两个Feature对应的Tree或Node List取交集即可,两个以上的情况你懂的…

输入Tree,检索其包含哪些Feature就没什么好讲的了,正排索引搞定,Tree-->Feature List即可。

比较有意思的是,对Feature的检索支持模糊匹配,比如你输入“PerStream%”用于检索前缀为PerStream的Feature所对应的Tree或Node。我提供的索引方案是字典树索引,在数据结构中又叫键树,或共享前缀树。这处结构可以用来实现输入法的智能提示功能,当然它仅支持前缀匹配,如果你硬是先把字符串先倒排一遍,然后再用字典树创建索引,当然也能实现后缀匹配,但这个…

关于字典树的设计比较精巧,我在之前的博文《输入法核心数据结构及算法的设计》已经做过简单介绍了,读者可以参阅,这里不再过多言论。总之,利用字典树存储了Fuzzy
FeatureàTree List或Node List的映射,从而实现了Feature的模糊检索。

四、Index优化

虽然说概念上索引结构大致是确定下来了,有倒排索引、正排索引还有字典树索引,但基于这一思想不同的人会有不同的实现。比如Feature-->Tree List的映射关系如何组织存储,有人直接就会用一个ArrayList存储就完了;有人会存完之后按照升序关系排个序。再比如,求同时包含两个Feature的Tree,则需要将两个Tree List求交集,如果Tree List之前是排过序的,O(n)时间就能搞定,没排序的就SB了。当然这只是举个例,我的意思是想说,同样一个思路,虽然说看似已经很明朗了,但是不同的实现其效率和效果相关甚远。

结合实际的开发场景,我们应该尽可能地设计出更加高效的结构和算法。我的实现方案即不是ArrayList,也不需要排序,而是引入了BitMap来存储Tree List。如果BitMap[100]=1表示ID=100的Tree存在于Tree List中,BitMap[99]=0表示ID=99的Tree不存在。这样设计的原因是分析了当时的场景,只有数千棵Tree,数量不大,可放入一个BitMap中,用下标来标识Tree ID,在一定程度上还能节点存储空间,因为一个Feature对应的ArrayList可能会很大。同时,也不需要排序过程,如果求两个List的交集,直接用BitMap做个&运算即可,方便快捷。

这是结合具体场景做性能优化的其中一个案例。当然,我说的只是一个基本的做法,如何做优化,还需要在开发过程中细细琢磨。

到此为止,索引方案有了,索引结构有了,索引也被我们一步一步地构建起来了,但各位有没有注意到少了点什么?你有没有发现所有的索引数据全部都存储于内存中啊。内存是什么,是一种不可靠的存储介质,断电就全部擦除,数据全部丢失啊。如果哪天服务器挂了,索引数据岂不是全部没了,服务器重启是不是又要加载原始数据重建所有索引呢?数据量小还好说,海量的大数据怎么处理?一次索引创建几天都搞不定。也即是说,我们还需要一个环节:索引落地!



索引落地即是将内存中的索引,以一定的格式和组织结构写入磁盘,这种格式必然会区别于内存,比如刚才提到的倒排索引表在磁盘上如何存储,如何能够更为迅速地被服务加载,并在内存中重构完整的倒排索引表等。当然索引落地就没那么简单了,道是可以参考一个Lucene的索引格式,笔者在实习的两个月中也没有那么多时间去研究。由于基于BitMap做了优化,除了在检索效率得到提升之外,索引所占用的空间并不大,只有1~2M的样子,对于一个实习Demo而言,不考虑容灾容错的话已经足够了。

但这并不妨碍我们继续扩展思路深入思考:海量数据情况下,巨大索引量不能全部载入内存,这时还需要借助于Cache去缓存当前最热的索引,用于提升检索效率,而磁盘则作为沉淀层,存储最不常使用的索引,当然又是一个比较困难却有趣的问题了。

五、Search设计

当然,我们创建索引的目的也即是为了Search,用户不需要知道Index,但用户一定要知道Search。一开始,我们Search的定位只是单一短语的Search,后来在笔者的建议下,我们确定了表达式级别Search的需求,如([FeatureName] = PerStreamBM25F_Body || [FeatureID] = 18) && ([TreeID] = 1 || [TreeID] = 2),当然单一短语也是一种最为简单的表达式。

我们的表达式是通过&&、||、!以及()等运算符组装在一起的,然而,表达式是不能直接被用于检索的,要想理解表达式的含义,则需要表达式的解析组件。关于表达式的解析算法,是借助于逆波兰表达式完成的,这些内容留在了本系列博文第三部分:算法篇中详细论述,请大家持续关注本博客~

既然是对表达式做检索,那么我们的检索类是如何定义的呢?笔者思考了一个上午,想到了一个算是比较巧妙的解决方案与大家分享。最顶层是抽象类为Query,抽象抽象方法search(),直接从Query派生出WordQuery、NotQuery和BinaryQuery。WordQuery和NotQuery只有一个操作数,而BinaryQuery却有两个操作数。从BinaryQuery派生出AndQuery和OrQuery分别对应于&&和||操作。一个简单的类继承关系图如下所示:



Query

检索抽象基类,提供抽象方法search()

WordQuery

从Query派生的类,查找给定的单词或短语,基础检索类

NotQuery

从Query派生的类,对应于运算符!

BinaryQuery

从Query派生的类,表示带两个Query操作数的查询

AndQuery

从BinaryQuery派生的类,表示两个Query的与操作,对应于运算符&&

OrQuery

从BinaryQuery派生的类,表示两个Query的或操作,对应于运算符||

query1 && query2

返回AndQuery(query1, query2)

query1 || query2

返回OrQuery(query1, query2)

!query

返回NotQuery(query)

按照我们的设计方式,则对于检索表达式:([FeatureName] = PerStreamBM25F_Body || [FeatureID] = 18) && ([TreeID] = 1 || [TreeID] = 2),分别对应于以下对象:

query1:WordQuery([FeatureName] = PerStreamBM25F_Body)

query2:WordQuery([FeatureID] = 18)

query3:WordQuery([TreeID] = 1)

query4:WordQuery([TreeID] = 2)

表达式:AndQuery(OrQuery(query1, query2), OrQuery(query3, query4))

六、Search优化

这块当时没有具体去做,只是大概地想了想。我们在用C语言写程序时,会知道运算符&&、||都有短路效应,基于短路效应,我们可以完成某些判断语句的优化。而我们的Search也不例外,对于检索请求:[FeatureName] = PerStreamBM25F_Body && [FeatureID] = 18,如果[FeatureName] = PerStreamBM25F_Body的Tree根本就不存在,则就没必要去执行[FeatureID] =
18的检索过程了。再比如类似于:[FeatureID] = 1 && [FeatureID] != 1这样的检索请求,则可以直接过滤即可,因为结果必为NULL。

当然优化是门高深的学问,想做到更好只有不断地实践和思考了~

七、总结

Index & Search这块,我个人认为是Demo中比较巧妙的一块,也是代码最优雅的部分,整个Demo做下来,细细品味,亦觉得写代码也是件非常快乐和有成就感的事情,特别是自己对于这个需求实现,是从需求分析、到方案设计、到编码实现、再到功能测试、以及后期性能优化都做过充分的思考,收获彼丰。当然本文并未粘贴任何代码,只谈思路只谈想法,只谈设计只谈框架,大而不虚,简而言之。

在本系列博文第三部分:算法篇中,我还会单独列出一段描述Search表达式的解析算法,请大家持续关注本博客~
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: 
相关文章推荐