OSChina 的全文搜索設計說明 —— 索引過程(轉)

前言: OSChina 的搜索作得並很差,好久以前一直想在細節方面進行改造,一直也沒什麼好的思路。但做爲總體的結構或許對你們仍是有一些參考的價值,以前也分享過一些代碼,此次主要是把整個模塊的設計思路詳細的介紹一下,本文要求瞭解 Lucene 的基本使用。 java

OSChina 使用的是全文搜索的技術,涉及到的開源軟件包括 Lucene 和國產的 IKAnalyzer。談到分詞,有些人喜歡問,你怎麼不用xxx呢?很很差意思,鄙人只用過和熟悉 IKAnalyzer ,因此選型的時候確定考慮的是它。另外 IKAnalyzer 的做者就在 OSC 上,有什麼問題也方便直接請教,請看以前OSC 對 IKAnalyzer 的做者林良益的訪談sql

如下內容來自百度百科: 數據庫

全文搜索引擎是目前普遍應用的主流搜索引擎。它的工做原理是計算機索引程序經過掃描文章中的每個詞,對每個詞創建一個索引,指明該詞在文章中出現的次數和位置,當用戶查詢時,檢索程序就根據事先創建的索引進行查找,並將查找的結果反饋給用戶的檢索方式。這個過程相似於經過字典中的檢索字表查字的過程。 apache

全文檢索系統是按照全文檢索理論創建起來的用於提供全文檢索服務的軟件系統。通常來講,全文檢索須要具有創建索引和提供查詢的基本功能,此外現代的全文檢索系統還須要具備方便的用戶接口、面向WWW的開發接口、二次應用開發接口等等。功能上,全文檢索系統核心具備創建索引、處理查詢返回結果集、增長索引、優化索引結構等等功能,外圍則由各類不一樣應用具備的功能組成。 併發

結構上,全文檢索系統核心具備索引引擎、查詢引擎、文本分析引擎、對外接口等等,加上各類外圍應用系統等等共同構成了全文檢索系統。 jvm

全文搜素技術的特色是:速度超快,但不及時,主要體如今剛發佈的文章並不能立刻搜到(這句話並不絕對,請勿糾結)。 優化

通常全文搜索引擎都有本身獨立的索引庫,這個索引庫跟數據庫是徹底隔離的,沒有任何關係。Lucene 使用的是文件系統來存儲索引庫。當咱們發佈一篇文章時,這篇文章是存在於數據庫中,須要經過 Lucene 提供的 API 將文章進行索引(Indexing),而後寫到索引庫中才能檢索獲得。 ui

因爲Web應用是一個多用戶併發的系統,簡單的說,同一個時間點可能有不止一我的在發帖,而 Lucene 的索引庫支持併發讀,但若是同時多人寫入或者更新就會致使索引庫被鎖住(索引庫目錄下有名爲 lock 文件),所以必須避免在發帖的時候同時更新索引庫,通常的作法由一個獨立的進程來負責索引的添加、刪除和修改操做,這也就是我前面說的 「不及時」 的緣由。 搜索引擎

總結一下,若是要用 Lucene 來作全文搜索,必須注意的問題是:避免有多個線程、進程同時操做索引庫,包括添加、修改和刪除編碼

下圖是一個 OSChina 全文搜索的基本結構:

因爲 OSC 須要作全文搜索的內容有好幾種,包括:軟件、新聞、問答、代碼、博客,目前這幾種類型的文章都是使用獨立的索引庫存儲(這也是我想改造的一個不足之一),爲了統一索引過程,我定義了一個接口 —— SearchEnabled

01 package my.search;
02  
03 import java.util.*;
04  
05 /**
06  * 支持搜索功能的Bean類須要實現該接口
07  * @author  Winter Lau
08  */
09 public interface SearchEnabled {
10  
11     /**
12      * 獲取搜索對象的關鍵字
13      * @return
14      */
15     public long getId();
16  
17     /**
18      * 返回搜索對象須要存儲的字段名,例如createTime, author等
19      * @return
20      */
21     public String[] GetStoreFields();
22  
23     /**
24      * 返回搜索對象的索引字段,例如title,content
25      * @return
26      */
27     public String[] GetIndexFields();
28      
29     /**
30      * 返回對象的擴展信息
31      * @return
32      */
33     public HashMap<String, String> GetExtendValues();
34  
35     /**
36      * 返回對象的擴展索引信息
37      * @return
38      */
39     public HashMap<String, String> GetExtendIndexValues();
40      
41     /**
42      * 列出id值大於指定值得全部對象
43      * @param id
44      * @param count
45      * @return
46      */
47     public List<? extends SearchEnabled> ListAfter(long id, int count) ;
48      
49     /**
50      * 返回文檔的權重
51      * @return
52      */
53     public float GetBoost();
54  
55 }

而軟件、帖子、新聞等對象對應的類必須實現 SearchEnabled 接口才能支持全文搜索。這個接口中的兩個擴展方法 GetExtendValues() 和 GetExtendIndexValues() 是爲了讓 Bean 類能有更靈活的方式來提供索引數據,例如帖子裏是包含了標籤列表,這個標籤自己並非 Bean 類的一個屬性,就能夠經過這個方式來提供給索引器。而爲何這裏有些方法是大寫字母開頭呢,這不符合 Java 的編碼規範,這裏主要是爲了不索引器把這個方法當成是 Java Bean 的 getter/setter 方法。

