一切福田,不離方寸,從心而覓,感無不通。

Category Archives: Programming Language

MVC数据验证

前一篇说了MVC数据验证的例子,这次来详细说说各种各样的验证注解。System.ComponentModel.DataAnnotations 一、基础特性 一、Required 必填选项,当提交的表单缺少该值就引发验证错误。 二、StringLength 指定允许的长度 指定最大长度:

指定最短于最长限制:

三、RegularExpression 正则表达式能够匹配的字符串,如果不能匹配,则报一个验证错误

如邮箱格式验证:

四、Range Range特性用来指定数值类型值的最小值和最大值。

五、Remote 允许利用服务器端的回调函数执行客户端的验证逻辑。说白了就是支持AJAX验证。 需要引用命名空间:System.Web.Mvc; 这个写个异步验证用户名是否存在的DEMO:

Controller代码:

显示结果: 留意到,是通过AJAX发送UserName的值到服务器端判断过的。 六、Compare 用于确保模板对象的两个对象拥有相同的值。 例如,通常输入密码之后还要求用户再次确认密码,这时候就是Compare属性发挥作用的时候了。

以上特性需要 using System.ComponentModel.DataAnnotations;   同时需要注意Web.config里面的这两个选项是否设置为了true;

    二、自定义错误提示消息 每个特性都允许传递一个带有自定义错误提示消息的参数。

自定义错误消息,还有一个格式项,如

则{0}在输入的时候会被Name替换。 如果以上的验证方式还不够,你还可以自定义验证,到时候忘记了可以翻《Asp.net MVC3 高级编程》第127页。这里就不写DEMO了,用到再写。 三、显示和编辑注解 1、Display 如果使用的是直接EditorForModel实现的话,Display特性可以帮助你设置友好的显示名称 例如:

显示结果如下: 其次Display还支持显示属性的顺序,如

后面的顺序是显示编辑框的顺序,默认是10000,按升值排序。 2、ScaffoldColumn 隐藏Html辅助方法(如EditorForModel,DisplayForModel)显示一些属性。

虽然ScaffoldColumn可以做到不显示一些属性在页面上被编辑,但是如果提交的Form有这个属性值,模型绑定机制依然会绑定此属性值。要解除就要显式的[Bind]了,这个与本篇无关。此处不提。 3、DisplayFormat DisplayFormat特性可以用来处理属性的各种格式化选项。当属性包含空值时,可以提供可选的显示文本,也可以为包含标记的属性关闭HTML编码,还可以为运行时指定一个应用于属性值的格式化字符串。 例如:

这样,当有初始值的时候,显示的代码将如下所示: 注意是初始值,如果是直接填写的并不会有符号。来看Controller中设置的初始值:

此显示样式在提交回Controller中是没用的,因为模型绑定器将不能解析返回的价格值。怎么用自己斟酌,用于显示还是OK的。 4、ReadOnly 如果确保默认的模型绑定器不使用请求中的新值更新属性,可以给属性添加ReadOnly特性:

注意,此属性仍然会显示一个可编辑的文本框来显示Name,但是模型绑定器不会接收其值,因此只有模型绑定器考虑ReadOnly属性。 5、DataType DataType特性可以为运行时提供关于属性的特定用途的信息。 例如:

显示效果如下: 该属性可以用于指定多选按钮,单选按钮,密码输入框,等等类型的数据。 6、UIHint UIHint特性给Asp.net MVC运行时提供了一个模板名字,以备调用模板辅助方法如(DisplayFor和EditorFor)渲染输出时使用。也可以自定义自己的模板辅助方法来重写Asp.net MVC的默认行为。 7、HiddenInput HiddentInput在名称空间System.Web.Mvc中,它可以告知运行时渲染一个type特性值为"hidden"的输入元素。说白了就是<input type="hidden" value="xxx" /> 四、验证示例 首先新建一个MVC项目。添加如下代码,代码非常简单:一个Person_Model类 […]

龙生   03 Aug 2015
View Details

DataType枚举

此枚举用于指定要与数据列或参数关联的数据类型。 成员名称 说明 Custom 表示自定义的数据类型。 DateTime 表示时间上的一刻,以日期和当天的时间表示。 Date 表示日期值。 Time 表示时间值。 Duration 表示对象存在的一段连续时间。 PhoneNumber 表示电话号码值。 Currency 表示货币值。 Text 表示所显示的文本。 Html 表示一个 HTML 文件。 MultilineText 表示多行文本。 EmailAddress 表示电子邮件地址。 Password 表示密码值。 Url 表示 URL 值。

龙生   03 Aug 2015
View Details

ASP.NET MVC3更新出错:ObjectStateManager中已存在具有同一键的对象

