当前位置: 首页 > 搜索 > 正文

使用Zoie构建实时检索系统

1 星2 星3 星4 星5 星 (1 次投票, 评分: 5.00, 总分: 5)
Loading ... Loading ...
baidu_share

Zoie是LinkedIn开源的实时检索系统,它本身用于LinkedIn的用户profile页检索。且不说专业的搜索引擎,大多数的站内检索基本都不是实时的,一般都会有几分钟的延迟。但LinkedIn认为,用户profile信息的检索需要实时的效果,因为如果搜索结果不正确或不理想会很影响用户体验。所以,LinkedIn的Zoie实现了秒级别的实时检索效果。Zoie在LinkedIn跑了有两年多时间,这个开源项目可以说是起点就比较成熟的,这个项目提供了较为丰富的功能和工具,除了核心的API外,它还提供了管理和监控工具、嵌入jetty的Web server demo、一些实用的DataProvider等。实现上自然是基于lucene了,也使用了一些我需要回顾或者了解的库和框架。对于社区类需要提供用户信息检索的系统来说,使用Zoie或许是个很不错的选择。

下面将要介绍的使用Zoie API来编写检索demo,并且为了省事,我的code sample直接来自于Zoie的Wiki(http://snaprojects.jira.com/wiki/display/ZOIE/Code+Samples),但会对其中的使用做些中文版的说明。由于Zoie是个很有针对性的实现,所以相比于Solr这样的通用系统来说,Zoie的代码要“直白”很多,这也促使我拿出精力把它的核心代码研究了一番。总结下来有两点:

1)要写一个基于lucene的精致的检索系统需要对lucene有深入的理解,针对实时性检索需求,Zoie本身就对lucene做了很多扩展。
2)Zoie的代码结构和代码风格有些糟糕,我看到git里有.project文件,所以可见其作者应该也是基于eclipse开发的,可他的代码就不能好好format下?并且包名和类层次也很诡异。闲言少叙(让我再次想起俞平伯先生),下面罗列下使用Zoie API的code sample。
在检索server端,需要索引的数据总要以一种结构表示,比如一个MAP或POJO之类的。让Zoie来索引数据,就需要Zoie知道你需要索引哪些Field到Document(lucene里的Document)里。说白了,要提供一份要索引的数据及将数据转换成Document结构的接口。这里的数据假定是一个Data:

1
2
3
4
class Data{
long id;
String content;
}

转换数据的工作由实现ZoieIndexableInterpreter接口的类实现,代码片断如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
class DataIndexable implements ZoieIndexable {
  private Data _data;
  public DataIndexable(Data data) {
    _data = data;
  }
 
  public long getUID() {
    return _data.id;
  }
 
  public IndexingReq[] buildIndexingReqs() {
    // it is possible we want to map 1 data object to multiple lucene documents
    // but not for this example
    Document doc = new Document();
    doc.add(new Field("content",_data.content,Store.NO,Index.ANALYZED));
 
    // no need to add the id field, Zoie will manage the id for you
    return new IndexingReq[]{new IndexingReq(doc)};
  }
 
  // the following methods in this example are kind of hacky,
  // but it is designed to be used when information needed to determine whether documents
  // are to be deleted and/or skipped are only known at runtime
 
  public boolean isDeleted() {
    return "_MARKED_FOR_DELETE".equals(_data.content);
  }
 
  public boolean isSkip(){
    return "_MARKED_FOR_SKIP".equals(_data.content);
  }
}
 
class DataIndexableInterpreter implements ZoieIndexableInterpreter {
  public ZoieIndexable interpret(Data src){
    return new DataIndexable(src);
  }
}

针对上面的代码,我还需要做一些解释。可以看出的是,Zoie在建索引时,需要使用public IndexingReq[] buildIndexingReqs()方法来取出数据,在这个方法里,需要自己build document结构。而方法public long getUID()表明,Zoie需要索引的数据包含uid(可以认为是唯一性主键的概念,而不必须是实际意义的uid),并且这个uid不要在buildIndexingReqs()方法里做成Field,Zoie自己会处理uid,实际上它内部是将uid和lucene的doc id对应上了。这有什么用处呢?对于像用户profile情景,是假设没有删除profile而只有add或update的情况,这使得Zoie在内存里记录了uid和doc id的对应关系,当检查发现是update时,就标记该doc需要在search时过滤掉。对于需要删除数据的场景,一个折中的方法是仅保留uid而将其他Field都去掉。还有那个isDeleted()方法,不要以为它是提供要删除的doc的判断的,它的作用和isSkip差不多,是在Zoie建索引时临时的将当前的Data去掉而不建它的索引(这需要外部修改Data的内容才行),就好像你发现建的数据有问题想中止操作一样,但可想见的是,你根本不知道Zoie建索引到什么阶段了,这个接口的用处就值得怀疑了。

