初次体验hiphop-php

2010年3月11日 没有评论

        昨天facebook在github上发布了hiphop-php的源代码。之前听说这玩意能把php代码翻译成c++代码,然后带来巨大的性能提升,所以第一时间编译了一份hiphop-php。

我的机器环境是

  • Centos 5.3 x86_64
  • 8G内存
  • Intel(R) Xeon(R) CPU E5420 @ 2.50GHz

安装注意事项

编译的时候碰到的问题很多,但是基本上都是按照wiki上的步骤进行的。我觉得比较重要的几点:

  • wiki上的Required Packages包包列表都要检查一遍,比如版本号,是否安装过,像binutils-dev这种就很容易忽略
  • 版本符合的话,直接用yum安装这些包就可以了
  • wiki上有类似Boost 1.37 is the minimumversion字样,说明开发者可能就是在这个版本下开发的,我试了下最新版本的boost,编译到后来反而出错
  • 如果yum上没有符合版本的lib库,可以手动编译,但是编译时建议就放在自己的home下,比如:
    ./configure--prefix=/home/user
  • tbb Intel’s Thread Building Blocks这个包有些麻烦,记得按照wiki上说的步骤安装

测试hiphop-php

安装完成之后,时间也不是太多,所以我仅仅是简单的测试了一个php文件,代码如下:

PHP:

  1. <?php
  2. $i = 0;
  3. for($j = 0; $j <1000000; $j++)
  4.        $i += $j;
  5.  
  6. echo $i, "\n";
  7. ?>

用hphp进行编译:


CODE:

  1. hphp/hphptest.php –keep-tempdir=1–log=3

提示生成新的可执行文件


CODE:

  1. /tmp/hphp_c9sbnG/program

做一下运行时间对比:


CODE:

  1. $time php test.php
  2. 499999500000
  3.  
  4. real   0m0.307s
  5. user   0m0.299s
  6. sys    0m0.007s
  7.  
  8. $time /tmp/hphp_c9sbnG/program
  9. 499999500000
  10.  
  11. real   0m0.259s
  12. user   0m0.194s
  13. sys    0m0.008s

没看出来编译成c++代码之后有太大的性能提升,估计是俺的使用手法问题?在邮件组里观察几天再说。

Update

facebook将优化之后的编译参数提交到了github,于是我重新编译并测试一遍这段相同的代码:


CODE:

  1. $time /tmp/hphp_c9sbnG/program
  2. 499999500000
  3.  
  4. real   0m0.140s
  5. user   0m0.076s
  6. sys    0m0.006s

可以看到,经hiphop编译后的php,执行时间几乎快了一倍。

http://www.ooso.net/archives/541

分类: PHP 标签:

hiphop-php

2010年3月11日 没有评论

       Facebook神秘的PHP项目HipHop for PHP终于揭开面纱。这个项目由一个PHP到C++的转换程序,一个重新实现的PHP运行库,和许多常用PHP扩展的重写版本构成,目的是旨在加速和优化PHP。

用Facebook项目负责人赵海平的话说,HipHop项目对Facebook影响巨大。它目前已经支撑了Facebook 90%的Web流量。由于HipHop,Facebook Web服务器上的CPU使用平均减少了50%,从而大大减少了服务器的需求。为了让这一改进也惠及社区,他们决定将之开源,希望能够进一步帮助提高更多大型复杂PHP网站的可伸缩性。

PHP和Facebook的问题

众所周知,Facebook的前端主要是用PHP写的。赵海平说,过去六年Facebook从PHP语言的进展上获益良多。PHP非常简单,易学易用,好读好调试,因此新工程师成长很快,有利地促进了Facebook的更快的创新。

PHP是一种脚本语言,其好处是编程效率高,能够支持产品的快速迭代。但是与传统的编译语言相比,脚本语言的CPU和内存使用效率不好。随着Ajax技术的广泛采用,加上SNS对动态要求较高,这些缺点更显得突出。对于每月超过4000亿次PV的Facebook来说,如何实现扩展,尤其具有挑战性。

常见的办法是直接用C++重写PHP应用中比较复杂的部分,作为PHP扩展。实际上,PHP就转变为一种胶水语言,连接前端HTML和C++应用逻辑。从技术角度讲这也没有问题,但是增加了技能需求,能够在整个应用上工作的工程师数量就大大减少了。学习C++只是编写PHP扩展的第一步,接下来还要理解Zend API。由于Facebook的工程团队较小,每个工程师要支持100万以上的用户。有些代码不是团队里每个人都能看懂,这对于Facebook是无法接受的。

Facebook网站本身的可伸缩性更具挑战性,因为几乎每次页面浏览都是有个性化体验的登录用户发起。浏览主页时,系统需要查询所有朋友、朋友最重要的状态更新、 根据**设置筛选结果,然后还要显示评论、照片等等动态,这一切都需要在一秒内完成。

自2007年以来,Facebook曾写过几种不同办法解决这些问题。其中包括用另一种语言重写Facebook,但是由于开发的复杂性和速度等原因,未能实现。他们还重写了PHP的核心部分Zend引擎,并提交给了PHP项目,但最终还是没有获得所需的性能。最后,他们选择了HipHop,终于得偿所愿。

有了HipHop,工程师可以编写代码,用PHP编写组合最后页面的逻辑,并能够继续快速迭代,同时后端服务使用C++, Erlang, Java, Python编写,提供新闻提要、搜索、聊天和其他核心功能。

HipHop开发故事

赵海平透露,项目最初是来自几年前Facebook公司一次Hackathon活动(员工在一个晚上**发挥,实验新的想法),他手工将PHP转换为C++代码,虽然语法上很类似,但是无论是CPU还是内存使用,转换后的C++代码都大大优于PHP。于是他想,如果构建一个系统,编程实现转换,会怎么样呢?

在此之前,已经有了不少改善PHP性能的方法。Zend引擎在运行时转换PHP源代码为运行在Zend虚拟机上的opcode。开源项目APC和eAccelerator将输出缓存,为大多数PHP网站所使用。此外,还有Zend Server这样的商业产品,通过opcode优化和缓存,提高PHP速度。赵海平选择了另一条道路,将PHP直接转为C++,然后再变成本地机器码。当然,有许多开源项目也是同样的思路,Roadsend和phc编译为C,Quercus编译为Java,而Phalanger编译为.NET。

Hackathon之后8个月,赵海平拿出了原型,足以说明这条路可以走通,编译后的代码的确更快。不久,Iain Proctor和** Yang加入进来。接下来又开发了10个月,在生产服务器上测试了6个月。然后正式上线部署,6个月之后,Facebook 90%以上的Web流量都使用了HipHop。

按赵海平的说法,凭借HipHop,Facebook Web服务器上的CPU使用平均减少了50%,从而大大减少了服务器的需求。项目对Facebook影响巨大。为了让这一改进也惠及社区,他们决定将之开源,希望能够进一步帮助提高更多大型复杂PHP网站的可伸缩性。

HipHop的原理

HipHop将PHP代码转换为高度优化的C++代码,然后再用g++编译器编译。它可以保持语义等效地执行源代码,但为了提高性能,牺牲了一些很少用到的特性,比如eval()。

HipHop开发中的主要困难在于,在PHP和C++这两种很不一样的语言之间怎么实现转换。虽然PHP也可以写一些很巧妙的动态特性,但是大多数PHP代码还是非常简单的。if (…) {…} else {..} 比foo($x) { include $x; } 肯定更常见。这为性能提高提供了机会。HipHop生成的代码尽可能地使用函数和变量的静态绑定。同时,还使用类型推演来选出变量最可能对应的某个类型,从而节省内存。

转换过程分三步:

・静态分析。收集声日月关系和依赖关系等信息。

・类型推演。选择最合适的类型,是C++的标量?还是String, Array, classes, Object或者Variant。

・代码生成。大部分直接将PHP语句和表达式对应为C++的语句和表达式。

在开发过程中,还有一个副产品:HPHPi,是一个实验性的解释器。通过它,不编译PHP源代码也可以运行。它已经用于HipHop自身的调试中。

HipHop在保持了PHP优点的同时,也兼得了C++的性能优势。项目总共有30万行代码,5000多个单元测试。所有这些都将以PHP开源许可证形式发布到GitHub。

观点:
据《纽约时报》网站报道,读写网记者与PHP的创造者Rasmus Lerdorf联系,询问他对Facebook刚刚开源的PHP优化项目HipHop有何看法。Lerdorf在邮件中说,这是一个很酷的项目,肯定会成为某些网站很好的选择。

但是,他接下来说,对于许多Web应用来说,执行速度并不是主要因素。即使将总请求成本中10%的代码的执行速度提高一倍,整体上也只提高了5%。如果每次请求都要访问memcache/PostgreSQL/MySQL 10次,在系统调用上耗费大量时间,难免不要指望HipHop会带来奇迹。

Lerdorf称HipHop代码转换程序为漂亮把戏(nifty trick),并担心会有开发人员将它错误地看成网站性能的某种魔弹。对于新的运行库,Lerdorf说,更愿意大家进行基本的性能分析(profiling),找到有用中成本最高的部分。与其加速系统中较快的部分,不如加速或者去除系统中较慢的部分。

他还说,PHP的执行速度往往不是问题最大的地方,应该好好分析系统的各个方面,找到元凶。工具方面,他推荐用Yahoo的YSlow和Google的Page Speed分析前端的问题,再用Valgrind的Callgrind分析低层的后端性能,用XDebug分析用户空间PHP的性能。此外,他还顺带手指出了读写网前端的性能问题。

当然,文章中也说到,Facebook的网站其他方面可能已经优化得很好,因此HipHop能够带来足够的效率。

总之还是那句话,没有防之四海而皆准的通用银弹,工程上,具体问题具体分析,选择最合适当前环境的工具最为重要。

分类: PHP 标签:

海量小文件存储解决方案

2010年3月11日 没有评论

       Web2.0网站,数据内容以几何级数增长,尤其是那些小文件,几K~几百K不等,数量巨多,传统的文件系统处理起来很是吃力,很多网站在 scaling的过程中都遇到了这样的问题:磁盘IO过高;备份困难;单点问题,容量和读写无法水平扩展,还存在故障的可能。

YouTube也碰到这样的问题,每一个视频有4个缩微图,这样的话缩微图数量是视频数量的四倍,想象一下YouTube有多少视频,看一下他们遇 到的问题:

  • 大量的磁盘寻址,在操作系统层面出现inodes cache和page cache的问题
  • 单个目录文件数限制,尤其是Ext3文件系统,采用目录分级的做法,最新的Linux Kernel2.6优化了Ext3文件系统,单目录能存储的文件数提高了100倍,但是把所有的文件存一个目录不是一个好的方法
  • 高RPS(requests per second每秒请求数),因为一个页面可能要显示60个缩微图
  • 高负载下Apache性能差
  • Apache前面加一层Squid,能抗一会,但负载上来之后,性能下降厉害,由300RPS降到20RPS
  • 尝试lighttpd,但是lighttpd是单线程,多线程的话也有问题,线程之间缓存不能共享
  • 加一台服务器的话需要24小时,因为文件数太多了
  • 存在“冷却”的问题,重启服务器后需要6~10个小时才能缓存好

