文档章节

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

eggbucket
 eggbucket
发布于 2013/12/14 15:57
字数 2327
阅读 311
收藏 41

前言: 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
博文 49
码字总数 3738
作品 0
綦江
私信 提问
加载中

评论(1)

光旭
光旭
太好了,学校实验室喜欢你们,开源中国,一直关注你们
lucene 增量索引问题请教

@红薯 你好,想跟你请教个问题:看了您的 OSChina 的全文搜索设计说明 —— 索引过程 这篇文章,我初学java 用lucene建增量索引的功能,每次有新的文章发表,我就触发增量索引逻辑,为避免锁...

csyangshungang
2013/03/21
452
1
OSChina 的全文搜索设计说明 —— 索引过程

前言: OSChina 的搜索做得并不好,很久之前一直想在细节方面进行改造,一直也没什么好的思路。但作为整体的结构或许对大家还是有一些参考的价值,之前也分享过一些代码,这次主要是把整个模...

红薯
2012/09/26
15.5K
58
使用 JavaScript 实现对 PDF 的全文索引

我曾今在一个售卖法律和财务数据库访问方案(他们称之为“智能信息”)的公司工作。大多数法庭记录都是通过PACER以PDF形式提供的,一个站点被特地开发出来用于发布法庭记录。基于这个数据集的...

oschina
2013/10/10
7.8K
11
Elasticsearch -- 映射和分析

映射(mapping)机制用于进行字段类型确认,将每个字段匹配为一种确定的数据类型(, ,, 等)。 分析(analysis)机制用于进行全文文本(Full Text)的分词,以建立供搜索用的反向索引。 映射及分析 ...

莫问viva
2016/05/10
173
0
使用 Docker 和 Elasticsearch 构建一个全文搜索应用程序

如何在超过 500 万篇文章的 Wikipedia 上找到与你研究相关的文章? 如何在超过 20 亿用户的 Facebook 中找到你的朋友(并且还拼错了名字)? 谷歌如何在整个因特网上搜索你的模糊的、充满拼写...

作者: Patrick Triest
05/01
0
0

没有更多内容

加载失败,请刷新页面

加载更多

ZooKeeper命令大全

创建节点 # 创建节点,-s表示顺序节点,-e表示临时节点,默认是持久节点create [-s] [-e] path data acl # 示例create /zk-book 123 查看节点 ls path [watch] # 示例ls /zk-book 获取...

爱宝贝丶
16分钟前
0
0
QLExpress基本语法

QLExpress基本语法 一、背景介绍 由阿里的电商业务规则、表达式(布尔组合)、特殊数学公式计算(高精度)、语法分析、脚本二次定制等强需求而设计的一门动态脚本引擎解析工具。 在阿里集团有...

xiaomin0322
16分钟前
0
0
Elasticsearch节点角色类型node.master和node.data说明s

一般地,ElasticSearch集群中每个节点都有成为主节点的资格,也都存储数据,还可以提供查询服务。这些功能是由两个属性控制的(node.master和node.data)。默认情况下这两个属性的值都是tru...

傲娇字符
32分钟前
5
0
rem的使用

(function (doc, win) { var docEl = doc.documentElement var resizeEvt = 'orientationchange' in window ? 'orientationchange' : 'resize' var recalc = function () { var cl......

Js_Mei
39分钟前
1
0
idea 删除代码的注释

搜索栏使用 正则表达式搜索 (/\*([^*]|[\r\n]|(\*+([^*/]|[\r\n])))*\*+/|[ \t]*//.*) 会搜索出来所有注释的代码 用空格replace替换掉就可以了。 或者搜索 (/\*([^*]|[\r\n]|(\*+([^*/]|[\r\...

时刻在奔跑
42分钟前
2
0

没有更多内容

加载失败,请刷新页面

加载更多

返回顶部
顶部