现在的位置: 首页 > 综合 > 正文

Lucene源代码之Payload元数据

2013年11月27日 ⁄ 综合 ⁄ 共 8291字 ⁄ 字号 评论关闭

 首先我们仍然来了解一下它是什么?有什么作用?

一个Payload是一个元数据(就是数据的数据) ,在Posting类中用到它,说明它是一个用于描述Posting对象的(构成和特征)。进行分词处理的时候,每切出一个词条Term,即这个词条存在了,则同时存储一个Payload信息。当把一个<Term,Posting>对放到postTable中的时候,就为该Posting增加了一个Payload元数据。

这个元数据最早被添加到哪个类的对象里了呢?从DocumentWriter类的invertDocument()方法中,可以看到对该类的addPosition()方法的调用,这调用addPosition()方法的时候,传了一个Payload,继续看这个Playload的来源,可以看到,是在一个TokenStream打开,进行分词的过程中,从一个Token中获取到的Payload,即 Payload payload = t.getPayload();,这里t是一个Token对象。

继续看addPostition()方法,这行代码ti.payloads[freq] = payload;,其中ti是Posting的实例,每个ti(即Posting)对应着一个Pauload[]数组:ti.payloads = new Payload[ti.positions.length];,这个数组的大小为这个Posting的位置数组positions[]的长度。

上面就是Payload的来源,及其一些去向和处理。

之后,根据Payload构造了一个Posting对象,并且在invertDocument()方法中进行分词,根据切出来的词构造了一个词条Term,将Posting实例和Term实例作为一个<Term,Posting>对放到postTable中,如下所示:

      Term term = new Term(field, text, false);
       postingTable.put(term, new Posting(term, position, payload, offset));

详细代码实现如下:

  1. package org.apache.lucene.index;
  2. import java.io.Serializable;
  3. import org.apache.lucene.analysis.Token;
  4. import org.apache.lucene.analysis.TokenStream;
  5.  /**
  6.   *  A Payload is metadata that can be stored together with each occurrence 
  7.   *  of a term. This metadata is stored inline in the posting list of the
  8.   *  specific term.  
  9.   *  <p>
  10.   *  To store payloads in the index a {@link TokenStream} has to be used that
  11.   *  produces {@link Token}s containing payload data.
  12.   *  <p>
  13.   *  Use {@link TermPositions#getPayloadLength()} and {@link TermPositions#getPayload(byte[], int)}
  14.   *  to retrieve the payloads from the index.<br>
  15.   */
  16.   public class Payload implements Serializable, Cloneable {
  17.     /** the byte array containing the payload data */
  18.     protected byte[] data;
  19.     
  20.     /** the offset within the byte array */
  21.     protected int offset;
  22.     
  23.     /** the length of the payload data */
  24.     protected int length;
  25.     
  26.     /** Creates an empty payload and does not allocate(分配) a byte array. */
  27.     public Payload() {
  28.       // nothing to do
  29.     }
  30.     
  31.     /**
  32.      * Creates a new payload with the given array as data.
  33.      * A reference to the passed-in array is held, i. e. no 
  34.      * copy is made.
  35.      * 
  36.      * @param data the data of this payload
  37.      */
  38.     public Payload(byte[] data) {
  39.       this(data, 0, data.length);
  40.     }
  41.     /**
  42.      * Creates a new payload with the the given array as data. 
  43.      * A reference(参考) to the passed-in array is held, i. e. no 
  44.      * copy is made.
  45.      * 
  46.      * @param data the data of this payload
  47.      * @param offset the offset in the data byte array
  48.      * @param length the length of the data
  49.      */
  50.     public Payload(byte[] data, int offset, int length) {
  51.       if (offset < 0 || offset + length > data.length) {
  52.         throw new IllegalArgumentException();
  53.       }
  54.       this.data = data;
  55.       this.offset = offset;
  56.       this.length = length;
  57.     }
  58.     
  59.     /**
  60.      * Sets this payloads data. 
  61.      * A reference to the passed-in array is held, i. e. no 
  62.      * copy is made.
  63.      */
  64.     public void setData(byte[] data) {
  65.       setData(data, 0, data.length);
  66.     }
  67.     /**
  68.      * Sets this payloads data. 
  69.      * A reference to the passed-in array is held, i. e. no 
  70.      * copy is made.
  71.      */
  72.     public void setData(byte[] data, int offset, int length) {
  73.       this.data = data;
  74.       this.offset = offset;
  75.       this.length = length;
  76.     }
  77.     
  78.     /**
  79.      * Returns a reference to the underlying byte array
  80.      * that holds this payloads data.
  81.      */
  82.     public byte[] getData() {
  83.       return this.data;
  84.     }
  85.     
  86.     /**
  87.      * Returns the offset in the underlying byte array 
  88.      */
  89.     public int getOffset() {
  90.       return this.offset;
  91.     }
  92.     
  93.     /**
  94.      * Returns the length of the payload data. 
  95.      */
  96.     public int length() {
  97.       return this.length;
  98.     }
  99.     
  100.     /**
  101.      * Returns the byte at the given index.
  102.      */
  103.     public byte byteAt(int index) {
  104.       if (0 <= index && index < this.length) {
  105.         return this.data[this.offset + index];    
  106.       }
  107.       throw new ArrayIndexOutOfBoundsException(index);
  108.     }
  109.     
  110.     /**
  111.      * Allocates(分配) a new byte array, copies the payload data into it
  112.      * and returns it. 
  113.      */
  114.     public byte[] toByteArray() {
  115.       byte[] retArray = new byte[this.length];
  116.       System.arraycopy(this.data, this.offset, retArray, 0, this.length);
  117.       return retArray;
  118.     }
  119.     
  120.     /**
  121.      * Copies the payload data to a byte array.
  122.      * 
  123.      * @param target the target byte array
  124.      * @param targetOffset the offset in the target byte array
  125.      */
  126.     public void copyTo(byte[] target, int targetOffset) {
  127.       if (this.length > target.length + targetOffset) {
  128.         throw new ArrayIndexOutOfBoundsException();
  129.       }
  130.       System.arraycopy(this.data, this.offset, target, targetOffset, this.length);
  131.     }
  132.     /**
  133.      * Clones this payload by creating a copy of the underlying
  134.      * byte array.
  135.      */
  136.     public Object clone() {
  137.       Payload clone = new Payload(this.toByteArray());
  138.       return clone;
  139.     }
  140. }