前面咱們講到了對索引的修改操做要獨立到一個進程或者線程來處理,並保證是單實例的。OSChina 是經過一個單獨的 Apache Ant 任務來執行這個索引過程,Ant 任務定義以下:

01 <!-- Lucene Tasks -->
02 <target name="lucene_clean">
03 <delete dir="${lucene.dir}"/>
04 </target>
05  
06 <target depends="init" name="lucene_init">
07 <mkdir dir="${lucene.dir}"/>
08 </target>
09  
10 <target depends="lucene_init" name="lucene_build">
11 <echo message="Build lucene index of ${ant.project.name}"/>      
12     <java classname="net.oschina.search.LuceneUpdater" classpathref="oschina.classpath" fork="true">
13         <jvmarg value="-Xmx1024m" />
14     </java>
15 </target>
16  
17 <target depends="lucene_init" name="lucene_build_all">
18 <echo message="Rebuild lucene index of ${ant.project.name}"/>    
19     <java classname="net.oschina.search.RebuildLuceneIndex" classpathref="oschina.classpath" fork="true">
20         <jvmarg value="-Xmx1024m" />         
21         <arg value="net.oschina.beans.Project" />
22         <arg value="net.oschina.beans.News" />
23         <arg value="net.oschina.beans.Blog" />
24         <arg value="net.oschina.code.Code" />        
25         <arg value="net.oschina.qa.Question" />
26     </java>
27 </target>
28  
29 <target depends="lucene_clean,lucene_build_all" name="lucene_rebuild"/>

而後經過 Linux 下的 crontab 每隔五分鐘運行一次 lucene_build 任務,這個任務對應的 Java 類是 net.oschina.search.LuceneUpdater 。這個類是負責增量的構建和更新索引,咱們會在下面詳細介紹。而另一個任務是 lucene_init ,對應的類是 net.oschina.search.RebuildLuceneIndex,這至關因而對索引庫進行初始化。它會讀取傳遞進來的參數(也就是一些Bean的類名),而後實例化這個類,並調用 ListAfter 方法獲取記錄並構建索引。

這個 RebuildLuceneIndex 主要是用於手工執行的,有時候須要徹底重構整個索引庫,有時候須要從某個 id 開始構建索引等等,這是維護上的須要。

如何實現增量的索引?

索引的更改包括:添加、修改和刪除,Lucene 沒有修改的操做,修改等同於刪除後重建。爲了實現增量操做,OSChina 把全部的這些操做記錄到一個專門的索引任務表中,表名 osc_lucene_tasks  結構以下:

請你們不要再糾結什麼 datetime 和 timestamp 的問題了,這不重要 :)

字段說明:

id  -> 記錄的惟一標識
obj_id -> 對象的編號,例如等同於軟件的編號
obj_type -> 對象類型,例如軟件的類型是1
opt -> 操做類型:添加、刪除或者是修改
create_time -> 操做時間
status -> 處理狀態
handle_time -> 處理的時間

當我發佈一個帖子時,obj_id 值爲帖子的編號;obj_type 值爲帖子對應類型常量,這裏是2;opt 值爲 OPT_ADD 常量值;create_time 爲發帖時間;status 值爲0表示待處理;handle_time 值爲空。

而 LuceneUpdater 這個類會由 Linux 下的 crontab 進程每 5 分鐘調用一次,LuceneUpdater 啓動後就掃描 osc_lucene_tasks 這個表中全部 status 爲待處理的記錄,而後根據 obj_id 和 obj_type 兩者的值到對應的表中讀取數據,並根據 opt 字段指定的值來決定是添加到索引庫,仍是從索引庫中刪除,又或者是更新索引庫的操做。

LuceneUpdater 完整代碼以下:

01 package net.oschina.search;
02  
03 import java.util.List;
04  
05 import org.apache.commons.logging.Log;
06 import org.apache.commons.logging.LogFactory;
07  
08 import my.db.QueryHelper;
09 import my.search.LuceneIndexUtils;
10  
11 /**
12  * 按期更新索引
13  * @author  Winter Lau
14  * @date 2010-1-4 下午04:52:12
15  */
16 public class LuceneUpdater {
17  
18     private final static Log log = LogFactory.getLog(LuceneUpdater.class);
19      
20     /**
21      * @param args
22      * @throws Exception
23      */
24     public static void main(String[] args) throws Exception {
25         String sql = "SELECT * FROM osc_lucene_tasks WHERE status=0";
26         List<LuceneTask> tasks = QueryHelper.query(LuceneTask.class, sql);
27         for(LuceneTask task : tasks) {
28             lucene(task, true);
29         }
30         if(tasks.size()>0)
31             log.info(tasks.size()+ " Lucene tasks executed finished.");
32         System.exit(0);
33     }
34      
35     public static void lucene(LuceneTask task, boolean update_status) throws Exception {
36         switch(task.getOpt()){
37         case LuceneTask.OPT_ADD:
38             LuceneIndexUtils.add(task.object());
39             break;
40         case LuceneTask.OPT_DELETE:
41             LuceneIndexUtils.delete(task.object());
42             break;
43         case LuceneTask.OPT_UPDATE:
44             LuceneIndexUtils.update(task.object());
45         }
46         if(update_status)
47             task.afterBuild();
48     }
49  
50 }

這就完成了索引的構建和增量更新的過程,而檢索的操做就跟你作普通的 Lucene 檢索沒有什麼兩樣。

相關文章
相關標籤/搜索