大数据下的实业解析
大数据时代的实体解析困境
<!--[if !supportLists]-->1. <!--[endif]-->什么是实体解析
<!--[if !supportLists]-->2. <!--[endif]-->入门级问题
<!--[if !supportLists]-->3. <!--[endif]-->商业价值
<!--[if !supportLists]-->4. <!--[endif]-->复杂一点的问题
<!--[if !supportLists]-->5. <!--[endif]-->系统设计
<!--[if !supportLists]-->6. <!--[endif]-->大数据解决方案
<!--[if !supportLists]-->7. <!--[endif]-->信息安全港
<!--[if !supportLists]-->8. <!--[endif]-->生词表
什么是实体解析
实体解析在国内是一个比较少有人研究的领域。它主要的目的是用来打通不同数据集的关联,在数据集之间建立一道信息共享的桥梁。在数字营销行业,实体解析技术有着广泛的应用,常常被用来整合互联网公司和传统企业的客户数据。
从狭义上来说,实体解析被定义为判断两条不同的记录(Record)是否指向真实世界的相同实体(Entity)的过程。而它的广义上的定义则涵盖识别并整合所有定义同一真实世界实体的记录的过程。
举个例子,某用户在微信的账号是他的手机号码,而他在微博的账号则是他的邮箱账号。那么对于以下两条记录而言,它们指向的便是现实生活中的同一个人(实体):
昵称 |
|
微博 |
来源 |
张三 |
123456789 |
|
微信 |
Zhangsan |
|
joe.doe@abc.com |
微博 |
弄清楚这点非常重要,如果我们需要对个体用户的特征有更深入的洞察,我们便需要有更多与该用户相关的信息。通过将存在多个互联网公司的同一客户数据连接起来,我们可以将这个人在互联网上的所有足迹关联到一起:他在豆瓣上的阅读类型,他在淘宝上的购物等级,他在微博上关注了哪些明星,他的QQ空间的文章是转载为主还是原创,他喜欢逛天涯社区还是网易论坛,等等。
这样的一种信息联通的状态,被称为信息互联(Connectivity),而实体解析的目的便是为了达到信息互联。
一旦用户的个人信息,行为特征,网络习惯被有效的组织到一起,便会产生一个1+1远大于2的结果,无数的潜在商业活动可以基于此来进行。我们会在下一章节了解到,一些常见的利用信息互联,来实现的广告营销目的的例子。
许多互联网公司早已意识到了信息互联的重要性,也为此做出了许多尝试,最常见的两种方案分别是cookie植入和单点登录。
Cookie植入
第三方Cookie植入是一种最早出现,也是最常见的解决方案,它的主要思想便是在用户的客户端植入一段Cookie数据,每当用户在该客户端进行操作时,详细的用户行为数据就会通过互联网发送给Cookie植入的服务商,这其中,便包含了用户隐私的行为数据。
Cookie植入这些年来使用的逐渐减少。一方面,用户越来越关注自己的隐私,使用Cookie这种偷偷窃取用户行为的办法遭到了许多用户的反感,前些日子中央电视台也报道了大公司利用Cookie窃取用户隐私的内容。当然,央视的报道中有许多不实之处,但是依然引起了很大的反响。
另一方面,移动时代的到来,Cookie植入也无法适应现代人多终端访问互联网的方式。伴随着单点登录的广泛采用,Cookie植入逐渐退出了历史舞台。
<!--[if !supportLineBreakNewLine]-->
<!--[endif]-->
单点登录
如果你曾经在登录某网站时,被要求可以选择使用QQ账号或者是微博帐号登录,那么你就已经是单点登录的用户了。大型的互联网公司通过自身的影响力,与其它网站进行合作,让其它网站的用户选择使用其自己的账号进行登录,通过这样的方法,便可以轻易的将同一用户的所有数据整合到一起去。
单点登录的优点在于解决方案十分简单,只要是使用相同的账号登录,便可以判定为同一个人。
单点登录技术的缺点在于,目前为止,没有任何一家互联网公司能够做到一统天下,让所有它网络应用都使用它的账号进行登录。同时考虑到数据安全问题,依然有很多公司即使可以,也不愿意使用其它公司的用户账号登录。
单点登录的另一个缺点是,它忽略了广大的传统企业。像许多传统家电企业,它们同样拥有着海量的个人信息数据,然而,由于缺乏一个受众广泛的站点或是网络应用,它们无法与当前知名的单点登录公司进行合作,以达到信息互联的目的。而这些公司对此却有着强烈的商业需求。
而今天我们要介绍的实体解析,则是从另一个角度来思考这个问题,通过允许信息孤岛的存在,利用技术手段,打通信息孤岛之间的通信,从而达到信息互联。
实体解析本身是一个非常简单的概念,理解起来并不困难。然而想要开发一个相关的实际应用却非常复杂,其中包含了关于一致性的问题。而当我们把数据源扩展到大规模的,不同来源的,异构的数据时,它所遇到的一些包括性能以及一致性的问题便更加难以解决。
本文便是发现这些问题的一次尝试,并想要给出一些可行的解决方案。
入门级问题
在深入研究整个实体解析所遇到的问题之前,让我们先来看一个简单的例子。
例一:
设有这样一个输入文件,文件中的每行是一条记录,包含以下三列信息。
输入文件格式: Record Number|EMAIL|MOBILE
要求实现这样的一个功能,它的输入为该文件,目的是为输入文件中的每一条记录生成一个UniqueID。
输出文件格式: Record Number|EMAIL| MOBILE|UniqueID
UniqueID是一个整数型的数值,需要符合以下条件: <!--[if !supportLists]-->1. <!--[endif]-->如果两条记录是一致的,则它们的UniqueID相同。 <!--[if !supportLists]-->2. <!--[endif]-->否则,它们的UniqueID不相同。
判断两条记录是否一致的条件非常简单: <!--[if !supportLists]-->1. <!--[endif]-->如果某两条记录的EMAIL值相同,则他们是一致的。 <!--[if !supportLists]-->2. <!--[endif]-->如果某两条记录的PHONE值相同,则他们是一致的。 |
初略一看,问题似乎很简单,我们只需要判断两条记录的EMAIL或者MOBILE是否相等,如果相等,则为他们生成相同的UniqueID即可。
第一版的伪代码是这样:
Int counter = 1 For each record in input file: If record. EMAIL exists: Get uniqueID by record EMAIL Apply existing uniqueID to record Else if record. MOBIL Eexists: Get uniqueID by record MOBILE Apply existing uniqueID to record Else : New unqiueID = counter++ Assign uniqueID to record |
这里我们使用了最简单的方法生成uniqueID, 一个从1开始自增长的数值。每次有需要时就在原有值上加一,生成新的uniqueID。
图一:简单算法
程序写完了,似乎也已经很好的解决了例子中的需求。写几个测试用来来试试看:
Case 1:
Record Number |
|
MOBILE |
期望结果 |
实际结果 |
1 |
aaa |
111 |
1 |
1 |
2 |
ccc |
222 |
2 |
2 |
3 |
aaa |
333 |
1 |
1 |
4 |
fff |
222 |
2 |
2 |
情况看起来相当令人满意。所有的期望结果与实际结果都是相同的,只要两条记录中的EMAIL和MOBILE有任一相同,便会为其生成相同的uniqueID。
然而紧接着,一个新的case有点小疑问。
Case 2:
Record Number |
|
MOBILE |
期望结果 |
实际结果 |
1 |
aaa |
111 |
1 |
1 |
2 |
aaa |
222 |
1 |
1 |
3 |
bbb |
222 |
1 |
1 |
在这个测试用例中,虽然记录1与记录3的EMAIL和MOBILE都不相同,然而,它们最后被分配到的uniqueID却是一样的。
仔细观察这三条记录,可以发现记录1与记录2的EMAIL相同,因此它们被赋予了相同的uniqueID,而记录2与记录3的MOBILE是相同的,所以记录3的uniqueID也与记录2相同。由于这样情况的发生,也就造成了记录1与记录3的结果相同。
根据我们在需求中的定义,如果两条记录的EMAIL值或是MOBILE值相同,则它们是一致的, 而这种一致性是具有传递性的,也就是说,如果记录1与记录2一致,且记录2与记录3又一致,那么他们三者互相是一致的。如果我们用=代表一致性,则:
R1 = R2 且R2 = R3 => R1 = R3 |
万幸的是,我们的程序具备一定的鲁棒性,虽然发生了这种传递性的情况,程序依然能够很好的处理,并返回期望的结果。
然而,当我们稍微调整Case 2的记录顺序之后,问题又出现了。
Case 3:
Record Number |
|
MOBILE |
期望结果 |
实际结果 |
1 |
aaa |
111 |
1 |
1 |
2 |
aaa |
222 |
1 |
2 |
3 |
bbb |
222 |
1 |
1 |
我们只是将记录2和记录3的顺序对调了一下,却得到了错误的结果,新的记录2的期望结果为1,可实际结果为2。
仔细的回顾一下我们的程序的处理过程:当我们检查记录2时,系统中只有一条记录1存在,而此时,记录2与记录1的EMAIL和MOBILE的值均不相同。因此,系统判断,这是一条新出现的记录,并给它赋新值为2。而当程序处理记录3时,发现其与记录1的EMAIL值相同,便相应的将已存在的ID值1赋给了记录3。而记录2与记录3的MOBILE值相同这个事实被忽略了。
心急的程序员可能马上就找到一个快速的解决方案:只需要在开始处理一条新的记录的时候,重新过一遍已经在内存中的记录,如果有需要更新的,将其更新为一致即可。
然而这样的解决办法的通用性并不是很好,当我们的数据集扩充到3个乃至更多的字段时,重复遍历内存中记录的次数也会越来越多,随着数据量的不断增加,往返检索验证的次数更是呈指数级的上升,系统性能会在这个过程中急剧的下降。而一旦不小心疏漏了某种特定回溯情景,程序便会产生错误的结果。
图二:更加复杂的关联关系
在找到合适的算法之前,我们再回头来看程序执行的过程,为什么它会在特定的阶段,进入错误的状态中呢?
这其中的关键就在于:程序在没有考虑所有事实之前,便草率的为记录赋予了错误的uniqueID值。而当一旦有新的事实进入,便只能再去找到前面出错的地方,利用新的事实依据来改正错误。
一个行之有效的解决办法就是,等到程序将所有的事实都加载到内存中之后,再根据所有事实作出判断,为每条记录分配uniqueID。
这里给出一个利用预分组算法的实现方法。
算法二:预分组算法
Int counter = 1 Map<String, List<Long>> emailRecordMap Map<String, List<Long>> phoneRecordMap For each record in input file: Add <record.email, record.id> pair into emailRecordMap Add <record.phone, record.id> pair into phoneRecordMap
## groups的每一个item是一个临时分组 ## 每个临时分组中是一个ID列表,包含了email相同或是phone相同的record的ID。 List<List<Long>> groups = emailRecordMap.values + phoneRecordMap.values
## 贪婪算法,遍历所有的组,如果两组之间有交集,则合并组。 List<List<Long>> finalGroups = merge(groups)
## 最终,每个分组拥有一个UniqueID。 Assign a UniqueID to each group in finalGroups. |
图三:预分组算法
TODO:添加算法描述
可以看出,只要稍微做一些调整,就可以将这个算法扩充到支持更多的字段。跑出来的结果堪称完美,不管你怎么调整你的输入的次序,生成的UniqueID都能够完美的保证准确性,测试用例的顺序已经不会影响到结果的正确性了。
分组算法性能的核心在于进行重组的贪婪算法,这个算法的优劣直接决定了整个解决方案的性能优劣。
这段程序里有两个小问题。
第一个问题是当待处理的数据不是一次性全部提供过来,而是分批次的出现的情况。那么当处理前面的数据时,依然有可能出现因为事实不完成导致的错误分配。
针对这种情况,解决办法则是在短期内允许由于知识不充分导致的错误分配,而当新的数据到来时,将老的数据和新的数据合并在一起,再进行一次分组算法,得到新的UniqueID,同时,对外提供新老UniqueID的映射关系表,以确保即使用户拿到的是之前老的UniqueID,也能够找到新的,对应的数据。
第二个问题是,当输入文件非常大,以至于无法将所有记录一次性读入到内存中时。这种情况下,无法取得全量的分组信息,也就无法保证信息的完整性了。下面的章节里对这个问题会有更深层次的介绍,尤其是在当今的大数据环境下,完美有效的解决这样一个问题,至关重要。
附:重组算法
关于重组算法,其实有很多可以选择的方案,这里将问题抽象化,可以当作算法题来单独研究,看是否有最优解。
输入:List<List<Integer>> groups
输出:List<List<Integer>> regroups
要求:输入数据为一个Integer型列表的列表,每一个Integer型列表代表了一个组,如果输入中的任意两个列表有交集,则将两列表合并。并将最终剩下的列表输出。
举例1: 输入[[1, 2, 3][2, 4][5]] => 输出[[1, 2, 3, 4], [5]] 举例2: 输入 [[1, 2, 5][2, 4][5]] => 输出[[1, 2, 3, 4, 5]] 举例3: 输入 [[1, 2][2, 3][3, 4]] => 输出[[1, 2, 3, 4]] 举例4: 输入 [[1, 2, 5][2, 3][4, 8][5, 7]] => 输出[[1, 2, 3, 5, 7], [4, 8]] |
几种裁剪思路:
<!--[if !supportLists]-->1. <!--[endif]-->将所有只包含一个元素的组剔除,因为这样的组即使与其它组有交集,也不会导致任何操作。
<!--[if !supportLists]-->2. <!--[endif]-->针对所有的组进行排序,如果所有排完序之后的组之间有空隙,则可以从空隙处将大的集合分成更小的集合,在小的集合中进行整合。
<!--[if !supportLists]-->3. <!--[endif]-->尽量多的采用HashMap进行查询,利用空间换时间。
商业价值
某快消品牌打算在年底举行一次大型的促销活动,需要针对其现有CRM系统中的会员发送广告。
传统的做法是根据会员的地址,向会员发送直邮广告,或是发送手机短信,将旗下所有所有产品不论低中高端一股脑发过去。然后搬张椅子坐在销售部旁,盯着销售指标,寄希望于业绩有着突然猛进。
如果幸运的话,销售指数直线上升,那么便说明这次营销活动是成功的,至于究竟为什么成功,是这次活动引发了销售提升,还是恰逢三八妇女节,女性购买意向增加导致的,那就完全不知道了。下次再做这样的活动,还能不能获得同样的效果,其结果也未可知。
相反,如果没那么幸运,销售指标反而下降了,似乎也完全没有很好的办法来分析究竟是什么原因导致的。
相对于现代社会的多元化,用户接收广告的手段也在不断增多。现在更多的用户通过视频网站,微博,微信等互联网的方式获取广告,而传统的直邮或是短信的方式,则很难覆盖到更多的用户。
另一方面,用户对于收到的各式各样的直邮或是短信不胜其烦。究其原因便是,企业给用户发送的产品列表并不是用户真正想要的。这就涉及到差异化营销的概念,我们需要针对每个用户的个人肖像,有针对性的进行营销。比如根据用户的收入情况,或是年龄分段来提供不同类型的广告,提高广告的命中率。而这,在传统营销模式下,都不可能实现。
根据一位大师的名言:“每个公司至少有一半的广告费是浪费的,问题是,没法知道究竟是哪一半。” 这句话很好的体现了广告行业的一个重要特征,那就是:广告效果无法衡量。
总而言之,传统的营销模式下,会有如下问题:营销手段单一,用户肖像模糊,营销效果无法量化。
互联网时代的到来,为这些问题提供了解决办法,通过RTB技术(不知道什么是RTB请百度),我们可以针对不同用户价值进行实时竞价,确保每一次广告的投放都物有所值。同时,根据用户看到广告后的反应,可以很容易的衡量这次广告投放的效果:用户是否点击了该广告,在广告页停留的时间,最终是否下单,所有的这些信息,都可以被客户端软件(网页,APP,公众号等等)记录下来并汇总,最终形成该次广告的效果报告。
而这一切能够成功的基本原则就是:我们需要了解不同用户的价值。
这里所说的价值并非用户自身价值,而是该用户对于企业产品的价值。比如说,相对于儿童纸尿裤产品,已婚女性的价值便远高于未婚女性及男性。而一款狗粮产品,则毫无疑问的追求那些拥有狗宠物的用户群。
Link vs ID
回到前面章节的例子,如果我们有这样的两种数据来源:
昵称 |
邮箱 |
手机 |
标签1 |
标签2 |
标签3 |
标签4 |
标签5 |
张三 |
Abc@abc.com |
|
逗逼 |
90后 |
爱艺术 |
|
|
李四 |
|
111111111 |
多金男 |
爱生活 |
已婚 |
爱音乐 |
|
昵称 |
邮箱 |
手机 |
购物1 |
购物2 |
购物3 |
购物4 |
购物5 |
Zhangsan |
Abc@abc.com |
123456789 |
狗粮 |
|
|
|
|
Lisi |
|
111111111 |
尿不湿 |
书籍 |
|
|
|
通过我们在前一章节的预分组算法,可以很轻易的为每条记录分配一个uniqueID,确保指向同一个人的记录具有相同的uniqueID。这里的uniqueID也代表了现实世界里的某个实体。在实体解析的领域中,我们将该uniqueID称为Link。接下来的描述中,我们都会使用Link来替代uniqueID。请注意,这个Link并非如同ID一般,具有distinct的特性,在同一套数据集中,很可能出现多条记录拥有相同的Link的情况,只要这些记录指向了同一个实体即可。这也是为什么该术语被称为Link而非ID的原因。
通过这样的一个Link值,我们可以很轻易的找到与该实体相关的所有记录。常见的Link有Address Link(地址Link),Consumer Link(消费者Link)等等。
在实体解析中,一个经常会被问到的问题是:为什么我会需要产生这样的一个Link?而不是直接的将多条指向相同记录的记录合并起来,然后给它一个ID,这样不是更加简洁?
合并多条记录并非不可行,但是它有一个问题,它会将信息的历史内容给抹去,然而某条信息的历史内容可能在特定场合下依然是有价值的。举例来说:某人在成为某网站的会员时,填写了他当前的个人信息,包括电话及家庭住址。后来,他因为工作原因,举家搬迁到了另一个城市,家庭地址都发生了变化,然而手机号并没有变。
另一家互联网企业也拥有该用户的个人信息,遗憾的是,它记录的也是此人在过去的地址,如果我们在系统中将此人过去的家庭地址信息覆盖掉,那么这家互联网公司便无法将身处异地的两个人关联起来。
鉴于类似问题的考虑,通常在实体解析系统当中,并不会将不同历史时期的数据进行合并,而是仅仅使它们拥有相同的Link,确保将来无论何是都能够恰当的回溯到。
这种思维其实非常符合大数据时代对于数据的态度:在你真正需要之前,你不知道哪些数据是有价值的。因此,最好的办法还是保存一切历史记录。
复杂一点的例子
真实生活中的例子可能比之前的要稍微复杂一些,几个常见的需要考虑到的问题包括:
模糊匹配
有的时候,两条记录是否一致无法通过信息的完全一致来发现。比如说地址信息:“江苏省南通市紫琅路120-1号”和“南通市崇川区紫琅路120-1号”很显然是同一个地方,但是计算机却很难做出这样的判断,它会认为这两个字符串的内容不相同,从而判定为不同地址。这个时候我们就要用到计算字符串相似度(即模糊匹配)的技术了。
作为通用的计算字符串相似度的算法,比如说编辑距离算法,ngram算法或者是soundex算法在实体解析领域的作用并不是很大,相较而言,更多时候是针对特殊业务开发的指定算法。举例来说:“南通市紫琅路120号”跟“南通市紫琅路180号”,如果我们用通用相似度算法来比较,可能会得出“两个地址相似度非常高”的结论。但事实上,它们所指代的地址可能一个在街头,一个在街尾,根本不是同一个地方。因此,开发出能够恰当的与业务紧密结合的相似度算法至关重要。
通用相似度算法还有另外一个问题,即这些算法不具备传导性。举例来说,A相似于B,B又相似于C,可是如果A跟C比较,它们很可能是不相似的。作为相似度匹配算法本身而言,这无可厚非。然而这种非传导性在实体解析时,却是一个严重的问题。实体解析系统会面临究竟是将A和B分为一组,还是将B和C分为一组的困境。
利用定制化的相似度匹配算法,我们可以规避这样的问题,我们只需要将算法设计得具备传导性,即可满足实体解析系统的需求。
另一方面,通用相似度算法也并非毫无用武之地,在许多场景下,由用户手写的信息常常会被粗心的录入员写错。比方说,我就经常会收到发往“安容诚”(正确的写法应为“安客诚”)的广告邮件。这种情况下,通用相似度算法则可以帮助我们在一定程度上进行纠错,规避人为造成的疏忽。
组合匹配
假设我们需要判断两条记录是否指向同一个用户,而我们可用来比较的数据只有姓名和地址。那么不论是只使用姓名,还是只使用地址来进行匹配,都是不准确的。在中国,姓名可用的汉字只有几万个,重名的概率相当高。同时,一个家庭地址可能住了三代同堂,而公司地址则可能有上百人在此工作。
此时,我们便需要将分组的方式从单一的字段信息匹配,变成多字段的组合匹配。
使用多级匹配的算法与之前的例子并无不同,通过不同分组条件的排列组合,我们还可以设计出更加复杂的分组规则,以满足业务需求。
初始化和更新
在初始化的模式下,所有记录的Link都为空,通过预分组算法,我们可以为每个实体创建独一无二的Link。
而在更新操作的模式下,并非所有记录的Link都为空。举例来说:信息系统首次提供100万条记录进行Link添加操作(初始化)。过了两个月之后,信息系统有了更新,提供了额外的100万条记录,我们需要将这批新记录和老的100万条记录进行合并,生成Link。此时,会出下如下几种情况:
<!--[if !supportLists]-->· <!--[endif]-->新记录没有找到与之一致的记录。那么我们便为该新记录赋予新的Link值。
<!--[if !supportLists]-->· <!--[endif]-->新记录找到了与之一致的记录,且该记录也为新记录,则该分组被赋予相同的新的Link值。
<!--[if !supportLists]-->· <!--[endif]-->新记录找到了与之一致的记录,且该记录为旧记录,则新记录会获得旧记录的Link值。
<!--[if !supportLists]-->· <!--[endif]-->新记录的出现导致了原本不在一个组当中的旧记录被合并为相同的组,这种情况下,旧记录的Link值需要被更新。常规的做法是选取最小的Link值作为新组的Link值,并将更新信息记录下来。
查询
当我们的信息库已经被完全更新之后,便可以通过某些接口对信息库进行检索,常见的方式是将信息库的内容存储到持久化存储系统中,比如文件系统,又或者数据库。
然而,正如我们初始化和更新的过程一般,查询操作也会涉及到单级查询和多级查询,这些在现代数据库系统中都是集成进去的功能,可以轻易实现。但如果涉及到模糊查询,问题就变得复杂起来。相对于完全匹配查询,模糊查询很难通过直接寻找的方式来实现,而只能通过对查询内容与库中数据一一对比计算,来得到结果。如果信息库中有一百万条记录,那么每次查询便都需要100万次数据比较,这样的性能在实际应用中,是不可接受的。
因此,如何设计性能良好,实时返回的查询接口,也是实体解析系统中的一个重要课题。根据业务的不同,可能有许多解决办法。比如上上面提到的地址信息,就可以通过分级查询的方式来实现。
系统设计
利用上面的所有知识,我将整个系统分为三个部分。
图三:
存储模块
存储模块被用来保存我们的实体解析信息库,可选的方案,包括文件系统以及数据库系统,而数据库系统又可以分为行存储数据库或是列存储数据库。在后面的章节,我们会介绍使用行存储数据库和列存储数据库的区别。
查询模块
实体解析信息库需要具备查询功能,而该查询功能仅仅通过自带的数据库系统的查询并不能满足要求,因此,通常需要设计独立的查询模块,去包含查询API以及定制化的查询功能。
计算模块
计算模块读入收集来的实体信息,并将其记录到存储模块中去。我们在之前的章节中已经看到了很多与之相关的内容。可以说,计算模块是整个实体信息系统的核心,一个叫实体解析系统的成功,完全取决于计算模块的质量好坏。
根据预分组算法,整个计算模块可以分为三部分。
图四:
分组(Grouping)
分组步骤根据预定义的分组逻辑,将每条输入记录分配到不同的组中去。
重组(Closure Grouping)
重组则是利用分组结果,将有交集的分组结果合并到同一组,以达到所有指向同一实体的所以记录在同一个组中的效果。
Link分配 (Link Assignment)
Link分配步骤的主要工作则是确保每个分组获得一个唯一独立的Link值。
接下来,我们就尝试着使用一些大数据的解决方案来应用到这样一个系统设计当中去。
大数据解决方案
我们在前面章节提到的预分组算法有一个不那么容易被察觉的问题:它需要把所有的记录全部加载到内存中才能够进行分组。这一点在数据量比较小的时候,并不是什么大问题。
然而在当今的大数据时代,每家公司都有着海量的,T级乃至P级的数据,如果在这种情况下,还想要使用这种方式来处理的话,很快就会陷入内存溢出的困境。举例来说,在我们公司,一家客户企业的CRM里就包含了约20亿条的客户信息,全部导出来大约有100GB,而这样的客户至少也有四五家。即使是非常强大的服务器,也很难将这些这些数据全部导入到内存中。
那么,简单的方案,就是将这些中间结果,就是Group和Closure Group阶段阶段生成的分组信息,放到缓存或者是数据库中去。然而这样导致的后果就是性能的急剧下降。任意一条记录的插入都可能包含了数次的查询和插入操作。而最终的组合并操作的复杂度更是无法想象。而且上百亿条的记录,传统的信息系统很难处理好。
于是我们在想,有没有可能采用一些大数据的技术手段来完成这项工作呢?
作为潜在的大数据解决方案,需要符合以下几个条件:
<!--[if !supportLists]-->· <!--[endif]-->存储系统:能够存储并处理上百亿级的数据量。
<!--[if !supportLists]-->· <!--[endif]-->计算系统:可以开发出有效的预分组算法,并为每条记录产生Link。
<!--[if !supportLists]-->· <!--[endif]-->查询系统:支持实时查询功能。
存储系统
作为分布式存储系统,最著名的当属Apache三剑客:HBase,Cassandra和CouchDB。
从目前看来,HBase是这三者中前景最好的一个,毕竟是Hadoop衍生的产品,无论是与Hadoop文件系统HDFS的整合,还是在Apache内部受到的支持,都是最好的。由于内部采用HDFS作为文件系统,所以Hbase能够很好的支持大量数据的存储,同时,Hbase的查询性能也非常的好。
Cassandra近些年的发展遇到了一些小问题,有一些大公司在放弃使用Cassandra,这也使得其它公司在选择Cassandra的时候更加的谨慎。基于我自身对Cassandra的了解,它的一致性是有一些问题的,并且查询操作非常的复杂。基于这些考虑,目前来看,Cassandra可能并不是一个非常好的选择。
而CouchDB作为一个新型的文档型数据库,近几年发展很快,吸引了很多的目光。可以说是非常值得期待的,然而相对于Hbase的成熟,使用CouchDB可能还需要忍受一些产品级的bug。
综上所述,我们认为目前来看,Hbase作为一个开源的成熟的列存储数据库,非常适合用作存储实体信息。
另一个可选方案是采用Redis,它基本上就是一个key-value的类缓存数据库。然而正因为如此,它的性能非常强大。就测试来看,它的读写性能非常的高,远超Hbase。
附:列存储数据库
Hbase与Cassandra都是列存储数据库,关于什么是列存储数据库,并不是本文的重点,有兴趣的朋友请自行查阅文章。然而列存储数据库有一个很有意思的特征,非常适用于实体解析信息库。
我们在前面的章节讲到,针对同一实体的不同记录,在实体解析信息库中是保存为多条记录的,这样做的目的是为了保留不同记录之间的差异性,确保该实体的历史信息也能够不受影响的存储。也正因为如此,我们需要给同一实体的不同记录相同的Link值,以方便将来检索。
作为列存储数据库的一个非常显著的优点就是,每个列有可以有多个数据。因此当我们存储实体记录时,便可以将多条记录合并起来,让其每一列包含记录中的所有的不同值。采用这种方式,每个实体便可以只保存为一条记录。相比于传统数据库,每个实体保存为多条记录,这种方式无疑显得更加直观易懂。
计算系统
分布式计算系统的选择基本上可以分为两类:MapReduce以及流处理。
MapReduce作为老牌分布式计算模型,在各种行业软件中都已经有着广泛的应用,它的核心思想是将需要计算的数据分布在多台计算机上,每台计算机使用相同的算法计算所有数据的一部分,从而达到“分而治之”的目的。同样的,在网上可以找到很多MapReduce相关的问题。
流处理基本思想与此相似,也是将一个大问题分解成多个小问题来解决。区别在于流处理主要针对实时数据,每条记录的处理都是独立的,并不会与其它记录有交集。这就使得流处理并不适合用于实体解析信息库的处理工作。
如果采用MapReduce计算模型,我们可以很容易的实现预分组算法的第一步,即Grouping操作,只需要根据不同的分组模型将同一条记录map到多个output当中即可,后续的reduce会自动根据比较算法对output中的key进行排序,并将key相同的数据合并在一起。
要注意的是,为了实现模糊比较,可能需要扩展Map的key值对象,确保使用自定义的相似度算法来比较两个key值。
然而接下来的Closure Grouping过程,则要求将所有的分组放入同一个上下文中进行比较,才能得出其中任意两个分组是否有交集的结论。这个也正是我们在前面章节讨论的重组算法。
关于重组算法,我一直没能很好的找到一个可以利用分布式计算能力的方法。这也是能否使用MapReduce来完成实体解析的关键。如果不能很好的解决的话,我们便被迫又回到了老路上:在一台功能强大的服务器上完成所有重组操作。通过优化算法,基本上可以在某个独立的节点上完成合并算法。
假设在这里,我们能够在分布式环境下解决重组问题,那么紧接下来的一个问题,可能就更加难以复杂:Link赋值问题。当得到最终分组之后,需要根据每个分组的内容去确定该组的Link值。根据上一章的内容可知有以下两种赋值方法:
<!--[if !supportLists]-->1. <!--[endif]-->如果组中有一个或以上的记录已包含Link值,则选取最最小的Link值,该组中其它所有Link值失效,记录在映射表中。
<!--[if !supportLists]-->2. <!--[endif]-->如果组中不包含任何有Link值的记录,则创建一个新的Link值,该值必须全局唯一,且从未被使用过。
在分布式计算系统中,如果多台机器都需要为自己所处理的数据提供一个全局唯一的值(Link),则需要某个同步机制。否则,你无法知道另一台机器上是否也使用了相同的Link值。
当然你也可以通过串行的方式来进行Link赋值,以此避归一致性问题。然而这依然会降低性能,同时也会使得整个系统变得更加复杂。
另一个可行的解决方案就是提供一个全局的Link生成服务。每当集群中的任意一台机器需要分配Link值时,便通过该服务申请,该服务则负责生成一个全局唯一的Link交给该机器使用。通过该服务,可以确保所以机器获得的Link值全局唯一。
这个方案的问题在于,该Link服务成为整个系统的SPoF(Single Point of Failure),任何时刻的失效,都会导致整个系统失效。同时,它也将成为整个系统的性能瓶颈。
还有一个更加有效的办法:就是在算法开始前就为每一条记录分配一个Link,而后在算法过程中,只需要对每个组中的数据进行Link的更新即可。这个做法看似多此一举,但实际上却可以很好的解决分布式环境下的一致性问题。即使最终选择单机环境执行重组过程,预先分配Link也会使得算法更加的简洁。
预先分配Link的办法对系统唯一的要求就是,数据存储层能够支持高效的随机写操作。此时,像Redis这样的数据库的优势便凸显出来。
查询系统
虽然无论我们的存储系统软件是采用Hbase还是Cassandra,都具备了一定的查询功能。然而几乎所有的实体识别软件的查询系统都不可避免的需要定制化的开发。其中的一个重要的问题就在于对模糊匹配的支持以及对缓存的需求。
定制化开发根据业务的不同,也会变得很不一样,因此很难有哪种方案可以解决所有问题。可以根据业务的需求以及团队的知识组成,灵活的应用各种技术。这里提供一种也许可行的解决方案,供大家参考。
假设在该实体解析系统中包含四个字段,三种分组条件:
<!--[if !supportLists]-->1. <!--[endif]-->Email,直接匹配查询。
<!--[if !supportLists]-->2. <!--[endif]-->Phone,直接匹配查询。
<!--[if !supportLists]-->3. <!--[endif]-->Address & Name,组合匹配查询,Address采用模糊匹配的算法,Name采用直接匹配。
可以在查询系统与存储系统中间,添加一台memcached服务器,用以提供查询缓存,一台中间服务器,用于处理查询到的结果,并进一步处理。
对于Email,Phone和Name的查询结果,直接在memcached服务器上存储查询结果。如果有新的查询进来,直接返回缓存结果。
对于Address的查询,在中间层剔除掉所有模糊匹配的部分,只保留直接匹配的部分,进入存储系统查询,并将查询结果放入memcached层。
当中间层获得所有查询结果之后,再利用Address的模糊匹配算法,在该查询结果中进行过滤,并获得一个新的结果集,返回到上一层。
通过这种将直接匹配部分与模糊匹配部分相分离的办法,可以确保该系统中的各个模块的独立性和简单性,同时也保证了查询性能。
系统架构
经过上面的描述,这里整体的将系统架构画出来。
图五:
为了解决重组问题,又引入了一种新的解决方案。
图六:
信息安全港
在前面的章节中,我们讨论了很多关于如何将不同数据源的实体信息整合在一起的办法。然而有一个很重要的问题一直被我们忽略掉:为什么企业之间需要分享信息?
在深入研究如何获得用户价值之前,我们先看另一个与此有关的概念:信息孤岛。
信息孤岛
随便翻开任意一家互联网公司的CEO访谈,都会听到这样的言论:“数据是我们公司最有价值的资产”。
确实,随着大数据时代的到来,许多公司都积累了大量的数据:包括用户数据,行为数据等等,这些数据对于公司来说,犹如一座不断生产的金矿,无数有价值的可以从这些数据中挖掘出来,从而为公司创造新的商业构想。可以说,这些数据就是公司的核心竞争力和推动力。
然而也正因为如此,任何一家公司都非常的注重数据的安全。任何可能导致信息泄露的行为都被绝对禁止,更别提将其拿出来共享。然而无论是哪家公司,都无法拥有所有的数据,相反,即使是最好的数据公司,也能够从其它公司的数据中获益。
举个例子,作为一家网上商店,京东拥有用户的购物行为记录,它的推荐系统也是根据此来进行商品推荐。然而,作为一家网上论坛,天涯社区则发现该用户最近活跃在数个怀孕新生妈妈群中。如果京东获得了天涯社区的信息,则他能够很轻易的推测该用户家庭中可能有了个待产妈妈,如果京东的推荐系统能够将这些信息考虑进来,它所推荐的物品便能更加好的预测到该用户的潜在购买计划。
然而,处于对信息泄露的担忧,公司之间失去了信息分享的渠道,本身可以做到1+1>2的合作,却因为互相的怀疑,变成了两个独立的1。成为一个个的信息孤岛。
对于如果解决信息孤岛的问题,我们会在后面的章节中仔细的介绍。在这里,我们更多的是考虑,如果我们解决了信息孤岛的问题,我们该如何将来源不同的数据整合到一起。或者说,如何判断这些数据中,哪些是指向了现实世界中的同一实体?
在“信息孤岛”章节我们提到,企业将自身拥有的数据视作为企业中最重要的资产,对其完全十分看重。因此,企业根本不愿意将内部的数据拿出来与其它企业共享。如果没有数据共享,那么所谓的实体解析就毫无价值,想要将数据联通便更加无从谈起。
如何才能让各个企业将数据分享出来,为其他企业提供价值,而又不必担心数据的安全问题呢?我所在的安客诚公司作为一家老牌数据营销企业,提出了一个绝妙的想法:信息安全港。
可以这样理解信息安全港:各家企业以匿名的方式将数据存放在信息安全港中,通过Link的方式与其它企业的数据进行连接。一旦某家企业需要针对某些特定的信息,则可以通过发送请求进入信息安全港,由安全港来执行覆盖所有企业数据的洞察,并获得相应结果。
通过这样的方式,所有企业所提供的信息都是匿名的,即使发生信息泄露,也不会有任何影响,对方得到的仅仅是一些无意义的行为数据。与此同时,该企业依然可以从其它企业的数据中获益,增强自身对已有信息的知识。该企业的数据信息也会帮助到其它企业。然而,如果某个实体的信息只存在于该企业的信息库,那么其它企业便无法获得这些信息。也因此避免了其它企业盗用自己的信息。
最终信息安全港将会由第三方的数据公司来管理维护,一方面提高了数据安全,另一方面也避免参与信息安全港的任意一家公司暗藏私心,将数据开后门的情况出现。
我们这里以各家企业的会员信息作为例子,详细解释信息安全港是如何运作的。
假设有互联网公司A和B,同意将他们的数据分享到C公司提供的信息安全港中。
A公司的会员数据是这样的:
昵称 |
邮箱 |
手机 |
标签1 |
标签2 |
标签3 |
标签4 |
标签5 |
张三 |
Abc@abc.com |
|
逗逼 |
90后 |
爱艺术 |
|
|
李四 |
|
111111111 |
多金男 |
爱生活 |
已婚 |
爱音乐 |
|
王五 |
|
222222222 |
篮球 |
科比 |
摇滚乐 |
|
|
B公司的会员数据是这样的:
昵称 |
邮箱 |
手机 |
购物1 |
购物2 |
购物3 |
购物4 |
购物5 |
Zhangsan |
Abc@abc.com |
123456789 |
狗粮 |
|
|
|
|
Lisi |
|
111111111 |
尿不湿 |
书籍 |
|
|
|
匿名化
在A公司和B公司将他们的数据提供给C公司之前,首先要做的第一件事情便是匿名化。
所谓匿名化,就是要将所有记录中的用户隐私相关信息(又称为Touchpoint Data,即可以利用这些数据接触到该用户)通过一定的方式进行转化,以达到匿名的目的。通常的做法包括使用ID替代用户名,以及用隐私信息计算MD5值。
匿名化后的A公司数据:
ID |
邮箱 |
手机 |
标签1 |
标签2 |
标签3 |
标签4 |
标签5 |
001 |
b2bcf83231a54c4d |
|
逗逼 |
90后 |
爱艺术 |
|
|
002 |
|
7c104cda40c93843 |
多金男 |
爱生活 |
已婚 |
爱音乐 |
|
003 |
|
30b918e9034ab610 |
篮球 |
科比 |
摇滚乐 |
|
|
匿名化后的B公司数据:
ID |
邮箱 |
手机 |
购物1 |
购物2 |
购物3 |
购物4 |
购物5 |
001 |
b2bcf83231a54c4d |
323b453885f5181f |
狗粮 |
|
|
|
|
002 |
|
7c104cda40c93843 |
尿不湿 |
书籍 |
|
|
|
为了显示的目的,我选择了16位的MD5值,通常企业级应用会使用32位的MD5值。
可以看到,通过将会员隐私数据转换成不可逆的MD5值,数据的安全性得到了极大的提高,这些数据即使发生了泄露,也不会对企业造成太大的影响。对方最多也只能得知有这样的一个人,他在B网站上购买了尿不湿和书籍,这些数据并没有太大的意义。
实体解析
接下来,通过我们之前提到的实体解析技术,为每条记录分配Link。
实体解析后的A公司数据:
ID |
邮箱 |
手机 |
标签1 |
标签2 |
标签3 |
标签4 |
标签5 |
Link |
001 |
b2bcf83231a54c4d |
|
逗逼 |
95后 |
爱艺术 |
|
|
1 |
002 |
|
7c104cda40c93843 |
多金男 |
爱生活 |
已婚 |
爱音乐 |
|
2 |
003 |
|
30b918e9034ab610 |
篮球 |
科比 |
摇滚乐 |
|
|
3 |
实体解析后的B公司数据:
ID |
邮箱 |
手机 |
购物1 |
购物2 |
购物3 |
购物4 |
购物5 |
Link |
001 |
b2bcf83231a54c4d |
323b453885f5181f |
狗粮 |
|
|
|
|
1 |
002 |
|
7c104cda40c93843 |
尿不湿 |
书籍 |
|
|
|
2 |
我们这里使用的实体匹配规则分别是邮箱和手机的完全匹配,可以看到,A公司的001记录与B公司的001记录匹配到了一起,A公司的002记录与B公司的002记录匹配到了一起。A公司的003记录没有找到其它匹配,因此获得了独立的Link值。
我们将每家公司的数据和与其相关的Link值发还给他们,通过ID的映射,他们可以获得每条会员记录的Link值,以备后面使用。
由于发送给A和B公司的数据都是Link值,这些值本身并没有任何意义,因此,即使在客户公司被窃取了,也并不会造成任何的损失。
附:模糊匹配字段的匿名化
在这个例子中,邮箱和手机的匹配都是直接匹配,因此,即使将他们转化成了MD5值,依然可以使用相同的算法来进行实体解析。
然而,如果是类似与地址这样需要进行模糊匹配的字段,一旦转化成MD5值,所以细节相关的信息便丢失了,之前的模糊匹配算法也就不再适用。
为了解决这一问题,就需要我们针对模糊匹配字段采用定制化的匿名方法,一方面可以达到匿名的作用,另一方面,依然可以采用某种匹配算法进行匹配。
这种定制化的匿名方法,与业务逻辑紧密关联,可以根据不同的需求来进行开发,这里不再赘述。
实体分析
B公司决定做一次市场营销活动,销售一款篮球鞋,为Link=1的数据进行用户肖像分析,它只需要将Link=1这样的查询条件传递给信息安全港,由信息安全港对所有Link=1的数据进行整合,分析,产生报告。
假设报告的结果是该用户有多大的潜在倾向购买该篮球鞋。信息安全港经过数据分析,并打分,得出该用户不大可能会购买篮球鞋的报告,并将该报告发送还给B公司。
在整个过程中,B公司得到了是一些经过分析和处理的数据,而完全不会接触到A公司提供的任何信息,因此,A公司的数据是安全的。
另一方面,Link=3的数据只存在与A公司的数据中,因此B公司完全无法通过一个Link=3这样的查询条件进行查询,即使可以,B公司也无法利用查询结果来获得该会员的联系方式或者任何联系信息,A公司私有的信息安全得到了保障。
当然,如果A公司与B公司之间有数据共享的协议,B公司也可以通过向信息安全港发出Link=3的查询,信息安全港得出结论是,该用户(注意,该用户并非B公司的会员)可能购买这款篮球鞋的概率非常高。那么B公司便可以将Link=3的信息提供给A公司,并要求A公司在其网站的侧边栏为该会员打这款篮球鞋的广告。
无论是否有数据共享的协议,A公司和B公司之间的数据共享都是以一种安全的,不分享的形式进行的。
采用信息安全港来将信息孤岛连接起来,可以真正的做到数据的价值最大化,每一个参与其中的企业都可以从中获益,而依然保证自身数据的私密性。
唯一需要担心的问题是,开发和实现信息安全港的公司必然需要选择一家可靠的,对于数据安全有着非常好的信誉的公司。安客诚作为一家在全球有着50年数据管理和安全的企业,无疑是作为信息安全港的最好选择。
生词表
在此有必要将一些用到的生词列出来:
实体(Entity) |
现实生活中的某件事物,比如说一个人,或是一个地方。 |
记录(Record) |
实体在信息系统中的体现,比如说数据库中的某一行。某个特定实体可能在多个信息系统中表现为多条不同的记录。 |
实体解析(Entity Resolution) |
将信息系统中的指向同一个实体的多条记录整合到一起的过程。 |
Link |
对实体的引用的ID值,所有指向同一实体的记录拥有相同的Link。 |
实体库(reference base) |
包含Link信息的记录库。 |
信息互联(Connectivity) |
通过实体解析过程将不同数据源中的记录整合起来,以达到信息联通的效果,被称为信息互联。 |