因为payload是2.2版本以后才出现的,所以我们来具体了解一下:

Payload (元数据) 诞生于 Lucene 的2.2 版本,它是在 Lucene 2.1 索引文件格式的基础上扩展而来,提供了一种可以灵活配置的高级索引技术。本文重点研究了 Payload 的实现原理、索引结构的变化、接口 API ,在本文的最后举例说明了 Payload 是如何帮助改善搜索体验的。

Payload 的出现

倒排索引就是说我们维护了一个词条表,对于这个表中的每个词条,都有一个链表描述了有哪些文档包含了这个词条。假定我们有三篇文档 D0,D1,D2:

D0 = "it is what it is"
D1 = "what is it" 
D2 = "it is a banana"

那么,我们可以创建如下倒排索引结构:

Term      Posting-list
"a":      {2}
"banana": {2}
"is":     {0, 1, 2}
"it":     {0, 1, 2}
"what":   {0, 1}

一般情况下,将一个词条所索引的文档(一般用文档编号表示)称之为 Posting,那么一个词条索引的多个文档就称之为 Posting-list。除了在 Posting-list 中记录文档编号,Lucene 也在Posting-list 中添加了词频和位置信息。词频的添加有助于结果的排序,位置信息的添加解决了短语检索的问题。

在Lucene 2.1 中,记录位置信息的即 .prx 文件,它的格式如下:

ProxFile (.prx) --> <TermPositions> TermCount 
TermPositions --> <Positions> DocFreq 
Positions --> <PositionDelta> Freq 
PositionDelta --> VInt 

仔细观察我们可以发现,文档 D0 中 what 词条是加粗显示而文档 D1 中的 what 则没有。显然,Lucene 2.1 的索引结构是无法表示出两者的差异。为了解决这个问题,从 Lucene2.2 开始,引入了 Payload 的概念。 Payload 即词条 (Term) 的元数据或称载荷, Lucene 支持用户在索引的过程中将词条的元数据添加的索引库中,同时也提供了在检索结果时读取 Payload 信息的功能。Payload 的诞生为用户提供了一种可灵活配置的高级索引技术,为支持更加丰富的搜索体验创造了条件。

那么 Lucene 是如何改进索引文件以支持 Payload 功能的呢?

索引结构的变化

为了更加形象的描述改进后的索引结构,我们用不同的颜色表示出文档 ID ,词频,位置和 Payload,如图 1 所示,(U表示下划线,B表示粗体)

图 1:Lucene的索引结构
Lucene的索引结构

对比 Lucene2.1 的索引结构,Lucene2.2 的索引结构的表达式如下:

 