YouTube的解决方案是Google的BigTable,一般人没戏。(原文参见:http://www.hfadeel.com/Blog/?p=127

Facebook也遇到了同样的问题,他们的方案参见:http://www.dbanotes.net/arch/facebook_photos_arch.html ,他们经历了三个阶段:

  1. NFS共享,挂一个盘阵,APP服务器通过NFS读写
  2. 加一个中间层Cachr:eventHttp + memcached(lighttpd +mod_memcache实现同样的功能),后端还是通过NFS连盘阵
  3. Haystacks,详细的去读这里 (E文)。

对于一般的网站来说,实用的方案有哪些呢?

一、NFS共享

是的,这个有很多问题,但实施成本低,很多公司都在用(我们也在用),在不是那么多文件,不是那么高并发的情况下还是很不错的,设置Hash目录, 不要让一个目录下文件数过多,对于一般的网站来说足够用了。

备份确实是一个问题,如果不是海量的话,根据文件更新时间每天增量备份+周期性的全量备份应该可以。

二、文件存数据库

真有人这么做,手机之家 用MySQL建了256个表来存储超过1T的文件,前端加一个多级缓存(具体未知,也许就是memcached也许还是文件),数据库做数据备份用,他们 用起来觉得还不错。

或者觉得MySQL太重,试试key->value的数据库,比如BDB,Tokyo Cabinet等。

三、分布式文件系统

开源的很多,好看簿 用的是MogileFS ,与memcached师出同门。傲游MFS 来存储用户的收藏夹文件,详细文章参见:分布式文件系统MFS(moosefs)实现存储共享(一) (二) ,据说数百万轻松处理。

分布式文件系统好处是可以均衡读写压力,数据可靠性大大增加,某个数据节点挂了也没事。

还不行?自己DIY一个去吧,豆瓣就这么做的,TokyoCabinet做为底层存储,封装了一个memcached协议接口(与TokyoTyrant 何异?),一致性哈希,应用程序根据哈希规则在node中读写数据:

DoubanFS结构图,版权由charlee所有

引用自:http://blog.csdn.net/starxu85/archive/2009/08/16/4453489.aspx


分类: 架构 标签:

新版 PHP 中 MySQL 连接方式的改变

2010年3月9日 没有评论

PHP5.3 和 PHP6 中,均采用了 mysqlnd 做为 mysql 数据库的默认驱动.

mysqlnd 是在 PHP 源码树中集成, 与原先的 libmysql 不同, mysqlnd 与内核联系更紧密.

官方说内存占用要节省 40% 左右.速度也更快.

顺便提一下.如果在升级到PHP5.3以后,数据库连接时出现

mysql_connect()[2002]tcp://localhost:3306

的错误提示时.

需要将 localhost 改成 127.0.0.1,或者将连接方式由 tcp 改为 socket.

在使用 phpmyadmin 这类工具时,也可以按照上述方式修改 config.inc.php

来看看 mysqlnd 和 libmysql 对比
新版 PHP 中 MySQL 连接方式的改变

原文地址: http://www.21andy.com/blog/20100308/1754.html

分类: PHP 标签:

利用Xapian构建自己的搜索引擎(2)

2010年3月9日 没有评论

四、检索

经过前面几篇的介绍,如果再参考一下Omega的话,估计应该可以顺利创建database和往database里添加document了。有了数据,下一步关心的当然是怎样将它们查出来,在一个IR系统(不单止Xapian)中,检索的方式是多元化的,排序则是多样化的,结果则是人性化的,这就是跟关系数据库相比的最大优势。由于内容较多,因此将检索、排序和取得结果分开讲述,这一篇先讲述如何检索。

IR系统有这么多的好处,因此终端用户对它是有很高期望的,世事万物总不会完美的,于是IR系统有三个评价标准:召回率、准确率与查询效率。三个指标相互矛盾,只有取舍、不能调和,这亦是一个博弈的过程,使用者关心不同的指标,自然会采用不同的观点和做法。拿Web搜索引擎来说,查询效率肯定是摆在第一位的,其次才能考虑准确率和召回率。看字面看上去,大家心里估计对准确率还有个谱,但召回率又如何解释呢?
准确率和召回率

有时候,准确率也称为精度,举个例子,一个数据库有500个文档,其中有50个文档符合定义的问题。系统检索到75个文档,但是只有45个符合定义的问题。

召回率R=45/50=90%

精度P=45/75=60%

本例中,系统检索是比较有效的,召回率为90%。但是结果有很大的噪音,有近一半的检索结果是不相关。通常来说,在不牺牲精度的情况下,获得一个高召回率是很困难的。对于一个检索系统来讲,召回率和精度不可能两全其美:召回率高时,精度低,精度高时,召回率低。对于搜索引擎系统来讲,它可以通过搜索更多更多的结果来查到更多相关结果,从而提高召回率(查全率),但也会导致查到更多不相关结果,从而降低了搜索结果的精度(查准率)。因为没有一个搜索引擎系统能够搜集到所有的WEB网页,所以召回率很难计算。所以一般来说,不会单独的使用召回率或精度,而是在其中一个值固定的基础上,讨论另一个值。如当召回率为60%时的精度值变化情况。因此在召回率与准确率中,Web搜索引擎会更倾向于后者,因为终端用户最想得到的他们要想得到的数据,而不是一堆似是而非的数据。

但是,对于一个传统的图书信息检索系统,情况会大不相同——书籍与文章有良好的关键字索引,包括标题、作者、摘要、正文、收录时间等定义明确的结构化数据,文档集合相对稳定并且规模相对较小,想更深一层,终端用户可能只知道某图书名的其中一两个字,那么如果在较低的召回率下,此用户可能会铩羽而归。

说到这里我们应该差不多知道IR系统在不同的应用场合下是有不同的准确率和召回率作为评价指标的,而准确率和召回率则是由分词策略直接影响的,拿我们最关心的中文分词来说,分词策略一般有以下几种:

l        第一种,默认的单字切分。这种分词策略实现起来最简单,举个例子,有以下句子:“我们在吃饭呢”,则按字切分为[我]、[们]、[在]、[吃]、[饭]、[呢]。按这种方法分词所得到的term是最少的,因为我们所使用的汉字就那么几千个,但随便所索引的数据量的增大,索引文件的增长比例却比下面的几种模型都要大,虽然其召回率是很高的,但精确率却非常低,而且一般情况下性能也是最差的。

l        第二种,二元切分,即以句子中的每两个字都作为一个词语。继续拿“我们在吃饭呢”这个句子作例子,用二元切分法会得到以下词:[我们]、[们在]、[在吃]、[吃饭]、[饭呢]。这种切分方法比第一种要好,精确率提高了,召回率也没降低多少(实际上两者都不高,太中庸了)。

l        第三种:按照词义切分。这种方法要用到词典,常见的有正向最大切分法和逆向最大切分法等。我们再拿“我们在吃饭呢”作为例子。使用正向切分法最终得到词语可能如下:[我们]、[在吃]、[饭]、[呢],而使用逆向最大切分法则可能最终得到以下词语:[我们]、[在]、[吃饭]、[呢]。只要处理好在庞大的词典中查找词语的性能,基于词典的分词结果会挺不错。

l        第四种:基于统计概率切分。 这种方法根据一个概率模型,可以从一个现有的词得出下一个词成立的概率,也以“我们在吃饭呢”这个句子举个可能不恰当的例子,假设已经存在[我们]这个词语,那么根据概率统计模型可以得出[吃饭]这个词语成立的概率。当然,实际应用中的模型要复杂得多,例如著名的隐马尔科夫模型。

在实际的中文分词应用中,一般会将按词典切分和基于统计概率切分综合起来,以便消除歧义,提高精确率。
性能

前面提到,按单字切分的查询性能可能反而是最差的,咋一眼看上去,这种分词方式低精度高召回率是没错,但为什么说它性能不好呢。为了方便解释,我们假设有两万篇文章需要被存储和索引,假设文章里所有内容都是汉字,我们常用的汉字有4000~5000个,那么最理想的情况下平均每个汉字索引了4~5篇文章,可惜实际上有很多汉字的出现频率是非常高的,就拿上面的[我]、[们]、[在]、[吃]、[饭]、[呢]这几个汉字来说,在每篇文章中出现的概率估计至少得有70%~80%。

常见的存储方式是将索引和数据(即文章内容)分开存放,以各种树(红黑树、AVL树或B*树)来存储索引,每个结点除了保存父结点和儿子结点的指针外,一般还会保存其索引的文章的Id(在Xapian里就是DocId),通过这个Id可以很快地找到文章内容。在Xapian中,DocId是以32位无符号整数来表示的,占4个字节,如果“我”字在两万篇文章中出现的概率是50%,那么“我”字这个结点就至少占了4*1000个字节,差不多足足40K!如果某天我们的永久存储体和内存的速度一样快了,这种存储方式问题其实还不大,但由于我们现在普遍使用硬盘/磁带机来保存永久数据,商用的硬盘/磁带机的结构是使用由机械臂控制的磁头来读写盘片来存取数据的,为了减少磁头定位的次数,硬盘/磁带机会设计成按页读取,每页占2 ~2 字节,虽然经过这样的精心设计,但硬盘/磁带机的存取速度还是比主存慢5个数量级左右,这就是I/O是最耗性能的原因,也是我们天天说的“数据库是瓶颈”的原因所在。

很明显,如果按上述的推论,“我”这个结点要占10个以上磁盘页,这太疯狂了。如果通过分词技术将文章切分为多个词语,那么每个词语所索引文章必定减少。前面提到大部分的IR系统或数据库系统的索引都是以B*树的形式来存储的,B*树是一种硬盘I/O性能非常好的数据结构,其特点是一般每个结点的大小和硬盘上每页的大小是一样的,每个结点能存放n个关键字,而每个结点又有n+1个子女,也就是说,在一棵高度为2的B*树中,最多只需要读取2个结点就可以到达目标结点,也就是说控制磁头的机械臂只移动了两次。在这个时候,良好的数据结构的优越性就显示出来了。

当然,这只是纯粹以硬盘/磁带机为中心来讨论,在实际应用中架构会更加良好,而且如果只有两万篇文章,当我们的主内存足够大的时候,甚至可以一次过将所有文章读到内存中以避免进行硬盘I/O操作,只是这样也带来了写入数据时非常缓慢的尴尬。现在的数据库或IR系统的数据文件动辄几个GB,因此怎样最大限度避免进行频繁的硬盘I/O读写还是放在提高性能的第一位的。

不过千万别以为IR系统一切都比关系数据库要好,IR系统的其中一个弱点是插入、修改和删除都相对缓慢,因为是中间要经过多层的工序处理,所以IR系统的首要任务是检索,其次才是存储。
布尔型检索

虽然IR系统会帮我们分词,但有时候我们却想“帮助”IR系统理解我们要搜索什么。例如,我们可能会在百度或Google的搜索栏里输入:“我们 吃饭”来寻找我们感兴趣的关于“我们”和“吃饭”的文章,而不是直接输入“我们吃饭”来搜索文章。这两种的输入得到的结果是完全不同的,因为“我们吃饭”已经成为了Google的IR系统里的其中一个term了。

像“我们 吃饭”这样的输入,其实就是布尔型检索。在Xapian里,则是将多个terms用AND、 OR或AND_NOT连接起来,举个例子:

t1索引了 documents1 2 3 5 8

   t2索引了 documents2 3 6

那么:

    t1 AND t2检索得2 3

    t1 OR t2  检索得1 2 3 5 6 8

    t1 AND_NOT t2检索得1 5 8

    t2 AND_NOT t1检索得6

     在很多系统中,这些documents并没有根据它们之间的相关度来排序的;但在Xapian里,布尔型风格的查询都可以在检索得出documents集合结果后,然后使用概率性的排序。
概率性IR和相关度

      布尔型检索是最常用的,但在IR系统中,其还没能担大旗,因为使用布尔型检索得到的结果并没有按任何机制使其能变得对用户更友好,在这种情况下,用户必须对这个IR系统有充分的了解才能更有效地使用之。虽然如此,但只有纯粹的布尔型检索的IR系统依然活得好好的。

相关度是概率模型里的核心概念,可以将documents的集合按相关度来排列。本质上,当某个document是用户需要的,那么它则是相关的,否则便是不相关的,在理想状态下,检索到的document都是相关的,而没检索到的则是一个都不相关的,这是一个黑与白的概念。不过检索很少是完美的,因此会出现风马牛不相及的情况,于是便用相关度来表示,指两个事物间存在相互联系的百分比,这是一个非常复杂的理论。

Xapian默认的排序模式称为BM25Weight,这是一种将词频和document等元素出现的频率通过一个固定的公式得出排序权重的模式,权重越高则相关度越高,如果不想使用BM25Weight作为排序模式,可以使用BoolWeight,BoolWeight模式里的各种元素的权重都为0。排序会在后续文章里继续讲述。
组合检索

默认情况下,Xapian可以使用任意组合的复杂的布尔型查询表达式来缩小检索的范围,然后将结果按概率性排序(某些布尔型系统只允许将查询表达式限制为某种格式)。

布尔型检索和概率性检索有两种组合的方式:
先用布尔型检索得到所有documents中的某个子集,然后在这个子集中再使用概率性检索。
先进行概率性检索,然后使用布尔型检索过滤查询结果。

这两种方式的结果还是有稍稍区别的。 举个例子 ,在某个database里包含了英文和法文两种documents,“grand”这个词语在这两种语言中都存在(意思都差不多),但在法文中更常见,不过如果使用第一种方式,先用布尔型检索先限定出英文子集,这个词语则会得到更多的权重。

      第一种方法更精确,不过执行效率不高,Xapian特地优化了第二种方法,别以为Xapian真的先进行概率性检索再进行布尔型检索的,实际上Xapian是同时执行这两种操作的。在Xapian内部进行了几种优化,例如如果通过概率性检索能得出结果,Xapian就会取消正在执行的布尔型AND操作。这些优化方法经过评测可以提高几倍的性能,并且在执行多个Terms查询时会有更好的表现。
QueryParser

      在IR系统中,终端用户按某种系统约定的格式输入,这些输入便称为“查询”。然后IR系统将此输入转交给查询器,查询器也是IR系统的一部分,其可以解析“查询“,匹配documents和对结果集进行排序,然后返回结果给终端用户。

      在Xapian中,Query类便起着“查询”的作用,Query类的生成方法有两种,第一种是由QueryParser类解析查询字符串生成,别一种则是创建多个表示不同描述表达式的Query类,然后再将这些Query按需组合起来。

      以下是Xapian::QueryParser支持的语法,其实这些语法跟其它IR系统的语法亦很相似。

l        AND

expression And expression 提取这两个表达式所匹配的documents的交集。

l        OR

expression OR expression 提取这两个表达式匹配的documents的并集。

l        NOT

expression NOT expression 提取只符合左边的表达式的documents集合。

如果FLAG_PURE_NOT标志被设置,那么NOT expression表达式不提取匹配符合此表达式的documents。

l        XOR

expression XOR expression 只提取左表达式和右表达式其中一个表达式匹配的documents,而不提取两者都匹配的documents。

l        组合表达式

可以使用括号将上述布尔操作符括起来从而控制其优先级,例如:(one OR two) AND three。

l        + 和 –

一组标记了+或-操作符的terms只提取匹配所有的+terms,而不匹配所有的-terms。如果terms不标记+或-操作符会有助于documents的排名。

l        NEAR

one NEAR two NEAR three会提取符合这三个关键字的词距在10之间的documents,词距从那里来?在《利用Xapian构建自己的搜索引擎:Document、Term和Value》这篇文章里就曾介绍过可以使用Document类的add_posting方法来添加带词距的terms。

NEAR默认的词距是10,可以使用NEAR/n来设置,例如one NEAR/6 two。

l        ADJ

ADJ跟NEAR很相似,不过ADJ两边的terms是按顺序来比较的。因此one ADJ two ADJ three是表示one与two与three之间的词距都是10。

l        短语搜索

一个短语是被双引号括着的,可以用在文件名或邮件地址等地方。

l        使用字段名的形式

如果database里的terms已经添加了前缀,那么可以使用QueryParser的add_prefix方法来设置前缀map。例如QueryParser.add_prefix("subject", "S")这样便将subject映射到S,如果某个term的值为“S标题”,那么可以使用“subject:标题”这样的表达式来检索结果。这时大家可能会记起Google也支持这种语法,例如在Google的搜索栏里输入“Site:www.wlstock.com 股票”时,只会检索出www.wlstock.com里的关于股票的网页,这功能其实亦实现了Lucene的Field功能。

l        范围搜索

范围搜索在Xapian中是由Xapian::ValueRangeProcessor类来支持的,在Xapian 1.0.0以后才出现。从Xapian::ValueRangeProcessor的名字可以知道,其只能搜索Value的范围,而不能搜索terms的范围。

Xapian::ValueRangeProcessor是一个抽象基类,因此在实际应用中要使用其子类,Xapian提供了三个开箱即用的Xapian::ValueRangeProcessor的子类,分别是StringValueRangeProcessor、DateValueRangeProcessor和NumberValueRangeProcessor,如果觉得这三个类不能满足需求,亦可以继承Xapian::ValueRangeProcessor来创建自己的子类。

当使用Xapian::ValueRangeProcessor的子类时,应该将开始范围和结束范围传给它,如果Xapian::ValueRangeProcessor的子类无法明白传进来的范围,它会返回Xapian::BAD_VALUENO。

下面仅以StringValueRangeProcessor举例,当database里将用户名保存在Number为4的Value中(Value是通过数字来标识的,详细请看《利用Xapian构建自己的搜索引擎:Document、Term和Value》),那么可以这样组织查询表达式:mars asimov..bradbury,只是这样当然还不够,还需要创建一个StringValueRangeProcessor

Xapian::QueryParser qp;

Xapian::StringValueRangeProcessor author_proc(4);

qp.add_valuerangeprocessor(&author_proc);

当QueryParser解析查询表达式时会使用OP_VALUE_RANGE标志,因此QueryParser生成的query会返回以下描述:

Xapian::Query(mars:(pos=1) FILTER (VALUE_RANGE 4 asimov bradbury)

(VALUE_RANGE 4 asimov Bradbury)这个子表达式使用仅仅匹配Number为4的Value的值是>= asimov 和<= bradbury(使用字符串比较)。

值范围搜索并不复杂,更多的介绍请看http://www.xapian.org/docs/valueranges.html

l        别名

QueryParser亦支持别名检索,使用这样的语法:~term。如何添加别名,后面会介绍。

l        通配符

QueryParser支持以“*”结尾的通配符,因此“wildc*”可以匹配“wildcard”、“wildcarded” 、“wildcards”、“wildcat”、“wildcats”。不过这功能默认是关闭的,可以将Xapian::QueryParser::FLAG_WILDCARD

作为标志传到Xapian::QueryParser::parse_query(query_string, flags)来开启按以下步骤来开启。
Query

      如果不想使用字符串形式的查询表达式,可以用下面这些操作符将多个Query组合起来:
OP_AND
等同于QueryParser所支持的AND

OP_OR
等同于QueryParser所支持的OR

OP_AND_NOT
等同于QueryParser所支持的AND_NOT

OP_XOR
等同于QueryParser所支持的XOR

OP_AND_MAYBE
只返回左边子表达式匹配的documents,不过两边的表达式所匹配的documents都加入权重计算。

OP_FILTER
作用跟AND相似,不过仅仅左边的表达式匹配的documents才加入权重计算。

OP_NEAR
等同于QueryParser所支持的NEAR

OP_PHRASE
等同于QueryParser所支持的ADJ

OP_VALUE_RANGE
等同于QueryParser所支持的范围搜索

OP_SCALE_WEIGHT
给子表达式指定权重,如果权重为0,则此表达式为纯布尔型查询

OP_ELITE_SET
作用跟OP_OR 很相似,不过有时候性能比OP_OR 要好。这里有详细的解释:http://trac.xapian.org/wiki/FAQ/EliteSet

OP_VALUE_GE
返回大于或等于给定的document value

OP_VALUE_LE
返回小于或等于给定的document value

l        如何创建一个只包含一个term的Query

可以使用默认的构造函数:Xapian::Query query(term);

亦可以使用多参数的构造函数:

Xapian::Query(const string & tname_,

       Xapian::termcount wqf_ = 1,

       Xapian::termpos term_pos_ = 0)   其中wqf的全称是Within Query Frequency,可以指定此term在query中的权重。如果整个查询只包含了一个term,这参数用处不大;但当组合查询时,威力便显出来了,因为可以便取得的结果集跟这个term是更相关的。

      而term_pos是指term在query中的位置,同样如果整个查询中只包含了一个term则用处不大,因此一般用在词组搜索中。

l        将多个Query组合起来查询

通过上面所说的Query操作符将Query组合起来,这时要用到Xapian::Query的另一个构造函数:

Xapian::Query(Xapian::Query::op op_,

       const Xapian::Query & left,

   const Xapian::Query & right)

l        概率性查询

一个普通的概率性查询其实是将terms用Xapian::Query::OP_OR连接起来。例如:

Xapian::Query query("regulation"));

   query = Xapian::Query(Xapian::Query::OP_OR, query, Xapian::Query("import"));

   query = Xapian::Query(Xapian::Query::OP_OR, query, Xapian::Query("export"));

   query = Xapian::Query(Xapian::Query::OP_OR, query, Xapian::Query("canned"));

query = Xapian::Query(Xapian::Query::OP_OR, query, Xapian::Query("fish"));

不过这样的风格太臃肿了,可以用下面这种清爽一点的风格:

   vector <string> terms;

   terms.push_back("regulation");

   terms.push_back("import");

   terms.push_back("export");

   terms.push_back("canned");

   terms.push_back("fish");
   Xapian::Query query(Xapian::Query::OP_OR, terms.begin(), terms.end());

l        布尔型查询

假设有这样的布尔型查询表达式:

   (‘EEC’ – ‘France’) and (‘1989’ or ‘1991’ or ‘1992’) and ‘Corporate Law’

This could be built up as bquery like this, 那么则用Query来表示则如下

   Xapian::Query bquery1(Xapian::Query::OP_AND_NOT, "EEC", "France");

   Xapian::Query bquery2("1989");

   bquery2 = Xapian::Query(Xapian::Query::OP_OR, bquery2, "1991");

   bquery2 = Xapian::Query(Xapian::Query::OP_OR, bquery2, "1992");

   Xapian::Query bquery3("Corporate Law");

    Xapian::Query bquery(Xapian::Query::OP_AND, bquery1, Xapian::Query(Xapian::Query::OP_AND(bquery2, bquery3)));

还可以将上面创建的bquery对象附加到另一个概率性查询作为布尔型过滤器用来过滤结果集:

query = Xapian::Query(Xapian::Query::OP_FILTER, query, bquery);

l        + 和 – 操作符

例如有这样的查询表达式:regulation import export +canned +fish –japan

转化为Query则是如下:

vector <string> plus_terms;

   vector <string> minus_terms;

   vector <string> normal_terms;

   plus_terms.push_back("canned");

   plus_terms.push_back("fish");

   minus_terms.push_back("japan");

   normal_terms.push_back("regulation");

   normal_terms.push_back("import");

   normal_terms.push_back("export");

   Xapian::Query query(Xapian::Query::OP_AND_MAYBE,

       Xapian::Query(Xapian::Query::OP_AND, plus_terms.begin(), plus_terms.end());

   Xapian::Query(Xapian::Query::OP_OR, normal_terms.begin(), normal_terms.end()));

   query = Xapian::Query(Xapian::Query::OP_AND_NOT,

       query,

       Xapian::Query(Xapian::Query::OP_OR, minus_terms.begin(), minus_terms.end()));
实战

当使用QueryParser类或Query类创建了Query对象后,只需要实例化一个查询器就可以使用这些Query对象了。例:

Xapian::Database db("Index");

Enquire enquire(db);

enquire.set_query(query);

当然,要想取得结果集、对结果集排序或扩展查询还需要更多的功夫,会在下一篇里继续讲述。

分类: 架构 标签:

利用Xapian构建自己的搜索引擎

2010年3月9日 1 条评论

一、简介

Xapian与开源

       Xapian的官方网站是http://www.xapian.org,这是一个非常优秀的开源搜索引擎项目,搜索引擎其实只是一个通俗的说法,正式的说法其实是IR(Information Retrieval)系统。Xapian的License是GPL,这意味着允许使用者自由地修改其源码并发布之。Xapian的中文资料非常少,可以说现在互联网上连一篇完整详细的Xapian中文介绍文档,更别说中文API文档了。其实,Xapian的英文资料也不多,除了官方网站上的Docs和Wiki外,还有一些网站上的邮件列表,在这方面跟Lucene没得比。当然,Lucene现在已经发展到2.x版本了,而Xapian的最新版本才1.012,国外开源项目一般对版本号控制得比较严格,一个项目一般到了1.x才算稳定和成熟的。
Xapian可以运行在那些平台?

      Xapian由C++编写,但可以绑定到Perl, Python, PHP, Java, Tcl, C# 和Ruby甚至更多的语言,Xapian可以说是STL编程的典范,在这里您可以找到熟悉的引用计数型智能指针、容器和迭代器,甚至连命名也跟STL相似,相信一定能引起喜好C++和STL的你的共鸣(实际上,很少C++程序员完全不使用STL)。由于Xapian使用的是STL和C运行时库,因此具有高度可移值性,官方说法是可以运行在Linux、 Mac OS X、 FreeBSD、 NetBSD、 OpenBSD、Solaris,、HP-UX,、Tru64和IRIX,,甚至其它的Unix平台,在Microsoft Windows上也跑得很好。当然,并不能像Java那样“一次编译,到处可以运行”,当移植到其它平台时,一般来说是需要重新编译的。至于如何在Windows32位系统下编译Xapian,请查阅我以前写的文章《nmake在windows平台下编译xapian》。
Xapian的特性

依官方的说法,Xapian是一个允许开发人员轻易地添加高级索引和搜索功能到他们的应用系统的高度可修改的工具,它在支持概率论检索模型的同时也支持布尔型操作查询集。

从功能特性上来说。Xapian和Lucene有点相似,两者都具有Term、Value(在Lucene里称为SortField)、Posting、Position和Document,不过Xapian没有Field的概念,这直接导致Xapian在使用上比Lucene麻烦了那么一点。但这完全不是问题,通过一些小技巧,完全可以自己在Xapian中实现Filed的概念。在Lucene里还有一个叫Payload的元素,即词条 (Term) 的元数据或称载荷。举一个例子,“回家吃饭吧”和“快回家吃饭”这两个句子都带有“吃饭”这个词语,但在检索的时候怎样才能将语气表达出来呢?虽然可以添加Term来解决这个问题,但由于Term的索引信息和存储信息是分开放的,相对来说I/O性能较差,Payload就是应这个问题而生的,因为Payload信息是直接放在索引里的。由于对Xapian的研究还不是很深,Xapian里是否有类似Payload这个概念,还需要继续研究。
Xapian与搜索

      搜索的目的是将结果数据展现给终端用户,搜索引擎与普通的数据库查询最大的区别就在于查询。Xapian提供了多种的查询机制。
概率性搜索排名 – 重要的词语会比不那么重要的词语得到更多的权重,因此与权重高的词语关联的Documents会排到结果列表的更前面。
相关度反馈 – 通过给予一个或多个Documents, Xapian可以显示最相关的Terms以便扩展一个Query,及显示最相关的Documents。
词组和邻近搜索 — 用户可以搜索一个精确短语或指定数组的词组。
全方位的布尔型搜索器,例如 ("stock NOT market", etc)。
支持提取搜索关键字的词干,例如当搜索“football”的时候,当Documents中含有"footballs" 或"footballer"的时候也被认作符合。这有助于找到相关结果,否则可能错过之。词干提取器现在支持Danish、Dutch、 English、 Finnish、 French、 German、 Hungarian、Italian、 Norwegian、Portuguese、Romanian、 Russian、Spanish、Swedish和Turkish。
支持通配符查询,例如“xap*”。
支持别名查询,打个比方,C++会自动转为CPlusPlus,C#则自动转为CSharp。
Xapian支持拼写纠正,例如xapian会被纠正为xapain,当然这必须基于词组已经被索引了。这特性跟Google提供的“你是不是想搜索xxx”有点相似。
Xapian的存储系统

      Xapian现在的版本默认是使用flint作为存储系统,flint是以块的形式来存储,默认每块是8K,理论上每一个文件最大可以达到2048GB。当然,在旧式的文件系统,例如FAT/FAT32是不可能实现的。熟悉Windows内存管理机制的朋友一定知道使用Windows32位系统每个进程的总虚拟地址空间只有4GB,而用户模式连2GB都不够(Windows2003可以将用户模式扩展到3GB左右),因此应用程序不可能一次过将整个Database文件读取到内存中,通常的做法是使用内存映射文件,先预订地址空间,在真正使用的时候才调拨内存,而内存分页粒度是4k,也就是说内存中每一页是4k,而在IA64系统中,内存分页粒度是8k。在内存中,除了页外,还有区块,X86和IA64的内存区块的粒度都是64k。Xapian这样存储数据估计是为了在各个平台上都能实现数据对齐,数据对齐对于cpu运算寻址是非常重要的,而8和64都是4的倍数,因此大胆猜想Xapian以8k作为存储系统的默认块大小是为了在性能和兼容性中取得最平衡和最优值。

Xapian使用unsigned 32-bit ints作为Documents的id值,因此在每个Xapian的Database中,最多可容纳40亿个Documents。而Xapian的Terms和Documents都是使用B-树来存储的,其实很多数据库系统(这里所指的是关系数据库)的索引都是用B-树或B+树来存储的,具有增删改查比较方便迅速的特点,缺点则是如果索引被删除后的空间不能重复利用,为了提高性能,通常要经常重建索引。
Xapian的性能

      搜索引擎的性能是用户非常关心的一部分,Xapian的性能如何?官方的原话如下:The short answer is "very well" – a previous version of the software powered BrightStation’s Webtop search engine, which offered a search over around 500 million web pages (around 1.5 terabytes of database files). Searches took less than a second.。在5亿个网页共1.5TB大小的文件中,搜索只需要小于一秒就完事了。当然,这跟运行的平台和机器是密切相关,在我们自己构建好Xapian搜索引擎应用后,我们也可以测测具体的速度。
Xapian的绝佳范例

Xapian的官方网站上有一个绝佳的使用范例,这个称为Omega的项目甚至可以开箱即用作为一个CGI应用程序。Omega附带了Omindex和ScriptIndex这两个索引生成工具,可以将硬盘上的html,pdf,图片甚至视频影片索引起来并生成Database,通过操作这些由Omindex或ScriptIndex生成的Database,Omega提供了搜索这些文件的功能。
关于《利用Xapian构建自己的搜索引擎》系列

在使用Xapian的过程中,我一般是查阅http://www.xapian.org/docs/上的Doc、API Doc和Wiki,遇到困难时则查阅Omega的源代码并互相印证之。实在没办法的时候只能从Google上找找一些网站的邮件列表,可以说是磕磕碰碰地将Xapian的大部分功能玩了一遍。有一些专有名词我虽然知道大概意思,但无法准确地翻译出来,因此《利用Xapian构建自己的搜索引擎》这一系列的内容可能会错漏百出。不过如果这一系列文章可以引起大家对Xapian的兴趣,它所得到的批评才是它最大的价值。

在后续文章中,我会从Xapian的Database开始一步一步构建搜索引擎应用,并配以自己的理解,请大家一起讨论。

由于工作原因,一般只有晚上才有时间写文章,在写的过程中还要不断印证自己的想法是否正确,免得经常作无谓猜测而导致欠缺严谨性,因此不能保证每天都能更新,请大家见谅。

二、Database

     在Xapian1.0之前,是使用quartz作为database文件格式的,不过自从1.0之后,便改用Flint作为database的文件格式了。有时候,我们会将database称为“索引”,在Xapian中,索引通常比被索引的documents还要多,这表示Xapian做一个信息检索系统比做一个信息存储系统更适合。
  Database的存储结构

Xapian的database是所有用于检索的信息表的集合,以下的表是必需的:

l        posting list table 保存了被每一个term索引的document,实际上保存的应该是document在database中的Id,此Id是唯一的。

l        record table 保存了每一个document所关联的data,data不能通过query检索,只能通过document来获取。

l        term list table 保存了索引每个document的所有的term。

以下的表是可选的,即当有以下的类型的数据需要被存储的时候才会出现(在1.0.1以前,position和value表就算是没有数据的时候也会被创建,而spelling和synonym表是1.0.2后才出现的)。

l        position list table 保存了每一个Term出现在每一个document中的位置

l        value table 保存了每一个document的values,values是用作保存、排序或其它作用的。

l        spelling table 保存了拼写纠正的数据。

l        synonym table 保存术语的字典,例如NBA、C#或C++等。

以上的每一个集合是保存在独立的文件中,以便允许管理员查看其中的数据。刚刚说了,有一些表不是必需的,例如当您不需要词组搜索的时候,没必要存储任何的postionlist信息。

如果你看过Xapian的database,你会发现以上的每一个表其实是使用了2到3个文件的,如果您正在使用“flint”作为database的存储格式,那么termlist表会被存储为以下三个文件“termlist.baseA”、“termlist.baseB”、“termlist.dB”。在这些文件中,其实只有”.db”文件存储了真实的数据,“.baseA”和“baseB”文件是用作跟踪如果于“.dB”文件中查找数据。通常只会出现一个“.baseA”文件和一个“.baseB”文件。

在前一篇《利用Xapian构建自己的搜索引擎:简介》中提到过,Xapian现在的版本默认是使用flint作为存储系统,“.dB”文件是以块的形式来存储,默认每块是8K,第一块是用作信息头,如果使用UltraEdit等二进制查看工具,会发现所有“.dB”文件的前三个字节都是0x00。因此,当“.dB”中仅有一条数据的时候整个文件也会有16KB时,切莫大惊小怪。

改变“.dB”文件的默认块大小会导致性能变化,但结果很难说是好是坏,因为这是跟所承载的硬件平台与操作系统平台有关的。一般来说,B树的分支因子(即每个结点能容纳的关键字的数量)越大,B树的查找性能就越强;但由于通常情况下,B树的结点都是存储在永久存储系统(例如硬盘/磁带)中,每次访问某个结点都会将整个结点由永久存储系统读入到内存中,这是一个博弈的过程:假设一棵数据量很大的B树,将B树的分支因子设到很大,这棵B树会长得很矮,从理论上来说查找性能可能很高。但这样就带来了一个弊端,每个结点所占的内存非常多,如果在一个并发访问量很大的IR系统中采用这种方式的话所使用的内存必定是非常可观的。因此在调整“.dB”文件的默认块大小的时候一定要充分考虑cpu体系和操作系统平台,以便调整到最佳性能。
原子性修改

Xapian能保证对database的所有修改都是原子性的,这意味着:

l        从一个独立的进程(或一个独立的database对象在同一个进程)角度来看,在读取数据库的时候,直到修改成功提交,所有对数据库的修改都是不可见的。

l        Database在硬盘中的状态始终是保持一致的。

l        如果在修改的过程中系统发生中断,只要硬件不发生故障(硬盘损坏),就算电源被切断,database应该总是被还原到有效的状态。

提交一个修改需要几次的系统调用,以便使所有缓存的修改能刷新到硬盘上,这样能确保就算系统在任何时候发生错误,database也能处于一致的状态。当然,这样相对于说会慢了一点(因为系统已经准备好往硬盘上写数据了),因此将几次的修改组合在一起往硬盘上写会有一定的性能提升。

多个修改操作可以显式地组合在一个事务中,如果一个应用程序不显式地使用事务来保护修改操作,Xapian会将这些修改组合在一个事务中,然后批量进行修改。请注意,Xapian现在暂时还不能跨database进行事务操作。

如果要想迅速地生成非常大的database,请使用“DANGEROUS”关键字搜索Xapian的邮件列表,其提供了可以重新编译Xapian而不采用原子性修改的方法,这功能已经不再整合在xapian的标准版本中了。
Single writer, multiple reader

Xapian实现了“单写多读”的模式,这意味着任何时候,同一时刻只允许一个对象可以修改database,但允许多个对象可以读取database。

在*nix系统下,Xapian使用“lock-files”强制约束来实现此模式,在一个flint database中,每一个Xapian的database目录包含了一个名为“flintlock”的文件以作锁定用途。此文件总在存在于database的目录里,当database被打开用作写入的时候,此文件会被fcntl()方法锁定。每一个WritableDatabase打开的时候,都会产生一个子进程以便进行锁定操作。如果某个database写入器(一般是指WritableDatabase)还没有机会执行释放锁的清除操作便退出了(例如这个WritableDatabase所在的应用程被杀死了),fcntl()产生的锁会自动被操作系统释放。

在Microsoft Windows下,使用的是另一种锁定的技术,此技术不需要产生了子进程来进行锁定操作,但同理,当写入器退出时,操作系统依然会自动释放锁。熟悉Windows机制的朋友知道,Windows是使用文件句柄来操作文件的,而文件句柄是属于内核资源的一种,在任何情况下,Windows都能保证应用程序在退出时能释放所有的资源。
网络文件系统

Xapian现在可以工作在一个网络文件系统中,但存在着大量的潜在问题。因此建议在部署前要大量地在特定的网络中测试。

请注意,Xapian是非常依赖I/O操作的,除非处于一个性能非常优秀的网络,在一个网络文件系统中进行操作会相对的慢一点。

Xapian需要可以在database的目录里创建一个lock file,在某些网络文件系统中(例如NFS),这需要一个锁定的守护进程在运行,也就是上面所提到的子进程。

创建database

      说了一堆的理论,下面我们来实战创建一个database。Xapian里的所有类都处于Xapian这个命名空间里,Xapian::Database是所有Database的基类,实际上,它只有一个子类,那就是WritableDatabase。从面向对象的角度来看,这两个类设计得非常好,Xapian::Database拥有大部分只读或内存操作的方法,而Xapian::WritableDatabase则拥有事务操作,刷新数据到硬盘等方法。

      有几种创建Database的方法:

l        Flint 如果你是使用远程后端(指网络文件系统),请使用Xapian::Flint::open()方法来创建database。使用此方法你能得到更多的控制,例如创建只读的database,或创建可写的database。

l        Auto auto并不是一种database格式,你可以创建一个“database存根”文件,此文件能列出一到多个database的路径,这些路径可以作为Xapian::Database的构造函数的参数,从而被自动检测是哪种类型的database。还有,如果将一个文件路径名称而非目录名称作为参数传入到Xapian::Database的构造函数中,Xapian::Database会认为你传入了一个“database存根”文件;当然,你也可以使用Xapian::Auto::open_stub()来显式打开一个存根文件。上面说的可能有点绕口,“database存根文件”的格式是每个database一行,例如:

remote localhost:23876
flint /var/spool/xapian/webindex

这下该明白了。

l        Inmemory 还可以创建内存databse,这种类型的database是保存在内存中。请注意,通过Xapian::InMemory::open()返回的类型是WritableDatabase,这意味着这是一个可刷新到硬盘上的database,它最初是为测试之用,但在建立临时的小数据库也可能是有用的。

实际上,创建一个database还有更通用的方法,例如通过将一个database所在的完整目录作为一个字符串传入到Xapian::Database的构造函数中实例化一个对象后,即可得到一个只读的database。而Xapian::WritableDatabase则复杂一点,除了要传入database的路径外,还需要设定如何打开database。有以下几个参数:

l        Xapian::DB_CREATE_OR_OPEN 打开以便读写,如果不存在则创建。

l        Xapian::DB_CREATE 总是创建新的database,如果存在则失败。

l        Xapian::DB_CREATE_OR_OVERWRITE 如果database存在的话则覆盖之,如果不存在则创建。

l        Xapian::DB_OPEN 打开以便读写,如果不存在则失败。

成功打开一个datababse是Xapian所有后续操作如检索,写入的基础。你甚至可以将通过add_database()方法则多个database组合在一起访问;如果想将database刷新到硬盘中,则执行flush()方法则可。最后,如果不想使用database了,将database对象销毁即可。
小结

在这一章里,似乎并没有多少具体的操作,但database是Xapian的存储系统,在Xapian所有操作的基础,只有清楚明白了Xapian的存储方式才能更好更高效地构造自己的搜索引擎。同时,如果您之前并没有对大型的文件存储系统有所了解的话,这篇文件可以多多少少带给您一些启示。在下一章里,我会继续介绍Document和Term等Xapian的组成部分。

三、Document、Term和Value

在上一篇《利用Xapian构建自己的搜索引擎:Database》里指出database是Xapian的基础,而这一篇里讲到的documents、terms和values则是索引和查询的必要组成部分。
Documents 、terms and posting

在信息检索(IR)中,我们企图要获取的项称之为“document”,每一个document是被一个terms集合所描述的。 “document”和“term”这两个词汇是IR中的术语,它们是来自“图书馆管理学”的。通常一个document认为是一块文本,. Usually a document is thought of as a piece of text, most likely in a machine readable form, 而一个term则是一个词语或短语以用作描述document的,在document中大多数会存在着多个term,例如某个document是跟口腔卫生相关的,那么可能会存在着以下的terms:“tooth”、“teeth”、“toothbrush”、“decay”、 “cavity”、“plaque”或“diet”等等。

如果在一个IR系统中,存在一个名为D的document,此document被一个名为t的term所描述,那么t被认为索引了D,可以用以下式子表示:t->D。在实际应用的一个IR系统中通常是多个documents,如D1, D2, D3 …组成的集合,且有多个term,如t1, t2, t3 …组成的集合,从而有以下关系:ti -> Dj。

如果某个特定的term索引了某个特定的document,那么称之为posting,说白了posting就是带position信息的term,在相关度检索中可能有一定的用途的。

给定一个名为D的document,存在着一个terms列表索引着它,我们称之为D的term list。

给定一个名为t的term,它索引着一个documents列表,这称之为t的posting list(使用“Document list”可能会在叫法上更一致,但听起来过于空泛)。

在一个存在于计算机的IR系统中,terms是存储于索引文件中的。term可以用作有效地查找它的posting list,在posting list里,每一个document带有一个很短的标识符,就是document id。简单来说,一个posting list可以被认为是一个由document ids组成的集合,而term list则是一个字符串组成的集合。在某些IR系统的内部是使用数字来表示term的,因此在这些系统中,term list则是数字组成的集合,而Xapian则不是这样,它使用原汁原味的term,而使用前缀来压缩存储空间。

Terms不一定是要是document中出现的词语,通常它们会被转换为小写,而且往往它们被词干提取算法处理过,因此通过一个值为“connect”的term可能会检索出一系列的词语,例如“connect”、“connects”、“connection”或“connected”等,而一个词语也可能产生多个的terms,例如你会将提取出的词干和未提取的词语都索引起来。当然,这可能只适用于英语、法语或拉丁语等欧美系列的语言,而中文的分词则有很大的区别,总的来说,欧美语系的语言分词与中文分词有以下的区别:

l        拿英语来说,通常情况下英语的每一个词语之间是用空格来隔开的,而中文则不然,甚至可以极端到整篇文章都不出现空格或标点符号。

l        像上面提到的,“connect”、“connects”、“connection”或“connected”分别的意思“动词性质的连接”、“动词性质的第三人称的连接”、“名称性质的连接”或“连接的过去式”,但在中文里,用“连接”就可以表示全部了,几乎不需要词干提取。这意味着英语的各种词性大部分是有章可循的,而中文的词性则是天马行空的。

l        第二点只是中文分词非常困难的一个缩影,要完全正确地标识出某个句子的语意是很困难的,例如“中华人民共和国成立了”这个句子,可以分出“中华”、“华人”、“人民”、“共和国”、“成立”等词语,不过其中“华人”跟这个句子其实关系不大。咋一眼看上去很简单,但机器那有这么容易懂这其中的奥妙呢?

Values

Values是附加在document上一种元数据,每一个document可以有多个values,这些values通过不同的数字来标识。Values被设计成在匹配过程中快速地访问,它们可以用作排序、排队多余重复的document和范围检索等用途。虽然values并没有长度限制,但最好让它们尽可能短,如果你仅仅是想存储某个字段以便作为结果显示,那么建议您最好将它们保存在document的data中。
Document data

      每一个Document只有一个data,可以是任意类型格式的数据,当然在存储的时候请先转换为字符串。这听上去可能有点古怪,实情是这样的:如果要存储的数据是文本格式,则可以直接存储;如果要存储的数据是各种的对象,请先序列化成二进制流再保存,而在读取的时候反序列化读取。
UTF-8与Unicode

      Xapian里的所有东西是用UTF-8来保存的,UTF-8是Unicode的一种实现。现在很多人用VC为了方便是将编码设成“未设置”或“多字节”的,也就是说用的是系统内码(GB2312/GBK/ GB18030),这样的话则将数据保存到Xapian前要先转码为UTF-8,而从Xapian里读出的数据则要转码为GB2312/GBK/ GB18030才能正确显示,这里推荐用iconv,这是一个非常方便的库。
分词

     很多文章都说现在的中文分词已经很成熟的,但据实际考察,google或百度等大公司的分词引擎都是自己开发或有专门的公司开发的,的确已经算比较成熟。但市场上提供免费甚至开源的分词引擎不多,中科院研发的ictclas30分词精确度和分词速度都非常不错,而且还有词性标注和自定义添加词的功能,可惜不开源。另外比较受欢迎的还有libmmseg和SCWS,因此都是开源的,不过经测试libmmseg的分词精度似乎不高,而SCWS由于使用了大量的递归,在生成词库的时候经常导致栈溢出(我是用vc2005编译的),需要自己将递归修改为循环,从演示的情况来看,SCWS的分词精度来算可以。
实战

由于Xapian并不像Lucene那样有Field的概念,因此一般采用以大写字母作为Term和posting的前缀,但单个字母的前缀对程序员太不友好了,所以一般的做法是自定义一个用户前缀到term前缀的映射,如Title=>T,而Xapian的QueryParser也支持这种映射,QueryParser是查询解释器,能将一段字符串解释为Xapian的Query,后面会陆续提到。

添加document的例子:

Xapian::Document doc;

   doc.add_term("K你好");

   doc.add_term("K那里");

   //posting是带position的term

   doc.add_posting("K吃饭", 14);

   doc.add_posting("K玩耍", 8);

   /*

   这里最好先用一个map<string, int>放置value的名称和索引的配对

   这里使用起来像Lucene的SortField一样了。

   */

   doc.add_value(1, "1");

   doc.set_data("你好啊,在那里玩耍呢?还没吃饭吗?");

   //创建一个可写的db

   Xapian::WritableDatabase db("c:\\db");

   //将document加入到db中,返回document的id,此id在db中是唯一的

   Xapian::docid id = db.add_document(doc);

   //刷新到硬盘中

   db.flush();

获取document信息的例子:

//获取

   Xapian::Document doc = db.get_document(id);

   string v = doc.get_value(1);

   printf(v);//输出

   string data = doc.get_data();

   printf(data);//输出"你好啊,在那里玩耍呢?还没吃饭吗?"

   for (Xapian::TermIterator iter = doc.termlist_begin(); iter != doc.termlist_end(); ++iter)

   {

       printf(*iter);//依次输出term和posting

   }

上面的两个例子比较简单,如果要想更深入请查阅Omega的代码,里面有更复杂的应用。值得一提的Xapian里有一个TermGenerator,可以更方便地索引数据,不过这个类有两个不知道算不算缺点的特点:首先是依赖Stem,对于中文来说除非自己实现了一个Stem,否则TermGenerator用处不大;另外TermGenerator会自动将生成的term或posting添加“Z”前缀。

在这里要提一下一个名为“Xapwrap”的东东,这是某个外国人用python写的一个封装Xapian的类库,里面某些思想还是不错的,只可惜只兼容Xapian 1.x之前的版本。我自己封装的类有一部分就是参考Xapwrap的。

下面是一段我正在用的代码:

//CXapianDocument是封装过的Xapian::Document

void  doSegment(CXapianDocument& document, const char* lpszInput, string strUserPrefix)

{

   //先分词,这里使用的是中科院的分词引擎

   int nCount = ICTCLAS_GetParagraphProcessAWordCount(lpszInput);

   result_t *result =(result_t*)malloc(sizeof(result_t)*nCount);

   //获取分词结果

   ICTCLAS_ParagraphProcessAW(nCount,result);

   string termPrefix;

   //通过用户前缀取得term前缀,这是我自定义的一个宏

   GetTermPrefixFromMap(this->m_userPrefixToTermPrefixMap, strUserPrefix, termPrefix)

   for (int i=0; i<nCount; i++)

   {

       //忽略标点符号,标点符号的词性标注为w开头的

       if(result[i].sPOS[0] == ‘w’)

       {

           continue;

       }

       char buf[100];

       memset(buf, 0, 100);

       int index = result[i].start;

       memcpy(buf,(void *)(lpszInput+index), result[i].length);

       //添加posting

       document.AppendPosting(termPrefix, buf, result[i].start);

   }
   free(result);
}

原文引用:http://visualcatsharp.javaeye.com/blog/392326

分类: 架构 标签:

mogileFS分布式文件存储解决方案

2010年3月9日 没有评论

      mogileFS是一个分布式文件存储的解决方案,他由Six Apart开发下面列出了他的一些特性(由mogileFS页面http://www.danga.com/mogilefs/ 介绍翻译而来)
         1. 应用层——不需要特殊的核心组件
       2. 无单点失败——MogileFS安装的三个组件(存储节点、跟踪器、跟踪用的数据库),均可运行在多个 机器上,因此没有单点失败。(你也可以将跟踪器和存储节点运行在同一台机器上,这样你就没有必要用4台机器)推荐至少两台机器。
       3. 自动的文件复制——基于不同的文件“分类”,文件可以被自动的复制到多个有足够存储空间的存储节点上,这样可以满足这个“类别”的最少复制要求。比如你有一 个图片网站,你可以设置原始的JPEG图片需要复制 至少三份,但实际只有1or2份拷贝,如果丢失了数据,那么Mogile可以重新建立遗失的拷贝数。用这种办法,MogileFS(不做RAID)可以节 约 磁盘,否则你将存储同样的拷贝多份,完全没有必要。
        4. “比RAID好多了”——在一个非存储区域网络的RAID(non-SAN RAID)的建立中,磁盘是冗余的,但主机不是,如果你整个机器坏了,那么文件也将不能访问。 MogileFS在不同的机器之间进行文件复制,因此文件始终是可用的。
      5. 传输中立,无特殊协议——MogileFS客户端可以通过NFS或HTTP来和MogileFS的存储节点来通信,但首先需要告知跟踪器一下。
        6. 简单的命名空间——文件通过一个给定的key来确定,是一个全局的命名空间。你可以自己生成多个命名空间,只要你愿意,不过这样可能在同一MogileFS中会造成key冲突。
      7. 不用共享任何东西——MogileFS不需要依靠昂贵的SAN来共享磁盘,每个机器只用维护好自己的磁盘。
      8. 不需要RAID——在MogileFS中的磁盘可以是做了RAID的也可以是没有,如果是为了安全性着想的话RAID没有必要买了,因为MogileFS已经提供了。
      9. 不会碰到文件系统本身的不可知情况——在MogileFS中的存储节点的磁盘可以被格式化成多种格式(ext3,reiserFS等等)。MogilesFS会做自己内部目录的哈希,所以它不会碰到文件系统本身的一些限制,比如一个目录中的最大文件数。你可以放心的使用。

Mogilefs 的网站地址(http:// www.danga.com /mogilefs )

php 扩展 的地址http://www.capoune.net/mogilefs/ )提供了一个php扩展用来在php中使用mogileFS。
这儿也有一个地址,svn的源码库 http://svn.usrportage.de/php-mogilefs/trunk/

mogileFS 安装步骤( http://durrett.net/mogilefs_setup.html
      mogileFS 使用perl 编写的,在安装前你应该先安装好perl。同时mogileFS也需要一个数据库用来保存文件数据的跟踪信息(目前好像可以使用MySQL推荐 , SQLite,Oracle,Postsql)。

这儿有一个兄弟的中文安装学习笔记 mogileFS学习

      mogileFS 适合于静态存储,就是那种一次保存,多次读取型的资源,比如以html方式静态化处理的动态文件,图片文件,其他只提供下载的文件等。

二、mogileFS的工作方式

下面简要阐述 MogileFS 是怎么工作的.

mogileFS由如下一些部分构成:
Application: 想要 保存/加载 文件的应用
Tracker (the mogilefsd process): 基于事件的(event-based) 父 进程/消息 总线来管理所有来之于客户端应用的交互(requesting operations to be performed), 包括将请求负载平衡到 “query workers” 中,让mogilefsd的子进程去处理. 你可以在不同的机器上运行两个Tracker, 为了高可用性, 或使用更多的Tracker为了负载平衡(你需要运行多于两个的Tracker). mogilefsd的子进程有:
Replication — 个机器间复制文件
Deletion — 从命名空间删除是立即的,从文件系统删除是异步的
Query — 响应客户端的请求
Reaper — 在磁盘失败后将文件复制请求重新放到队列中
Monitor — 监测主机和设配的健康度和状态

Database — 数据库用来存放MogileFS的元数据 (命名空间, 和文件在哪里). 这应该设置一个高可用性(HA)的环境以防止单点失败.
Storage Nodes — 实际文件存放的地方. 存储节点是一个HTTP服务器,用来做 删除,存放等事情,任何WebDAV服务器都可以, 不过推荐使用 mogstored 。 mogilefsd 可以配置到两个机器上使用不同端口… mogstored 为所有 DAV 操作 (和流量监测), 并且你自己选择的快速的HTTP服务器用来做 GET 操作(给客户端提供文件). 典型的用户没一个加载点有一个大容量的 SATA 磁盘,他们被加载到 /var/mogdata/devNN.

High-level 流程:
应用程序请求打开一个文件 (通过RPC 通知到 tracker, 找到一个可用的机器). 做一个 “create_open” 请求.
tracker 做一些负载均衡(load balancing)处理,决定应该去哪儿,然后给应用程序一些可能用的位置。
应用程序写到其中的一个位置去 (如果写失败,他会重新尝试并写到另外一个位置去).
应用程序 (client) 通过”create_close” 告诉tracker文件写到哪里去了.
tracker 将该名称和域命的名空间关联 (通过数据库来做的)
tracker, 在后台, 开始复制文件,知道他满足该文件类别设定的复制规则
然后,应用程序通过 “get_paths” 请求 domain+key (key == “filename”) 文件, tracker基于每一位置的I/O繁忙情况回复(在内部经过 database/memcache/etc 等的一些抉择处理), 该文件可用的完整 URLs地址列表.
应用程序然后按顺序尝试这些URL地址. (tracker’持续监测主机和设备的状态,因此不会返回死连接,默认情况下他对返回列表中的第一个元素做双重检查,除非你不要他这么做..)

[ 概念定义 ]
可以参考官方wiki的这儿,简单说一下
domain:最高域,在一个域下key是唯一的。
class:包含在domain中,可以针对每一个class定义保存的份数。
key:对文件的唯一标识。
file:文件。

[ 适用性 ]
      由于Mogilefs不支持对一个文件的随机读写,因此注定了只适合做一部分应用。比如图片服务,静态HTML服务。
      即文件写入后基本上不需要修改的应用,当然你也可以生成一个新的文件覆盖上去。

三、【安装Perl和相关包】

[ 安装环境 ]
操作系统:RHEL 4 (AS 4)
Perl版本:Perl v5.8.8
注意:请确保你的Perl版本为大于 v5.8.8,不然可能安装会有问题,另外,本文所有操作都是在一台机器上完成,可能多台机器上稍微有些不同,请自行调整

[ MySQL ]
安装前请安装好MySQL,如果MySQL Server和MogileFS不是一台机器,请在MogileFS机器安装好MySQL Client,本文假设MySQL Client安装在:/usr/local/mysql 目录,安装完客户端后请建立一个软链接:
ln -s /usr/local/mysql/lib/libmysqlclient.so.15 /usr/lib/libmysqlclient.so.15

[ Perl包安装 ]

      因为MogileFS需要使用很多相关的Perl包,请逐一按照一下次序安装,本文采用的是比较笨,比较简单的安装方法,逐个把包下载回来本机手工安装,没有使用cpan的包安装方式。
要下载包,请在http://search.cpan.org中输入包名,然后下载,一般提供的是最新版,如果版本跟下面不一致,请以最新版为准。

为方便使用,本文包下载地址列表:(不保证您阅读本文的时候有效)
http://search.cpan.org/CPAN/authors/id/J/JH/JHI/BSD-Resource-1.2901.tar.gz
http://search.cpan.org/CPAN/authors/id/B/BR/BRADFITZ/Sys-Syscall-0.22.tar.gz
http://search.cpan.org/CPAN/authors/id/J/JH/JHI/Time-HiRes-1.9715.tar.gz
http://search.cpan.org/CPAN/authors/id/M/MS/MSERGEANT/Danga-Socket-1.59.tar.gz
http://search.cpan.org/CPAN/authors/id/M/MU/MUIR/modules/Net-Netmask-1.9015.tar.gz
http://search.cpan.org/CPAN/authors/id/M/ML/MLEHMANN/IO-AIO-3.07.tar.gz
http://search.cpan.org/CPAN/authors/id/D/DO/DORMANDO/Perlbal-1.71.tar.gz
http://search.cpan.org/CPAN/authors/id/S/SO/SOENKE/String-CRC32-1.4.tar.gz
http://search.cpan.org/CPAN/authors/id/B/BR/BRADFITZ/Gearman-1.09.tar.gz
http://search.cpan.org/CPAN/authors/id/B/BR/BRADFITZ/Gearman-Client-Async-0.94.tar.gz
http://search.cpan.org/CPAN/authors/id/B/BR/BRADFITZ/Gearman-Server-1.09.tar.gz
http://search.cpan.org/CPAN/authors/id/T/TI/TIMB/DBI-1.607.tar.gz
http://search.cpan.org/CPAN/authors/id/C/CA/CAPTTOFU/DBD-mysql-4.008.tar.gz
http://search.cpan.org/CPAN/authors/id/B/BR/BRADFITZ/MogileFS-Client-1.08.tar.gz
http://search.cpan.org/CPAN/authors/id/D/DO/DORMANDO/mogilefs-server-2.20.tar.gz
http://search.cpan.org/CPAN/authors/id/P/PM/PMQS/IO-Compress-Base-2.015.tar.gz
http://search.cpan.org/CPAN/authors/id/P/PM/PMQS/Compress-Raw-Zlib-2.015.tar.gz
http://search.cpan.org/CPAN/authors/id/P/PM/PMQS/IO-Compress-Zlib-2.015.tar.gz
http://search.cpan.org/CPAN/authors/id/P/PM/PMQS/Compress-Zlib-2.015.tar.gz
http://search.cpan.org/CPAN/authors/id/D/DS/DSKOLL/IO-stringy-2.110.tar.gz
http://search.cpan.org/CPAN/authors/id/P/PM/PMQS/Compress-Raw-Zlib-2.015.tar.gz

包安装次序:(务必按照本次序进行)

BSD-Resource-1.2901.tar.gz
Sys-Syscall-0.22.tar.gz

Time-HiRes-1.9715.tar.gz
Danga-Socket-1.57.tar.gz

Net-Netmask-1.9015.tar.gz
IO-AIO-3.07.tar.gz (本包要求Perl v5.8.8以上)
Perlbal-1.71.tar.gz

String-CRC32-1.4.tar.gz
Gearman-1.07.tar.gz
Gearman-Client-Async-0.93.tar.gz
Gearman-Server-1.08.tar.gz

#make test 有可能会有一个错误,不用管它
DBI-1.607.tar.gz
DBD-mysql-4.008.tar.gz

#以下命令假设MySQL服务器是在本机
perl Makefile.PL–testhost=localhost –testuser=root –mysql_config=/usr/local/mysql/bin/mysql_config

IO-Compress-Base-2.015.tar.gz
IO-Compress-Zlib-2.015.tar.gz
Compress-Raw-Zlib-2.015.tar.gz
Compress-Zlib-2.015.tar.gz
IO-stringy-2.110.tar.gz

MogileFS-Client-1.08.tar.gz
mogilefs-server-2.20.tar.gz
MogileFS-Utils-2.13.tar.gz

一定要按以上顺序安装,解压缩,生成Makefile,测试,安装 这样的步骤来执行,命令是:

# tar zxvf xxxxx.tar.gz
# cd xxxxx
# perl Makefile.PL
# make
# make test
# make install  

这样就安装成功了,注意make test的错误提示,如果不是很严重的错误提示,基本可以忽略继续下一步。

四、【配置MogileFS】

        以下操作除非明确指定,不然是以root用户来运行,当然,你也可以使用自己的帐户来执行(除了一些特权操作),另外 10.15.6.28 是本机IP,本试验只使用了一台机器。如果配置过程不太顺利,请反复检查后反复试验。

1. 创建数据库(初始化)
可以预先在数据库服务器上建立好一个叫做 mogilefs 的数据库,便于进行下面的步骤.
#mogdbsetup –dbhost=10.15.6.28 –dbname=mogilefs –dbuser=root

–dbhost 是数据库主机地址, –dbname是数据库名,–dbuser是访问该数据库的用户,如果有密码请加上–dbpass选项,最好访问数据库的是超级用户root或者具有很高权限的新建数据库用户,因为MogileFS需要一些比较高权限的初始化操作。

2. tracker配置
新建tracker配置文件 /etc/mogilefsd.conf ,写入以下文件内容:(请去掉#后面的注意信息)

db_dsn DBI:mysql:mogilefs #数据库配置
db_user mogile #数据库用户
db_pass 123123 #数据库密码
conf_port 6001 #tracker端口
listener_jobs 5

db_dsn指向的是你数据库的位置,如果你数据库不在同一个机器上,请改为:
db_dsn DBI:mysql:mogilefs:127.0.0.1

由于mogilefsd不能用root用户启动.所以添加mogile用户
# adduser mogile

在配置下面以前先启动 trackers server
# su mogile
# mogilefsd -c /etc/mogilefsd.conf –daemon

3. Storage Server 配置
说明:以下命令假设你的 Perl 的包文件都在 /usr/lib/perl5/5.8.8 目录,如果不是,请自行修改。

用mogadm工具将storage server加到数据库中:
# mogadm –lib=/usr/lib/perl5/5.8.8 –trackers=10.15.6.28:6001 host add mogilestorage –ip=10.15.6.28 –port=7500 –status=alive
(由于本文是在一台机器上配,故trackers的地址和ip地址是一样的)

用下面命令来检测是否成功:
# mogadm –lib=/usr/lib/perl5/5.8.8 –trackers=10.15.6.28:6001 host list

加入一个设备到你的storage server:
# mogadm -lib=/usr/lib/perl5/5.8.8 -trackers=10.15.6.28:6001 device add mogilestorage 1

用下面命令来检测是否成功:
# mogadm –lib=/usr/lib/perl5/5.8.8 –trackers=10.15.6.28:6001 device list

Device ID 是唯一的,一旦创建将无法删除,只能mark为dead. 所以,如果你某个磁盘坏了,你mark为dead, 后来又修好了,
那么你必须重新格式化并命名为新的device id, 不支持将device从dead变为alive.

新建Storage配置文件: /etc/mogstored.conf 内容是: (请去掉#后面的注意信息)

httplisten=0.0.0.0:7500 #HTTP监听端口
mgmtlisten=0.0.0.0:7501 #MongileFS监听端口
docroot=/opt/mogdata #数据存储物理路径

建立存放数据的路径:(必须使用root权限才能创建)
# mkdir -p /opt/mogdata/dev1

说明:mogadm 参数的用法请参考 http://search.cpan.org/~dormando/MogileFS-Utils/mogadm

4. 运行MogileFS

启动 Storage Server
# mogstored -c /etc/mogstored.conf –daemon

如果没有启动Trackers,请启动 Trackers
# su mogile
$ mogilefsd -c /etc/mogilefsd.conf –daemon

查看你所有的服务都起来没有
# ps -ef | grep mogilefsd
# ps -ef | grep mogstored

五、【MogileFS使用测试】

生成domain
# mogadm –lib=/usr/lib/perl5/5.8.8 –trackers=10.15.6.28:6001 domain add testdomain

加一个 class 到domain
# mogadm –lib=/usr/lib/perl5/5.8.8 –trackers=10.15.6.28:6001 class add testdomain testclass

写一个perl文件试一下test.pl
#=======================================
use MogileFS::Client;
my $mogfs = MogileFS::Client->new(domain=>’testdomain’, hosts=>[‘10.0.22.184:6001′], root=>’/home/xiehl/mogdata’,);
my $fh = $mogfs->new_file("file_key", "testclass");
die $fh unless $fh->print($mogfs->readonly);
my $content = "file.txt";
@num = $mogfs->store_content("file_key","testclass",$content);
print "@num \n";
my $file_contents = $mogfs->get_file_data("file_key");
print "$file_contents \n";
#$mogfs->delete("file_key");
$fh->print($file_contents);
@urls = $mogfs->get_paths("file_key");
print "@urls \n";
#=======================================

执行脚本
# perl test.pl

本文返回的内容是:(可能你返回的内容会不同,格式类似就行)

8
SCALAR(0×9ddaaa8)
http://10.15.6.28:7500/dev1/0/000/000/0000000008.fid

在浏览器里输入:http://10.15.6.28:7500/dev1/0/000/000/0000000008.fid,将会看到输出:
file.txt

能够访问我们存储的数据,配置成功!

六、【附加内容】

[ MySQL检测代码 ]

如果需要检测你的DBI和MySQL Client是否安装正常,可以使用一下数据库测试代码来检测能否正常连接到MySQL:

#=======================================
#!/usr/bin/perl
# DBI is perl module used to connect to the database
use DBI;
# hostname or ip of server (for local testing, localhost should work)
$config{’dbServer’} = "localhost";
$config{’dbUser’} = "root";
$config{’dbPass’} = "";
$config{’dbName’} = "test";
$config{’dataSource’} = "DBI:mysql:$config{’dbName’}:$config{’dbServer’}";
# Connect to MySQL
my $dbh = DBI->connect($config{’dataSource’},$config{’dbUser’},$config{’dbPass’}) or
die "Can’t connect to $config{’dataSource’}<br>$DBI::errstr";
print "Connected successfully<br>";
$dbh->disconnect();
#=======================================

[ 相关参考 ]

mogilefs 最新版本安装: http://www.wuei.net/?p=33
Mogilefs学习: http://blog.chifeng.name/?p=220
mogileFS的工作方式: http://www.sunnyu.com/?p=32
mogileFS分布式文件存储解决方案: http://www.sunnyu.com/?p=10

[ 扩展阅读 ]

编译mogileFS的php模块: http://www.sunnyu.com/?p=78
mogileFS体系结构分析: http://www.sunnyu.com/?p=31
mogileFS体系结构分析: http://www.sunnyu.com/?p=30

分类: 架构 标签:

服务器扩展性中的Scale Out and Scale up的区别

2010年3月9日 没有评论

       Scale Out 按字面意思是超过尺寸范围,而Scale Up则是按比例增高。即使用靠增加处理器来提升运算能力和增加独立服务器来增加运算能力。
      对于服务器体系来说必须要考虑的一点就是可扩展性(Scalability)。除非业务永不增长,否则随着使用人数不断增多,服务器就一定会很快达到性能和并发极限。解决这个问题,通常只有两个办法:即代表分布式计算的Scale out和以主机或机箱式为主的Scale up。
      Scale Out(向外扩展):就是指企业可以根据需求增加不同的服务器应用,依靠多部服务器协同运算,借负载平衡及容错等功能来提高运算能力及可靠度。
      Scale Up(向上扩展):指企业后端大型服务器以增加处理器等运算资源进行升级以获得对应用性能的要求。
       在现今这两种技术已经没有明显的区别,各个提供商不仅提供用于分布式计算的Unix和Windows平台,还提供用于集中式计算的Unix和 Windows平台。甚至传统的集中式计算大型机也正具有分布式计算的性质——如在IBM zSeries服务器上使用Linux和z/VM的虚拟性能,就是其中的证明。
       但是更大更强的服务器同时也是更昂贵的,往往成本会大于部署大量相对便宜的服务器来实现性能的提升。而且服务器性能所能提高的程度也有一定的上限(分布式 的部署相对来说性能提高的上限更高些)。所以一种呼声是应该使用向外扩展(Scale Out)来实现可扩展性,同时可以让使用者得以保留通过增加服务器以提升系统能力的后路。
      但是在实现中也有很多困难需要解决:
      首先,要想成功地实现向外扩展架构必须解决复杂的分布式计算问题(相对来说Scale Up方案不需要考虑这个问题),而这个问题的解决往往需要很复杂的技术和相对多的资金.大型站点如Google、Yahoo和Amazon.com,都自行研发大量相关技术。
       其次,Scale Out方案还需要对原先是用的软件进行大量的重写工作,以保证系统能在分布式服务器上运行(Scale Up方案则对现有软件几乎没有改动要求)。这一步往往是每个公司的开发人员的噩梦。一个不好会使开发人员的所有工作白费。
       再者,Scale Out方案始终面临着数据集中的问题,即拆分过的数据在服务器逻辑体系中仍然是各自相对集中的而非无限随意拆分。如果大量的逻辑放在数据库服务器一端,数 据库服务器将会使得系统失去Scale out的能力和可能。因此,要保证Scale out的能力就必须保证数据库只处理实质性的数据提交和不可避免的数据查询,对于能够避免的数据查询和非实质性数据提交都应该想办法予以避免。而具体的策 略和方案相对没有最优的方法。

原文地址: http://hi.baidu.com/jackywdx/blog/item/1ba1d68f6d8de6e8f11f3604.html

分类: 架构 标签:

基于Sphinx+MySQL全文检索架构设计

2010年3月9日 没有评论

sphinx英音:[sfiŋks]美音:[sfɪŋks]
名词 n.
1.(大写)【希神】斯芬克斯(带翼的狮身女怪,传说常叫过路行人猜谜,猜不出者即遭噬食)
2.(古埃及)狮身人面像;狮身羊头(或鹰头)像[C]

前言

     本文阐述的是一款经过生产环境检验的千万级数据全文检索(搜索引擎)架构。本文只列出前几章的内容节选,不提供全文内容。

       在DELL PowerEdge 6850服务器(四颗64 位Inter Xeon MP 7110N处理器 / 8GB内存)、RedHat AS4 Linux操作系统、MySQL 5.1.26、MyISAM存储引擎、key_buffer=1024M环境下实测,单表1000万条记录的数据量(这张MySQL表拥有int、datetime、varchar、text等类型的10多个字段,只有主键,无其它索引),用主键(PRIMARY KEY)作为WHERE条件进行SQL查询,速度非常之快,只耗费0.01秒。

       出自俄罗斯的开源全文搜索引擎软件 Sphinx ,单一索引最大可包含1亿条记录,在1千万条记录情况下的查询速度为0.x秒(毫秒级)。Sphinx创建索引的速度为:创建100万条记录的索引只需3~4分钟,创建1000万条记录的索引可以在50分钟内完成,而只包含最新10万条记录的增量索引,重建一次只需几十秒。

       基于以上几点,我设计出了这套搜索引擎架构。在生产环境运行了一周,效果非常不错。有时间我会专为配合Sphinx搜索引擎,开发一个逻辑简单、速度快、占用内存低、非表锁的MySQL存储引擎插件,用来代替MyISAM引擎,以解决MyISAM存储引擎在频繁更新操作时的锁表延迟问题。另外,分布式搜索技术上已无任何问题。

一、搜索引擎架构设计

1、搜索引擎架构图

2、搜索引擎架构设计思路

(1)、调用方式最简化

       尽量方便前端Web工程师,只需要一条简单的SQL语句“SELECT … FROM myisam_table JOIN sphinx_table ON (sphinx_table.sphinx_id=myisam_table.id) WHERE query=’…’;”即可实现高效搜索。

(2)、创建索引、查询速度快

①、Sphinx Search 是由俄罗斯人Andrew Aksyonoff 开发的高性能全文搜索软件包,在GPL与商业协议双许可协议下发行。

Sphinx的特征

  • Sphinx支持高速建立索引(可达10MB/秒,而Lucene建立索引的速度是1.8MB/秒)
  • 高性能搜索(在2-4 GB的文本上搜索,平均0.1秒内获得结果)
  • 高扩展性(实测最高可对100GB的文本建立索引,单一索引可包含1亿条记录)
  • 支持分布式检索
  • 支持基于短语和基于统计的复合结果排序机制
  • 支持任意数量的文件字段(数值属性或全文检索属性)
  • 支持不同的搜索模式(“完全匹配”,“短语匹配”和“任一匹配”)
  • 支持作为Mysql的存储引擎

②、通过国外《High Performance MySQL》专家组的测试可以看出,根据主键进行查询的类似“SELECT … FROM … WHERE id = …”的SQL语句(其中id为PRIMARY KEY),每秒钟能够处理10000次以上的查询,而普通的SELECT查询每秒只能处理几十次到几百次:

③、Sphinx不负责文本字段的存储。假设将数据库的id、date、title、body字段,用sphinx建立搜索索引。根据关键字、时间、类别、范围等信息查询一下sphinx,sphinx只会将查询结果的ID号等非文本信息告诉我们。要显示title、body等信息,还需要根据此ID号去查询MySQL数据库,或者从Memcachedb等其他的存储中取得。安装SphinxSE作为MySQL的存储引擎,将MySQL与Sphinx结合起来,是一种便捷的方法。

      创建一张Sphinx类型表,将MyISAM表的主键ID和Sphinx表的ID作一个JOIN联合查询。这样,对于MyISAM表来所,只相当于一个WHERE id=…的主键查询,WHERE后的条件都交给Sphinx去处理,可以充分发挥两者的优势,实现高速搜索查询。

(3)、按服务类型进行分离

      为了保证数据的一致性,我在配置Sphinx读取索引源的MySQL数据库时,进行了锁表。Sphinx读取索引源的过程会耗费一定时间,由于MyISAM存储引擎的读锁和写锁是互斥的,为了避免写操作被长时间阻塞,导致数据库同步落后跟不上,我将提供“搜索查询服务”的和提供“索引源服务”的MySQL数据库进行了分开。监听3306端口的MySQL提供“搜索查询服务”,监听3406端口的MySQL提供“索引源服务”。

(4)、“主索引+增量索引”更新方式

      一般网站的特征:信息发布较为频繁;刚发布完的信息被编辑、修改的可能性大;两天以前的老帖变动性较小。

       基于这个特征,我设计了Sphinx主索引和增量索引。对于前天17:00之前的记录建立主索引,每天凌晨自动重建一次主索引;对于前天17:00之后到当前最新的记录,间隔3分钟自动重建一次增量索引。

(5)、“Ext3文件系统+tmpfs内存文件系统”相结合

      为了避免每3分钟重建增量索引导致磁盘IO较重,从而引起系统负载上升,我将主索引文件创建在磁盘,增量索引文件创建在tmpfs内存文件系统“/dev/shm/”内。“/dev/shm/”内的文件全部驻留在内存中,读写速度非常快。但是,重启服务器会导致“/dev/shm/”内的文件丢失,针对这个问题,我会在服务器开机时自动创建“/dev/shm/”内目录结构和Sphinx增量索引。

(6)、中文分词词库

      我根据“百度早期中文分词库”+“搜狗拼音输入法细胞词库”+“LibMMSeg高频字库”+… 综合整理成一份中文分词词库,出于某些考虑暂不提供。你可以使用LibMMSeg自带的中文分词词库。

二、MySQL+Sphinx+SphinxSE安装步骤

1、安装python支持(以下针对CentOS系统,其他Linux系统请使用相应的方法安装)

yum install -y python python-devel

2、编译安装LibMMSeg(LibMMSeg是为Sphinx全文搜索引擎设计的中文分词软件包,其在GPL协议下发行的中文分词法,采用Chih-Hao Tsai的MMSEG算法。LibMMSeg在本文中用来生成中文分词词库。)

以下压缩包“sphinx-0.9.8-rc2-chinese.zip”中包含mmseg-0.7.3.tar.gz、sphinx-0.9.8-rc2.tar.gz以及中文分词补丁。

点击这里下载文件

unzip sphinx-0.9.8-rc2-chinese.zip
tar zxvf mmseg-0.7.3.tar.gz
cd mmseg-0.7.3/
./configure
make
make install
cd ../

3、编译安装MySQL 5.1.26-rc、Sphinx、SphinxSE存储引擎

wget http://dev.mysql.com/get/Downloads/MySQL-5.1/mysql-5.1.26-rc.tar.gz/from/http://mirror.x10.com/mirror/mysql/
tar zxvf mysql-5.1.26-rc.tar.gz

tar zxvf sphinx-0.9.8-rc2.tar.gz
cd sphinx-0.9.8-rc2/
patch -p1 < ../sphinx-0.98rc2.zhcn-support.patch
patch -p1 < ../fix-crash-in-excerpts.patch
cp -rf mysqlse ../mysql-5.1.26-rc/storage/sphinx
cd ../

cd mysql-5.1.26-rc/
sh BUILD/autorun.sh
./configure –with-plugins=sphinx –prefix=/usr/local/mysql-search/ –enable-assembler –with-extra-charsets=complex –enable-thread-safe-client –with-big-tables –with-readline –with-ssl –with-embedded-server –enable-local-infile
make && make install
cd ../

cd sphinx-0.9.8-rc2/
CPPFLAGS=-I/usr/include/python2.4
LDFLAGS=-lpython2.4
./configure –prefix=/usr/local/sphinx –with-mysql=/usr/local/mysql-search
make
make install
cd ../

mv /usr/local/sphinx/etc/sphinx.conf /usr/local/sphinx/
etc/sphinx.conf.old

第二章第3节之后的正文内容不予公布,全文的目录如下(共24页):

原文地址:http://blog.s135.com/post/360/
资料贡献:基于Sphinx+MySQL的千万级数据全文检索(搜索引擎)架构设计

分类: MySQL 标签:

PHP Memcached + APC + 文件缓存封装

2010年3月8日 没有评论

使用方法:
Memcached
$cache = new Cache_MemCache();
$cache->addServer(‘www1’);
$cache->addServer(‘www2’,11211,20); // this server has double the memory, and gets double the weight
$cache->addServer(‘www3’,11211);

// Store some data in the cache for 10 minutes
$cache->store(‘my_key’,’foobar’,600);

// Get it out of the cache again
echo($cache->fetch(‘my_key’));

文件缓存
$cache = new Cache_File();

$key = ‘getUsers:selectAll’;

// check if the data is not in the cache already
if (!$data = $cache->fetch($key)) {
// assuming there is a database connection
$result = mysql_query("SELECT * FROM users");
$data = array();

// fetching all the data and putting it in an array
while($row = mysql_fetch_assoc($result)) { $data[] = $row; }

// Storing the data in the cache for 10 minutes
$cache->store($key,$data,600);
}
下载: class_cache3.php
<?php

abstract class Cache_Abstract {
abstract function fetch($key);
abstract function store($key, $data, $ttl);
abstract function delete($key);
}

class Cache_APC extends Cache_Abstract {

function fetch($key) {
return apc_fetch($key);
}

function store($key, $data, $ttl) {
return apc_store($key, $data, $ttl);
}

function delete($key) {
return apc_delete($key);
}

}

class Cache_MemCache extends Cache_Abstract {
public $connection;

function __construct() {
$this->connection = new MemCache;
}

function store($key, $data, $ttl) {
return $this->connection->set($key, $data, 0, $ttl);
}

function fetch($key) {
return $this->connection->get($key);
}

function delete($key) {
return $this->connection->delete($key);
}

function addServer($host, $port = 11211, $weight = 10) {
$this->connection->addServer($host, $port, true, $weight);
}

}

class Cache_File extends Cache_Abstract {

function store($key, $data, $ttl) {
$h = fopen($this->getFileName($key), ‘a+’);
if (!$h)
throw new Exception(‘Could not write to cache’);
flock($h, LOCK_EX);
fseek($h, 0);
ftruncate($h, 0);
$data = serialize(array(time() + $ttl, $data));
if (fwrite($h, $data) === false) {
throw new Exception(‘Could not write to cache’);
}
fclose($h);
}

function fetch($key) {
$filename = $this->getFileName($key);
if (!file_exists($filename))
return false;
$h = fopen($filename, ‘r’);
if (!$h)
return false;
flock($h, LOCK_SH);
$data = file_get_contents($filename);
fclose($h);
$data = @ unserialize($data);
if (!$data) {
unlink($filename);
return false;
}
if (time() > $data[0]) {
unlink($filename);
return false;
}
return $data[1];
}

function delete($key) {
$filename = $this->getFileName($key);
if (file_exists($filename)) {
return unlink($filename);
}
else {
return false;
}
}

private function getFileName($key) {
return ‘/tmp/s_cache’ . md5($key);
}

}
?>

原文地址:http://www.21andy.com/blog/20100206/1643.html

分类: PHP 标签: