`
twh1224
  • 浏览: 95575 次
  • 性别: Icon_minigender_1
  • 来自: 武汉
社区版块
存档分类
最新评论

Lucene学习(21)

阅读更多
回到IndexWriter索引器类中来,学习该类添加Document的方法。

这时,需要用到一个非常重要的类:DocumentWriter,该类对Document进行了很多处理,比如“文档倒排”就是其中的一项重要内容。

实例化一个IndexWriter索引器之后,要向其中添加Document,在IndexWriter类中有两个实现该功能的方法:

public void addDocument(Document doc) throws CorruptIndexException, IOException { 
    addDocument(doc, analyzer); 
} 
 
public void addDocument(Document doc, Analyzer analyzer) throws CorruptIndexException, IOException { 
    ensureOpen();    // 确保IndexWriter是打开的,这样才能向其中添加Document 
    SegmentInfo newSegmentInfo = buildSingleDocSegment(doc, analyzer);    // 构造一个SegmentInfo实例,SegmentInfo是用来维护索引段信息的 
    synchronized (this) { 
      ramSegmentInfos.addElement(newSegmentInfo); 
      maybeFlushRamSegments(); 
    } 
} 


可以看出,第一个addDocument方法调用了第二个重载的方法,所以关键在于第二个addDocument方法。

这里,ramSegmentInfos是IndexWriter类的一个成员,该ramSegmentInfos是存在于RAMDirectory中的,定义为:

SegmentInfos ramSegmentInfos = new SegmentInfos(); 


关于SegmentInfos类,可以参考文章 Lucene-2.2.0 源代码阅读学习(18) 。

上面,buildSingleDocSegment()方法,通过给定的Document和Analyzer来构造一个SegmentInfo实例,关于SegmentInfo类,可以参考文章 Lucene-2.2.0 源代码阅读学习(19) ,buildSingleDocSegment()方法的实现如下所示:

SegmentInfo buildSingleDocSegment(Document doc, Analyzer analyzer) 
      throws CorruptIndexException, IOException { 
    DocumentWriter dw = new DocumentWriter(ramDirectory, analyzer, this);    // 实例化一个DocumentWriter对象 
    dw.setInfoStream(infoStream);    // 设置一个PrintStream infoStream流对象 
    String segmentName = newRamSegmentName();    // 在内存中新建一个索引段名称 
    dw.addDocument(segmentName, doc);    // 将Document添加到指定的名称为segmentName的索引段文件中 
 
/* 根据指定的segmentName、ramDirectory, 
 
Document的数量为1个,构造一个SegmentInfo对象,根据SegmentInfo的构造函数: 
 
public SegmentInfo(String name, int docCount, Directory dir, boolean isCompoundFile, boolean hasSingleNormFile) 
 
可知,指定构造的不是一个复合文件,也不是一个具有单独norm文件的SegmentInfo对象,因为我们使用的是2.2版本的,从2.1版本往后,就统一使用一个.nrm文件来代替以前使用的norm文件*/    
    SegmentInfo si = new SegmentInfo(segmentName, 1, ramDirectory, false, false); 
    si.setNumFields(dw.getNumFields());    // 设置SegmentInfo中Field的数量 
    return si;    // 返回构造好的SegmentInfo对象 
} 


在内存中新建一个索引段名称,调用了IndexWriter类的一个方法:

final synchronized String newRamSegmentName() {    // synchronized,需要考虑线程同步问题 
    return "_ram_" + Integer.toString(ramSegmentInfos.counter++, Character.MAX_RADIX); 
} 


初始化一个SegmentInfo实例时,counter的值为0,counter是用来为一个新的索引段命名的,在SegmentInfo类中定义了这个成员:

public int counter = 0; 


上面的newRamSegmentName()方法返回的是一个索引段的名称(该名称用来在内存中,与RAMDirectory相关的),即文件名称为_ram_1。

从上面可以看出,IndexWriter类的addDocument()方法中,最重要的调用buildSingleDocSegment()方法,创建一个SegmentInfo对象,从而在buildSingleDocSegment()方法中使用到了DocumentWriter类,这才是关键了。

下面研究DocumentWriter这个核心类,从IndexWriter类中addDocument()方法入手,先把用到DocumentWriter类的一些具体细节拿出来研究。

一个DocumentWriter的构造
DocumentWriter(Directory directory, Analyzer analyzer, IndexWriter writer) { 
    this.directory = directory; 
    this.analyzer = analyzer; 
    this.similarity = writer.getSimilarity(); 
    this.maxFieldLength = writer.getMaxFieldLength(); 
    this.termIndexInterval = writer.getTermIndexInterval(); 
} 


在这个构造方法中,最大的Field长度为10000,即this.maxFieldLength = writer.getMaxFieldLength();,可以在IndexWriter类中找到定义:

public int getMaxFieldLength() { 
    ensureOpen(); 
    return maxFieldLength; 
} 


然后maxFieldLength定义为:
private int maxFieldLength = DEFAULT_MAX_FIELD_LENGTH; 


其中,DEFAULT_MAX_FIELD_LENGTH值为:

public final static int DEFAULT_MAX_FIELD_LENGTH = 10000; 


同理,默认词条索引区间为128,即this.termIndexInterval = writer.getTermIndexInterval();,也可以在IndexWriter类中找到定义。

另外,this.similarity = writer.getSimilarity();,其实DocumentWriter的这个成员similarity=new DefaultSimilarity();。DefaultSimilarity类继承自Similarity抽象类,该类是用来处理有关“相似性”的,与检索密切相关,其实就是对一些数据在运算过程中可能涉及到数据位数的舍入与进位。具体地,Similarity类的定义可查看 org.apache.lucene.search.Similarity。

这样,一个DocumentWriter就构造完成了。

DocumentWriter类的addDocument()方法

final void addDocument(String segment, Document doc) 
          throws CorruptIndexException, IOException { 
    // 创建一个FieldInfos对象,用来存储加入到索引的Document中的各个Field的信息
    fieldInfos = new FieldInfos(); 
    fieldInfos.add(doc);   // 将Document加入到FieldInfos中
     
    // postingTable是用于存储所有词条的HashTable
    postingTable.clear();     // clear postingTable
    fieldLengths = new int[fieldInfos.size()];    // 初始化int[]数组fieldLengths,用来记录当前Document中所有Field的长度
    fieldPositions = new int[fieldInfos.size()]; // 初始化int[]数组fieldPositions,用来记录当前Document中所有Field在分析完成后所处位置
    fieldOffsets = new int[fieldInfos.size()];    // 初始化int[]数组fieldOffsets,用来记录当前Document中所有Field的offset
    fieldStoresPayloads = new BitSet(fieldInfos.size()); 
    
    fieldBoosts = new float[fieldInfos.size()];   // 初始化int[]数组fieldBoosts,用来记录当前Document中所有Field的boost值
    Arrays.fill(fieldBoosts, doc.getBoost());    // 为fieldBoosts数组中的每个元素赋值,根据Document中记录的boost值
 
    try { 
    
      // 在将FieldInfos写入之前,要对Document中的各个Field进行“倒排”
      invertDocument(doc); 
    
      // 对postingTable中的词条进行排序,返回一个排序的Posting[]数组
      Posting[] postings = sortPostingTable(); 
    
      // 将FieldInfos写入到索引目录directory中,即写入到文件segments.fnm中
      fieldInfos.write(directory, segment + ".fnm"); 
 
      // 构造一个FieldInfos的输出流FieldsWriter,将Field的详细信息(包括上面提到的各个数组中的值)写入到索引目录中
      FieldsWriter fieldsWriter = 
        new FieldsWriter(directory, segment, fieldInfos); 
      try { 
        fieldsWriter.addDocument(doc);    // 将Document加入到FieldsWriter
      } finally { 
        fieldsWriter.close();    // 关闭FieldsWriter输出流
      } 
 
     // 将经过排序的Posting[]数组写入到索引段文件中(segmentsv.frq文件和segments.prx文件)
      writePostings(postings, segment); 
 
      // 写入被索引的Field的norm信息
      writeNorms(segment); 
    } finally { 
      // 关闭TokenStreams
      IOException ex = null; 
      
      Iterator it = openTokenStreams.iterator();    // openTokenStreams是DocumentWriter类定义的一个链表成员,即:private
													// List openTokenStreams =
													// new LinkedList();
      while (it.hasNext()) { 
        try { 
          ((TokenStream) it.next()).close(); 
        } catch (IOException e) { 
          if (ex != null) { 
            ex = e; 
          } 
        } 
      } 
      openTokenStreams.clear();    // 清空openTokenStreams
       
      if (ex != null) { 
        throw ex; 
      } 
    } 
} 


DocumentWriter实现对Document的“倒排”

在DocumentWriter类的addDocument()方法中,在对Document中的各个Field输出到索引目录之前,要对所有加入到IndexWriter索引器(一个IndexWriter的构造,指定了一个Analyzer分析器)的Document执行倒排,即调用倒排的方法 invertDocument()。

invertDocument()方法的实现如下所示:

// 调用底层分析器接口,遍历Document中的Field,对数据源进行分析
private final void invertDocument(Document doc) 
          throws IOException { 
    Iterator fieldIterator = doc.getFields().iterator();    // 通过Document获取Field的List列表doc.getFields()
    while (fieldIterator.hasNext()) { 
      Fieldable field = (Fieldable) fieldIterator.next(); 
      String fieldName = field.name(); 
      int fieldNumber = fieldInfos.fieldNumber(fieldName);    // 根据一个Field的fieldName得到该Field的编号number(number是FieldInfo类的一个成员)
 
      int length = fieldLengths[fieldNumber];     // 根据每个Field的编号,设置每个Field的长度
      int position = fieldPositions[fieldNumber]; // 根据每个Field的编号,设置每个Field的位置
      if (length>0) position+=analyzer.getPositionIncrementGap(fieldName); 
      int offset = fieldOffsets[fieldNumber];       // 根据每个Field的编号,设置每个Field的offset
 
      if (field.isIndexed()) {    // 如果Field被索引
        if (!field.isTokenized()) {    // 如果Field没有进行分词
          String stringValue = field.stringValue();    // 获取Field的String数据值
          if(field.isStoreOffsetWithTermVector())    // 是否把整个Field的数据作为一个词条存储到postingTable中
 
        // 把整个Field的数据作为一个词条存储到postingTable中
            addPosition(fieldName, stringValue, position++, null, new TermVectorOffsetInfo(offset, offset + stringValue.length())); 
          else    // 否则,不把整个Field的数据作为一个词条存储到postingTable中
            addPosition(fieldName, stringValue, position++, null, null); 
          offset += stringValue.length(); 
          length++; 
        } else 
        { // 需要对Field进行分词
          TokenStream stream = field.tokenStreamValue(); 
          if (stream == null) {     // 如果一个TokenStream不存在,即为null,则必须从一个Analyzer中获取一个TokenStream流
            Reader reader;      
            if (field.readerValue() != null)    // 如果从Field获取的Reader数据不为null
              reader = field.readerValue();    // 一个Reader流存在
            else if (field.stringValue() != null) 
              reader = new StringReader(field.stringValue());    // 根据从Field获取的字符串数据构造一个Reader输入流
            else 
              throw new IllegalArgumentException 
                      ("field must have either String or Reader value"); 
 
            // 把经过分词处理的Field加入到postingTable中
            stream = analyzer.tokenStream(fieldName, reader); 
          } 
          
          // 将每个Field对应的TokenStream加入到链表openTokenStreams中,等待整个Document中的所有Field都分析处理完毕后,对链表openTokenStreams中的每个链表TokenStream进行统一关闭
          openTokenStreams.add(stream); 
          
          // 对第一个Token,重置一个TokenStream
          stream.reset(); 
          
 
          Token lastToken = null; 
          for (Token t = stream.next(); t != null; t = stream.next()) { 
            position += (t.getPositionIncrement() - 1);    // 每次切出一个词,就将position加上这个词的长度
               
            Payload payload = t.getPayload();    // 每个词都对应一个Payload,它是关于一个词存储到postingTable中的元数据(metadata)
            if (payload != null) { 
              fieldStoresPayloads.set(fieldNumber);    // private BitSet
														// fieldStoresPayloads;,BitSet是一个bits的向量,调用BitSet类的set方法,设置该Field的在索引fieldNumber处的bit值
            } 
              
            TermVectorOffsetInfo termVectorOffsetInfo; 
            if (field.isStoreOffsetWithTermVector()) {    // 如果指定了Field的词条向量的偏移量,则存储该此条向量
              termVectorOffsetInfo = new TermVectorOffsetInfo(offset + t.startOffset(), offset + t.endOffset()); 
            } else { 
              termVectorOffsetInfo = null; 
            } 
 
        // 把该Field的切出的词条存储到postingTable中
            addPosition(fieldName, t.termText(), position++, payload, termVectorOffsetInfo); 
              
            lastToken = t; 
            if (++length >= maxFieldLength) {// 如果当前切出的词条数已经达到了该Field的最大长度
 
              if (infoStream != null) 
                infoStream.println("maxFieldLength " +maxFieldLength+ " reached, ignoring following tokens"); 
              break; 
            } 
          } 
            
          if(lastToken != null)    // 如果最后一个切出的词不为null,设置offset的值
            offset += lastToken.endOffset() + 1; 
        } 
 
        fieldLengths[fieldNumber] = length;   // 存储Field的长度
        fieldPositions[fieldNumber] = position;   // 存储Field的位置
        fieldBoosts[fieldNumber] *= field.getBoost();    // 存储Field的boost值
        fieldOffsets[fieldNumber] = offset;    // 存储Field的offset值
      } 
    } 
    
    // 所有的Field都有经过分词处理的具有Payload描述的词条,更新FieldInfos
    for (int i = fieldStoresPayloads.nextSetBit(0); i >= 0; i = fieldStoresPayloads.nextSetBit(i+1)) { 
    fieldInfos.fieldInfo(i).storePayloads = true; 
    } 
} 
 


使用快速排序对postingTable进行排序

当FieldInfos中的每个Field进行分词以后,所有切出的词条都放到了一个HashTable postingTable中,这时所有的词条在postingTable中是无序的。在DocumentWriter的addDocument()方法中调用了sortPostingTable()方法,对词条进行了排序,排序使用“快速排序”方式,“快速排序”的时间复杂度O(N*logN),排序速度很快。

sortPostingTable()方法的实现如下所示:

private final Posting[] sortPostingTable() { 
    // 将postingTable转换成Posting[]数组,便于快速排序 
    Posting[] array = new Posting[postingTable.size()]; 
    Enumeration postings = postingTable.elements(); 
    for (int i = 0; postings.hasMoreElements(); i++) 
      array[i] = (Posting) postings.nextElement(); 
 
    // 调用quickSort()方法,使用快速排序对Posting[]数组进行排序 
    quickSort(array, 0, array.length - 1); 
 
    return array; 
} 


快速排序的算法都不陌生,在Lucene中也给出了实现,快速排序方法如下:

private static final void quickSort(Posting[] postings, int lo, int hi) { 
    if (lo >= hi) 
      return; 
 
    int mid = (lo + hi) / 2; 
 
    if (postings[lo].term.compareTo(postings[mid].term) > 0) { 
      Posting tmp = postings[lo]; 
      postings[lo] = postings[mid]; 
      postings[mid] = tmp; 
    } 
 
    if (postings[mid].term.compareTo(postings[hi].term) > 0) { 
      Posting tmp = postings[mid]; 
      postings[mid] = postings[hi]; 
      postings[hi] = tmp; 
 
      if (postings[lo].term.compareTo(postings[mid].term) > 0) { 
        Posting tmp2 = postings[lo]; 
        postings[lo] = postings[mid]; 
        postings[mid] = tmp2; 
      } 
    } 
 
    int left = lo + 1; 
    int right = hi - 1; 
 
    if (left >= right) 
      return; 
 
    Term partition = postings[mid].term; 
 
    for (; ;) { 
      while (postings[right].term.compareTo(partition) > 0) 
        --right; 
 
      while (left < right && postings[left].term.compareTo(partition) <= 0) 
        ++left; 
 
      if (left < right) { 
        Posting tmp = postings[left]; 
        postings[left] = postings[right]; 
        postings[right] = tmp; 
        --right; 
      } else { 
        break; 
      } 
    } 
 
    quickSort(postings, lo, left); 
    quickSort(postings, left + 1, hi); 
} 
 



关于Posting类

该类是为排序服务的,提取了与词条信息有关的一些使用频率较高的属性,定义成了该Posting类,实现非常简单,如下所示:

final class Posting { // 在一个Document中与词条有关的信息 
	Term term; // 一个词条 
	int freq; // 词条Term term在该Document中的频率 
	int[] positions; // 位置 
	Payload[] payloads; // Payloads信息 
	TermVectorOffsetInfo[] offsets; // 词条向量的offset(偏移量)信息 

	Posting(Term t, int position, Payload payload, TermVectorOffsetInfo offset) { // Posting构造器 
		term = t;
		freq = 1;
		positions = new int[1];
		positions[0] = position;

		if (payload != null) {
			payloads = new Payload[1];
			payloads[0] = payload;
		} else
			payloads = null;

		if (offset != null) {
			offsets = new TermVectorOffsetInfo[1];
			offsets[0] = offset;
		} else
			offsets = null;
	}
}


Document的倒排非常重要。总结一下:

1、该invertDocument()方法遍历了FieldInfos的每个Field,根据每个Field的属性进行分析,如果需要分词,则调用底层分析器接口,执行分词处理。

2、在invertDocument()方法中,对Field的信息进行加工处理,尤其是每个Field的切出的词条,这些词条最后将添加到postingTable中。

上面DocumentWriter类的addDocument()方法中writePostings()方法,是对已经经过倒排的文档,将词条的一些有用信息写入到索引段文件中。

关于writePostings()方法的实现参考文章 Lucene-2.2.0 源代码阅读学习(23)。

 

最后的总结:

在学习DocumentWriter类的addDocument()方法的过程中,涉及到了该类的很多方法,其中关于文档的倒排的方法是非常重要的。

此外,还涉及到了FieldInfos类和FieldInfo类,他们的关系很像SegmentInfos类和SegmentInfo类。 FieldInfos类主要是对Document添加到中的Field进行管理的,可以通过FieldInfos类来访问Document中所有 Field的信息。每个索引段(索引段即Segment)都拥有一个单独的FieldInfos。

应该对FieldInfos类和FieldInfo类有一个了解。
分享到:
评论

相关推荐

    【分享:lucene学习资料】---<下载不扣分,回帖加1分,欢迎下载,童叟无欺>

    1&gt; lucene学习笔记 2&gt; 全文检索的实现机制 【1】lucene学习笔记的目录如下 1. 概述 3 2. lucene 的包结构 3 3. 索引文件格式 3 4. lucene中主要的类 4 4.1. Document文档类 4 4.1.1. 常用方法 4 4.1.2. 示例 4 4.2...

    lucene 2.1.0 好用实例

    **Lucene 2.1.0 实例详解** Lucene 是一个开源的全文搜索引擎库,由 Apache 软件基金会开发。...通过学习这个实例,初学者可以快速掌握全文搜索引擎的核心概念,并为进一步深入学习和使用现代Lucene版本打下坚实基础。

    lucene简介

    进入21世纪,Infoseek、AltaVista、Google和百度等搜索引擎的兴起标志着搜索引擎技术进入了繁荣期。 #### 三、Lucene的特点与优势 Lucene作为一款全文检索引擎,拥有以下几个显著特点: 1. **平台独立性**:...

    Lucene的应用

    ### Lucene的应用 #### Lucene简介 Lucene是一款由Doug Cutting开发并维护的高性能全文检索工具包,最初在2001年10月被贡献给...无论是对于初学者还是经验丰富的开发者来说,Lucene都是一个值得深入学习的技术。

    Java学习的30个目标

    熟悉日志框架(如Log4J)、任务调度(如Quartz)、分布式缓存(如JCache)、全文搜索(如Lucene)等常用框架和API。 #### 21. 本地接口与连接器架构 学习Java Native Interface(JNI)和Java Connector ...

    学习java的30个目标.txt

    #### 目标21:掌握网络通信与缓存技术 - **技术框架**:JGroups、JCache等。 - **应用场景**:分布式系统的通信与数据缓存。 #### 目标22:学习全文搜索技术 - **技术框架**:Lucene等。 - **应用场景**:网站...

    学习Java语言的30个参考,让你坐拥别人之上的30个擦考

    - **Lucene**:学习全文检索技术的基础知识。 ### 21. 本地接口与连接器 - **JNI、JCA**:掌握Java Native Interface、Java Connector Architecture等技术,实现Java与其他语言或平台的交互。 通过以上知识点的...

    java的30个学习目标

    #### 21. **JavaBeans组件(EJB)** - Stateless/Stateful Session Beans、Entity Beans、Message-Driven Beans等模型用于企业级应用开发。 #### 22. **应用服务器** - WebLogic、JBoss等应用服务器的部署和配置,...

    0-1-knapsack-problem-master (21).zip

    【描述】中的"elasticsearch" 是一个开源的全文搜索引擎,它基于Lucene构建,设计用于分布式、实时的数据存储和搜索。Elasticsearch不仅能够处理文本数据,还可以处理结构化和半结构化数据,广泛应用于日志分析、...

    收集java学习资料和面试题包括git上好的项目

    ##### 21. FastDFS - **简介**:一个开源的分布式文件系统。 - **链接**:[https://github.com/happyfish100/fastdfs](https://github.com/happyfish100/fastdfs) - **核心特性**: - 文件上传下载 - 文件管理 - ...

    Java个人简历模板21.doc

    【Java个人简历模板21.doc】的文档是一个Java软件工程师的简历,展示了他在JavaEE领域的专业知识和实践经验。以下是他所掌握的关键技能和经验的详细解释: 1. **Java基础技能**: - **反射**:Java反射允许在运行...

    开源的搜索引擎[转]

    1.前台结合Lucene的搜索引擎功能,使得数据搜索更快; 2.新增加采集功能,采集时图片下载,flash下载功能,默认配置的是南海网分类信息的采集规则; 3.该代码简洁,完全开源,可以与网博多款新闻系统无缝整合; 4....

    Eclipse开发分布式商城系统+完整视频代码及文档

    │ workspace.zip │ 列表生成.reg │ 淘淘商城源代码.zip │ ├─01....│ 01....│ 02....│ 03....│ 04....│ 08....├─02....│ 07....│ 01....│ 02....│ 03....│ 04....│ 05....│ 06....│ 08....│ 10....│ 13....├─03....│ 01....│ 02....

    C#搜索引擎

    四、ShootSearch0[1].2_Src.rar和ShootSearch0[1].21_Src.rar 这两个压缩包文件可能包含了C#搜索引擎的源代码。开发者可能通过它们学习如何构建自己的搜索引擎,包括抓取、解析、索引和查询处理的各个部分。源代码中...

    大数据技术基础组件介绍.pptx

    大数据,作为21世纪信息技术的核心组成部分,源于互联网、移动互联网、社交网络、电子商务等领域的爆炸性数据增长。这些数据不仅在数量上急剧增加,还呈现出多样性和复杂性的特点,催生了大数据这一概念。大数据的...

    45个小众而实用的NLP开源工具.rar

    32. **Apache Lucene**:搜索引擎库,包含文本分析组件,适用于信息检索和NLP应用。 33. **GATE (General Architecture for Text Engineering)**:综合的NLP框架,支持多种任务的开发和评估。 34. **Apache Tika**...

    数据结构算法

    团队沟通利器之UML——活动图 wcf系列(5)wcf系列学习5天速成——第五天 服务托管 wcf系列学习5天速成——第四天 wcf之分布式架构 wcf系列学习5天速成——第三天 事务的使用 wcf系列5天速成——第二天 binding的使用...

    CDH-HDP-MAPR-DKH-星环组件比较.pdf

    32. **Mahout**:Apache的机器学习库,提供了各种算法来实现推荐系统、分类和聚类。 33. **HttpFS**:安全的HTTP文件系统,提供WebHDFS服务。 34. **Sentry**:提供Hadoop集群的细粒度访问控制。 35. **Sahara**...

    Java面试题2023最新版大合集(485页).pdf

    答:MyBatis 框架的缺点包括学习成本高、配置复杂、不支持级联操作等。 6. {}和${}的区别是什么? 答:{}用于将参数传递给 SQL 语句,${}用于将参数传递给 SQL 语句,但是需要手动编写 SQL 语句。 7. 当实体类中的...

Global site tag (gtag.js) - Google Analytics