ProxFile (.prx) --> <TermPositions> TermCount 
TermPositions --> <Positions> DocFreq 
Positions --> <PositionDelta,Payload?> Freq 
Payload --> <PayloadLength?,PayloadData> 
PositionDelta --> VInt 
PayloadLength --> VInt 
PayloadData --> bytePayloadLength

从上面的索引表达式中可以看出只有当一个词汇包含 Payload 信息时,Lucene 才会为之分配相应的 Payload 存储空间,这是一种高效率的空间实现。Lucene2.2 之后的 Payload 特指词条的元数据,那么文档的元数据如何表示呢?

我们知道,Lucene 中 Document 由 Field 组成,而 Field 由 Term 组成,文档的 Payload 可以用存储的 Field 表示。这样存在的问题是,如果需要读取大量的文档的元数据,因为 Field 的索引信息与存储信息是分开的,那么 I/O 效率将是较差的。而 Payload 信息则是直接存储在索引中,因此可以利用词条的 Payload 功能存储文档级别的元数据。文档级 Payload 可表示为如下图2所示格式(省略了词频和位置信息):

图 2:文档级的 Payload 表示
文档级的 Payload 表示

如图2所示,以文档的 url 信息为例,通过为每一个文档构造一个特殊的词条 ”url” ,将每个文档的 ur l值作为 payload 信息,把 Payload 与文档编号关联起来,这样就可以实现文档级的 Payload。

Payload 相关的 API

从 Lucene2.2 的索引结构可以看出,Payload 的存储与词条的位置信息是紧密联系在一起的,因此 Payload 的存储和检索 API 位于Token类和 TermPositions 类当中。

 

向词条中存储 Payload 信息

org.apache.lucene.analysis.Token
void setPayload(Payload payload)

Payload 信息的构造函数

org.apache.lucene.index.Payload
Payload(byte[] data)

Payload(byte[] data, 
        int offset,
        int length)

从位置信息中检索 Payload

org.apache.lucene.index.TermPositions
boolean next();
int doc()
int freq();

int nextPosition();

int getPayloadLength();
byte[] getPayload(byte[] data, 
                  int offset)

Payload 的应用场景举例

场景一:改进的 Lucene 的区间检索

日期检索是区间检索的常见例子,如用户需要在图书馆中检索特定年代的图书,满足如下条件:

Date>1954/08/01 & Date<1955/08/01

常见的做法就是将日期作为一个独立 Field 进行存储,利用 RangeQuery 进行区间检索,Posting-list 的格式如图3中左图所示。如果图书日期分布区间很广,用 Field 存储日期信息,我们需要将日期细化到年月日进行存储,因此词条数目是非常庞大的。这种情况下,我们可以利用 Payload 功能来减少词条的数目,提高检索效率,可以将日期的年月作为词条,日作为 Payload 信息,这几乎将词条数目减小了近 30 倍,改进后的存储结构如图3右图所示:

图 3:使用 Payload 存储日期信息
使用 Payload 存储日期信息

场景二:提高特定词汇的评分

利用 Payload 功能,还可以提高文档中特定词汇的评分,如黑体词汇、斜体词汇等,从而优化搜索结果排序。

下面还以文档 D0 和 D1 为例说明如何设置和检索 Payload。

D0 = "it is what it is"
D1 = "what is it" 

Step1:在 Analyzer 处理过程中,为特殊词汇添加评分 Payload

final byte BoldBoost = 5;

final byte ItalicBoost = 5;
…
Token token = new Token(…);
…
if (isBold) {//如果是黑体字
  token.setPayload(
    new Payload(new byte[] {BoldBoost}));
}else if(isItalic){//如果是斜体字
  token.setPayload(
    new Payload(new byte[] { ItalicBoost }));
}
…
return token;

Step2:重写 Similarity (主要负责排名和评分)

Similarity boostingSimilarity = 
  new DefaultSimilarity() {
    // @override
    public float scorePayload(byte [] payload,
                              int offset, 
                              int length) {

      //读取payload的值,payload存储的即为词汇的评分。
      if (length == 1) return payload[offset];
    };

Step3:使用重写的 boostingSimilarity 进行检索

Query btq = new BoostingTermQuery(
                new Term(“field”, “what”));



Searcher searcher = new IndexSearcher(…);
Searcher.setSimilarity(boostingSimilarity);
…
Hits hits = searcher.search(btq);

总结

Payload 是 Lucene 一个允许在索引中为词条储存元数据信息。希望通过阅读本篇,你可以对Payload 功能有一个整体的了解,进而可以灵活运用 Payload 的功能来优化具体的应用。

抱歉!评论已关闭.