您的位置:首页 > 其它

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

2012-11-17 22:42 260 查看
前言: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
*
@authorWinterLau
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
*
@paramid
44
*
@paramcount
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的编码规范,这里主要是为了避免索引器把这个方法当成是JavaBean的getter/setter
方法。

前面我们讲到了对索引的修改操作要独立到一个进程或者线程来处理,并保证是单实例的。OSChina是通过一个单独的ApacheAnt任务来执行这个索引过程,Ant任务定义如下:

01
<!--
LuceneTasks-->
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
luceneindexof${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
luceneindexof${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
*
@authorWinterLau
14
*
@date2010-1-4下午04:52:12
15
*/
16
public
class
LuceneUpdater
{
17
18
private
final
static
Log
log=LogFactory.getLog(LuceneUpdater.
class
);
19
20
/**
21
*
@paramargs
22
*
@throwsException
23
*/
24
public
static
void
main(String[]
args)
throws
Exception
{
25
String
sql=
"SELECT
*FROMosc_lucene_tasksWHEREstatus=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()+
"
Lucenetasksexecutedfinished."
);
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检索没有什么两样。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: