<?xml version="1.0" encoding="UTF-8" ?>
<rss version="2.0">
<channel>
<title><![CDATA[四季如風]]></title> 
<link>http://blog.lough.com.cn/index.php</link> 
<description><![CDATA[學習筆記，每一天我們都在進步。]]></description> 
<language>zh-cn</language> 
<copyright><![CDATA[四季如風]]></copyright>
<item>
<link>http://blog.lough.com.cn/post/242/</link>
<title><![CDATA[復原 Windows MBR 的各種方式]]></title> 
<author>jock &lt;jockhuang@gmail.com&gt;</author>
<category><![CDATA[技术相关]]></category>
<pubDate>Mon, 26 May 2008 02:22:18 +0000</pubDate> 
<guid>http://blog.lough.com.cn/post/242/</guid> 
<description>
<![CDATA[ 
	玩 Linux 最常見的方式就是切 Partition 做多重開機, 讓 GRUB 寫入 MBR 做開機選單. 但是若不小心把 Linux Partition 刪掉了, 或是哪天不想玩 Linux 了, 想恢復為 Windows 單一作業環境的時候, 就得把 Windows Pre-Boot 程式寫回 MBR 的前 446 Bytes 喔!<br/><br/>方法一：使用 DOS / Win9x / ME 開機片開機<br/><br/>開機後在 MS-DOS 模式下執行 fdisk /mbr<br/>重新啟動電腦即可<br/><br/><br/>方法二：使用 Windows 2000 / XP 光碟片開機 (過程中需要 Administrator 密碼)<br/><br/>在 Welcome to Setup (歡迎使用安裝程式) 畫面中按 R 或 F10 進入「Windows 修復主控台」<br/><br/>進入主控台後, 輸入 fixmbr<br/>重新啟動電腦即可<br/><br/>詳細操作過程請參考微軟官方網頁 - Windows XP 修復主控台的說明<br/><br/><br/>方法三：使用 Linux 光碟片開機, 進入 Rescue 模式; 或用 KNOPPIX 之類的 Linux 作業系統開機 (過程中請啟用網路)<br/><br/>依序輸入以下指令恢復 Windows MBR<br/><br/>wget ftp://cha.homeip.net/ms.mbr -O /tmp/ms.mbr<br/>dd if=/tmp/ms.mbr of=/dev/hda bs=446 count=1<br/>exit (重開機時請移除光碟片)<br/><br/>以上操作結果同 fdisk /mbr <br/>網路上流傳 dd if=/dev/zero of=/dev/hda bs=446 count=1 效果等同 fdisk /mbr 是錯誤的, 這樣只會清掉 MBR 的 Pre-Boot 內容而已. <br/>警告: 請勿於頭腦混沌、神智不清時使用 dd 指令; 使用時請特別注意數字是否正確! <br/><br/>方法四：什麼開機片都沒有, 但開機後還有 GRUB> 提示符號<br/><br/>依序輸入以下指令啟動 Windows<br/><br/>rootnoverify (hd0,0)<br/>makeactive<br/>chainloader +1<br/>boot<br/><br/>(hd0,0) 其中 hd0 表示 Primary IDE Master HDD, 0 表示第一個分割區, 請依實際狀況套用. <br/>注意: 這只是解決 Windows 的啟動問題而已, 仍要靠前三種方法的其中一種才能讓 MBR 恢復正常喔! 當然, Windows 開機後您也可以上網尋找其他的解決方案 :) <br/>
]]>
</description>
</item><item>
<link>http://blog.lough.com.cn/post/241/</link>
<title><![CDATA[Apache中URL的Rewrite 在jsp上的应用]]></title> 
<author>jock &lt;jockhuang@gmail.com&gt;</author>
<category><![CDATA[技术相关]]></category>
<pubDate>Tue, 26 Feb 2008 08:03:36 +0000</pubDate> 
<guid>http://blog.lough.com.cn/post/241/</guid> 
<description>
<![CDATA[ 
	Apache中URL的Rewrite<br/>现在讲究面向搜索引擎的开发。当我们使用jsp时，搜索引擎的收藏效果不是很好，所以我们可以转义这样的URL。比如：我们可以借助rewrite使用/blog/user/0.html来访问/blog/user/blog_show.jsp?userid＝0使搜索引擎更容易搜集到我们的网站信息。具体配置很简单，介绍如下<br/><br/>首先：还是打开%Apache2%&#92;conf文件夹，找到http.conf文件，搜索rewrite找到#LoadModule rewrite_module modules/mod_rewrite.so，将它前面的#去掉。<br/>然后在你要使用的VirtualHost中加入<br/><br/>RewriteEngine&nbsp;&nbsp; on<br/>＃打开rewrite引擎<br/>RewriteLog&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; logs/rewrite.log<br/>＃rewrite日志，可选配置，建议使用<br/>RewriteLogLevel 3<br/>＃rewrite日志等级，默认为0不记录，所以只要使用日志，就要配置，3就全记录了，可以更大但没有什么意义。<br/>RewriteRule&nbsp;&nbsp;&nbsp;&nbsp; /blog/user/(.+)&#92;.html$ /blog/user/blog_show.jsp?userid=$1&nbsp;&nbsp;[PT ]<br/>＃转义的规则，使用正则表达式（我也不熟，自己去研究吧），我上面的例子是将/blog/user/*.html的URL转义成/blog/user.blog_show.jsp?userid=*。<br/>注意后面的 PT 这是保证你这样转义之后Apache能将它转给Tomcat否则Apache会自己处理这个链接，造成什么也得不到。<br/>注意 PT ，[R]的区别，使用 PT 在客户地址栏的显示还是*.html,如果将 PT 换成[R]则地址栏也会显示成.jsp<br/><br/>Tags - <a href="http://blog.lough.com.cn/tags/%25E9%2585%258D%25E7%25BD%25AE/" rel="tag">配置</a> , <a href="http://blog.lough.com.cn/tags/apache/" rel="tag">apache</a>
]]>
</description>
</item><item>
<link>http://blog.lough.com.cn/post/239/</link>
<title><![CDATA[用Lucene实现在检索结果中再检索]]></title> 
<author>jock &lt;jockhuang@gmail.com&gt;</author>
<category><![CDATA[Lucene]]></category>
<pubDate>Tue, 29 Jan 2008 08:37:45 +0000</pubDate> 
<guid>http://blog.lough.com.cn/post/239/</guid> 
<description>
<![CDATA[ 
	Lucene是可以做到的，利用lucene的Filter，具体可以查看lucene的api中的org.apache.lucene.search.CachingWrapperFilter，它可以缓存上次的搜索结果，从而实现在结果中的搜索。<br/><br/>测试实例：<br/>package com.wsjava;<br/>import java.io.IOException;<br/>import org.apache.lucene.analysis.SimpleAnalyzer;<br/>import org.apache.lucene.document.Document;<br/>import org.apache.lucene.document.Field;<br/>import org.apache.lucene.index.IndexWriter;<br/>import org.apache.lucene.queryParser.ParseException;<br/>import org.apache.lucene.queryParser.QueryParser;<br/>import org.apache.lucene.search.CachingWrapperFilter;<br/>import org.apache.lucene.search.Filter;<br/>import org.apache.lucene.search.Hits;<br/>import org.apache.lucene.search.IndexSearcher;<br/>import org.apache.lucene.search.Query;<br/>import org.apache.lucene.search.QueryFilter;<br/><br/>public class IndexTest &#123;<br/><br/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;/**<br/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; * @param args<br/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; * @throws ParseException <br/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; * @throws IOException <br/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; */<br/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;public static void main(String[] args) throws IOException, ParseException &#123;<br/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;index();<br/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;search("day"); //简单搜索<br/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;searchInResult("day", "you"); //在结果集中搜索<br/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&#125;<br/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<br/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;public static void index() throws IOException &#123;<br/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;IndexWriter writer = new IndexWriter("d:/tesindex",new SimpleAnalyzer(), true);<br/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;writer.setMaxMergeDocs(1000);<br/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;writer.setMergeFactor(100);<br/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;for (int i = 0; i < 10; i++) &#123;<br/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;Document doc = new Document();<br/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;String content = "How do you do?";<br/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;if (i >= 5) &#123;<br/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;content = "What's a good day. ";<br/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&#125;<br/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;if (i >= 7) &#123;<br/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;content = "Nice day. Thanks you!";<br/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&#125;<br/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;doc.add(new Field("content", content, Field.Store.YES,Field.Index.TOKENIZED));<br/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;writer.addDocument(doc);<br/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&#125;<br/><br/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&#125;<br/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<br/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;//简单实现对qw的搜索.<br/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;public static void search(String qw) throws IOException, ParseException &#123;<br/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;QueryParser queryParser = new QueryParser("content",new SimpleAnalyzer());<br/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;Query query = queryParser.parse(qw.trim());<br/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;QueryFilter filter = new QueryFilter(query);<br/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<br/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;search(query, filter);<br/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&#125;<br/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<br/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;//在搜索oldqw的结果集中搜索qw.<br/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;public static void searchInResult(String qw, String oldqw) throws ParseException, IOException &#123;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<br/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;QueryParser queryParser = new QueryParser("content",new SimpleAnalyzer());<br/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;Query query = queryParser.parse(qw.trim());<br/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;Query oldQuery = queryParser.parse(oldqw.trim());<br/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;QueryFilter oldFilter = new QueryFilter(oldQuery);<br/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;CachingWrapperFilter filter = new CachingWrapperFilter(oldFilter);<br/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<br/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;search(query, filter);<br/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&#125;<br/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<br/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;private static void search(Query query, Filter filter) throws IOException, ParseException &#123;<br/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;IndexSearcher ins = new IndexSearcher("d:/tesindex");<br/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;Hits hits = ins.search(query, filter);<br/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;for (int i = 0; i < hits.length(); i++) &#123;<br/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;Document doc = hits.doc(i);<br/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;System.out.println(doc.get("content"));<br/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&#125;<br/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;System.out.println();<br/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&#125;<br/>&#125;<br/><br/>上面是简单的测试程序。当然在实际应用中可以做得比较复杂。<br/>Tags - <a href="http://blog.lough.com.cn/tags/lucene/" rel="tag">lucene</a> , <a href="http://blog.lough.com.cn/tags/%25E6%25BA%2590%25E7%25A0%2581/" rel="tag">源码</a>
]]>
</description>
</item><item>
<link>http://blog.lough.com.cn/post/238/</link>
<title><![CDATA[博弈论中的囚徒问题]]></title> 
<author>jock &lt;jockhuang@gmail.com&gt;</author>
<category><![CDATA[技术相关]]></category>
<pubDate>Wed, 02 Jan 2008 10:34:09 +0000</pubDate> 
<guid>http://blog.lough.com.cn/post/238/</guid> 
<description>
<![CDATA[ 
	“囚徒困境”最早是由美国普林斯顿大学数学家曾克1950年提出来的。他当时编了一个故事向斯坦福大学的一群心理学家们解释什么是博弈论，这个故事后来成为博弈论中最著名的案例。故事内容是：两个嫌疑犯(A和B)作案后被警察抓住，隔离审讯；警方的政策是“坦白从宽，抗拒从严”，如果两人都坦白则各判8年；如果一人坦白另一人不坦白，坦白的放出去，不坦白的判10年；如果都不坦白则因证据不足各判1年。在这里，博弈者就是两个嫌疑犯，他们每个人都有两个选择，即坦白和不坦白。 <br/>&nbsp;&nbsp;&nbsp;&nbsp;最终结果是A和B均选择了坦白。这是因为，假定A选择坦白的话，B最好是选择坦白，因为B坦白判8年而抵赖却要判10年；假定A选择抵赖的话，B最好还是选择坦白，因为B坦白不被判刑而抵赖却要被判刑1年。即是说，不管A坦白或抵赖，B的最佳选择都是坦白。反过来也一样，不管B是坦白还是抵赖，A的最佳选择也是坦白。结果，两个人都选择了坦白，各被判刑8年。在(坦白、坦白)这个组合中，A和B都不能通过单方面的改变行动增加自己的收益，于是谁也没有动力游离这个组合，因此这个组合叫做纳什均衡。 <br/>&nbsp;&nbsp;&nbsp;&nbsp;囚徒困境所反映出的深刻问题是，人类的个人理性有时能导致集体的非理性——聪明的人类会因自己的聪明而作茧自缚。
]]>
</description>
</item><item>
<link>http://blog.lough.com.cn/post/237/</link>
<title><![CDATA[如何把lucene索引放在内存中提供查询服务]]></title> 
<author>jock &lt;jockhuang@gmail.com&gt;</author>
<category><![CDATA[Lucene]]></category>
<pubDate>Wed, 02 Jan 2008 10:29:43 +0000</pubDate> 
<guid>http://blog.lough.com.cn/post/237/</guid> 
<description>
<![CDATA[ 
	200W以下的数据量全部加载到内存最简单的方式是修改Lucene（1.9版本）源码 org.apache.lucene.index.IndexReader文件的第127行 将<br/>return open(FSDirectory.getDirectory(path, false), true);<br/>修改为<br/>return open(new RAMDirectory(FSDirectory.getDirectory(path, false)), true);<br/><br/>将133行的 <br/>return open(FSDirectory.getDirectory(path, false), true);<br/>修改为<br/>return open(new RAMDirectory( FSDirectory.getDirectory(path, false)), true);<br/>这样就可以在查询的时候将所有索引一次加载到内存，查询响应时间即使是第一次查询，也会小于0.1秒，但仅适用于索引文档数量不超过200W个，并且平均的页面大小不超过10K，文献检索系统 ，这种方式尤其适合。<br/>创建索引的时候也能够使用 RAMDirectory ， 比FSDirectory 要快很多 。<br/><br/>令一种方法：<br/><br/>FSDirectory directory = FSDirectory.getDirectory(args[0], false);&nbsp;&nbsp; 1<br/><br/>RAMDirectory directory = new RAMDirectory(args[0]);&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; 2<br/><br/>用2代替1即可<br/><br/><br/>Tags - <a href="http://blog.lough.com.cn/tags/%25E6%25BA%2590%25E7%25A0%2581/" rel="tag">源码</a> , <a href="http://blog.lough.com.cn/tags/lucene/" rel="tag">lucene</a>
]]>
</description>
</item><item>
<link>http://blog.lough.com.cn/post/236/</link>
<title><![CDATA[Inside Lucene/超人气搜索引擎学习]]></title> 
<author>jock &lt;jockhuang@gmail.com&gt;</author>
<category><![CDATA[Lucene]]></category>
<pubDate>Wed, 02 Jan 2008 10:27:49 +0000</pubDate> 
<guid>http://blog.lough.com.cn/post/236/</guid> 
<description>
<![CDATA[ 
	转：http://www.lucene.org.cn/read.php?tid=65<br/><搜索引擎的典型周期: 搜集数据->建立索引->应答搜索请求><br/><br/>无 论有多少精彩的应用, 这个印刷时代就诞生的公式都不会作废, 公式中最关键的成分是 1. 数据, 2.算法. 虽然二者中谁对结果质量更重要依然引起争论, 我的精力并未放在这两者上. 关于spider已有数本出名的专著, 算法原理的本质也早不是秘密, 这是搜索引擎必备的条件, 是基础设备而不是制胜的杀手锏. 面对一个活生生的搜索引擎, 研究这两者就像面对解剖台上的小白鼠, 却放下手术刀去查上课用的内脏图解. 我只注意实现, 实现是商业效率的体现, 这才是真刀真枪狼烟四起的地方.<br/><br/>抛开数据采集, 上边的周期分成两个相(phase), 1. Indexing(建立索引), 2.Searching(搜索). 还有一个现在看来显得古怪的词-检索, 图书馆系统大多用这个词. 中国人的词汇总是显得睿智, “索”本有求取寻找的意思, "索引"原本就是为搜索创造的. 那些题目是"倒排索引原理"大篇幅对索引和模式匹配进行比较的文章, 因此有些让人哭笑不得. 上述的两个phase正是lucene覆盖的范围(Lucene核心不提供crawler), 这两个phase一个用于生成索引(index), 一个用于从index取出数据, 可以看出Lucene的一切行为都和index有关. 幸好Lucene官方指南 Lucene in Action 在附录中用浅显的语言介绍了index的结构, 这让人了解Lucene怎么把一片片文档塞进去, 就像绞肉机把一片片五花肉里脊肉搅成肉酱. 但是有点遗憾的感觉: 搜索过程怎么把index中支离破碎的数据记录(index的结构用法着实很像数据库)恢复成检索结果? 就像主妇们怎么用屠夫的肉酱作出香喷喷的肉丸?<br/><br/>我从Query 的构造开始, 详细检验了Lucene处理Query的过程. 这种努力使我了解到index中的记录(就是数据库中的record概念), 如何在检索过程中起作用. 这是深入把握Lucene实现的基础, 若要对Lucene作基础的调整和改动, 自然缺不了这一步(这是我的动机之一). 这种探索也帮助我了解原始文档资料的各种属性如何决定搜索结果, 进一步的研究可以揭示出文档各属性的重要性以及文档对查询条件的敏感性, 从这里出发可以提炼出更普遍的原理, 再结合通用的Search Engine原理, 把"Search Engine是什么, 长什么样"深深地刻在脑海中. 这对我是一次很好的搜索引擎进阶的途径. 接下来的就是所谓SE优化( Search Engine Optimizing ), 不敢讲这种天怒人怨的话题...<br/><br/><br/>查询机制<br/>TAG：framework_lucene<br/>Searching with TermQuery 查询机制<br/>任 何用户, 包括系统开发者, 使用搜索引擎的共同方式只有一个: 查询(query). 整个搜索过程的目的是为了满足查询要求, 搜索过程是由查询贯穿的. 若没有指定查询, 而是从索引(index)的内容出发, "搜索"将是漫无目的且毫无意义的过程. 搜索过程的起点只能是索引.<br/><br/>以下引用自Lucene in Action 的入门章节, 在其中能看到Query是如何用来启动查询的.<br/><br/>CODE:<br/><br/>public class BasicSearchingTest extends LiaTestCase &#123;<br/>public void testTerm() throws Exception &#123;<br/>IndexSearcher searcher = new IndexSearcher(directory);<br/>Term t = new Term(“subject”, “ant”);<br/>Query query = new TermQuery(t);<br/>Hits hits = searcher.search(query);<br/>assertEquals(“JDwA”, 1, hits.length());<br/><br/>t = new Term(“subject”, “junit”);<br/>hits = searcher.search(new TermQuery(t));<br/>assertEquals(2, hits.length());<br/><br/>searcher.close();<br/><br/>&#125;<br/>&#125;<br/>被 称为面向对象设计经典范例的Lucene, 其架构确实能反映"查询"行为在真实世界模式, 这不愧是OOA/OOD方法带来的成熟设计. 这也让我对Lucene设计者的软件设计能力产生充足的敬佩, 要知道人家可是研究算法的. 通常搞学术和搞工程的人思想极不统一, 我同学在微软工作时曾经为研究院和工程院的代码差异头疼不已. Lucene的设计者们能把算法和代码集合得如此完美, 可以说是牛中之牛了.<br/><br/>我真正关心的是搜索算法如何依据Query导出查询结果. 上面的代码给我一些启示, 我知道了起点, 从searcher.search(query)开始, 我可以一步步了解Query在搜索过程里的作用机制.<br/>为 了满足真实世界的语义, Lucene提供了众多Query. 上面代码中的TermQuery是最简单的Query, 日常搜索有许多直接或间就由TermQuery组合而成. search(termQuery)的构造最基础, 不经过繁琐的转换. 所以我从TermQuery出发, 一步步考察搜索的核心机制.<br/><br/>进入search 方法前, 我了解了这些限制条件: TermQuery的语义中每一个Term指一个(field, keyword)对, 其描述的查询条件是: "在指定的字段(标题、作者、内容...)中出现指定的keyword"; 高级的search方法可以处理自定义的Filter, Sort, 此处的考察对象是不假这些自定义选项的最简单的search.<br/><br/>有了以上的限制, 我考察的对象--TermQuery查询过程已经被彻底简化了. 但在他和更复杂的重载方法中, 开发人员应用了相同的思路, 举一反三是我们可以期望做到的.<br/><br/>CODE:<br/><br/>public class IndexSearcher extends Searcher &#123;<br/>public TopDocs search(Query query, Filter filter, final int nDocs)<br/>&nbsp;&nbsp;&nbsp;&nbsp;throws IOException &#123;<br/>Scorer scorer = query.weight(this).scorer(reader);<br/>if (scorer == null)<br/>&nbsp;&nbsp;return new TopDocs(0, new ScoreDoc[0]);<br/><br/>final BitSet bits = filter!=null?filter.bits(reader):null;<br/>final HitQueue hq = new HitQueue(nDocs);<br/>final int[] totalHits = new int[1];<br/>scorer.score(new HitCollector() &#123;<br/>&nbsp;&nbsp;public final void collect(int doc, float score) &#123;<br/>&nbsp;&nbsp;if (score > 0.0f && <br/>&nbsp;&nbsp;&nbsp;&nbsp;(bits==null &#124;&#124; bits.get(doc))) &#123; <br/>&nbsp;&nbsp;totalHits[0]++;<br/>&nbsp;&nbsp;hq.insert(new ScoreDoc(doc, score));<br/>&nbsp;&nbsp;&#125;<br/>&nbsp;&nbsp;&#125;<br/>&#125;);<br/><br/>ScoreDoc[] scoreDocs = new ScoreDoc[hq.size()];<br/>for (int i = hq.size()-1; i >= 0; i--)<br/>&nbsp;&nbsp;scoreDocs[i] = (ScoreDoc)hq.pop();<br/><br/>return new TopDocs(totalHits[0], scoreDocs);<br/>&#125;<br/>...<br/>&#125;<br/>主干流程是:1.获取scorer对象<br/><br/>CODE:<br/><br/>Scorer scorer = termQuery.weight.scorer(indexReader)<br/><br/>2.调用这个Scorer对象(此处是TermScorer)的score (Collector)方法.<br/>scorer.score(new HitCollector() &#123;<br/>...<br/>&#125;);<br/>这 两步之后, collector就把hq用查询结果填满了, 而用户得到的结果就是从hq中一个一个取出的. 这段代码中, indexReader用于读取index数据, 以供查询使用, Scorer负责用查询结果把Collector填满. 问题是,scorer的'查询结果'从哪里来? 如果IndexReader向Scorer提供数据, 数据内容是如何从索引文件中选取的?<br/><br/>Scorer 用一个匿名类Collector来收集满足TermQuery的Doc, 但Scorer怎么能够知道哪些文档符合Query? 真实查询并非在score()方法中进行, 从数据提取的角度来说, 现代搜索引擎都是从inverted index中提取的满足Term的文档列表. 是个人都知道inverted index里有什么, 由指定的keyword得到所有对应的文档就是利用inverted index数据结构完成的, 这也是搜索过程的核心--一个延续了数百年的索引方法. 我要考察的是, 在这个新式的OO搜索引擎框架中, 谁(哪个对象/类)负责提取term对应的记录, 他怎样把结果交给Scorer?<br/><br/>读取inverted index是IndexReader的责任, 这时已经提到过的. 关于这一点的知识来自于Lucene手册中星星点点的暗示和大量的代码阅读, 此处不纠缠这个问题, 现在关心的是Scorer和IndexReader怎样发生联系? 回忆一下上边的代码中TermQuery创建Scorer时用的参数, 正是IndexReader对象, 动动脚趾头也猜得出来TermQuery利用"创建"这种特权对score秘密动了手脚. 现在瞄一眼weight在构造scorer时玩了什么花样. 代码是这样写的<br/><br/>CODE:<br/><br/>public scorer weight#scorer(IndexReader reader)&#123;<br/>TermDocs termDocs = reader.termDocs(term);<br/>&nbsp;&nbsp;<br/>if (termDocs == null)<br/>&nbsp;&nbsp;return null;<br/>&nbsp;&nbsp;<br/>return new TermScorer(this, termDocs, getSimilarity(searcher),<br/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;reader.norms(term.field()));<br/>&#125;<br/><br/>class Scorer&#123;<br/>...<br/>public void score(HitCollector hc) throws IOException &#123;<br/>&nbsp;&nbsp;while (next()) &#123;<br/>&nbsp;&nbsp;&nbsp;&nbsp;hc.collect(doc(), score());<br/>&nbsp;&nbsp;&#125;<br/>&#125;<br/>...<br/>public boolean next() throws IOException &#123;<br/>&nbsp;&nbsp;pointer++;<br/>&nbsp;&nbsp;if (pointer >= pointerMax) &#123;<br/>&nbsp;&nbsp;pointerMax = termDocs.read(docs, freqs);&nbsp;&nbsp; // refill buffer<br/>&nbsp;&nbsp;if (pointerMax != 0) &#123;<br/>&nbsp;&nbsp;&nbsp;&nbsp;pointer = 0;<br/>&nbsp;&nbsp;&#125; else &#123;<br/>&nbsp;&nbsp;&nbsp;&nbsp;termDocs.close();&nbsp;&nbsp; // close stream<br/>&nbsp;&nbsp;&nbsp;&nbsp;doc = Integer.MAX_VALUE;&nbsp;&nbsp; // set to sentinel value<br/>&nbsp;&nbsp;&nbsp;&nbsp;return false;<br/>&nbsp;&nbsp;&#125;<br/>&nbsp;&nbsp;&#125;<br/>&nbsp;&nbsp;doc = docs[pointer];<br/>&nbsp;&nbsp;return true;<br/>&#125;<br/>...<br/>&#125;<br/><br/>可以看出, 上面的问题唯一可能的答案是: weight在构造Scorer时已经为Scorer决定了查询内容就在那个termDocs里. Scorer的代码也表明, 它在遍历所有合法文档时,背后的查询动作是在穷举一个数组:doc[], 而这个数组的来源就是TermDocs. 剩下的问题是,TermDoc在整个查询中扮演何种角色--它是怎么读数据的<br/><br/>看看TermDoc是个虾米东东<br/>reader 创建好TermDoc后调用TermDoc.seek(term). 这个方法在硬盘索引文件中找到term所对应的所有文档记录, 每条记录包含文档id和term在文档中出现的次数tf. 这些文档信息是编制索引时就建好的, 索引文件中每个term对应的文档记录按顺序紧密排列在一起, seek方法能找到这些记录在索引中的开始位置及满足term的文档总数. 以后, TermDoc在scorer中的作用就是读入每个符合term的文档及term在该文档中的tf, 由于建好了索引只需在索引文件中遍历即可, termDoc包含的df将用于此过程的遍历计数.就是说scorer接收到的那个termDoc是调用过seek的, 已经定位到了term对应的数据位置,这便让scorer能遍历termQuery中所有包含那个term的Doc.<br/><br/>scorer怎样遍历全部doc<br/>读 数据又是另一门学问, 感兴趣的人也许研究过读一个100M文件,与读1000个1K byte文件有何区别,结果当然是震撼的.只要有可能,尽量读取整块数据而不是零碎地读取小数总不会让人失望，然而同样没有人会在构造一个对象时就去对一 个未知大小(可能真的包含100万个文档)的数据.只要没收到必要请求,任何人都会尽力避免这种冗长的操作. Lucene的设计者同样只是让在scorer在需要读入数据即第一次调用next()方法时调用termDoc的read方法读数据(代码中黑体部 分).为了避免零碎读取降低硬盘效率,termDoc.read()会一次性读入所有合法文档(当然仅包括文档id和tf, 建立索引的过程中,这两个数据一组组的放在专门的文件中,每个term对应的全部文档在这个文件里连续排列以避免零碎读取),scorer调用next ()语句,遍历read()返回的文档id数组,整个遍历过程只需把读出来的doc里的i进行++便万事大吉.<br/><br/>遍历过程中发生什么 事情大家心里应该很清楚,无非是把这些doc(这就是搜索结果)一个个添加到Collector中. 查询结束后, 我们将得到一个int数组, 里面保存着每个结果文档的id. 要使用这些查询结果,用户还需要从按照每篇文档的id文档库中取出结果, 这些只需调用searcher.doc(id)即可完成的事务性过程不在本"技术"文章讨论范围中.<br/><br/>所谓搜索原来如此简单...<br/><br/>面向对象<br/><br/>OO In Lucene Search 面向对象的Lucene<br/><br/>Nutch 正式立项以后, Apache 基金会的incubator里现存Lucene相关项目还有一个: Lucene4C. 顾名思义, 这是一个完完全全用C做成的搜索引擎, 很多对用Java做SE有意见的哥们热烈盼望这个东东快点孵化出来. 不过我担心已经已经用惯Lucene Java的同志们对Lucene4C的热情纯属叶公好龙, 理由很简单, 就像Lucene4C的说明中提到的, Lucene4C和Lucene Java的兼容是"底层兼容"即算法和索引格式的兼容, Lucene4C只是为那些惯于C代码的程序员而专门设计. 换言之, 虽然我们可以用Lucene4C搜索LuceneJava的索引或反过来用, 但绝对别指望Doug Cutting们会提供一个类似Java的接口API. 即使不提编程风格的差异, 那些只会把继承后的对象扔给API的家伙们怎么能在没有类的世界中生存呢, 没有了对象封装, 没有OCP/SRP, 还要处理种种指针, 光是调用memset/strcpy就够他们抓狂了.<br/><br/>从top level来讲, 将OOD的作品移植到C, 不仅是个浩大的工程, 还是个没法预期结果的定时炸弹. 不过我还是很盼望看到Lucene4C的成品, 我想抓住它的代码一行一行的读. 两种截然不同的设计理念生出一对双胞胎, 我要瞧瞧它们有什么不同, 相信Apache的天才们能让我在对比中学到很多.<br/><br/>所以要先搞清楚OO在Lucene中如何体现.<br/><br/>在 利用TermQuery这个最基本的Query研讨查询机制后, 细心观察就会发现, 本来满以为用来实现查询机制的那个类Searcher, 其实和检索机制几乎毫无瓜葛, 就算有, 也不过是调用各参数的方法, 把这个作为那个的参数, 再把那个传给另外的某某. 诸如此类. Searcher是个指挥家, 负责调度各功能使之协调, 用OO的话说, 就是对象间的协作. 各对象的功能被Searcher的操作过程利用了, 至于这些功能如何实现, Searcher全然不知.<br/><br/>OO的一大强项是业务逻辑层面的简洁表达, 清晰的语义甚至可以用优雅来形容. 这可以视为高内聚低耦合设计的副产品. Searcher.search()方法描述的逻辑是, Query把满足查询条件的文档ID结果放到Scorer中, Scorer把结果塞进Collector. 这里还有个暗示的语义: 无论输入顺序是什么Collector中的结果总是按照score排序的. 从上述语义出发, 很容易分辨出职责的内聚以及类间的耦合关系.<br/>具体 考察一下TermQuery, 才更好理解内聚和耦合度. 在TermQuery的查询中, 对文档是否符合Term条件的判断由TermQuery负责, 而评分则由TermScorer负责. 单一职责原则SRP体现得非常明显, 而正是由于SRP被满足, Query/Scorer的耦合才得以解开.<br/><br/>设计的过程中, 职责内聚和抽象往往是同时完成. 所以在了解了Query/Scorer的职责后, Lucene的类结构显示出另一个十分重要的特性: 这就是开头提到的, Searcher与Query的查询过程无关. Searcher所要面对的查询是无类型的, 它需要处理各种查询. 查询的共同外部特征(在这里就是Searcher能访问的那些成员)抽象成了Query接口, Searcher与Scorer的关系也是这样. 由于使用了面向对象技术中至关重要的多态性, Searcher只面对接口(依赖倒置原则DIP), 这样就把查询的业务逻辑同各查询类型的算法逻辑有效的隔离开(解耦合)了. 这是职责内聚的结果, 同时也是把"查询"概念高度抽象的结果.<br/><br/>于 是我们看到Searcher实现了针对"查询"的业务逻辑的描述, 对任何种类的查询, 这一业务流程都适用(抽象), 而如何实现查询的算法(结果提取和评分), 则是Query/Searcher各实现类负责(内聚). 这么做的好处不言而喻——OCP, 对扩展来说, Lucene是完全开放的, 而对更改则是封闭因而是安全的.<br/>设想现在需要查询连续出现的Term, 单个TermQuery当然不够用, 检索算法已经变了. 但我们不需要改变现存任何一个类, 只要构建新的Query/Scorer实现类, 比如PhraseQuery/Scorer. 目前Lucene 1.4.3版已经提供这样一个检索类型.<br/><br/>当然, Lucene是OOD方法的杰作, 不止上述几点, 在各个实现层次上都能找到类似的设计痕迹, 这为整个软件包的扩展和应用提供了强大的架构保证. 相信这也是Lucene得以流行的深层原因.<br/><br/><br/>Index in Practice 索引: 按图索骥<br/><br/>TermDoc从哪读取数据,自然是硬盘上已经建好的某个index, 具体说, 是从index中的某个文件读取. 要了解TermDoc读了什么东东,怎么读这些东东,必要时得考察Lucene index的细部结构.<br/><br/>TermDoc是个抽象类,这很好,以后可以创建自己的index结构,建立自己的搜索算法.不过这之前先要了解Lucene是怎么干的,而这个抽象类并不包含这个信息,所以,我们首先要找到TermQuery使用哪个TermDoc实现.<br/><br/>回想一下scorer中的TermDoc从哪里来.<br/><br/>public class TermQuery extends Query &#123;<br/>private class TermWeight implements Weight &#123;<br/>&nbsp;&nbsp;public Scorer scorer(IndexReader reader) throws IOException &#123;<br/>&nbsp;&nbsp;&nbsp;&nbsp;TermDocs termDocs = reader.termDocs(term);<br/>&nbsp;&nbsp;&nbsp;&nbsp;<br/>&nbsp;&nbsp;&nbsp;&nbsp;if (termDocs == null)<br/>&nbsp;&nbsp;&nbsp;&nbsp;return null;<br/>&nbsp;&nbsp;&nbsp;&nbsp;<br/>&nbsp;&nbsp;&nbsp;&nbsp;return new TermScorer(this, termDocs, getSimilarity(searcher),<br/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;reader.norms(term.field()));<br/>&nbsp;&nbsp;&#125;<br/>&nbsp;&nbsp;...<br/>&#125;<br/>...<br/>&#125;<br/><br/>从这段代码能找到真正创建TermDocs的那个类: IndexReader<br/>用 哪个TermDocs实现并不是TermQuery说了算,而是IndexReader的权利. TermQuery得到怎样一个TermDocs, 全由我们传递给TermQuery.weight.scorer()的那个IndexReader决定. 将这个TermDocs定位到指定的Term也完全由IndexReader负责。很遗憾,IndexReader也是抽象类. 想知道内幕?先找找IndexReader实现类。<br/><br/>如果按照用户手册的方法进行搜索, IndexReader的一个静态方法将被调用,它返回我们需要的一个IndexReader实现:SegmentReader, 这是整个查询中用到的reader。<br/><br/>顺藤摸瓜,很容易找到SegmentTermDocs这个类,也就是默认查询中SegmentReader使用的TermDocs,大部分查询结果通过这个类的实例来遍历.现在是时候翻它老底了,看看它怎么遍历数据,这些数据又从哪来.<br/><div class="code"><br/>class IndexReader&#123;<br/>public TermDocs termDocs(Term term) throws IOException &#123;<br/>&nbsp;&nbsp;TermDocs termDocs = termDocs();<br/>&nbsp;&nbsp;termDocs.seek(term);<br/>&nbsp;&nbsp;return termDocs;<br/>&#125;<br/>...<br/>&#125;<br/><br/>class SegmentReader extends IndexReader&#123;<br/>public final TermDocs termDocs() throws IOException &#123;<br/>&nbsp;&nbsp;return new SegmentTermDocs(this);<br/>&#125;<br/>...<br/>&#125;<br/></div><br/><br/>从已经列出的代码中, 能清晰地看到SegmentTermDocs从创建到传递给scorer前进行的一系列动作:<br/><br/>1. SegmentTermDocs构造: 根据parent设定自己的属性<br/>2. IndexReader调用TermDocs.seek(term); 实现类中这一步具体化为SegmentReader调用SegmentTermDocs.seek(term)<br/><br/>第二步中, SegmentTermDocs进行了实际对index文件的读取. 而为了进行这些IO操作, 像前边说的, 必须依靠IndexReader才能完成, 这就是SegmentTermDocs构造是需要参数SegmentReader的原因.<br/><br/>seek (term)方法中SegmentTermDocs利用构造函数的唯一参数IndexReader(也就是创建它的那个reader, 称作parent"), 在硬盘索引文件中定位指定的term, 读入相关信息:df(包含term的文档数), 以及满足该term的文档集合在index文件中的位置. 这个位置后面, 是创建索引时就已排好的包含这个term的文档信息. <br/><br/>seek 完成后, TermDoc已经准备好读取数据了, 只要一声令下, TermDoc.read方法立刻能把每一篇文档的id和该term在这篇文档中的次数tf. 前面的记载是, scorer对象调用read方法, 尔后遍历其返回的全部文档, 把他们一个个塞到Collector中<br/><br/>精妙繁复的步骤: seek如何完成?<br/><br/><br/>这要涉及索引结构, 现在可以掀开索引文件的一个角, 偷窥下.<br/><br/>tis文件: Term InformationS<br/>frq文件: FReQuency<br/><br/>必 须注意到IO动作一定是在IndexReader的几个成员中作的, 所有其他类中的IO要么用这些成员的Clone来完成, 要么直接代理给IndexReader. SegmentTermDocs.seek(term)动作是通过IndexReader进行的, SegmentTermDocs把创建他的IndexReader尊为parent, 在seek这种关键时刻利用IndexReader来读取索引数据. 没办法, 索引文件的读取(输入流的建立和定位)全由IndexReader负责. <br/>seek方法中为了实现定位而利用了IndexReader一个负责 Term定位的成员tis, 从他的类名TermInfoReader看就知道有什么用途. 这个tis从.tis文件中找到我们指定的term, 读出一切我们需要的信息: 这个term在多少个文档中出现过(df)/这些文档记录在frq文件的什么位置(起始位置) 等等.<br/><br/>得到这 些信息后, TermDoc再自己seek, 这一步很简单, 除了几个加法和赋值, 唯一有特色的是对.frq文件的输入流(FileInputStream)进行seek(), seek()的数量正好是tis返回的"文档记录在frq文件中的位置". 这个流是IndexReader初始化时创建的, 专门从frq文件读数据. IndexReader创建TermDoc时, TermDoc把这个输入流Clone()了一下, 赋给自己的成员. 这一seek()把.frq文件的输入指针定位好, 以后真正需要这个流的地方只有从frq文件读文档数据那一阵. 读数据的过程就发生在屡次提到过的termDoc.read()里, 现在我知道这个方法的实现是SegmentTermDocs.read().<br/><br/>read ()的实现是简单的顺序读取文件流, 具体过程涉及Lucene索引文件的二进制结构, 我不想这时候过多地纠缠. 大致了解termDoc如何定位数据, 心中的疑惑就能解开一半. 关于索引文件结构、各文件的关系、程序如何厘清这些关系, 还值得更多的讨论. <br/><br/>到这一步, 结合已熟知的scorer调用TermDoc的方式, 查询过程的基本途径已经隐约呈现出来了.<br/><br/>Tags - <a href="http://blog.lough.com.cn/tags/lucene/" rel="tag">lucene</a>
]]>
</description>
</item><item>
<link>http://blog.lough.com.cn/post/235/</link>
<title><![CDATA[lucene PriorityQueue 优先队列]]></title> 
<author>jock &lt;jockhuang@gmail.com&gt;</author>
<category><![CDATA[Lucene]]></category>
<pubDate>Wed, 02 Jan 2008 10:23:26 +0000</pubDate> 
<guid>http://blog.lough.com.cn/post/235/</guid> 
<description>
<![CDATA[ 
	PriorityQueue&nbsp;&nbsp;实现一优先队列框架，实例非常简单，只需实现lessThan(Object a, Object b)方法即可，通过该方法可以控制大优先或小优先。 <br/> <br/><br/>package org.apache.lucene.search;<br/><br/>/**<br/> * Copyright 2004 The Apache Software Foundation<br/> *<br/> * Licensed under the Apache License, Version 2.0 (the "License");<br/> * you may not use this file except in compliance with the License.<br/> * You may obtain a copy of the License at<br/> *<br/> *&nbsp;&nbsp;&nbsp;&nbsp; http://www.apache.org/licenses/LICENSE-2.0<br/> *<br/> * Unless required by applicable law or agreed to in writing, software<br/> * distributed under the License is distributed on an "AS IS" BASIS,<br/> * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.<br/> * See the License for the specific language governing permissions and<br/> * limitations under the License.<br/> */<br/><br/>import org.apache.lucene.util.PriorityQueue;<br/><br/>final class HitQueue extends PriorityQueue &#123;<br/>&nbsp;&nbsp;HitQueue(int size) &#123;<br/>&nbsp;&nbsp;&nbsp;&nbsp;initialize(size);<br/>&nbsp;&nbsp;&#125;<br/><br/>&nbsp;&nbsp;/**<br/>&nbsp;&nbsp; * <br/>&nbsp;&nbsp; * Annotator:yezheng<br/>&nbsp;&nbsp; */<br/>&nbsp;&nbsp;protected final boolean lessThan(Object a, Object b) &#123;<br/>&nbsp;&nbsp;&nbsp;&nbsp;ScoreDoc hitA = (ScoreDoc)a;<br/>&nbsp;&nbsp;&nbsp;&nbsp;ScoreDoc hitB = (ScoreDoc)b;<br/>&nbsp;&nbsp;&nbsp;&nbsp;if (hitA.score == hitB.score)<br/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;return hitA.doc > hitB.doc; // 如果分数相同则按索引先后顺序排序<br/>&nbsp;&nbsp;&nbsp;&nbsp;else<br/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;return hitA.score < hitB.score;//否则,按score进行排序<br/>&nbsp;&nbsp;&#125;<br/>&#125; <br/><br/><br/>Tags - <a href="http://blog.lough.com.cn/tags/%25E6%25BA%2590%25E7%25A0%2581/" rel="tag">源码</a> , <a href="http://blog.lough.com.cn/tags/lucene/" rel="tag">lucene</a>
]]>
</description>
</item><item>
<link>http://blog.lough.com.cn/post/234/</link>
<title><![CDATA[lucene HitCollector 的作用]]></title> 
<author>jock &lt;jockhuang@gmail.com&gt;</author>
<category><![CDATA[Lucene]]></category>
<pubDate>Wed, 02 Jan 2008 10:22:20 +0000</pubDate> 
<guid>http://blog.lough.com.cn/post/234/</guid> 
<description>
<![CDATA[ 
	HitCollector 的作用很简单，通过collect（）方法控制检索返回的结果，下面是lucene自带的一个例子----使用一个优先队<br/>列，返回指定数目的Top n Doc。<br/><br/><br/>package org.apache.lucene.search;<br/><br/>/**<br/> * Copyright 2004 The Apache Software Foundation<br/> *<br/> * Licensed under the Apache License, Version 2.0 (the "License");<br/> * you may not use this file except in compliance with the License.<br/> * You may obtain a copy of the License at<br/> *<br/> *&nbsp;&nbsp;&nbsp;&nbsp; http://www.apache.org/licenses/LICENSE-2.0<br/> *<br/> * Unless required by applicable law or agreed to in writing, software<br/> * distributed under the License is distributed on an "AS IS" BASIS,<br/> * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or<br/>implied.<br/> * See the License for the specific language governing permissions and<br/> * limitations under the License.<br/> */<br/><br/>import java.io.IOException;<br/>import java.util.BitSet;<br/><br/>import org.apache.lucene.store.Directory;<br/>import org.apache.lucene.document.Document;<br/>import org.apache.lucene.index.IndexReader;<br/>import org.apache.lucene.index.Term;<br/>import org.apache.lucene.util.PriorityQueue;<br/><br/>/** A &#123;@link HitCollector&#125; implementation that collects the top-<br/>scoring<br/> * documents, returning them as a &#123;@link TopDocs&#125;.&nbsp;&nbsp;This is used by<br/>&#123;@link<br/> * IndexSearcher&#125; to implement &#123;@link TopDocs&#125;-based search.<br/> *<br/> * <p>This may be extended, overriding the collect method to, e.g.,<br/> * conditionally invoke <code>super()</code> in order to filter which<br/> * documents are collected.<br/> **/<br/>public class TopDocCollector extends HitCollector &#123;<br/>&nbsp;&nbsp;private int numHits;<br/>&nbsp;&nbsp;private float minScore = 0.0f;<br/><br/>&nbsp;&nbsp;int totalHits;<br/>&nbsp;&nbsp;PriorityQueue hq;<br/><br/>&nbsp;&nbsp;/** Construct to collect a given number of hits.<br/>&nbsp;&nbsp; * @param numHits the maximum number of hits to collect<br/>&nbsp;&nbsp; */<br/>&nbsp;&nbsp;public TopDocCollector(int numHits) &#123;<br/>&nbsp;&nbsp;&nbsp;&nbsp;this(numHits, new HitQueue(numHits));<br/>&nbsp;&nbsp;&#125;<br/><br/>&nbsp;&nbsp;TopDocCollector(int numHits, PriorityQueue hq) &#123;<br/>&nbsp;&nbsp;&nbsp;&nbsp;this.numHits = numHits;<br/>&nbsp;&nbsp;&nbsp;&nbsp;this.hq = hq;<br/>&nbsp;&nbsp;&#125;<br/><br/>&nbsp;&nbsp;// javadoc inherited<br/>&nbsp;&nbsp;public void collect(int doc, float score) &#123;<br/>&nbsp;&nbsp;&nbsp;&nbsp;if (score > 0.0f) &#123;<br/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;totalHits++;<br/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;if (hq.size() < numHits &#124;&#124; score >= minScore) &#123;<br/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;hq.insert(new ScoreDoc(doc, score));<br/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;minScore = ((ScoreDoc)hq.top()).score; // maintain minScore<br/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&#125;<br/>&nbsp;&nbsp;&nbsp;&nbsp;&#125;<br/>&nbsp;&nbsp;&#125;<br/><br/>&nbsp;&nbsp;/** The total number of documents that matched this query. */<br/>&nbsp;&nbsp;public int getTotalHits() &#123; return totalHits; &#125;<br/><br/>&nbsp;&nbsp;/** The top-scoring hits. */<br/>&nbsp;&nbsp;public TopDocs topDocs() &#123;<br/>&nbsp;&nbsp;&nbsp;&nbsp;ScoreDoc[] scoreDocs = new ScoreDoc[hq.size()];<br/>&nbsp;&nbsp;&nbsp;&nbsp;for (int i = hq.size()-1; i >= 0; i--)&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;// put docs in array<br/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;scoreDocs[i] = (ScoreDoc)hq.pop();<br/><br/>&nbsp;&nbsp;&nbsp;&nbsp;float maxScore = (totalHits==0)<br/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;? Float.NEGATIVE_INFINITY<br/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;: scoreDocs[0].score;<br/><br/>&nbsp;&nbsp;&nbsp;&nbsp;return new TopDocs(totalHits, scoreDocs, maxScore);<br/>&nbsp;&nbsp;&#125;<br/>&#125; <br/><br/><br/>Tags - <a href="http://blog.lough.com.cn/tags/lucene/" rel="tag">lucene</a> , <a href="http://blog.lough.com.cn/tags/%25E6%25BA%2590%25E7%25A0%2581/" rel="tag">源码</a>
]]>
</description>
</item><item>
<link>http://blog.lough.com.cn/post/233/</link>
<title><![CDATA[Lucene的排序修改]]></title> 
<author>jock &lt;jockhuang@gmail.com&gt;</author>
<category><![CDATA[Lucene]]></category>
<pubDate>Wed, 02 Jan 2008 10:20:00 +0000</pubDate> 
<guid>http://blog.lough.com.cn/post/233/</guid> 
<description>
<![CDATA[ 
	修改Similarity（相似度计算）<br/><br/>DefaultSimilarity基本上可以满足一般的搜索要求。但是在有些应用中，你可以定制你自己的Similarity来服务你自己的应用需求。例如：有些人认为没有必要让文档短的文章得分更高一点 (参考 a "fair" similarity).<br/><br/>修改Similarity需要同时对索引和搜索都进行修改，必须在搜索或者排序之间修改Similarity。 <br/><br/>要定制你自己的Similarity，也就是你不想直接使用DefaultSimilarity，你只要在建立索引的之前调用IndexWriter.setSimilarity，或者在搜索之前调用Searcher.setSimilarity. <br/><br/>你如果想知道，别人都是怎么修改similarity的，你可以参考一下Lucene的邮件列表Overriding Similarity. 总的来说有下面这些修改: <br/><br/>SweetSpotSimilarity -- SweetSpotSimilarity gives small increases as the frequency increases a small amount and then greater increases when you hit the "sweet spot", i.e. where you think the frequency of terms is more significant.<br/>Overriding tf -- In some applications, it doesn't matter what the score of a document is as long as a matching term occurs. In these cases people have overridden Similarity to return 1 from the tf() method.<br/>Changing Length Normalization -- By overriding lengthNorm, it is possible to discount how the length of a field contributes to a score. In DefaultSimilarity, lengthNorm = 1 / (numTerms in field)^0.5, but if one changes this to be 1 / (numTerms in field), all fields will be treated "fairly".<br/>因为你对你自己的数据更了解，所以你有必要重写自己的Similarity方法。<br/><br/>定制你自己的评分系统（专家级）<br/><br/>修改评分系统是专家级的工作，所以你要谨慎工作，随时和别人交流。在Lucene中，修改评分系统将比修改similarity更加能够影响结果。Lucene的评分系统是一个非常复杂的机制，主要由下面三个类来实现： <br/><br/>Query -- The abstract object representation of the user's information need. <br/>Weight -- The internal interface representation of the user's Query, so that Query objects may be reused. <br/>Scorer -- An abstract class containing common functionality for scoring. Provides both scoring and explanation capabilities. <br/>下面我来具体介绍一下这三个类：<br/><br/>The Query Class<br/><br/>从某种意义上来说，Query是评分开始的地方。没有查询就没有什么可以评分的。更重要的是它是其他的评分系统的催化剂，由它来生成其他的评分系统，然后将他们整合起来。Query有一些重要的方法需要被继承: <br/><br/>createWeight(Searcher searcher) -- A Weight is the internal representation of the Query, so each Query implementation must provide an implementation of Weight. See the subsection on The Weight Interface below for details on implementing the Weight interface. <br/>rewrite(IndexReader reader) -- Rewrites queries into primitive queries. Primitive queries are: TermQuery, BooleanQuery, OTHERS???? <br/>The Weight Interface<br/><br/>Weight 接口<br/><br/>权重接口主要用来定义Query的一个代表实现接口，所以可以被重用。任何可以用来被搜索的类都应该内置一个Weight，而不是在Query类。这个接口定义了6个要被执行的方法： <br/><br/>Weight#getQuery() -- Pointer to the Query that this Weight represents. <br/>Weight#getValue() -- The weight for this Query. For example, the TermQuery.TermWeight value is equal to the idf^2 * boost * queryNorm <br/>Weight#sumOfSquaredWeights() -- The sum of squared weights. Tor TermQuery, this is (idf * boost)^2 <br/>Weight#normalize(float) -- Determine the query normalization factor. The query normalization may allow for comparing scores between queries. <br/>Weight#scorer(IndexReader) -- Construct a new Scorer for this Weight. See The Scorer Class below for help defining a Scorer. As the name implies, the Scorer is responsible for doing the actual scoring of documents given the Query. <br/>Weight#explain(IndexReader, int) -- Provide a means for explaining why a given document was scored the way it was. <br/>The Scorer Class<br/><br/>评分类：<br/><br/>Scorer是评分的抽象类，提供一些基本的计分功能供所有的评分类实现，是Lucene评分机制的核心类。Scorer定义了一下的方法，必须被实现。: <br/><br/>Scorer#next() -- Advances to the next document that matches this Query, returning true if and only if there is another document that matches. <br/>Scorer#doc() -- Returns the id of the Document that contains the match. Is not valid until next() has been called at least once. <br/>Scorer#score() -- Return the score of the current document. This value can be determined in any appropriate way for an application. For instance, the TermScorer returns the tf * Weight.getValue() * fieldNorm. <br/>Scorer#skipTo(int) -- Skip ahead in the document matches to the document whose id is greater than or equal to the passed in value. In many instances, skipTo can be implemented more efficiently than simply looping through all the matching documents until the target document is identified. <br/>Scorer#explain(int) -- Provides details on why the score came about. <br/><br/>Tags - <a href="http://blog.lough.com.cn/tags/lucene/" rel="tag">lucene</a>
]]>
</description>
</item><item>
<link>http://blog.lough.com.cn/post/232/</link>
<title><![CDATA[Lucene的使用与优化 zz]]></title> 
<author>jock &lt;jockhuang@gmail.com&gt;</author>
<category><![CDATA[Lucene]]></category>
<pubDate>Wed, 02 Jan 2008 10:14:13 +0000</pubDate> 
<guid>http://blog.lough.com.cn/post/232/</guid> 
<description>
<![CDATA[ 
	1 lucene简介<br/>1.1 什么是lucene<br/>Lucene是一个全文搜索框架，而不是应用产品。因此它并不像www.baidu.com 或者google Desktop那么拿来就能用，它只是提供了一种工具让你能实现这些产品。<br/><br/>1.2 lucene能做什么<br/>要回答这个问题，先要了解lucene的本质。实际上lucene的功能很单一，说到底，就是你给它若干个字符串，然后它为你提供一个全文搜索服务，告诉你你要搜索的关键词出现在哪里。知道了这个本质，你就可以发挥想象做任何符合这个条件的事情了。你可以把站内新闻都索引了，做个资料库；你可以把一个数据库表的若干个字段索引起来，那就不用再担心因为“%like%”而锁表了；你也可以写个自己的搜索引擎……<br/><br/>1.3 你该不该选择lucene<br/>下面给出一些测试数据，如果你觉得可以接受，那么可以选择。<br/>测试一：250万记录，300M左右文本，生成索引380M左右，800线程下平均处理时间300ms。<br/>测试二：37000记录，索引数据库中的两个varchar字段，索引文件2.6M，800线程下平均处理时间1.5ms。<br/><br/>2 lucene的工作方式<br/>lucene提供的服务实际包含两部分：一入一出。所谓入是写入，即将你提供的源（本质是字符串）写入索引或者将其从索引中删除；所谓出是读出，即向用户提供全文搜索服务，让用户可以通过关键词定位源。<br/><br/>2.1写入流程<br/>源字符串首先经过analyzer处理，包括：分词，分成一个个单词；去除stopword（可选）。<br/>将源中需要的信息加入Document的各个Field中，并把需要索引的Field索引起来，把需要存储的Field存储起来。<br/>将索引写入存储器，存储器可以是内存或磁盘。<br/><br/>2.2读出流程<br/>用户提供搜索关键词，经过analyzer处理。<br/>对处理后的关键词搜索索引找出对应的Document。<br/>用户根据需要从找到的Document中提取需要的Field。<br/><br/>3 一些需要知道的概念<br/>lucene用到一些概念，了解它们的含义，有利于下面的讲解。<br/><br/>3.1 analyzer<br/>Analyzer 是分析器，它的作用是把一个字符串按某种规则划分成一个个词语，并去除其中的无效词语，这里说的无效词语是指英文中的“of”、“the”，中文中的 “的”、“地”等词语，这些词语在文章中大量出现，但是本身不包含什么关键信息，去掉有利于缩小索引文件、提高效率、提高命中率。<br/>分词的规则千变万化，但目的只有一个：按语义划分。这点在英文中比较容易实现，因为英文本身就是以单词为单位的，已经用空格分开；而中文则必须以某种方法将连成一片的句子划分成一个个词语。具体划分方法下面再详细介绍，这里只需了解分析器的概念即可。<br/><br/>3.2 document<br/>用户提供的源是一条条记录，它们可以是文本文件、字符串或者数据库表的一条记录等等。一条记录经过索引之后，就是以一个Document的形式存储在索引文件中的。用户进行搜索，也是以Document列表的形式返回。<br/><br/>3.3 field<br/>一个Document可以包含多个信息域，例如一篇文章可以包含“标题”、“正文”、“最后修改时间”等信息域，这些信息域就是通过Field在Document中存储的。<br/>Field有两个属性可选：存储和索引。通过存储属性你可以控制是否对这个Field进行存储；通过索引属性你可以控制是否对该Field进行索引。这看起来似乎有些废话，事实上对这两个属性的正确组合很重要，下面举例说明：<br/>还是以刚才的文章为例子，我们需要对标题和正文进行全文搜索，所以我们要把索引属性设置为真，同时我们希望能直接从搜索结果中提取文章标题，所以我们把标题域的存储属性设置为真，但是由于正文域太大了，我们为了缩小索引文件大小，将正文域的存储属性设置为假，当需要时再直接读取文件；我们只是希望能从搜索解果中提取最后修改时间，不需要对它进行搜索，所以我们把最后修改时间域的存储属性设置为真，索引属性设置为假。上面的三个域涵盖了两个属性的三种组合，还有一种全为假的没有用到，事实上Field不允许你那么设置，因为既不存储又不索引的域是没有意义的。<br/><br/>3.4 term<br/>term是搜索的最小单位，它表示文档的一个词语，term由两部分组成：它表示的词语和这个词语所出现的field。<br/><br/>3.5 tocken<br/>tocken是term的一次出现，它包含trem文本和相应的起止偏移，以及一个类型字符串。一句话中可以出现多次相同的词语，它们都用同一个term表示，但是用不同的tocken，每个tocken标记该词语出现的地方。<br/><br/>3.6 segment<br/>添加索引时并不是每个document都马上添加到同一个索引文件，它们首先被写入到不同的小文件，然后再合并成一个大索引文件，这里每个小文件都是一个segment。<br/><br/>4 lucene的结构<br/>lucene包括core和sandbox两部分，其中core是lucene稳定的核心部分，sandbox包含了一些附加功能，例如highlighter、各种分析器。<br/>Lucene core有七个包：analysis，document，index，queryParser，search，store，util。<br/>4.1 analysis<br/>Analysis包含一些内建的分析器，例如按空白字符分词的WhitespaceAnalyzer，添加了stopwrod过滤的StopAnalyzer，最常用的StandardAnalyzer。<br/>4.2 document<br/>Document包含文档的数据结构，例如Document类定义了存储文档的数据结构，Field类定义了Document的一个域。<br/>4.3 index<br/>Index 包含了索引的读写类，例如对索引文件的segment进行写、合并、优化的IndexWriter类和对索引进行读取和删除操作的IndexReader 类，这里要注意的是不要被IndexReader这个名字误导，以为它是索引文件的读取类，实际上删除索引也是由它完成，IndexWriter只关心如何将索引写入一个个segment，并将它们合并优化；IndexReader则关注索引文件中各个文档的组织形式。<br/>4.4 queryParser<br/>QueryParser 包含了解析查询语句的类，lucene的查询语句和sql语句有点类似，有各种保留字，按照一定的语法可以组成各种查询。Lucene有很多种Query 类，它们都继承自Query，执行各种特殊的查询，QueryParser的作用就是解析查询语句，按顺序调用各种Query类查找出结果。<br/>4.5 search<br/>Search包含了从索引中搜索结果的各种类，例如刚才说的各种Query类，包括TermQuery、BooleanQuery等就在这个包里。<br/>4.6 store<br/>Store包含了索引的存储类，例如Directory定义了索引文件的存储结构，FSDirectory为存储在文件中的索引，RAMDirectory为存储在内存中的索引，MmapDirectory为使用内存映射的索引。<br/><br/>4.7 util<br/>Util包含一些公共工具类，例如时间和字符串之间的转换工具。<br/><br/>5 如何建索引<br/>5.1 最简单的能完成索引的代码片断<br/><br/>IndexWriter writer = new IndexWriter(“/data/index/”, new StandardAnalyzer(), true);<br/>Document doc = new Document();<br/>doc.add(new Field("title", "lucene introduction", Field.Store.YES, Field.Index.TOKENIZED));<br/>doc.add(new Field("content", "lucene works well", Field.Store.YES, Field.Index.TOKENIZED));<br/>writer.addDocument(doc);<br/>writer.optimize();<br/>writer.close();<br/><br/>下面我们分析一下这段代码。<br/>首先我们创建了一个writer，并指定存放索引的目录为“/data/index”，使用的分析器为StandardAnalyzer，第三个参数说明如果已经有索引文件在索引目录下，我们将覆盖它们。<br/>然后我们新建一个document。<br/>我们向document添加一个field，名字是“title”，内容是“lucene introduction”，对它进行存储并索引。<br/>再添加一个名字是“content”的field，内容是“lucene works well”，也是存储并索引。<br/>然后我们将这个文档添加到索引中，如果有多个文档，可以重复上面的操作，创建document并添加。<br/>添加完所有document，我们对索引进行优化，优化主要是将多个segment合并到一个，有利于提高索引速度。<br/>随后将writer关闭，这点很重要。<br/><br/>对，创建索引就这么简单！<br/>当然你可能修改上面的代码获得更具个性化的服务。<br/><br/>5.2 将索引直接写在内存<br/>你需要首先创建一个RAMDirectory，并将其传给writer，代码如下：<br/><br/>Directory dir = new RAMDirectory();<br/>IndexWriter writer = new IndexWriter(dir, new StandardAnalyzer(), true);<br/>Document doc = new Document();<br/>doc.add(new Field("title", "lucene introduction", Field.Store.YES, Field.Index.TOKENIZED));<br/>doc.add(new Field("content", "lucene works well", Field.Store.YES, Field.Index.TOKENIZED));<br/>writer.addDocument(doc);<br/>writer.optimize();<br/>writer.close();<br/><br/>5.3 索引文本文件<br/>如果你想把纯文本文件索引起来，而不想自己将它们读入字符串创建field，你可以用下面的代码创建field：<br/><br/>Field field = new Field("content", new FileReader(file));<br/><br/>这里的file就是该文本文件。该构造函数实际上是读去文件内容，并对其进行索引，但不存储。<br/><br/>6 如何维护索引<br/>索引的维护操作都是由IndexReader类提供。<br/><br/>6.1 如何删除索引<br/>lucene提供了两种从索引中删除document的方法，一种是<br/><br/>void deleteDocument(int docNum)<br/><br/>这种方法是根据document在索引中的编号来删除，每个document加进索引后都会有个唯一编号，所以根据编号删除是一种精确删除，但是这个编号是索引的内部结构，一般我们不会知道某个文件的编号到底是几，所以用处不大。另一种是<br/><br/>void deleteDocuments(Term term)<br/><br/>这种方法实际上是首先根据参数term执行一个搜索操作，然后把搜索到的结果批量删除了。我们可以通过这个方法提供一个严格的查询条件，达到删除指定document的目的。<br/>下面给出一个例子：<br/><br/>Directory dir = FSDirectory.getDirectory(PATH, false);<br/>IndexReader reader = IndexReader.open(dir);<br/>Term term = new Term(field, key);<br/>reader.deleteDocuments(term);<br/>reader.close();<br/><br/>6.2 如何更新索引<br/>lucene并没有提供专门的索引更新方法，我们需要先将相应的document删除，然后再将新的document加入索引。例如：<br/><br/>Directory dir = FSDirectory.getDirectory(PATH, false);<br/>IndexReader reader = IndexReader.open(dir);<br/>Term term = new Term(“title”, “lucene introduction”);<br/>reader.deleteDocuments(term);<br/>reader.close();<br/><br/>IndexWriter writer = new IndexWriter(dir, new StandardAnalyzer(), true);<br/>Document doc = new Document();<br/>doc.add(new Field("title", "lucene introduction", Field.Store.YES, Field.Index.TOKENIZED));<br/>doc.add(new Field("content", "lucene is funny", Field.Store.YES, Field.Index.TOKENIZED));<br/>writer.addDocument(doc);<br/>writer.optimize();<br/>writer.close();<br/><br/><br/>7 如何搜索<br/>lucene 的搜索相当强大，它提供了很多辅助查询类，每个类都继承自Query类，各自完成一种特殊的查询，你可以像搭积木一样将它们任意组合使用，完成一些复杂操作；另外lucene还提供了Sort类对结果进行排序，提供了Filter类对查询条件进行限制。你或许会不自觉地拿它跟SQL语句进行比较： “lucene能执行and、or、order by、where、like ‘%xx%’操作吗？”回答是：“当然没问题！”<br/><br/>7.1 各种各样的Query<br/>下面我们看看lucene到底允许我们进行哪些查询操作：<br/><br/>7.1.1 TermQuery<br/>首先介绍最基本的查询，如果你想执行一个这样的查询：“在content域中包含‘lucene’的document”，那么你可以用TermQuery：<br/><br/>Term t = new Term("content", " lucene";<br/>Query query = new TermQuery(t);<br/><br/>7.1.2 BooleanQuery<br/>如果你想这么查询：“在content域中包含java或perl的document”，那么你可以建立两个TermQuery并把它们用BooleanQuery连接起来：<br/><br/>TermQuery termQuery1 = new TermQuery(new Term("content", "java");<br/>TermQuery termQuery 2 = new TermQuery(new Term("content", "perl");<br/>BooleanQuery booleanQuery = new BooleanQuery();<br/>booleanQuery.add(termQuery 1, BooleanClause.Occur.SHOULD);<br/>booleanQuery.add(termQuery 2, BooleanClause.Occur.SHOULD);<br/><br/>7.1.3 WildcardQuery<br/>如果你想对某单词进行通配符查询，你可以用WildcardQuery，通配符包括’?’匹配一个任意字符和’*’匹配零个或多个任意字符，例如你搜索’use*’，你可能找到’useful’或者’useless’：<br/><br/>Query query = new WildcardQuery(new Term("content", "use*"); <br/><br/>7.1.4 PhraseQuery<br/>你可能对中日关系比较感兴趣，想查找‘中’和‘日’挨得比较近（5个字的距离内）的文章，超过这个距离的不予考虑，你可以：<br/><br/>PhraseQuery query = new PhraseQuery();<br/>query.setSlop(5);<br/>query.add(new Term("content ", “中”));<br/>query.add(new Term(“content”, “日”));<br/><br/>那么它可能搜到“中日合作……”、“中方和日方……”，但是搜不到“中国某高层领导说日本欠扁”。<br/><br/>7.1.5 PrefixQuery<br/>如果你想搜以‘中’开头的词语，你可以用PrefixQuery：<br/><br/>PrefixQuery query = new PrefixQuery(new Term("content ", "中");<br/><br/>7.1.6 FuzzyQuery<br/>FuzzyQuery用来搜索相似的term，使用Levenshtein算法。假设你想搜索跟‘wuzza’相似的词语，你可以：<br/>Query query = new FuzzyQuery(new Term("content", "wuzza");<br/><br/>你可能得到‘fuzzy’和‘wuzzy’。<br/><br/>7.1.7 RangeQuery<br/>另一个常用的Query是RangeQuery，你也许想搜索时间域从20060101到20060130之间的document，你可以用RangeQuery：<br/><br/>RangeQuery query = new RangeQuery(new Term(“time”, “20060101”), new Term(“time”, “20060130”), true);<br/><br/>最后的true表示用闭合区间。<br/><br/>7.2 QueryParser<br/>看了这么多Query，你可能会问：“不会让我自己组合各种Query吧，太麻烦了！”当然不会，lucene提供了一种类似于SQL语句的查询语句，我们姑且叫它lucene语句，通过它，你可以把各种查询一句话搞定，lucene会自动把它们查分成小块交给相应Query执行。下面我们对应每种Query演示一下：<br/>TermQuery可以用“field:key”方式，例如“content:lucene”。<br/>BooleanQuery中‘与’用‘+’，‘或’用‘ ’，例如“content:java contenterl”。<br/>WildcardQuery仍然用‘?’和‘*’，例如“content:use*”。<br/>PhraseQuery用‘~’，例如“content:&#92;"中日&#92;"~5”。<br/>PrefixQuery用‘*’，例如“中*”。<br/>FuzzyQuery用‘~’，例如“content: wuzza ~”。<br/>RangeQuery用‘[]’或‘&#123;&#125;’，前者表示闭区间，后者表示开区间，例如“time:[20060101 TO 20060130]”，注意TO区分大小写。<br/>你可以任意组合query string，完成复杂操作，例如“标题或正文包括lucene，并且时间在20060101到20060130之间的文章”可以表示为：“+ (title:lucene content:lucene) +time:[20060101 TO 20060130]”。代码如下：<br/><br/>Directory dir = FSDirectory.getDirectory(PATH, false);<br/>IndexSearcher is = new IndexSearcher(dir);<br/>QueryParser parser = new QueryParser("content", new StandardAnalyzer());<br/>Query query = parser.parse("+(title:lucene content:lucene) +time:[20060101 TO 20060130]";<br/>Hits hits = is.search(query);<br/>for (int i = 0; i < hits.length(); i++)<br/>&#123;<br/>Document doc = hits.doc(i);<br/>System.out.println(doc.get("title");<br/>&#125;<br/>is.close();<br/><br/>首先我们创建一个在指定文件目录上的IndexSearcher。<br/>然后创建一个使用StandardAnalyzer作为分析器的QueryParser，它默认搜索的域是content。<br/>接着我们用QueryParser来parse查询字串，生成一个Query。<br/>然后利用这个Query去查找结果，结果以Hits的形式返回。<br/>这个Hits对象包含一个列表，我们挨个把它的内容显示出来。<br/><br/>7.3 Filter<br/>filter 的作用就是限制只查询索引的某个子集，它的作用有点像SQL语句里的where，但又有区别，它不是正规查询的一部分，只是对数据源进行预处理，然后交给查询语句。注意它执行的是预处理，而不是对查询结果进行过滤，所以使用filter的代价是很大的，它可能会使一次查询耗时提高一百倍。<br/>最常用的filter是RangeFilter和QueryFilter。RangeFilter是设定只搜索指定范围内的索引；QueryFilter是在上次查询的结果中搜索。<br/>Filter的使用非常简单，你只需创建一个filter实例，然后把它传给searcher。继续上面的例子，查询“时间在20060101到20060130之间的文章”除了将限制写在query string中，你还可以写在RangeFilter中：<br/><br/>Directory dir = FSDirectory.getDirectory(PATH, false);<br/>IndexSearcher is = new IndexSearcher(dir);<br/>QueryParser parser = new QueryParser("content", new StandardAnalyzer());<br/>Query query = parser.parse("title:lucene content:lucene";<br/>RangeFilter filter = new RangeFilter("time", "20060101", "20060230", true, true);<br/>Hits hits = is.search(query, filter);<br/>for (int i = 0; i < hits.length(); i++)<br/>&#123;<br/>Document doc = hits.doc(i);<br/>System.out.println(doc.get("title");<br/>&#125;<br/>is.close();<br/><br/>7.4 Sort<br/>有时你想要一个排好序的结果集，就像SQL语句的“order by”，lucene能做到：通过Sort。<br/>Sort sort = new Sort(“time”); //相当于SQL的“order by time”<br/>Sort sort = new Sort(“time”, true); // 相当于SQL的“order by time desc”<br/>下面是一个完整的例子：<br/><br/>Directory dir = FSDirectory.getDirectory(PATH, false);<br/>IndexSearcher is = new IndexSearcher(dir);<br/>QueryParser parser = new QueryParser("content", new StandardAnalyzer());<br/>Query query = parser.parse("title:lucene content:lucene";<br/>RangeFilter filter = new RangeFilter("time", "20060101", "20060230", true, true);<br/>Sort sort = new Sort(“time”);<br/>Hits hits = is.search(query, filter, sort);<br/>for (int i = 0; i < hits.length(); i++)<br/>&#123;<br/>Document doc = hits.doc(i);<br/>System.out.println(doc.get("title");<br/>&#125;<br/>is.close();<br/><br/>8 分析器<br/>在前面的概念介绍中我们已经知道了分析器的作用，就是把句子按照语义切分成一个个词语。英文切分已经有了很成熟的分析器： StandardAnalyzer，很多情况下StandardAnalyzer是个不错的选择。甚至你会发现StandardAnalyzer也能对中文进行分词。<br/>但是我们的焦点是中文分词，StandardAnalyzer能支持中文分词吗？实践证明是可以的，但是效果并不好，搜索“如果”会把“牛奶不如果汁好喝”也搜索出来，而且索引文件很大。那么我们手头上还有什么分析器可以使用呢？core里面没有，我们可以在sandbox里面找到两个：ChineseAnalyzer和CJKAnalyzer。但是它们同样都有分词不准的问题。相比之下用StandardAnalyzer和 ChineseAnalyzer建立索引时间差不多，索引文件大小也差不多，CJKAnalyzer表现会差些，索引文件大且耗时比较长。<br/>要解决问题，首先分析一下这三个分析器的分词方式。StandardAnalyzer和ChineseAnalyzer都是把句子按单个字切分，也就是说“牛奶不如果汁好喝”会被它们切分成“牛 奶 不 如 果 汁 好 喝”；而CJKAnalyzer则会切分成“牛奶 奶不 不如 如果 果汁 汁好好喝”。这也就解释了为什么搜索“果汁”都能匹配这个句子。<br/>以上分词的缺点至少有两个：匹配不准确和索引文件大。我们的目标是将上面的句子分解成 “牛奶 不如 果汁好喝”。这里的关键就是语义识别，我们如何识别“牛奶”是一个词而“奶不”不是词语？我们很自然会想到基于词库的分词法，也就是我们先得到一个词库，里面列举了大部分词语，我们把句子按某种方式切分，当得到的词语与词库中的项匹配时，我们就认为这种切分是正确的。这样切词的过程就转变成匹配的过程，而匹配的方式最简单的有正向最大匹配和逆向最大匹配两种，说白了就是一个从句子开头向后进行匹配，一个从句子末尾向前进行匹配。基于词库的分词词库非常重要，词库的容量直接影响搜索结果，在相同词库的前提下，据说逆向最大匹配优于正向最大匹配。<br/><br/>当然还有别的分词方法，这本身就是一个学科，我这里也没有深入研究。回到具体应用，我们的目标是能找到成熟的、现成的分词工具，避免重新发明车轮。经过网上搜索，用的比较多的是中科院的ICTCLAS和一个不开放源码但是免费的JE-Analysis。ICTCLAS有个问题是它是一个动态链接库，java调用需要本地方法调用，不方便也有安全隐患，而且口碑也确实不大好。JE-Analysis效果还不错，当然也会有分词不准的地方，相比比较方便放心。<br/><br/>9 性能优化<br/>一直到这里，我们还是在讨论怎么样使lucene跑起来，完成指定任务。利用前面说的也确实能完成大部分功能。但是测试表明lucene的性能并不是很好，在大数据量大并发的条件下甚至会有半分钟返回的情况。另外大数据量的数据初始化建立索引也是一个十分耗时的过程。那么如何提高lucene的性能呢？下面从优化创建索引性能和优化搜索性能两方面介绍。<br/><br/>9.1 优化创建索引性能<br/>这方面的优化途径比较有限，IndexWriter提供了一些接口可以控制建立索引的操作，另外我们可以先将索引写入RAMDirectory，再批量写入FSDirectory，不管怎样，目的都是尽量少的文件IO，因为创建索引的最大瓶颈在于磁盘IO。另外选择一个较好的分析器也能提高一些性能。<br/><br/>9.1.1 通过设置IndexWriter的参数优化索引建立<br/>setMaxBufferedDocs(int maxBufferedDocs) <br/>控制写入一个新的segment前内存中保存的document的数目，设置较大的数目可以加快建索引速度，默认为10。<br/>setMaxMergeDocs(int maxMergeDocs) <br/>控制一个segment中可以保存的最大document数目，值较小有利于追加索引的速度，默认Integer.MAX_VALUE，无需修改。<br/>setMergeFactor(int mergeFactor) <br/>控制多个segment合并的频率，值较大时建立索引速度较快，默认是10，可以在建立索引时设置为100。<br/><br/>9.1.2 通过RAMDirectory缓写提高性能<br/>我们可以先把索引写入RAMDirectory，达到一定数量时再批量写进FSDirectory，减少磁盘IO次数。<br/><br/>FSDirectory fsDir = FSDirectory.getDirectory("/data/index", true);<br/>RAMDirectory ramDir = new RAMDirectory();<br/>IndexWriter fsWriter = new IndexWriter(fsDir, new StandardAnalyzer(), true);<br/>IndexWriter ramWriter = new IndexWriter(ramDir, new StandardAnalyzer(), true);<br/>while (there are documents to index)<br/>&#123;<br/>... create Document ...<br/>ramWriter.addDocument(doc);<br/>if (condition for flushing memory to disk has been met)<br/>&#123;<br/>fsWriter.addIndexes(new Directory[] &#123; ramDir &#125;);<br/>ramWriter.close();<br/>ramWriter = new IndexWriter(ramDir, new StandardAnalyzer(), true);<br/>&#125;<br/>&#125;<br/><br/>9.1.3 选择较好的分析器<br/>这个优化主要是对磁盘空间的优化，可以将索引文件减小将近一半，相同测试数据下由600M减少到380M。但是对时间并没有什么帮助，甚至会需要更长时间，因为较好的分析器需要匹配词库，会消耗更多cpu，测试数据用StandardAnalyzer耗时133分钟；用MMAnalyzer耗时150分钟。<br/><br/>9.2 优化搜索性能<br/>虽然建立索引的操作非常耗时，但是那毕竟只在最初创建时才需要，平时只是少量的维护操作，更何况这些可以放到一个后台进程处理，并不影响用户搜索。我们创建索引的目的就是给用户搜索，所以搜索的性能才是我们最关心的。下面就来探讨一下如何提高搜索性能。<br/><br/>9.2.1 将索引放入内存<br/>这是一个最直观的想法，因为内存比磁盘快很多。Lucene提供了RAMDirectory可以在内存中容纳索引：<br/><br/>Directory fsDir = FSDirectory.getDirectory(“/data/index/”, false);<br/>Directory ramDir = new RAMDirectory(fsDir);<br/>Searcher searcher = new IndexSearcher(ramDir);<br/><br/>但是实践证明RAMDirectory和FSDirectory速度差不多，当数据量很小时两者都非常快，当数据量较大时（索引文件400M）RAMDirectory甚至比FSDirectory还要慢一点，这确实让人出乎意料。<br/>而且lucene的搜索非常耗内存，即使将400M的索引文件载入内存，在运行一段时间后都会out of memory，所以个人认为载入内存的作用并不大。<br/><br/>9.2.2 优化时间范围限制<br/>既然载入内存并不能提高效率，一定有其它瓶颈，经过测试发现最大的瓶颈居然是时间范围限制，那么我们可以怎样使时间范围限制的代价最小呢？<br/>当需要搜索指定时间范围内的结果时，可以：<br/>1、用RangeQuery，设置范围，但是RangeQuery的实现实际上是将时间范围内的时间点展开，组成一个个BooleanClause加入到 BooleanQuery中查询，因此时间范围不可能设置太大，经测试，范围超过一个月就会抛BooleanQuery.TooManyClauses，可以通过设置 BooleanQuery.setMaxClauseCount(int maxClauseCount)扩大，但是扩大也是有限的，并且随着maxClauseCount扩大，占用内存也扩大<br/>2、用 RangeFilter代替RangeQuery，经测试速度不会比RangeQuery慢，但是仍然有性能瓶颈，查询的90%以上时间耗费在 RangeFilter，研究其源码发现RangeFilter实际上是首先遍历所有索引，生成一个BitSet，标记每个document，在时间范围内的标记为true，不在的标记为false，然后将结果传递给Searcher查找，这是十分耗时的。<br/>3、进一步提高性能，这个又有两个思路：<br/>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，所以只能把它作为一个封装类。<br/>b、降低时间精度。研究Filter的工作原理可以看出，它每次工作都是遍历整个索引的，所以时间粒度越大，对比越快，搜索时间越短，在不影响功能的情况下，时间精度越低越好，有时甚至牺牲一点精度也值得，当然最好的情况是根本不作时间限制。<br/>下面针对上面的两个思路演示一下优化结果（都采用800线程随机关键词随即时间范围）：<br/>第一组，时间精度为秒：<br/>方式 直接用RangeFilter 使用cache 不用filter<br/>平均每个线程耗时 10s 1s 300ms<br/><br/>第二组，时间精度为天<br/>方式 直接用RangeFilter 使用cache 不用filter<br/>平均每个线程耗时 900ms 360ms 300ms<br/><br/>由以上数据可以得出结论：<br/>1、 尽量降低时间精度，将精度由秒换成天带来的性能提高甚至比使用cache还好，最好不使用filter。<br/>2、 在不能降低时间精度的情况下，使用cache能带了10倍左右的性能提高。<br/><br/>9.2.3 使用更好的分析器<br/>这个跟创建索引优化道理差不多，索引文件小了搜索自然会加快。当然这个提高也是有限的。较好的分析器相对于最差的分析器对性能的提升在20%以下。<br/><br/>10 一些经验<br/><br/>10.1关键词区分大小写<br/>or AND TO等关键词是区分大小写的，lucene只认大写的，小写的当做普通单词。<br/><br/>10.2 读写互斥性<br/>同一时刻只能有一个对索引的写操作，在写的同时可以进行搜索<br/><br/>10.3 文件锁<br/>在写索引的过程中强行退出将在tmp目录留下一个lock文件，使以后的写操作无法进行，可以将其手工删除<br/><br/>10.4 时间格式<br/>lucene只支持一种时间格式yyMMddHHmmss，所以你传一个yy-MM-dd HH:mm:ss的时间给lucene它是不会当作时间来处理的<br/><br/>10.5 设置boost<br/>有些时候在搜索时某个字段的权重需要大一些，例如你可能认为标题中出现关键词的文章比正文中出现关键词的文章更有价值，你可以把标题的boost设置的更大，那么搜索结果会优先显示标题中出现关键词的文章（没有使用排序的前题下）。使用方法：<br/>Field. setBoost(float boost);默认值是1.0，也就是说要增加权重的需要设置得比1大。 <br/><br/><br/>Tags - <a href="http://blog.lough.com.cn/tags/lucene/" rel="tag">lucene</a>
]]>
</description>
</item>
</channel>
</rss>