下面再来构建IndexDecorator。Zoie提供的这个接口是允许客户端(相对于Zoie来说是客户端)对给定的ZoieIndexReader装饰成自定义的IndexReader。就像demo给的那样,除非有必要,否则默认实现就可以了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class MyDoNothingFilterIndexReader extends FilterIndexReader {
public MyDoNothingFilterIndexReader(IndexReader reader) {
super(reader);
}
 
public void updateInnerReader(IndexReader inner) {
in = inner;
}
}
 
class MyDoNothingIndexReaderDecorator implements IndexReaderDecorator {
 
  public MyDoNothingIndexReaderDecorator decorate(ZoieIndexReader indexReader) throws IOException {
    return new MyDoNothingFilterIndexReader(indexReader);
  }
 
  public MyDoNothingIndexReaderDecorator redecorate(MyDoNothingIndexReaderDecorator decorated,
                                                    ZoieIndexReader copy) throws IOException {
    // underlying segment has not changed, just change the inner reader
 
    decorated.updateInnerReader(copy);
    return decorated;
  }
}

下面是build Zoie的核心:ZoieSystem。indexingSystem的start()方法将启动新的线程准备接收数据来建索引.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// index directory
File idxDir = new File("myIdxDir");
 
// create an analyzer
Analyzer analyzer = new StandardAnalyzer(Version.LUCENE_CURRENT);
 
// create similarity
Similarity similarity = new DefaultSimilarity();
 
ZoieIndexableInterpreter myInterpreter = new DataIndexableInterpreter();
 
IndexReaderDecorator decorator = new MyDoNothingIndexReaderDecorator();
 
ZoieSystem indexingSystem = new ZoieSystem(idxDir,         // index direcotry
                                           myInterpreter,  // my interpreter
                                           decorator,      // index decorator
                                           analyzer,       // my analyzer
                                           similarity,     // my similarity
                                           1000,           // # events to hold in mem before flushing to disk
                                           300000,         // time(ms) to wait before flushing to disk
                                           true);          // true for realtime
 
indexingSystem.start();  // ready to accept indexing even

下面给出建索引和查询的使用片断,两者可分别在不同线程执行。
索引线程的代码片断:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
long batchVersion = 0;
while(true){
  Data[] data = buildDataEvents(...); // build a batch of data object to index
 
  // construct a collection of indexing events
  ArrayList eventList = new ArrayList(data.length);
  for (Data datum : data){
    eventList.add(new DataEvent(batchVersion,datum));
  }
 
  // do indexing
  indexingSystem.consume(events);
 
 // increment my version
  batchVersion++;
}

上面的buildDataEvents方法是产生数据的方法,比如接收客户端请求的数据(也许它还会阻塞在那?),或者是查询数据库的数据之类的。ZoieSystem的consume方法就是用来消费数据建索引,并且是可以批量处理的。
再看看查询线程的代码片断:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
	// get the IndexReaders
List> readerList = indexingSystem.getIndexReaders();
 
// MyDoNothingFilterIndexReader instances can be obtained by calling
// ZoieIndexReader.getDecoratedReaders()
 
// combine the readers
MultiReader reader = new MultiReader(readerList.toArray(new IndexReader[readerList.size()]),false);
// do search
IndexSearcher searcher = new IndexSearcher(reader);
Query q = buildQuery("myquery",indexingSystem.getAnalyzer());
 
TopDocs docs = searcher.search(q,10);
 
// return readers
indexingSystem.returnIndexReaders(readerList);

懒得解释了,跟使用lucene的查询接口差不多。
上面的代码稍加完善就可以真正跑起来了。Zoie也提供了独立的Server功能,但它的那个Server很有可能是你不想要的,而使用上述的嵌入式接口来实现一个Server显然已经极大简化工作了。Zoie还提供了DataProvider机制及实用的实现,对于像通过数据库数据来新建或重建索引,实现自定义的DataProvider就ok了。
总结来说,Zoie是个不错的开源项目,会适合一些站内检索需求。至于它的发展如何就很难说了,LinkedIn开源的几个项目都没什么发展,和Facebook形成了较为鲜明的对比,所以Zoie是否会持续发展下去也很难说,需要看社区及其作者是否能推动这个项目。不过,鉴于这个系统并不复杂,应用及扩展成本是可以控制的。

本文固定链接: http://www.chepoo.com/zoie-build-real-time-retrieval-system-using.html | IT技术精华网

使用Zoie构建实时检索系统:等您坐沙发呢!

发表评论