文档章节

OSChina 的全文搜索设计说明 —— 索引过程

eggbucket
 eggbucket
发布于 2013/12/14 15:57
字数 2327
阅读 308
收藏 41
点赞 0
评论 1

前言: OSChina 的搜索做得并不好,很久之前一直想在细节方面进行改造,一直也没什么好的思路。但作为整体的结构或许对大家还是有一些参考的价值,之前也分享过一些代码,这次主要是把整个模块的设计思路详细的介绍一下,本文要求了解 Lucene 的基本使用。

OSChina 使用的是全文搜索的技术,涉及到的开源软件包括 Lucene 和国产的 IKAnalyzer。谈到分词,有些人喜欢问,你怎么不用xxx呢?很不好意思,鄙人只用过和熟悉 IKAnalyzer ,所以选型的时候肯定考虑的是它。另外 IKAnalyzer 的作者就在 OSC 上,有什么问题也方便直接请教,请看之前OSC 对 IKAnalyzer 的作者林良益的访谈

以下内容来自百度百科:

全文搜索引擎是目前广泛应用的主流搜索引擎。它的工作原理是计算机索引程序通过扫描文章中的每一个词,对每一个词建立一个索引,指明该词在文章中出现的次数和位置,当用户查询时,检索程序就根据事先建立的索引进行查找,并将查找的结果反馈给用户的检索方式。这个过程类似于通过字典中的检索字表查字的过程。

全文检索系统是按照全文检索理论建立起来的用于提供全文检索服务的软件系统。一般来说,全文检索需要具备建立索引和提供查询的基本功能,此外现代的全文检索系统还需要具有方便的用户接口、面向WWW的开发接口、二次应用开发接口等等。功能上,全文检索系统核心具有建立索引、处理查询返回结果集、增加索引、优化索引结构等等功能,外围则由各种不同应用具有的功能组成。

结构上,全文检索系统核心具有索引引擎、查询引擎、文本分析引擎、对外接口等等,加上各种外围应用系统等等共同构成了全文检索系统。

全文搜索技术的特点是:速度超快,但不及时,主要体现在刚发布的文章并不能马上搜到(这句话并不绝对,请勿纠结)。

一般全文搜索引擎都有自己独立的索引库,这个索引库跟数据库是完全隔离的,没有任何关系。Lucene 使用的是文件系统来存储索引库。当我们发布一篇文章时,这篇文章是存在于数据库中,需要通过 Lucene 提供的 API 将文章进行索引(Indexing),然后写到索引库中才能检索得到。

由于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) throwsException {
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 检索没有什么两样。

© 著作权归作者所有

共有 人打赏支持
eggbucket
粉丝 0
博文 3
码字总数 3738
作品 0
綦江
加载中

评论(1)

光旭
光旭
太好了,学校实验室喜欢你们,开源中国,一直关注你们

暂无相关文章

Linux kernel脉络和主干总结

写在前面 前人常说,对Linux操作系统/内核的理解,是计算机行业从业者的内功,决定了你在技术领域想走多远。但内核的庞大以及学习曲线之陡峭,总让我在学习途中觉得犹如“管中窥豹”。 随着工...

Markz0928 ⋅ 14分钟前 ⋅ 0

在gcc中使用intel风格的内联汇编

很简单,内联汇编使用asm(“.intel_syntax noprefix/n”)声明一下,以后的内联汇编就可以用intel风格了,构建可执行文件时给gcc加上-masm=intel参数。 先写一个小程序测试一下: [cpp] view...

simpower ⋅ 25分钟前 ⋅ 0

NIO 之 ByteBuffer实现原理

相关文章 BIO、NIO、AIO 内部原理分析 NIO 之 Selector实现原理 NIO 之 Channel实现原理 前言 Java NIO 主要由下面3部分组成: Buffer Channel Selector 在传统IO中,流是基于字节的方式进行...

轨迹_ ⋅ 34分钟前 ⋅ 0

Jenkins docker权限问题

环境Ubuntu Server 工具 jenkins-war:2.89.2 报错信息 Cannot connect to the Docker daemon. Is the docker daemon running on this host?Build step 'Execute shell' marked build as fai......

Pulsar-V ⋅ 34分钟前 ⋅ 0

180621-一个简单的时间窗口设计与实现

如何设计一个计数的时间窗口 时间窗口,通常对于一些实时信息展示中用得比较多,比如维持一个五分钟的交易明细时间窗口,就需要记录当前时间,到五分钟之前的所有交易明细,而五分钟之前的数...

小灰灰Blog ⋅ 57分钟前 ⋅ 0

Android之Dalvik、ART、JIT、AOT

Android之Dalvik、ART、JIT、AOT 本文内容:Dalvik、ART、JIT、AOT之间关系 本文定位:知识记录 学习过程记录,加深理解,提升文字组合表达能力。也希望能给学习的同学一些灵感 本文整理于[...

lichuangnk ⋅ 今天 ⋅ 0

Thrift RPC实战(五) thrift连接池

Thrift本身没有提供连接池,我们可以用Apache Commons Pool2来实现一个 一、定义对象工厂 BasePooledObjectFactory<T> extends BaseObject implements PooledObjectFactory<T> public class......

lemonLove ⋅ 今天 ⋅ 0

git 命令简写

简写 命令 g git gst git status gd git diff gdc git diff --cached gdv git diff -w "$@" | view - gl git pull gup git pull --rebase gp git push gc git commit -v gc! git commit -v ......

charley158 ⋅ 今天 ⋅ 0

Java中的锁使用与实现

1.Lock接口 锁是用来控制多个线程访问共享资源的方式,一般来说,一个锁能够防止多个线程同时访问共享资源。 在Lock出现之前,java程序是靠synchronized关键字实现锁功能的,而Java SE5之后,...

ZH-JSON ⋅ 今天 ⋅ 0

Intellij IDEA神器常用技巧四-类和方法注释模板设置

IDEA自带的注释模板不是太好用,我本人到网上搜集了很多资料系统的整理了一下制作了一份比较完整的模板来分享给大家,我不是专业玩博客的,写这篇文章只是为了让大家省事。 这里设置的注释模...

Mkeeper ⋅ 今天 ⋅ 0

没有更多内容

加载失败,请刷新页面

加载更多

下一页

返回顶部
顶部