[HttpPost] public ActionResult Edit(Person person) { if (ModelState.IsValid) { Person oldperson = db.People.Where(p => p.Id == person.Id).FirstOrDefault(); //TODO db.Entry(person).State = EntityState.Modified; db.SaveChanges(); return RedirectToAction("Index"); } return View(person); } 上边代码试图在更新person到数据库前取出旧数据(oldperson)进行文件删除等操作,结果页面报错: 引用内容 ObjectStateManager 中已存在具有同一键的对象。ObjectStateManager 无法跟踪具有相同键的多个对象。 若是不取旧数据,该段代码执行可以正常执行,经过一番测试,猜测原因是读取旧数据时,对象会被添加到db中,当执行"db.Entry(person)"时,试图把person也添加到db中,但person和先前添加对象的键值是一样的,因而报错,这也解释了为什么不取旧数据不出错。 下边提供三种解决方法: Person oldperson = db.People.Where(p => p.Id == person.Id).FirstOrDefault(); //TODO oldperson.PicUrl = person.PicUrl; db.SaveChanges();   Person oldperson = db.People.AsNoTracking().Where(p => p.Id == person.Id).FirstOrDefault(); //TODO db.Entry(person).State = EntityState.Modified; db.SaveChanges();   Person oldperson = db.People.Where(p => p.Id == person.Id).FirstOrDefault(); //TODO db.Entry(oldperson).CurrentValues.SetValues(person); db.SaveChanges(); from:http://www.mzwu.com/article.asp?id=3527

龙生   31 Jul 2015
View Details

移动 App 开发人员应该关注的 7 件事

Apple App Store里有130万的app,而Google Play则托管了140万的app。在竞争如此激烈的移动应用环境中如何确保我们制作的app能获得大量的下载量呢? 广受用户欢迎,并创下了上百万的下载量是每一个移动app开发人员的终极梦想。但是,这谈何容易呢! 在最近的一次网络研讨会上,Rahui Nischal,Nucleus Software Exports公司的高级产品经理,就谈到了开发人员要想创建有吸引力的移动app,所需要关注的几个关键方面。 1.理念是根本 一个移动app的成功取决于它是建立在什么样的核心理念上的。对于开发人员而言,第一步要做的是像目标用户那样思考,用他们的眼光去看待问题,并 在此基础上构建移动app,构建可以让他们的生活更简单的app。步步为营,不断地检查app与最终用户的相关性,积极调整产品以满足用户的需求。 2.单一的服务目标 一个移动app只需要具备单一的功能。确定你app的主要目的,然后钻研能否只通过一到两次点击就可以访问进入。 3.保持简单 保持一切简单化其实是最难的。如果移动开发人员可以成功地将复杂的技术隐藏在简单界面的后面,那么他就越靠近成功。 4.保持轻量级 三年前,Facebook的应用程序重达35-40M。现在,它只有大约10M。之所以会产生如此大的变化,是因为Facebook认识到,在印度和非洲——甚至连3G还尚未普及的区域,下载重量级的移动应用程序往往需要一段又长又难挨的时间。 由于现在的移动app都是面向全球的,所以开发人员最好能够创建轻量级的应用程序以便适应不同地域。 5.UI很重要 WhatsApp在推出之前从来不做广告。虽然现在网上提供了许多的聊天应用程序,但是WhatsApp的用户使用量还是达到了成倍的增长。它成功的秘诀就是干净和简单的UI。为了确保界面友好,我们在创造移动应用的时候需要关注三件事——愉悦性、实用性和功能性。 6.倾听用户的声音 应用程序在发布之后,开发人员应该时刻关注用户的反馈,更新进步,努力提供更新更好的功能。 7.了解用户行为 游戏“愤怒的小鸟”的开发人员发现,Android用户不喜欢为下载应用程序付费,而iOS用户,大多数则愿意购买app,但却讨厌在免费app上面看到任何广告。了解不同的用户行为,有助于移动开发人员为不同类型的用户创建令人满意的消费体验。 from:http://www.oschina.net/news/64613/mobile-app-developers-should-focus-on-7-things

龙生   28 Jul 2015
View Details

Membership的配置文件

Lucene的IndexWriter初始化时报Lock obtain timed out: NativeFSLock

有一次更新完代码启动系统,系统报如下异常:

 

经查,IndexWriter的构造函数在试图获取另外一个IndexWriter已经加锁的索引目录时就会抛出一个LockObtainFailedException。但是我们系统中并没有多个IndexWriter去指定同一个目录,走查代码才发现,原来是同一个初始化方法调用了两次,一次是spring监听器里调用的,另一个是另一同事测试方便,在static模块里调用的,导致同一个lucene上下文初始化方法被调用两次,建议static模块要慎用,因为它会悄悄神不知鬼不知自己悄悄被调用。

from:http://www.tuicool.com/articles/2UBVnq7

影响Lucene索引速度原因以及提高索引速度技巧

在网上看了一篇外文文章,里面介绍了提高Lucene索引速度的技巧,分享给大家。
先来看下影响索引的主要因素:

MaxMergeDocs
该参数决定写入内存索引文档个数,到达该数目后就把该内存索引写入硬盘,生成一个新的索引segment文件。
所以该参数也就是一个内存buffer,一般来说越大索引速度越快。
MaxBufferedDocs这个参数默认是disabled的,因为Lucene中还用另外一个参数(RAMBufferSizeMB)控制这个bufffer的索引文档个数。
其实MaxBufferedDocs和RAMBufferSizeMB这两个参数是可以一起使用的,一起使用时只要有一个触发条件满足就写入硬盘,生成一个新的索引segment文件。

RAMBufferSizeMB
控制用于buffer索引文档的内存上限,如果buffer的索引文档个数到达该上限就写入硬盘。当然,一般来说也只越大索引速度越快。
当我们对文档大小不太确定时,这个参数就相当有用,不至于outofmemory error.

MergeFactor
这个参数是用于子索引(Segment)合并的。
Lucene中索引总体上是这样进行,索引现写到内存,触发一定限制条件后写入硬盘,生成一个独立的子索引-lucene中叫Segment。一般来说这些子索引需要合并成一个索引,也就是optimize(),否则会影响检索速度,而且也可能导致open too many files。
MergeFactor 这个参数就是控制当硬盘中有多少个子索引segments,我们就需要现把这些索引合并冲一个稍微大些的索引了。
MergeFactor这个不能设置太大,特别是当MaxBufferedDocs比较小时(segment 越多),否则会导致open too many files错误,甚至导致虚拟机外面出错。

Note: Lucene 中默认索引合并机制并不是两两合并,好像是多个segment 合并成最终的一个大索引,所以MergeFactor越大耗费内存越多,索引速度也会快些,但我的感觉太大譬如300,最后合并的时候还是很满。Batch indexing 应 MergeFactor>10

加快索引的一些技巧:

• 确认你在使用最新的Lucene版本。

• 尽量使用本地文件系统

远程文件系统一般来说都会降低索引速度。如果索引必须分布在远程服务器,请尝试先在本地生成索引,然后分发到远程服务器上。
• 使用更快的硬件设备,特别是更快的IO设备

• 在索引期间复用单一的IndexWriter实例

• 使用按照内存消耗Flush代替根据文档数量Flush
在Lucene 2.2之前的版本,可以在每次添加文档后调用ramSizeInBytes方法,当索引消耗过多的内存时,然后在调用flush()方法。这样做在索引大量小文档或者文档大小不定的情况下尤为有效。你必须先把maxBufferedDocs参数设置足够大,以防止writer基于文档数量flush。但是注意,别把这个值设置的太大,否则你将遭遇Lucene-845号BUG。不过这个BUG已经在2.3版本中得到解决。

在Lucene2.3之后的版本。IndexWriter可以自动的根据内存消耗调用flush()。你可以通过writer.setRAMBufferSizeMB()来设置缓存大小。当你打算按照内存大小flush后,确保没有在别的地方设置MaxBufferedDocs值。否则flush条件将变的不确定(谁先符合条件就按照谁)。

• 在你能承受的范围内使用更多的内存
在flush前使用更多的内存意味着Lucene将在索引时生成更大的segment,也意味着合并次数也随之减少。在Lucene-843中测试,大概48MB内存可能是一个比较合适的值。但是,你的程序可能会是另外一个值。这跟不同的机器也有一定的关系,请自己多加测试,选择一个权衡值。

• 关闭复合文件格式
调用setUseCompoundFile(false)可以关闭复合文件选项。生成复合文件将消耗更多的时间(经过Lucene-888测试,大概会增加7%-33%的时间)。但是请注意,这样做将大大的增加搜索和索引使用的文件句柄的数量。如果合并因子也很大的话,你可能会出现用光文件句柄的情况。

• 重用Document和Field实例
在lucene 2.3中,新增了一个叫setValue的方法,可以允许你改变字段的值。这样的好处是你可以在整个索引进程中复用一个Filed实例。这将极大的减少GC负担。

最好创建一个单一的Document实例,然后添加你想要的字段到文档中。同时复用添加到文档的Field实例,通用调用相应的SetValue方法改变相应的字段的值。然后重新将Document添加到索引中。

注意:你不能在一个文档中多个字段共用一个Field实例,在文档添加到索引之前,Field的值都不应该改变。也就是说如果你有3个字段,你必须创建3个Field实例,然后再之后的Document添加过程中复用它们。

• 在你的分析器Analyzer中使用一个单一的Token实例
在分析器中共享一个单一的token实例也将缓解GC的压力。

• 在Token中使用char[]接口来代替String接口来表示数据
在Lucene 2.3中,Token可以使用char数组来表示他的数据。这样可以避免构建字符串以及GC回收字符串的消耗。通过配合使用单一Token实例和使用char[]接口你可以避免创建新的对象。

• 设置autoCommit为false
在Lucene 2.3中对拥有存储字段和Term向量的文档进行了大量的优化,以节省大索引合并的时间。你可以将单一复用的IndexWriter实例的autoCommit设置为false来见证这些优化带来的好处。注意这样做将导致searcher在IndexWriter关闭之前不会看到任何索引的更新。如果你认为这个对你很重要,你可以继续将autoCommit设置为true,或者周期性的打开和关闭你的writer。

• 如果你要索引很多小文本字段,如果没有特别需求,建议你将这些小文本字段合并为一个大的contents字段,然后只索引contents。(当然你也可以继续存储那些字段)

• 加大mergeFactor合并因子,但不是越大越好
大的合并因子将延迟segment的合并时间,这样做可以提高索引速度,因为合并是索引很耗时的一个部分。但是,这样做将降低你的搜索速度。同时,你有可能会用光你的文件句柄如果你把合并因子设置的太大。值太大了设置可能降低索引速度,因为这意味着将同时合并更多的segment,将大大的增加硬盘的负担。

• 关闭所有你实际上没有使用的功能
如果你存储了字段,但是在查询时根本没有用到它们,那么别存储它们。同样Term向量也是如此。如果你索引很多的字段,关闭这些字段的不必要的特性将对索引速度提升产生很大的帮助。

• 使用一个更快的分析器
有时间分析文档将消耗很长的时间。举例来说,StandardAnalyzer就比较耗时,尤其在Lucene 2.3版本之前。你可以尝试使用一个更简单更快但是符合你需求的分析器。

• 加速文档的构建时间
在通常的情况下,文档的数据来源可能是外部(比如数据库,文件系统,蜘蛛从网站上的抓取等),这些通常都比较耗时,尽量优化获取它们的性能。

• 在你真的需要之前不要随意的优化optimize索引(只有在需要更快的搜索速度的时候)

• 在多线程中共享一个IndexWriter
最新的硬件都是适合高并发的(多核CPU,多通道内存构架等),所以使用多线程添加文档将会带来不小的性能提升。就算是一台很老的机器,并发添加文档都将更好的利用IO和CPU。多测试并发的线程数目,获得一个临界最优值。

• 将文档分组在不同的机器上索引然后再合并
如果你有大量的文本文档需要索引,你可以把你的文档分为若干组,在若干台机器上分别索引不同的组,然后利用writer.addIndexesNoOptimize来将它们合并到最终的一个索引文件中。

• 运行性能测试程序
如果以上的建议都没有发生效果。建议你运行下性能检测程序。找出你的程序中哪个部分比较耗时。这通常会给你想不到的惊喜。
本翻译属于原创,转载时请注明出处,英文原版请查看:

http://wiki.apache.org/jakarta-lucene/ImproveIndexingSpeed

from:http://www.cnblogs.com/gaoweipeng/archive/2009/10/16/1584503.html

lucene、lucene.NET详细使用与优化详解

1 lucene简介
1.1 什么是lucene
Lucene是一个全文搜索框架,而不是应用产品。因此它并不像www.baidu.com 或者google Desktop那么拿来就能用,它只是提供了一种工具让你能实现这些产品。

1.2 lucene能做什么
要 回答这个问题,先要了解lucene的本质。实际上lucene的功能很单一,说到底,就是你给它若干个字符串,然后它为你提供一个全文搜索服务,告诉你 你要搜索的关键词出现在哪里。知道了这个本质,你就可以发挥想象做任何符合这个条件的事情了。你可以把站内新闻都索引了,做个资料库;你可以把一个数据库 表的若干个字段索引起来,那就不用再担心因为“%like%”而锁表了;你也可以写个自己的搜索引擎……

1.3 你该不该选择lucene
下面给出一些测试数据,如果你觉得可以接受,那么可以选择。
测试一:250万记录,300M左右文本,生成索引380M左右,800线程下平均处理时间300ms。
测试二:37000记录,索引数据库中的两个varchar字段,索引文件2.6M,800线程下平均处理时间1.5ms。

2 lucene的工作方式
lucene提供的服务实际包含两部分:一入一出。所谓入是写入,即将你提供的源(本质是字符串)写入索引或者将其从索引中删除;所谓出是读出,即向用户提供全文搜索服务,让用户可以通过关键词定位源。

2.1写入流程
源字符串首先经过analyzer处理,包括:分词,分成一个个单词;去除stopword(可选)。
将源中需要的信息加入Document的各个Field中,并把需要索引的Field索引起来,把需要存储的Field存储起来。
将索引写入存储器,存储器可以是内存或磁盘。

2.2读出流程
用户提供搜索关键词,经过analyzer处理。
对处理后的关键词搜索索引找出对应的Document。
用户根据需要从找到的Document中提取需要的Field。

3 一些需要知道的概念
lucene用到一些概念,了解它们的含义,有利于下面的讲解。

3.1 analyzer
Analyzer 是分析器,它的作用是把一个字符串按某种规则划分成一个个词语,并去除其中的无效词语,这里说的无效词语是指英文中的 “of”、 “the”,中文中的“的”、“地”等词语,这些词语在文章中大量出现,但是本身不包含什么关键信息,去掉有利于缩小索引文件、提高效率、提高命中率。
分词的规则千变万化,但目的只有一个:按语义划分。这点在英文中比较容易实现,因为英文本身就是以单词为单位的,已经用空格分开;而中文则必须以某种方法将连成一片的句子划分成一个个词语。具体划分方法下面再详细介绍,这里只需了解分析器的概念即可。

3.2 document
用户提供的源是一条条记录,它们可以是文本文件、字符串或者数据库表的一条记录等等。一条记录经过索引之后,就是以一个Document的形式存储在索引文件中的。用户进行搜索,也是以Document列表的形式返回。

3.3 field
一个Document可以包含多个信息域,例如一篇文章可以包含“标题”、“正文”、“最后修改时间”等信息域,这些信息域就是通过Field在Document中存储的。
Field有两个属性可选:存储和索引。通过存储属性你可以控制是否对这个Field进行存储;通过索引属性你可以控制是否对该Field进行索引。这看起来似乎有些废话,事实上对这两个属性的正确组合很重要,下面举例说明:
还 是以刚才的文章为例子,我们需要对标题和正文进行全文搜索,所以我们要把索引属性设置为真,同时我们希望能直接从搜索结果中提取文章标题,所以我们把标题 域的存储属性设置为真,但是由于正文域太大了,我们为了缩小索引文件大小,将正文域的存储属性设置为假,当需要时再直接读取文件;我们只是希望能从搜索解 果中提取最后修改时间,不需要对它进行搜索,所以我们把最后修改时间域的存储属性设置为真,索引属性设置为假。上面的三个域涵盖了两个属性的三种组合,还 有一种全为假的没有用到,事实上Field不允许你那么设置,因为既不存储又不索引的域是没有意义的。

3.4 term
term是搜索的最小单位,它表示文档的一个词语,term由两部分组成:它表示的词语和这个词语所出现的field。

3.5 tocken
tocken是term的一次出现,它包含trem文本和相应的起止偏移,以及一个类型字符串。一句话中可以出现多次相同的词语,它们都用同一个term表示,但是用不同的tocken,每个tocken标记该词语出现的地方。

3.6 segment
添加索引时并不是每个document都马上添加到同一个索引文件,它们首先被写入到不同的小文件,然后再合并成一个大索引文件,这里每个小文件都是一个segment。

4 lucene的结构
lucene包括core和sandbox两部分,其中core是lucene稳定的核心部分,sandbox包含了一些附加功能,例如highlighter、各种分析器。
Lucene core有七个包:analysis,document,index,queryParser,search,store,util。
4.1 analysis
Analysis包含一些内建的分析器,例如按空白字符分词的WhitespaceAnalyzer,添加了stopwrod过滤的StopAnalyzer,最常用的StandardAnalyzer。
4.2 document
Document包含文档的数据结构,例如Document类定义了存储文档的数据结构,Field类定义了Document的一个域。
4.3 index
Index 包含了索引的读写类,例如对索引文件的segment进行写、合并、优化的IndexWriter类和对索引进行读取和删除操作的 IndexReader类,这里要注意的是不要被IndexReader这个名字误导,以为它是索引文件的读取类,实际上删除索引也是由它完成, IndexWriter只关心如何将索引写入一个个segment,并将它们合并优化;IndexReader则关注索引文件中各个文档的组织形式。
4.4 queryParser
QueryParser 包含了解析查询语句的类,lucene的查询语句和sql语句有点类似,有各种保留字,按照一定的语法可以组成各种查询。 Lucene有很多种Query类,它们都继承自Query,执行各种特殊的查询,QueryParser的作用就是解析查询语句,按顺序调用各种 Query类查找出结果。
4.5 search
Search包含了从索引中搜索结果的各种类,例如刚才说的各种Query类,包括TermQuery、BooleanQuery等就在这个包里。
4.6 store
Store包含了索引的存储类,例如Directory定义了索引文件的存储结构,FSDirectory为存储在文件中的索引,RAMDirectory为存储在内存中的索引,MmapDirectory为使用内存映射的索引。
4.7 util
Util包含一些公共工具类,例如时间和字符串之间的转换工具。
5 如何建索引
5.1 最简单的能完成索引的代码片断

IndexWriter writer = new IndexWriter(“/data/index/”, new StandardAnalyzer(), true);
Document doc = new Document();
doc.add(new Field("title", "lucene introduction", Field.Store.YES, Field.Index.TOKENIZED));
doc.add(new Field("content", "lucene works well", Field.Store.YES, Field.Index.TOKENIZED));
writer.addDocument(doc);
writer.optimize();
writer.close();

下面我们分析一下这段代码。
首先我们创建了一个writer,并指定存放索引的目录为“/data/index”,使用的分析器为StandardAnalyzer,第三个参数说明如果已经有索引文件在索引目录下,我们将覆盖它们。
然后我们新建一个document。
我们向document添加一个field,名字是“title”,内容是“lucene introduction”,对它进行存储并索引。
再添加一个名字是“content”的field,内容是“lucene works well”,也是存储并索引。
然后我们将这个文档添加到索引中,如果有多个文档,可以重复上面的操作,创建document并添加。
添加完所有document,我们对索引进行优化,优化主要是将多个segment合并到一个,有利于提高索引速度。
随后将writer关闭,这点很重要。

对,创建索引就这么简单!
当然你可能修改上面的代码获得更具个性化的服务。

5.2 将索引直接写在内存
你需要首先创建一个RAMDirectory,并将其传给writer,代码如下:

Directory dir = new RAMDirectory();
IndexWriter writer = new IndexWriter(dir, new StandardAnalyzer(), true);
Document doc = new Document();
doc.add(new Field("title", "lucene introduction", Field.Store.YES, Field.Index.TOKENIZED));
doc.add(new Field("content", "lucene works well", Field.Store.YES, Field.Index.TOKENIZED));
writer.addDocument(doc);
writer.optimize();
writer.close();

5.3 索引文本文件
如果你想把纯文本文件索引起来,而不想自己将它们读入字符串创建field,你可以用下面的代码创建field:

Field field = new Field("content", new FileReader(file));

这里的file就是该文本文件。该构造函数实际上是读去文件内容,并对其进行索引,但不存储。

6 如何维护索引
索引的维护操作都是由IndexReader类提供。

6.1 如何删除索引
lucene提供了两种从索引中删除document的方法,一种是

void deleteDocument(int docNum)

这种方法是根据document在索引中的编号来删除,每个document加进索引后都会有个唯一编号,所以根据编号删除是一种精确删除,但是这个编号是索引的内部结构,一般我们不会知道某个文件的编号到底是几,所以用处不大。另一种是

void deleteDocuments(Term term)

这种方法实际上是首先根据参数term执行一个搜索操作,然后把搜索到的结果批量删除了。我们可以通过这个方法提供一个严格的查询条件,达到删除指定document的目的。
下面给出一个例子:

Directory dir = FSDirectory.getDirectory(PATH, false);
IndexReader reader = IndexReader.open(dir);
Term term = new Term(field, key);
reader.deleteDocuments(term);
reader.close();

6.2 如何更新索引
lucene并没有提供专门的索引更新方法,我们需要先将相应的document删除,然后再将新的document加入索引。例如:

Directory dir = FSDirectory.getDirectory(PATH, false);
IndexReader reader = IndexReader.open(dir);
Term term = new Term(“title”, “lucene introduction”);
reader.deleteDocuments(term);
reader.close();

IndexWriter writer = new IndexWriter(dir, new StandardAnalyzer(), true);
Document doc = new Document();
doc.add(new Field("title", "lucene introduction", Field.Store.YES, Field.Index.TOKENIZED));
doc.add(new Field("content", "lucene is funny", Field.Store.YES, Field.Index.TOKENIZED));
writer.addDocument(doc);
writer.optimize();
writer.close();
7 如何搜索
lucene 的搜索相当强大,它提供了很多辅助查询类,每个类都继承自Query类,各自完成一种特殊的查询,你可以像搭积木一样将它们任意组合使用,完成一些复杂操 作;另外lucene还提供了Sort类对结果进行排序,提供了Filter类对查询条件进行限制。你或许会不自觉地拿它跟SQL语句进行比 较:“lucene能执行and、or、order by、where、like ‘%xx%’操作吗?”回答是:“当然没问题!”

7.1 各种各样的Query
下面我们看看lucene到底允许我们进行哪些查询操作:

7.1.1 TermQuery
首先介绍最基本的查询,如果你想执行一个这样的查询:“在content域中包含‘lucene’的document”,那么你可以用TermQuery:

Term t = new Term("content", " lucene";
Query query = new TermQuery(t);

7.1.2 BooleanQuery
如果你想这么查询:“在content域中包含java或perl的document”,那么你可以建立两个TermQuery并把它们用BooleanQuery连接起来:

TermQuery termQuery1 = new TermQuery(new Term("content", "java");
TermQuery termQuery 2 = new TermQuery(new Term("content", "perl");
BooleanQuery booleanQuery = new BooleanQuery();
booleanQuery.add(termQuery 1, BooleanClause.Occur.SHOULD);
booleanQuery.add(termQuery 2, BooleanClause.Occur.SHOULD);

7.1.3 WildcardQuery
如果你想对某单词进行通配符查询,你可以用WildcardQuery,通配符包括’?’匹配一个任意字符和’*’匹配零个或多个任意字符,例如你搜索’use*’,你可能找到’useful’或者’useless’:

Query query = new WildcardQuery(new Term("content", "use*");

7.1.4 PhraseQuery
你可能对中日关系比较感兴趣,想查找‘中’和‘日’挨得比较近(5个字的距离内)的文章,超过这个距离的不予考虑,你可以:

PhraseQuery query = new PhraseQuery();
query.setSlop(5);
query.add(new Term("content ", “中”));
query.add(new Term(“content”, “日”));

那么它可能搜到“中日合作……”、“中方和日方……”,但是搜不到“中国某高层领导说日本欠扁”。

7.1.5 PrefixQuery
如果你想搜以‘中’开头的词语,你可以用PrefixQuery:

PrefixQuery query = new PrefixQuery(new Term("content ", "中");

7.1.6 FuzzyQuery
FuzzyQuery用来搜索相似的term,使用Levenshtein算法。假设你想搜索跟‘wuzza’相似的词语,你可以:

Query query = new FuzzyQuery(new Term("content", "wuzza");

你可能得到‘fuzzy’和‘wuzzy’。

7.1.7 RangeQuery
另一个常用的Query是RangeQuery,你也许想搜索时间域从20060101到20060130之间的document,你可以用RangeQuery:

RangeQuery query = new RangeQuery(new Term(“time”, “20060101”), new Term(“time”, “20060130”), true);

最后的true表示用闭合区间。

7.2 QueryParser
看 了这么多Query,你可能会问:“不会让我自己组合各种Query吧,太麻烦了!”当然不会,lucene提供了一种类似于SQL语句的查询语句,我们 姑且叫它lucene语句,通过它,你可以把各种查询一句话搞定,lucene会自动把它们查分成小块交给相应Query执行。下面我们对应每种 Query演示一下:
TermQuery可以用“field:key”方式,例如“content:lucene”。
BooleanQuery中‘与’用‘+’,‘或’用‘ ’,例如“content:java contenterl”。
WildcardQuery仍然用‘?’和‘*’,例如“content:use*”。
PhraseQuery用‘~’,例如“content:"中日"~5”。
PrefixQuery用‘*’,例如“中*”。
FuzzyQuery用‘~’,例如“content: wuzza ~”。
RangeQuery用‘[]’或‘{}’,前者表示闭区间,后者表示开区间,例如“time:[20060101 TO 20060130]”,注意TO区分大小写。
你 可以任意组合query string,完成复杂操作,例如“标题或正文包括lucene,并且时间在20060101到20060130之间的文章” 可以表示为:“+ (title:lucene content:lucene) +time:[20060101 TO 20060130]”。代码如下:

Directory dir = FSDirectory.getDirectory(PATH, false);
IndexSearcher is = new IndexSearcher(dir);
QueryParser parser = new QueryParser("content", new StandardAnalyzer());
Query query = parser.parse("+(title:lucene content:lucene) +time:[20060101 TO 20060130]";
Hits hits = is.search(query);
for (int i = 0; i < hits.length(); i++)
{
Document doc
= hits.doc(i);
System.out.println(doc.get("title");
}
is.close();

首先我们创建一个在指定文件目录上的IndexSearcher。
然后创建一个使用StandardAnalyzer作为分析器的QueryParser,它默认搜索的域是content。
接着我们用QueryParser来parse查询字串,生成一个Query。
然后利用这个Query去查找结果,结果以Hits的形式返回。
这个Hits对象包含一个列表,我们挨个把它的内容显示出来。

7.3 Filter
filter 的作用就是限制只查询索引的某个子集,它的作用有点像SQL语句里的 where,但又有区别,它不是正规查询的一部分,只是对数据源进行预处理,然后交给查询语句。注意它执行的是预处理,而不是对查询结果进行过滤,所以使 用filter的代价是很大的,它可能会使一次查询耗时提高一百倍。
最常用的filter是RangeFilter和QueryFilter。RangeFilter是设定只搜索指定范围内的索引;QueryFilter是在上次查询的结果中搜索。
Filter的使用非常简单,你只需创建一个filter实例,然后把它传给searcher。继续上面的例子,查询“时间在20060101到20060130之间的文章”除了将限制写在query string中,你还可以写在RangeFilter中:

Directory dir = FSDirectory.getDirectory(PATH, false);
IndexSearcher is = new
IndexSearcher(dir);
QueryParser parser = new
QueryParser("content", new StandardAnalyzer());
Query query = parser.parse("title:lucene
content:lucene";
RangeFilter filter = new
RangeFilter("time", "20060101", "20060230", true, true);
Hits hits = is.search(query,
filter);
for (int i i < hits.length(); i++)
{
Document doc
= hits.doc(i);
System.out.println(doc.get("title");
}
is.close();

7.4 Sort
有时你想要一个排好序的结果集,就像SQL语句的“order by”,lucene能做到:通过Sort。
Sort sort
Sort(“time”); //相当于SQL的“order by time”
Sort sort = new
Sort(“time”, true); // 相当于SQL的“order by time desc”
下面是一个完整的例子:

Directory dir = FSDirectory.getDirectory(PATH, false);
IndexSearcher is = new
IndexSearcher(dir);
QueryParser parser = new
QueryParser("content", new StandardAnalyzer());
Query query = parser.parse("title:lucene
content:lucene";
RangeFilter filter = new
RangeFilter("time", "20060101", "20060230", true, true);
Sort sort = new
Sort(“time”);
Hits hits = is.search(query,
filter, sort);
for (int i
= 0; i < hits.length(); i++)
{
Document doc
= hits.doc(i);
System.out.println(doc.get("title");
}
is.close();

8 分析器
在 前面的概念介绍中我们已经知道了分析器的作用,就是把句子按照语义切分成一个个词语。英文切分已经有了很成熟的分析器: StandardAnalyzer,很多情况下StandardAnalyzer是个不错的选择。甚至你会发现StandardAnalyzer也能对中 文进行分词。
但是我们的焦点是中文分词,StandardAnalyzer能支持中文分词吗?实践证明是可以的,但是效果并不好,搜索“如果”会 把“牛奶不如果汁好喝 ”也搜索出来,而且索引文件很大。那么我们手头上还有什么分析器可以使用呢?core里面没有,我们可以在sandbox里面找到两个: ChineseAnalyzer和CJKAnalyzer。但是它们同样都有分词不准的问题。相比之下用StandardAnalyzer 和 ChineseAnalyzer建立索引时间差不多,索引文件大小也差不多,CJKAnalyzer表现会差些,索引文件大且耗时比较长。
要解决 问题,首先分析一下这三个分析器的分词方式。StandardAnalyzer和ChineseAnalyzer都是把句子按单个字切分,也就是说 “牛奶不如果汁好喝”会被它们切分成“牛 奶 不 如 果 汁 好 喝”;而CJKAnalyzer则会切分成“牛奶 奶不 不如 如果 果汁 汁好好喝”。这也就解释了为什么搜索“果汁”都能匹配这个句子。
以上分词的缺点至少有两个:匹配不准确和索引文件大。我们的目标是将上面的句子分 解成“牛奶 不如 果汁好喝”。这里的关键就是语义识别,我们如何识别“ 牛奶”是一个词而“奶不”不是词语?我们很自然会想到基于词库的分词法,也就是我们先得到一个词库,里面列举了大部分词语,我们把句子按某种方式切分,当 得到的词语与词库中的项匹配时,我们就认为这种切分是正确的。这样切词的过程就转变成匹配的过程,匹配的方式最简单的有正向最大匹配和逆向最大匹配两种,说白了就是一个从句子开头向后进行匹配,一个从句子末尾向前进行匹配。基于词库的分词词库非常重要,词库的容量直接影响搜索结果,在相同词库的前提下,据说逆向最大匹配优于正向最大匹配。
当 然还有别的分词方法,这本身就是一个学科,我这里也没有深入研究。回到具体应用,我们的目标是能找到成熟的、现成的分词工具,避免重新发明车轮。经过网上 搜索,用的比较多的是中科院的ICTCLAS和一个不开放源码但是免费的JE-Analysis。ICTCLAS有个问题是它是一个动态链接库, java调用需要本地方法调用,不方便也有安全隐患,而且口碑也确实不大好。JE-Analysis效果还不错,当然也会有分词不准的地方,相比比较方便 放心。= new = 0;

9 性能优化
一 直到这里,我们还是在讨论怎么样使lucene跑起来,完成指定任务。利用前面说的也确实能完成大部分功能。但是测试表明lucene的性能并不是很好, 在大数据量大并发的条件下甚至会有半分钟返回的情况。另外大数据量的数据初始化建立索引也是一个十分耗时的过程。那么如何提高lucene的性能呢?下面 从优化创建索引性能和优化搜索性能两方面介绍。

9.1 优化创建索引性能
这方面的优化途径比较有限,IndexWriter提供 了一些接口可以控制建立索引的操作,另外我们可以先将索引写入RAMDirectory,再批量写入FSDirectory,不管怎样,目的都是尽量少的 文件IO,因为创建索引的最大瓶颈在于磁盘IO。另外选择一个较好的分析器也能提高一些性能。

9.1.1 通过设置IndexWriter的参数优化索引建立
setMaxBufferedDocs(int maxBufferedDocs)
控制写入一个新的segment前内存中保存的document的数目,设置较大的数目可以加快建索引速度,默认为10。
setMaxMergeDocs(int maxMergeDocs)
控制一个segment中可以保存的最大document数目,值较小有利于追加索引的速度,默认Integer.MAX_VALUE,无需修改。
setMergeFactor(int mergeFactor)
控制多个segment合并的频率,值较大时建立索引速度较快,默认是10,可以在建立索引时设置为100。

9.1.2 通过RAMDirectory缓写提高性能
我们可以先把索引写入RAMDirectory,达到一定数量时再批量写进FSDirectory,减少磁盘IO次数。

FSDirectory fsDir = FSDirectory.getDirectory("/data/index", true);
RAMDirectory ramDir = new
RAMDirectory();
IndexWriter fsWriter = new
IndexWriter(fsDir, new StandardAnalyzer(), true);
IndexWriter ramWriter = new
IndexWriter(ramDir, new StandardAnalyzer(), true);
while (there are documents to index)
{
… create Document …
ramWriter.addDocument(doc);
if (condition for flushing memory to disk has been met)
{
fsWriter.addIndexes(new Directory[] { ramDir });
ramWriter.close();
ramWriter
= new IndexWriter(ramDir, new StandardAnalyzer(), true);
}
}

9.1.3 选择较好的分析器
这 个优化主要是对磁盘空间的优化,可以将索引文件减小将近一半,相同测试数据下由600M减少到380M。但是对时间并没有什么帮助,甚至会需要更长时间, 因为较好的分析器需要匹配词库,会消耗更多cpu,测试数据用StandardAnalyzer耗时133分钟;用MMAnalyzer耗时150分钟。

9.2 优化搜索性能
虽然建立索引的操作非常耗时,但是那毕竟只在最初创建时才需要,平时只是少量的维护操作,更何况这些可以放到一个后台进程处理,并不影响用户搜索。我们创建索引的目的就是给用户搜索,所以搜索的性能才是我们最关心的。下面就来探讨一下如何提高搜索性能。

9.2.1 将索引放入内存
这是一个最直观的想法,因为内存比磁盘快很多。Lucene提供了RAMDirectory可以在内存中容纳索引:

Directory fsDir = FSDirectory.getDirectory(“/data/index/”, false);
Directory ramDir = new
RAMDirectory(fsDir);
Searcher searcher = new
IndexSearcher(ramDir);

但是实践证明RAMDirectory和FSDirectory速度差不多,当数据量很小时两者都非常快,当数据量较大时(索引文件400M)RAMDirectory甚至比FSDirectory还要慢一点,这确实让人出乎意料。
而且lucene的搜索非常耗内存,即使将400M的索引文件载入内存,在运行一段时间后都会out of memory,所以个人认为载入内存的作用并不大。

9.2.2 优化时间范围限制
既然载入内存并不能提高效率,一定有其它瓶颈,经过测试发现最大的瓶颈居然是时间范围限制,那么我们可以怎样使时间范围限制的代价最小呢?
当需要搜索指定时间范围内的结果时,可以:
1、 用RangeQuery,设置范围,但是RangeQuery的实现实际上是将时间范围内的时间点展开,组成一个个BooleanClause加入到 BooleanQuery中查询,因此时间范围不可能设置太大,经测试,范围超过一个月就会抛 BooleanQuery.TooManyClauses,可以通过设置 BooleanQuery.setMaxClauseCount(int maxClauseCount)扩大,但是扩大也是有限的,并且随着 maxClauseCount扩大,占用内存也扩大
2、用 RangeFilter代替RangeQuery,经测试速度不会比RangeQuery慢,但是仍然有性能瓶颈,查询的90%以上时间耗费在 RangeFilter,研究其源码发现RangeFilter实际上是首先遍历所有索引,生成一个BitSet,标记每个document,在时间范围 内的标记为true,不在的标记为false,然后将结果传递给Searcher查找,这是十分耗时的。
3、进一步提高性能,这个又有两个思路:
a、 缓存Filter结果。既然RangeFilter的执行是在搜索之前,那么它的输入都是一定的,就是IndexReader,而 IndexReader是由Directory决定的,所以可以认为RangeFilter的结果是由范围的上下限决定的,也就是由具体的 RangeFilter对象决定,所以我们只要以RangeFilter对象为键,将filter结果BitSet缓存起来即可。 lucene API已经提供了一个CachingWrapperFilter类封装了Filter及其结果,所以具体实施起来我们可以 cache CachingWrapperFilter对象,需要注意的是,不要被CachingWrapperFilter的名字及其说明误导, CachingWrapperFilter看起来是有缓存功能,但的缓存是针对同一个filter的,也就是在你用同一个filter过滤不同 IndexReader时,它可以帮你缓存不同IndexReader的结果,而我们的需求恰恰相反,我们是用不同filter过滤同一个 IndexReader,所以只能把它作为一个封装类。
b、降低时间精度。研究Filter的工作原理可以看出,它每次工作都是遍历整个索引的,所以时间粒度越大,对比越快,搜索时间越短,在不影响功能的情况下,时间精度越低越好,有时甚至牺牲一点精度也值得,当然最好的情况是根本不作时间限制。
下面针对上面的两个思路演示一下优化结果(都采用800线程随机关键词随即时间范围):
第一组,时间精度为秒:
方式 直接用RangeFilter 使用cache 不用filter
平均每个线程耗时 10s 1s 300ms

第二组,时间精度为天
方式 直接用RangeFilter 使用cache 不用filter
平均每个线程耗时 900ms 360ms 300ms

由以上数据可以得出结论:
1、 尽量降低时间精度,将精度由秒换成天带来的性能提高甚至比使用cache还好,最好不使用filter。
2、 在不能降低时间精度的情况下,使用cache能带了10倍左右的性能提高。

9.2.3 使用更好的分析器
这个跟创建索引优化道理差不多,索引文件小了搜索自然会加快。当然这个提高也是有限的。较好的分析器相对于最差的分析器对性能的提升在20%以下。

10 一些经验

10.1关键词区分大小写
or AND TO等关键词是区分大小写的,lucene只认大写的,小写的当做普通单词。

10.2 读写互斥性
同一时刻只能有一个对索引的写操作,在写的同时可以进行搜索

10.3 文件锁
在写索引的过程中强行退出将在tmp目录留下一个lock文件,使以后的写操作无法进行,可以将其手工删除

10.4 时间格式
lucene只支持一种时间格式yyMMddHHmmss,所以你传一个yy-MM-dd HH:mm:ss的时间给lucene它是不会当作时间来处理的

10.5 设置boost
有些时候在搜索时某个字段的权重需要大一些,例如你可能认为标题中出现关键词的文章比正文中出现关键词的文章更有价值,你可以把标题的boost设置的更大,那么搜索结果会优先显示标题中出现关键词的文章(没有使用排序的前题下)。使用方法:
Field. setBoost(float boost);默认值是1.0,也就是说要增加权重的需要设置得比1大。

from:http://blog.itpub.net/12639172/viewspace-626546/

Lucene.NET精华实例

1.1Lucene.NET是一个全文搜索框架,而不是应用产品。

1.2 lucene能做什么要回答这个问题,先要了解lucene的本质。实际上lucene的功能很单一,说到底,就是你给它若干个字符串,然后它为你提供一个全文搜索服务,告诉你你要搜索的关键词出现在哪里。知道了这个本质,你就可以发挥想象做任何符合这个条件的事情了。你可以把站内新闻都索引了,做个资料库;你可以把一个数据库表的若干个字段索引起来,那就不用再担心因为“%like%”而锁表了;你也可以写个自己的搜索引擎……

1.3 你该不该选择lucene 下面给出一些测试数据,如果你觉得可以接受,那么可以选择。测试一:250万记录,300M左右文本,生成索引380M左右,800线程下平均处理时间 300ms。测试二:37000记录,索引数据库中的两个varchar字段,索引文件2.6M,800线程下平均处理时间1.5ms。

2 lucene的工作方式 lucene提供的服务实际包含两部分:一入一出。所谓入是写入,即将你提供的源(本质是字符串)写入索引或者将其从索引中删除;所谓出是读出,即向用户提供全文搜索服务,让用户可以通过关键词定位源。

2.1写入流程源字符串首先经过analyzer处理,包括:分词,分成一个个单词;去除stopword(可选)。将源中需要的信息加入 Document的各个Field中,并把需要索引的Field索引起来,把需要存储的Field存储起来。将索引写入存储器,存储器可以是内存或磁盘。

2.2读出流程用户提供搜索关键词,经过analyzer处理。对处理后的关键词搜索索引找出对应的Document。用户根据需要从找到的Document中提取需要的Field。

3 一些需要知道的概念 lucene用到一些概念,了解它们的含义,有利于下面的讲解。

3.1 analyzer Analyzer是分析器,它的作用是把一个字符串按某种规则划分成一个个词语,并去除其中的无效词语,这里说的无效词语是指英文中的“of”、 “the”,中文中的“的”、“地”等词语,这些词语在文章中大量出现,但是本身不包含什么关键信息,去掉有利于缩小索引文件、提高效率、提高命中率。分词的规则千变万化,但目的只有一个:按语义划分。这点在英文中比较容易实现,因为英文本身就是以单词为单位的,已经用空格分开;而中文则必须以某种方法将连成一片的句子划分成一个个词语。具体划分方法下面再详细介绍,这里只需了解分析器的概念即可。

3.2 document 用户提供的源是一条条记录,它们可以是文本文件、字符串或者数据库表的一条记录等等。一条记录经过索引之后,就是以一个Document的形式存储在索引文件中的。用户进行搜索,也是以Document列表的形式返回。

3.3 field 一个Document可以包含多个信息域,例如一篇文章可以包含“标题”、“正文”、“最后修改时间”等信息域,这些信息域就是通过Field在 Document中存储的。 Field有两个属性可选:存储和索引。通过存储属性你可以控制是否对这个Field进行存储;通过索引属性你可以控制是否对该Field进行索引。这看起来似乎有些废话,事实上对这两个属性的正确组合很重要,下面举例说明:还是以刚才的文章为例子,我们需要对标题和正文进行全文搜索,所以我们要把索引属性设置为真,同时我们希望能直接从搜索结果中提取文章标题,所以我们把标题域的存储属性设置为真,但是由于正文域太大了,我们为了缩小索引文件大小,将正文域的存储属性设置为假,当需要时再直接读取文件;我们只是希望能从搜索解果中提取最后修改时间,不需要对它进行搜索,所以我们把最后修改时间域的存储属性设置为真,索引属性设置为假。上面的三个域涵盖了两个属性的三种组合,还有一种全为假的没有用到,事实上Field不允许你那么设置,因为既不存储又不索引的域是没有意义的。

3.4 term term是搜索的最小单位,它表示文档的一个词语,term由两部分组成:它表示的词语和这个词语所出现的field。

3.5 tocken tocken是term的一次出现,它包含trem文本和相应的起止偏移,以及一个类型字符串。一句话中可以出现多次相同的词语,它们都用同一个term表示,但是用不同的tocken,每个tocken标记该词语出现的地方。

3.6 segment 添加索引时并不是每个document都马上添加到同一个索引文件,它们首先被写入到不同的小文件,然后再合并成一个大索引文件,这里每个小文件都是一个segment。

4 lucene的结构 lucene包括core和sandbox两部分,其中core是lucene稳定的核心部分,sandbox包含了一些附加功能,例如 highlighter、各种分析器。 Lucene core有七个包:analysis,document,index,queryParser,search,store,util。

4.1 analysis Analysis包含一些内建的分析器,例如按空白字符分词的WhitespaceAnalyzer,添加了stopwrod过滤的StopAnalyzer,最常用的StandardAnalyzer。

4.2 document Document包含文档的数据结构,例如Document类定义了存储文档的数据结构,Field类定义了Document的一个域。

4.3 index Index包含了索引的读写类,例如对索引文件的segment进行写、合并、优化的IndexWriter类和对索引进行读取和删除操作的 IndexReader类,这里要注意的是不要被IndexReader这个名字误导,以为它是索引文件的读取类,实际上删除索引也是由它完成, IndexWriter只关心如何将索引写入一个个segment,并将它们合并优化;IndexReader则关注索引文件中各个文档的组织形式。

4.4 queryParser QueryParser包含了解析查询语句的类,lucene的查询语句和sql语句有点类似,有各种保留字,按照一定的语法可以组成各种查询。 Lucene有很多种Query类,它们都继承自Query,执行各种特殊的查询,QueryParser的作用就是解析查询语句,按顺序调用各种 Query类查找出结果。

4.5 search Search包含了从索引中搜索结果的各种类,例如刚才说的各种Query类,包括TermQuery、BooleanQuery等就在这个包里。

4.6 store Store包含了索引的存储类,例如Directory定义了索引文件的存储结构,FSDirectory为存储在文件中的索引,RAMDirectory为存储在内存中的索引,MmapDirectory为使用内存映射的索引。 4.7 util Util包含一些公共工具类,例如时间和字符串之间的转换工具。

5 如何建索引

5.1 最简单的能完成索引的代码片断

IndexWriter writer = new IndexWriter(“/data/index/”, new StandardAnalyzer(), true);

Document doc = new Document();

doc.add(new Field(“title”, “lucene introduction”, Field.Store.YES, Field.Index.TOKENIZED));

doc.add(new Field(“content”, “lucene works well”, Field.Store.YES, Field.Index.TOKENIZED));

writer.addDocument(doc);

writer.optimize();

writer.close();

下面我们分析一下这段代码。首先我们创建了一个writer,并指定存放索引的目录为“/data/index”,使用的分析器为 StandardAnalyzer,第三个参数说明如果已经有索引文件在索引目录下,我们将覆盖它们。然后我们新建一个document。我们向 document添加一个field,名字是“title”,内容是“lucene introduction”,对它进行存储并索引。再添加一个名字是“content”的field,内容是“lucene works well”,也是存储并索引。然后我们将这个文档添加到索引中,如果有多个文档,可以重复上面的操作,创建document并添加。添加完所有 document,我们对索引进行优化,优化主要是将多个segment合并到一个,有利于提高索引速度。随后将writer关闭,这点很重要。

对,创建索引就这么简单!当然你可能修改上面的代码获得更具个性化的服务。

5.2 将索引直接写在内存你需要首先创建一个RAMDirectory,并将其传给writer,代码如下:

Directory dir = new RAMDirectory();

IndexWriter writer = new IndexWriter(dir, new StandardAnalyzer(), true);

Document doc = new Document();

doc.add(new Field(“title”, “lucene introduction”, Field.Store.YES, Field.Index.TOKENIZED));

doc.add(new Field(“content”, “lucene works well”, Field.Store.YES, Field.Index.TOKENIZED));

writer.addDocument(doc);

writer.optimize();

writer.close();

5.3 索引文本文件如果你想把纯文本文件索引起来,而不想自己将它们读入字符串创建field,你可以用下面的代码创建field:

Field field = new Field(“content”, new FileReader(file));

这里的file就是该文本文件。该构造函数实际上是读去文件内容,并对其进行索引,但不存储。

6 如何维护索引索引的维护操作都是由IndexReader类提供。

6.1 如何删除索引 lucene提供了两种从索引中删除document的方法,一种是

void deleteDocument(int docNum)

这种方法是根据document在索引中的编号来删除,每个document加进索引后都会有个唯一编号,所以根据编号删除是一种精确删除,但是这个编号是索引的内部结构,一般我们不会知道某个文件的编号到底是几,所以用处不大。另一种是

void deleteDocuments(Term term)

这种方法实际上是首先根据参数term执行一个搜索操作,然后把搜索到的结果批量删除了。我们可以通过这个方法提供一个严格的查询条件,达到删除指定document的目的。下面给出一个例子:

Directory dir = FSDirectory.getDirectory(PATH, false);

IndexReader reader = IndexReader.open(dir);

Term term = new Term(field, key);

reader.deleteDocuments(term);

reader.close();

6.2 如何更新索引 lucene并没有提供专门的索引更新方法,我们需要先将相应的document删除,然后再将新的document加入索引。例如:

Directory dir = FSDirectory.getDirectory(PATH, false);

IndexReader reader = IndexReader.open(dir);

Term term = new Term(“title”, “lucene introduction”);

reader.deleteDocuments(term); reader.close();

IndexWriter writer = new IndexWriter(dir, new StandardAnalyzer(), true);

Document doc = new Document();

doc.add(new Field(“title”, “lucene introduction”, Field.Store.YES, Field.Index.TOKENIZED));

doc.add(new Field(“content”, “lucene is funny”, Field.Store.YES, Field.Index.TOKENIZED));

writer.addDocument(doc);

writer.optimize();

writer.close();

7 如何搜索 lucene的搜索相当强大,它提供了很多辅助查询类,每个类都继承自Query类,各自完成一种特殊的查询,你可以像搭积木一样将它们任意组合使用,完成一些复杂操作;另外lucene还提供了Sort类对结果进行排序,提供了Filter类对查询条件进行限制。你或许会不自觉地拿它跟SQL语句进行比较:“lucene能执行and、or、order by、where、like ‘%xx%’操作吗?”回答是:“当然没问题!”

7.1 各种各样的Query 下面我们看看lucene到底允许我们进行哪些查询操作:

7.1.1 TermQuery 首先介绍最基本的查询,如果你想执行一个这样的查询:“在content域中包含‘lucene’的document”,那么你可以用TermQuery:

Term t = new Term(“content”, ” lucene”;

Query query = new TermQuery(t);

7.1.2 BooleanQuery 如果你想这么查询:“在content域中包含java或perl的document”,那么你可以建立两个TermQuery并把它们用BooleanQuery连接起来:

TermQuery termQuery1 = new TermQuery(new Term(“content”, “java”);

TermQuery termQuery 2 = new TermQuery(new Term(“content”, “perl”);

BooleanQuery booleanQuery = new BooleanQuery();

booleanQuery.add(termQuery 1, BooleanClause.Occur.SHOULD);

booleanQuery.add(termQuery 2, BooleanClause.Occur.SHOULD);

7.1.3 WildcardQuery 如果你想对某单词进行通配符查询,你可以用WildcardQuery,通配符包括’?’匹配一个任意字符和’*’匹配零个或多个任意字符,例如你搜索’use*’,你可能找到’useful’或者’useless’:

Query query = new WildcardQuery(new Term(“content”, “use*”);

7.1.4 PhraseQuery 你可能对中日关系比较感兴趣,想查找‘中’和‘日’挨得比较近(5个字的距离内)的文章,超过这个距离的不予考虑,你可以:

PhraseQuery query = new PhraseQuery();

query.setSlop(5);

query.add(new Term(“content “, “中”));

query.add(new Term(“content”, “日”));

那么它可能搜到“中日合作……”、“中方和日方……”,但是搜不到“中国某高层领导说日本欠扁”。

7.1.5 PrefixQuery 如果你想搜以‘中’开头的词语,你可以用PrefixQuery:

PrefixQuery query = new PrefixQuery(new Term(“content “, “中”);

7.1.6 FuzzyQuery FuzzyQuery用来搜索相似的term,使用Levenshtein算法。假设你想搜索跟‘wuzza’相似的词语,你可以:

Query query = new FuzzyQuery(new Term(“content”, “wuzza”);

你可能得到‘fuzzy’和‘wuzzy’。

7.1.7 RangeQuery 另一个常用的Query是RangeQuery,你也许想搜索时间域从20060101到20060130之间的document,你可以用RangeQuery:

RangeQuery query = new RangeQuery(new Term(“time”, “20060101”), new Term(“time”, “20060130”), true);

最后的true表示用闭合区间。

7.2 QueryParser 看了这么多Query,你可能会问:“不会让我自己组合各种Query吧,太麻烦了!”当然不会,lucene提供了一种类似于SQL语句的查询语句,我们姑且叫它lucene语句,通过它,你可以把各种查询一句话搞定,lucene会自动把它们查分成小块交给相应Query执行。下面我们对应每种 Query演示一下: TermQuery可以用“field:key”方式,例如“content:lucene”。 BooleanQuery中‘与’用‘+’,‘或’用‘ ’,例如“content:java contenterl”。 WildcardQuery仍然用‘?’和‘*’,例如“content:use*”。 PhraseQuery用‘~’,例如“content:”中日”~5”。 PrefixQuery用‘*’,例如“中*”。 FuzzyQuery用‘~’,例如“content: wuzza ~”。 RangeQuery用‘[]’或‘{}’,前者表示闭区间,后者表示开区间,例如“time:[20060101 TO 20060130]”,注意TO区分大小写。你可以任意组合query string,完成复杂操作,例如“标题或正文包括lucene,并且时间在20060101到20060130之间的文章”可以表示为:“+ (title:lucene content:lucene) +time:[20060101 TO 20060130]”。代码如下:

Directory dir = FSDirectory.getDirectory(PATH, false);

IndexSearcher is = new IndexSearcher(dir);

QueryParser parser = new QueryParser(“content”, new StandardAnalyzer());

Query query = parser.parse(“+(title:lucene content:lucene) +time:[20060101 TO 20060130]“;

Hits hits = is.search(query);

for (int i = 0; i < hits.length(); i++)

{

        Document doc = hits.doc(i);

        System.out.println(doc.get(“title”);

}

 is.close();

首先我们创建一个在指定文件目录上的IndexSearcher。然后创建一个使用StandardAnalyzer作为分析器的 QueryParser,它默认搜索的域是content。接着我们用QueryParser来parse查询字串,生成一个Query。然后利用这个 Query去查找结果,结果以Hits的形式返回。这个Hits对象包含一个列表,我们挨个把它的内容显示出来。

7.3 Filter filter的作用就是限制只查询索引的某个子集,它的作用有点像SQL语句里的where,但又有区别,它不是正规查询的一部分,只是对数据源进行预处理,然后交给查询语句。注意它执行的是预处理,而不是对查询结果进行过滤,所以使用filter的代价是很大的,它可能会使一次查询耗时提高一百倍。最常用的filter是RangeFilter和QueryFilter。RangeFilter是设定只搜索指定范围内的索引;QueryFilter是在上次查询的结果中搜索。 Filter的使用非常简单,你只需创建一个filter实例,然后把它传给searcher。继续上面的例子,查询“时间在20060101到 20060130之间的文章”除了将限制写在query string中,你还可以写在RangeFilter中:

Directory dir = FSDirectory.getDirectory(PATH, false);

IndexSearcher is = new IndexSearcher(dir);

QueryParser parser = new QueryParser(“content”, new StandardAnalyzer());

Query query = parser.parse(“title:lucene content:lucene”);

RangeFilter filter = new RangeFilter(“time”, “20060101″, “20060230″, true, true);

Hits hits = is.search(query, filter);

for (int i = 0; i < hits.length(); i++)

{

     Document doc = hits.doc(i);

     System.out.println(doc.get(“title”);

}

is.close();

7.4 Sort 有时你想要一个排好序的结果集,就像SQL语句的“order by”,lucene能做到:通过Sort。 Sort sort = new Sort(“time”); //相当于SQL的“order by time” Sort sort = new Sort(“time”, true); // 相当于SQL的“order by time desc” 下面是一个完整的例子:

Directory dir = FSDirectory.getDirectory(PATH, false);

IndexSearcher is = new IndexSearcher(dir);

QueryParser parser = new QueryParser(“content”, new StandardAnalyzer());

Query query = parser.parse(“title:lucene content:lucene”);

RangeFilter filter = new RangeFilter(“time”, “20060101″, “20060230″, true, true);

Sort sort = new Sort(“time”);

Hits hits = is.search(query, filter, sort);

for (int i = 0; i < hits.length(); i++)

{

      Document doc = hits.doc(i);

      System.out.println(doc.get(“title”);

}

is.close();

8 分析器在前面的概念介绍中我们已经知道了分析器的作用,就是把句子按照语义切分成一个个词语。英文切分已经有了很成熟的分析器: StandardAnalyzer,很多情况下StandardAnalyzer是个不错的选择。甚至你会发现StandardAnalyzer也能对中文进行分词。但是我们的焦点是中文分词,StandardAnalyzer能支持中文分词吗?实践证明是可以的,但是效果并不好,搜索“如果”会把“牛奶不如果汁好喝”也搜索出来,而且索引文件很大。那么我们手头上还有什么分析器可以使用呢?core里面没有,我们可以在sandbox里面找到两个: ChineseAnalyzer和CJKAnalyzer。但是它们同样都有分词不准的问题。相比之下用StandardAnalyzer和 ChineseAnalyzer建立索引时间差不多,索引文件大小也差不多,CJKAnalyzer表现会差些,索引文件大且耗时比较长。要解决问题,首先分析一下这三个分析器的分词方式。StandardAnalyzer和ChineseAnalyzer都是把句子按单个字切分,也就是说 “牛奶不如果汁好喝”会被它们切分成“牛 奶 不 如 果 汁 好 喝”;而CJKAnalyzer则会切分成“牛奶 奶不 不如 如果 果汁汁好好喝”。这也就解释了为什么搜索“果汁”都能匹配这个句子。以上分词的缺点至少有两个:匹配不准确和索引文件大。我们的目标是将上面的句子分解成“牛奶 不如果汁好喝”。这里的关键就是语义识别,我们如何识别“牛奶”是一个词而“奶不”不是词语?我们很自然会想到基于词库的分词法,也就是我们先得到一个词库,里面列举了大部分词语,我们把句子按某种方式切分,当得到的词语与词库中的项匹配时,我们就认为这种切分是正确的。这样切词的过程就转变成匹配的过程,而匹配的方式最简单的有正向最大匹配和逆向最大匹配两种,说白了就是一个从句子开头向后进行匹配,一个从句子末尾向前进行匹配。基于词库的分词词库非常重要,词库的容量直接影响搜索结果,在相同词库的前提下,据说逆向最大匹配优于正向最大匹配。当然还有别的分词方法,这本身就是一个学科,我这里也没有深入研究。回到具体应用,我们的目标是能找到成熟的、现成的分词工具,避免重新发明车轮。经过网上搜索,用的比较多的是中科院的ICTCLAS和一个不开放源码但是免费的JE-Analysis。ICTCLAS有个问题是它是一个动态链接库, java调用需要本地方法调用,不方便也有安全隐患,而且口碑也确实不大好。JE-Analysis效果还不错,当然也会有分词不准的地方,相比比较方便放心。

9 性能优化一直到这里,我们还是在讨论怎么样使lucene跑起来,完成指定任务。利用前面说的也确实能完成大部分功能。但是测试表明lucene的性能并不是很好,在大数据量大并发的条件下甚至会有半分钟返回的情况。另外大数据量的数据初始化建立索引也是一个十分耗时的过程。那么如何提高lucene的性能呢?下面从优化创建索引性能和优化搜索性能两方面介绍。

9.1 优化创建索引性能这方面的优化途径比较有限,IndexWriter提供了一些接口可以控制建立索引的操作,另外我们可以先将索引写入 RAMDirectory,再批量写入FSDirectory,不管怎样,目的都是尽量少的文件IO,因为创建索引的最大瓶颈在于磁盘IO。另外选择一个较好的分析器也能提高一些性能。

9.1.1 通过设置IndexWriter的参数优化索引建立 setMaxBufferedDocs(int maxBufferedDocs) 控制写入一个新的segment前内存中保存的document的数目,设置较大的数目可以加快建索引速度,默认为10。 setMaxMergeDocs(int maxMergeDocs) 控制一个segment中可以保存的最大document数目,值较小有利于追加索引的速度,默认Integer.MAX_VALUE,无需修改。 setMergeFactor(int mergeFactor) 控制多个segment合并的频率,值较大时建立索引速度较快,默认是10,可以在建立索引时设置为100。

9.1.2 通过RAMDirectory缓写提高性能我们可以先把索引写入RAMDirectory,达到一定数量时再批量写进FSDirectory,减少磁盘IO次数。

FSDirectory fsDir = FSDirectory.getDirectory(“/data/index”, true);

RAMDirectory ramDir = new RAMDirectory();

IndexWriter fsWriter = new IndexWriter(fsDir, new StandardAnalyzer(), true);

IndexWriter ramWriter = new IndexWriter(ramDir, new StandardAnalyzer(), true);

while (there are documents to index)

{

… create Document … ramWriter.addDocument(doc);

      if (condition for flushing memory to disk has been met)

     {

               fsWriter.addIndexes(new Directory[] { ramDir });

              ramWriter.close();

             ramWriter = new IndexWriter(ramDir, new StandardAnalyzer(), true);

     }

}

9.1.3 选择较好的分析器这个优化主要是对磁盘空间的优化,可以将索引文件减小将近一半,相同测试数据下由600M减少到380M。但是对时间并没有什么帮助,甚至会需要更长时间,因为较好的分析器需要匹配词库,会消耗更多cpu,测试数据用StandardAnalyzer耗时133分钟;用 MMAnalyzer耗时150分钟。

9.2 优化搜索性能虽然建立索引的操作非常耗时,但是那毕竟只在最初创建时才需要,平时只是少量的维护操作,更何况这些可以放到一个后台进程处理,并不影响用户搜索。我们创建索引的目的就是给用户搜索,所以搜索的性能才是我们最关心的。下面就来探讨一下如何提高搜索性能。

9.2.1 将索引放入内存这是一个最直观的想法,因为内存比磁盘快很多。Lucene提供了RAMDirectory可以在内存中容纳索引:

Directory fsDir = FSDirectory.getDirectory(“/data/index/”, false);

Directory ramDir = new RAMDirectory(fsDir);

Searcher searcher = new IndexSearcher(ramDir);

但是实践证明RAMDirectory和FSDirectory速度差不多,当数据量很小时两者都非常快,当数据量较大时(索引文件 400M)RAMDirectory甚至比FSDirectory还要慢一点,这确实让人出乎意料。而且lucene的搜索非常耗内存,即使将400M的索引文件载入内存,在运行一段时间后都会out of memory,所以个人认为载入内存的作用并不大。

9.2.2 优化时间范围限制既然载入内存并不能提高效率,一定有其它瓶颈,经过测试发现最大的瓶颈居然是时间范围限制,那么我们可以怎样使时间范围限制的代价最小呢?当需要搜索指定时间范围内的结果时,可以: 1、用RangeQuery,设置范围,但是RangeQuery的实现实际上是将时间范围内的时间点展开,组成一个个BooleanClause加入到 BooleanQuery中查询,因此时间范围不可能设置太大,经测试,范围超过一个月就会抛BooleanQuery.TooManyClauses,可以通过设置 BooleanQuery.setMaxClauseCount(int maxClauseCount)扩大,但是扩大也是有限的,并且随着maxClauseCount扩大,占用内存也扩大 2、用RangeFilter代替RangeQuery,经测试速度不会比RangeQuery慢,但是仍然有性能瓶颈,查询的90%以上时间耗费在 RangeFilter,研究其源码发现RangeFilter实际上是首先遍历所有索引,生成一个BitSet,标记每个document,在时间范围内的标记为true,不在的标记为false,然后将结果传递给Searcher查找,这是十分耗时的。 3、进一步提高性能,这个又有两个思路: a、缓存Filter结果。既然RangeFilter的执行是在搜索之前,那么它的输入都是一定的,就是IndexReader,而 IndexReader是由Directory决定的,所以可以认为RangeFilter的结果是由范围的上下限决定的,也就是由具体的 RangeFilter对象决定,所以我们只要以RangeFilter对象为键,将filter结果BitSet缓存起来即可。lucene API已经提供了一个CachingWrapperFilter类封装了Filter及其结果,所以具体实施起来我们可以cache CachingWrapperFilter对象,需要注意的是,不要被CachingWrapperFilter的名字及其说明误导, CachingWrapperFilter看起来是有缓存功能,但的缓存是针对同一个filter的,也就是在你用同一个filter过滤不同 IndexReader时,它可以帮你缓存不同IndexReader的结果,而我们的需求恰恰相反,我们是用不同filter过滤同一个 IndexReader,所以只能把它作为一个封装类。 b、降低时间精度。研究Filter的工作原理可以看出,它每次工作都是遍历整个索引的,所以时间粒度越大,对比越快,搜索时间越短,在不影响功能的情况下,时间精度越低越好,有时甚至牺牲一点精度也值得,当然最好的情况是根本不作时间限制。下面针对上面的两个思路演示一下优化结果(都采用800线程随机关键词随即时间范围):第一组,时间精度为秒:方式 直接用RangeFilter 使用cache 不用filter 平均每个线程耗时 10s 1s 300ms

第二组,时间精度为天方式 直接用RangeFilter 使用cache 不用filter 平均每个线程耗时 900ms 360ms 300ms

由以上数据可以得出结论: 1、 尽量降低时间精度,将精度由秒换成天带来的性能提高甚至比使用cache还好,最好不使用filter。 2、 在不能降低时间精度的情况下,使用cache能带了10倍左右的性能提高。

9.2.3 使用更好的分析器这个跟创建索引优化道理差不多,索引文件小了搜索自然会加快。当然这个提高也是有限的。较好的分析器相对于最差的分析器对性能的提升在20%以下。

10 一些经验

10.1关键词区分大小写 or AND TO等关键词是区分大小写的,lucene只认大写的,小写的当做普通单词。

10.2 读写互斥性同一时刻只能有一个对索引的写操作,在写的同时可以进行搜索

10.3 文件锁在写索引的过程中强行退出将在tmp目录留下一个lock文件,使以后的写操作无法进行,可以将其手工删除

10.4 时间格式 lucene只支持一种时间格式yyMMddHHmmss,所以你传一个yy-MM-dd HH:mm:ss的时间给lucene它是不会当作时间来处理的

10.5 设置boost 有些时候在搜索时某个字段的权重需要大一些,例如你可能认为标题中出现关键词的文章比正文中出现关键词的文章更有价值,你可以把标题的boost设置的更大,那么搜索结果会优先显示标题中出现关键词的文章(没有使用排序的前题下)。使用方法: Field. setBoost(float boost);默认值是1.0,也就是说要增加权重的需要设置得比1大。

from:http://blog.sina.com.cn/s/blog_681316550100imbl.html

1 129 130 131 175