自己动手写分布式搜索引擎
上QQ阅读APP看书,第一时间看更新

3.2.3 向索引库中添加索引文档

先增加文档,然后提交更新到索引库。和数据库表一样,索引库是结构化的,一个文档往往有很多列,例如标题和内容列等。往索引添加数据时涉及的几个类的关系如图3-5所示。

图3-5 往索引中添加文档

TextField是一个快捷类,它不存储词向量。如果你需要词向量,就只使用Field类。这需要更多的代码,因为首先要创建一个FieldType类的实例,然后设置storeTermVectors和tokenizer为true,并在Field构造器中使用这个FieldType实例。

        FieldType t = new FieldType();
        t.setStoreTermVectorOffsets(true);
        t.setTokenized(true);
        fieldTitle = new Field("title", "标题", t);

使用StringField:

        field = new StringField("url", "bar", Store.YES);

使用FieldType实现的等价代码:

        FieldType fieldType = new FieldType();
        fieldType.setStored(true);
        fieldType.setTokenized(false);
        fieldType.setIndexed(true);
        field = new Field("url", "lietu.com", fieldType);

下面这段程序是向索引库中添加网页地址、标题和内容列:

        //如果初次使用Lucene,往索引中写入的每条记录最好都新创建一个Document与之对应,
        //也就是说不要重复使用Document对象,否则可能会出现意想不到的错误
        Document doc = new Document();
        //创建网址列
        Field f = new Field("url", news.URL ,
              Field.Store.YES, Field.Index.UN_TOKENIZED,
              Field.TermVector.NO);
        doc.add(f);
        //创建标题列
        f = new Field("title", news.title ,
                  Field.Store.YES, Field.Index.TOKENIZED,
                  Field.TermVector.WITH_POSITIONS_OFFSETS);
        doc.add(f);
        //创建内容列
        f = new Field("body", news.body.toString() ,
                  Field.Store.YES, Field.Index.TOKENIZED,
                  Field.TermVector.WITH_POSITIONS_OFFSETS);
        doc.add(f);
        index.addDocument(doc);

下面两种写法是等价的:

        new Field("title", "标题", TextField.TYPE_STORED);
        new TextField("title", "标题", Store.YES );

和一般的数据库不一样,一个文档的一个列可以有多个值。例如一篇文档既可以属于互联网类,又可以属于科技类。

Lucene中的API相对数据库来说比较灵活,没有类似数据库先定义表结构后再使用的过程。如果前后两次写索引时定义的列名称不一样,Lucene会自动创建新的列,所以Field的一致性需要我们自己掌握。

列选项组合见表3-2。

表3-2 Field的类型

这里的POSITIONS表示以词为单位的位置,也就是语义位置,而OFFSETS表示词在文本中的实际物理位置。

FieldType类设置这些组合。例如:

        Analyzer ca = new CJKAnalyzer();
        IndexWriterConfig indexWriterConfig = new IndexWriterConfig(ca);
        FieldType nameType = new FieldType();
        nameType.setIndexed(true);
        nameType.setStored(true);
        IndexWriter indexWriter = new IndexWriter(DIRECTORY, indexWriterConfig);
        Document document = new Document();
        document.add(new Field("name", "我购买了道具和服装", nameType));
        indexWriter.addDocument(document);

在增加文档的阶段,给新的词分配TokenID,新的文档分配DocID。

增加索引后,记得提交索引,否则reader不一定能搜索到。

        writer.commit();  //提交更新到索引库

索引创建完成后可以用索引查看工具Luke(https://github.com/dmitrykey/luke)来查看索引内容并维护索引库。Luke是一个可以执行的jar包,是用Java实现的Windows程序。在Windows下可以双击lukeall-1.0.1.jar,启动Luke。然后,可以选择菜单File→Open Lucene index,打开data/index文件夹,然后可以在窗口看到索引创建的详细信息。

为了提高索引速度,可以重用Field,而不是每次都创建新的。从Lucene 2.3开始,有新的setValue()方法,可以改变Field的值。这样可以在增加许多Document的时候重用单个的Field实例,从而节省许多垃圾回收消耗的时间。

开始新建一个独立的Document实例,然后增加许多Field实例,并且增加每个文档到索引的时候都重用这些Field。例如,有一个idField、bodyField和nameField等。当加入一个Document后,可以通过idField.setValue()等直接改变Field的值,然后再增加文档实例。下面是一个重用Field的例子。

        //创建索引
        IndexWriter writer = new IndexWriter(directory, analyzer);
        //创建文档结构
        Document doc = new Document();
        //定义id重用列
        Field idField = new Field("id",
                      Field.Store.YES, Field.Index.UN_TOKENIZED,
                      Field.TermVector.NO);
        doc.add(idField);


        //定义标题重用列
        Field titleField = new Field("title", null ,
                      Field.Store.YES, Field.Index.ANALYZED,
                      Field.TermVector.WITH_POSITIONS_OFFSETS);
        doc.add(titleField);


        //定义内容重用列
        Field bodyField = new Field("body", null ,
                      Field.Store.YES, Field.Index.ANALYZED,
                      Field.TermVector.WITH_POSITIONS_OFFSETS);
        doc.add(bodyField);


        for(all document) { //处理所有的文档,填充Field值,并索引文档
                Article a = parse(document);
                idField.setValue(a.id);
                titleField.setValue(a.title);
                bodyField.setValue(a.body);
                writer.addDocument(doc); //doc仅仅是一个容器
        }
        writer.close();

注意,不能在文档中重用单个Field实例,不应该改变列的值,直到包含这个Field的Document已经加入到索引库。

可以同时增加多个文档到索引:addDocuments()。

        List<Document> docs = new ArrayList<Document>();
        Document doc1 = new Document();
        doc1.add(new Field("content", "猎兔搜索",   Field.Store.YES,
        Field.Index.ANALYZED));
        Document doc2 = new Document();
        doc2.add(new Field("content", "中文分词",   Field.Store.YES,
        Field.Index.ANALYZED));
        Document doc3 = new Document();
        doc3.add(new Field("content", "中国",   Field.Store.YES,
        Field.Index.ANALYZED));
        Document doc4 = new Document();
        doc4.add(new Field("content", "NBA",  Field.Store.YES,
        Field.Index.ANALYZED));


        docs.add(doc1); docs.add(doc2); docs.add(doc3); docs.add(doc4);


        writer.addDocuments(docs);

当在Windows系统下使用的时候,最好取消勾选杀毒软件的“自动删除已感染病毒文件”选项,否则当索引带病毒特征的文档时,杀毒软件可能会破坏Lucene的索引文件。

每一个添加的文档都被传递给DocConsumer类,它处理该文档并且与索引链表(indexing chain)中其他的consumers相互发生作用。确定的consumers,就像StoredFieldWriter和TermVectorsTermsWriter,提取一个文档中的词,并且马上把字节写入文件。

IndexWriter.setRAMBufferSizeMB方法可以设置当更新文档使用的内存达到指定大小之后才写入硬盘。这样可以提高写索引的速度,尤其是在批量创建索引的时候。

在Lucene 2.3版本之前,存入索引的每个Token都是新创建的。重复利用Token可以加快索引速度。新的Tokenizer类可以回收利用已用过的Token。