如同磁铁吸引四周的铁粉,热情也能吸引周围的人,改变周围的情况。
lucene

lucene

Lucene源码和历史版本的问题

默认分类pzw9696 回复了问题 • 2 人关注 • 1 个回复 • 1310 次浏览 • 2022-03-13 17:46 • 来自相关话题

lucene的源码在github上怎么只能找到9的版本

回复

默认分类匿名用户 发起了问题 • 1 人关注 • 0 个回复 • 1062 次浏览 • 2022-03-11 19:40 • 来自相关话题

社区日报 第1247期 (2021-11-12)

社区日报kin122 发表了文章 • 0 个评论 • 929 次浏览 • 2021-11-13 23:20 • 来自相关话题

1. Elasticsearch 异步搜索 Async search 实战 https://mp.weixin.qq.com/s/XYXp5Xn2r7Vnqxj3iswi9A 2.TB级微服务海量日志监控平台 https://mp.weixin.qq.com/s/8BgCxczzFBMTweyRINA0rw 3. Lucene源码解析——DocValue存储方式 https://zhuanlan.zhihu.com/p/384487150 编辑:kin122 归档:https://ela.st/cn-daily-all 订阅:https://ela.st/cn-daily-sub 沙龙:https://ela.st/cn-meetup
1. Elasticsearch 异步搜索 Async search 实战 https://mp.weixin.qq.com/s/XYXp5Xn2r7Vnqxj3iswi9A 2.TB级微服务海量日志监控平台 https://mp.weixin.qq.com/s/8BgCxczzFBMTweyRINA0rw 3. Lucene源码解析——DocValue存储方式 https://zhuanlan.zhihu.com/p/384487150 编辑:kin122 归档:https://ela.st/cn-daily-all 订阅:https://ela.st/cn-daily-sub 沙龙:https://ela.st/cn-meetup

社区日报 第1245期 (2021-11-10)

社区日报kin122 发表了文章 • 0 个评论 • 1091 次浏览 • 2021-11-10 09:41 • 来自相关话题

1. Lucene源码解析——StoredField存储方式 https://zhuanlan.zhihu.com/p/384486147 2.如何在 Kibana 的 Discover 界面中让显示图片字段 https://www.bilibili.com/video/BV14P4y1L7XL/ https://elasticstack.blog.csdn ... 19074 3. 几种常见的查询性能问题及优化方式--字节:张超 https://mp.weixin.qq.com/s/w7hEn2JE9ZXUBh3kZ0uOGg 编辑:kin122 归档:https://ela.st/cn-daily-all 订阅:https://ela.st/cn-daily-sub 沙龙:https://ela.st/cn-meetup
1. Lucene源码解析——StoredField存储方式 https://zhuanlan.zhihu.com/p/384486147 2.如何在 Kibana 的 Discover 界面中让显示图片字段 https://www.bilibili.com/video/BV14P4y1L7XL/ https://elasticstack.blog.csdn ... 19074 3. 几种常见的查询性能问题及优化方式--字节:张超 https://mp.weixin.qq.com/s/w7hEn2JE9ZXUBh3kZ0uOGg 编辑:kin122 归档:https://ela.st/cn-daily-all 订阅:https://ela.st/cn-daily-sub 沙龙:https://ela.st/cn-meetup

Lucene索引倒排链数据结构为什么采用单链表,是基于什么考虑的?

ElasticsearchOmbres 回复了问题 • 4 人关注 • 3 个回复 • 2079 次浏览 • 2021-02-07 14:35 • 来自相关话题

Lucene中如何获取一个字段中所有term的tf最大的那个值

Lucenesuisuimu 回复了问题 • 2 人关注 • 1 个回复 • 5318 次浏览 • 2020-10-27 12:59 • 来自相关话题

倒排索引删除文档

LuceneCharele 回复了问题 • 2 人关注 • 1 个回复 • 3576 次浏览 • 2020-07-22 15:51 • 来自相关话题

lucene 或者 es中不存储原字段的应用场景是什么呢???

LuceneCharele 回复了问题 • 3 人关注 • 2 个回复 • 2385 次浏览 • 2020-07-22 15:32 • 来自相关话题

关于默认排序的问题。

ElasticsearchWarrenW 回复了问题 • 3 人关注 • 2 个回复 • 3145 次浏览 • 2020-07-10 14:24 • 来自相关话题

修改Lucene源码,重新打包,替换elasticsearch中原有的Lucene-core.jar包,出现问题

ElasticsearchKalasearch 回复了问题 • 2 人关注 • 1 个回复 • 6959 次浏览 • 2019-12-29 04:06 • 来自相关话题

es是构建在lucene之上,中国目前有lucene的社区吗?国外的也行

Elasticsearchmedcl 回复了问题 • 2 人关注 • 1 个回复 • 1931 次浏览 • 2019-11-15 09:18 • 来自相关话题

Lucene doc value结构自己的一点理解

Elasticsearchcode4j 回复了问题 • 5 人关注 • 3 个回复 • 4284 次浏览 • 2019-07-31 14:57 • 来自相关话题

lucence构建索引几分钟后报错

回复

Lucenelvwendong 发起了问题 • 1 人关注 • 0 个回复 • 2161 次浏览 • 2019-03-01 16:43 • 来自相关话题

Day 7 - Elasticsearch中数据是如何存储的

Adventweizijun 发表了文章 • 7 个评论 • 68633 次浏览 • 2018-12-07 13:55 • 来自相关话题

前言

很多使用Elasticsearch的同学会关心数据存储在ES中的存储容量,会有这样的疑问:xxTB的数据入到ES会使用多少存储空间。这个问题其实很难直接回答的,只有数据写入ES后,才能观察到实际的存储空间。比如同样是1TB的数据,写入ES的存储空间可能差距会非常大,可能小到只有300~400GB,也可能多到6-7TB,为什么会造成这么大的差距呢?究其原因,我们来探究下Elasticsearch中的数据是如何存储。文章中我以Elasticsearch 2.3版本为示例,对应的lucene版本是5.5,Elasticsearch现在已经来到了6.5版本,数字类型、列存等存储结构有些变化,但基本的概念变化不多,文章中的内容依然适用。

Elasticsearch索引结构

Elasticsearch对外提供的是index的概念,可以类比为DB,用户查询是在index上完成的,每个index由若干个shard组成,以此来达到分布式可扩展的能力。比如下图是一个由10个shard组成的index。

elasticsearch_store_arc.png

shard是Elasticsearch数据存储的最小单位,index的存储容量为所有shard的存储容量之和。Elasticsearch集群的存储容量则为所有index存储容量之和。

一个shard就对应了一个lucene的library。对于一个shard,Elasticsearch增加了translog的功能,类似于HBase WAL,是数据写入过程中的中间数据,其余的数据都在lucene库中管理的。

所以Elasticsearch索引使用的存储内容主要取决于lucene中的数据存储。

lucene数据存储

下面我们主要看下lucene的文件内容,在了解lucene文件内容前,大家先了解些lucene的基本概念。

lucene基本概念

  • segment : lucene内部的数据是由一个个segment组成的,写入lucene的数据并不直接落盘,而是先写在内存中,经过了refresh间隔,lucene才将该时间段写入的全部数据refresh成一个segment,segment多了之后会进行merge成更大的segment。lucene查询时会遍历每个segment完成。由于lucene* 写入的数据是在内存中完成,所以写入效率非常高。但是也存在丢失数据的风险,所以Elasticsearch基于此现象实现了translog,只有在segment数据落盘后,Elasticsearch才会删除对应的translog。
  • doc : doc表示lucene中的一条记录
  • field :field表示记录中的字段概念,一个doc由若干个field组成。
  • term :term是lucene中索引的最小单位,某个field对应的内容如果是全文检索类型,会将内容进行分词,分词的结果就是由term组成的。如果是不分词的字段,那么该字段的内容就是一个term。
  • 倒排索引(inverted index): lucene索引的通用叫法,即实现了term到doc list的映射。
  • 正排数据:搜索引擎的通用叫法,即原始数据,可以理解为一个doc list。
  • docvalues :Elasticsearch中的列式存储的名称,Elasticsearch除了存储原始存储、倒排索引,还存储了一份docvalues,用作分析和排序。

lucene文件内容

lucene包的文件是由很多segment文件组成的,segments_xxx文件记录了lucene包下面的segment文件数量。每个segment会包含如下的文件。

Name Extension Brief Description
Segment Info .si segment的元数据文件
Compound File .cfs, .cfe 一个segment包含了如下表的各个文件,为减少打开文件的数量,在segment小的时候,segment的所有文件内容都保存在cfs文件中,cfe文件保存了lucene各文件在cfs文件的位置信息
Fields .fnm 保存了fields的相关信息
Field Index .fdx 正排存储文件的元数据信息
Field Data .fdt 存储了正排存储数据,写入的原文存储在这
Term Dictionary .tim 倒排索引的元数据信息
Term Index .tip 倒排索引文件,存储了所有的倒排索引数据
Frequencies .doc 保存了每个term的doc id列表和term在doc中的词频
Positions .pos Stores position information about where a term occurs in the index
全文索引的字段,会有该文件,保存了term在doc中的位置
Payloads .pay Stores additional per-position metadata information such as character offsets and user payloads
全文索引的字段,使用了一些像payloads的高级特性会有该文件,保存了term在doc中的一些高级特性
Norms .nvd, .nvm 文件保存索引字段加权数据
Per-Document Values .dvd, .dvm lucene的docvalues文件,即数据的列式存储,用作聚合和排序
Term Vector Data .tvx, .tvd, .tvf Stores offset into the document data file
保存索引字段的矢量信息,用在对term进行高亮,计算文本相关性中使用
Live Documents .liv 记录了segment中删除的doc

测试数据示例

下面我们以真实的数据作为示例,看看lucene中各类型数据的容量占比。

写100w数据,有一个uuid字段,写入的是长度为36位的uuid,字符串总为3600w字节,约为35M。

数据使用一个shard,不带副本,使用默认的压缩算法,写入完成后merge成一个segment方便观察。

使用线上默认的配置,uuid存为不分词的字符串类型。创建如下索引:

PUT test_field
{
  "settings": {
    "index": {
      "number_of_shards": "1",
      "number_of_replicas": "0",
      "refresh_interval": "30s"
    }
  },
  "mappings": {
    "type": {
      "_all": {
        "enabled": false
      }, 
      "properties": {
        "uuid": {
          "type": "string",
          "index": "not_analyzed"
        }
      }
    }
  }
}

首先写入100w不同的uuid,使用磁盘容量细节如下:


health status index      pri rep docs.count docs.deleted store.size pri.store.size 
green  open   test_field   1   0    1000000            0    122.7mb        122.7mb 

-rw-r--r--  1 weizijun  staff    41M Aug 19 21:23 _8.fdt
-rw-r--r--  1 weizijun  staff    17K Aug 19 21:23 _8.fdx
-rw-r--r--  1 weizijun  staff   688B Aug 19 21:23 _8.fnm
-rw-r--r--  1 weizijun  staff   494B Aug 19 21:23 _8.si
-rw-r--r--  1 weizijun  staff   265K Aug 19 21:23 _8_Lucene50_0.doc
-rw-r--r--  1 weizijun  staff    44M Aug 19 21:23 _8_Lucene50_0.tim
-rw-r--r--  1 weizijun  staff   340K Aug 19 21:23 _8_Lucene50_0.tip
-rw-r--r--  1 weizijun  staff    37M Aug 19 21:23 _8_Lucene54_0.dvd
-rw-r--r--  1 weizijun  staff   254B Aug 19 21:23 _8_Lucene54_0.dvm
-rw-r--r--  1 weizijun  staff   195B Aug 19 21:23 segments_2
-rw-r--r--  1 weizijun  staff     0B Aug 19 21:20 write.lock

可以看到正排数据、倒排索引数据,列存数据容量占比几乎相同,正排数据和倒排数据还会存储Elasticsearch的唯一id字段,所以容量会比列存多一些。

35M的uuid存入Elasticsearch后,数据膨胀了3倍,达到了122.7mb。Elasticsearch竟然这么消耗资源,不要着急下结论,接下来看另一个测试结果。

我们写入100w一样的uuid,然后看看Elasticsearch使用的容量。

health status index      pri rep docs.count docs.deleted store.size pri.store.size 
green  open   test_field   1   0    1000000            0     13.2mb         13.2mb 

-rw-r--r--  1 weizijun  staff   5.5M Aug 19 21:29 _6.fdt
-rw-r--r--  1 weizijun  staff    15K Aug 19 21:29 _6.fdx
-rw-r--r--  1 weizijun  staff   688B Aug 19 21:29 _6.fnm
-rw-r--r--  1 weizijun  staff   494B Aug 19 21:29 _6.si
-rw-r--r--  1 weizijun  staff   309K Aug 19 21:29 _6_Lucene50_0.doc
-rw-r--r--  1 weizijun  staff   7.0M Aug 19 21:29 _6_Lucene50_0.tim
-rw-r--r--  1 weizijun  staff   195K Aug 19 21:29 _6_Lucene50_0.tip
-rw-r--r--  1 weizijun  staff   244K Aug 19 21:29 _6_Lucene54_0.dvd
-rw-r--r--  1 weizijun  staff   252B Aug 19 21:29 _6_Lucene54_0.dvm
-rw-r--r--  1 weizijun  staff   195B Aug 19 21:29 segments_2
-rw-r--r--  1 weizijun  staff     0B Aug 19 21:26 write.lock

这回35M的数据Elasticsearch容量只有13.2mb,其中还有主要的占比还是Elasticsearch的唯一id,100w的uuid几乎不占存储容积。

所以在Elasticsearch中建立索引的字段如果基数越大(count distinct),越占用磁盘空间。

我们再看看存100w个不一样的整型会是如何。

health status index      pri rep docs.count docs.deleted store.size pri.store.size 
green  open   test_field   1   0    1000000            0     13.6mb         13.6mb 

-rw-r--r--  1 weizijun  staff   6.1M Aug 28 10:19 _42.fdt
-rw-r--r--  1 weizijun  staff    22K Aug 28 10:19 _42.fdx
-rw-r--r--  1 weizijun  staff   688B Aug 28 10:19 _42.fnm
-rw-r--r--  1 weizijun  staff   503B Aug 28 10:19 _42.si
-rw-r--r--  1 weizijun  staff   2.8M Aug 28 10:19 _42_Lucene50_0.doc
-rw-r--r--  1 weizijun  staff   2.2M Aug 28 10:19 _42_Lucene50_0.tim
-rw-r--r--  1 weizijun  staff    83K Aug 28 10:19 _42_Lucene50_0.tip
-rw-r--r--  1 weizijun  staff   2.5M Aug 28 10:19 _42_Lucene54_0.dvd
-rw-r--r--  1 weizijun  staff   228B Aug 28 10:19 _42_Lucene54_0.dvm
-rw-r--r--  1 weizijun  staff   196B Aug 28 10:19 segments_2
-rw-r--r--  1 weizijun  staff     0B Aug 28 10:16 write.lock

从结果可以看到,100w整型数据,Elasticsearch的存储开销为13.6mb。如果以int型计算100w数据的长度的话,为400w字节,大概是3.8mb数据。忽略Elasticsearch唯一id字段的影响,Elasticsearch实际存储容量跟整型数据长度差不多。

我们再看一下开启最佳压缩参数对存储空间的影响:

health status index      pri rep docs.count docs.deleted store.size pri.store.size 
green  open   test_field   1   0    1000000            0    107.2mb        107.2mb 

-rw-r--r--  1 weizijun  staff    25M Aug 20 12:30 _5.fdt
-rw-r--r--  1 weizijun  staff   6.0K Aug 20 12:30 _5.fdx
-rw-r--r--  1 weizijun  staff   688B Aug 20 12:31 _5.fnm
-rw-r--r--  1 weizijun  staff   500B Aug 20 12:31 _5.si
-rw-r--r--  1 weizijun  staff   265K Aug 20 12:31 _5_Lucene50_0.doc
-rw-r--r--  1 weizijun  staff    44M Aug 20 12:31 _5_Lucene50_0.tim
-rw-r--r--  1 weizijun  staff   322K Aug 20 12:31 _5_Lucene50_0.tip
-rw-r--r--  1 weizijun  staff    37M Aug 20 12:31 _5_Lucene54_0.dvd
-rw-r--r--  1 weizijun  staff   254B Aug 20 12:31 _5_Lucene54_0.dvm
-rw-r--r--  1 weizijun  staff   224B Aug 20 12:31 segments_4
-rw-r--r--  1 weizijun  staff     0B Aug 20 12:00 write.lock

结果中可以发现,只有正排数据会启动压缩,压缩能力确实强劲,不考虑唯一id字段,存储容量大概压缩到接近50%。

我们还做了一些实验,Elasticsearch默认是开启_all参数的,_all可以让用户传入的整体json数据作为全文检索的字段,可以更方便的检索,但在现实场景中已经使用的不多,相反会增加很多存储容量的开销,可以看下开启_all的磁盘空间使用情况:


health status index      pri rep docs.count docs.deleted store.size pri.store.size 
green  open   test_field   1   0    1000000            0    162.4mb        162.4mb 

-rw-r--r--  1 weizijun  staff    41M Aug 18 22:59 _20.fdt
-rw-r--r--  1 weizijun  staff    18K Aug 18 22:59 _20.fdx
-rw-r--r--  1 weizijun  staff   777B Aug 18 22:59 _20.fnm
-rw-r--r--  1 weizijun  staff    59B Aug 18 22:59 _20.nvd
-rw-r--r--  1 weizijun  staff    78B Aug 18 22:59 _20.nvm
-rw-r--r--  1 weizijun  staff   539B Aug 18 22:59 _20.si
-rw-r--r--  1 weizijun  staff   7.2M Aug 18 22:59 _20_Lucene50_0.doc
-rw-r--r--  1 weizijun  staff   4.2M Aug 18 22:59 _20_Lucene50_0.pos
-rw-r--r--  1 weizijun  staff    73M Aug 18 22:59 _20_Lucene50_0.tim
-rw-r--r--  1 weizijun  staff   832K Aug 18 22:59 _20_Lucene50_0.tip
-rw-r--r--  1 weizijun  staff    37M Aug 18 22:59 _20_Lucene54_0.dvd
-rw-r--r--  1 weizijun  staff   254B Aug 18 22:59 _20_Lucene54_0.dvm
-rw-r--r--  1 weizijun  staff   196B Aug 18 22:59 segments_2
-rw-r--r--  1 weizijun  staff     0B Aug 18 22:53 write.lock

开启_all比不开启多了40mb的存储空间,多的数据都在倒排索引上,大约会增加30%多的存储开销。所以线上都直接禁用。

然后我还做了其他几个尝试,为了验证存储容量是否和数据量成正比,写入1000w数据的uuid,发现存储容量基本为100w数据的10倍。我还验证了数据长度是否和数据量成正比,发现把uuid增长2倍、4倍,存储容量也响应的增加了2倍和4倍。在此就不一一列出数据了。

lucene各文件具体内容和实现

lucene数据元信息文件

文件名为:segments_xxx

该文件为lucene数据文件的元信息文件,记录所有segment的元数据信息。

该文件主要记录了目前有多少segment,每个segment有一些基本信息,更新这些信息定位到每个segment的元信息文件。

lucene元信息文件还支持记录userData,Elasticsearch可以在此记录translog的一些相关信息。

文件示例

elasticsearch_store_segments.png

具体实现类

public final class SegmentInfos implements Cloneable, Iterable<SegmentCommitInfo> {
  // generation是segment的版本的概念,从文件名中提取出来,实例中为:2t/101
  private long generation;     // generation of the "segments_N" for the next commit

  private long lastGeneration; // generation of the "segments_N" file we last successfully read
                               // or wrote; this is normally the same as generation except if
                               // there was an IOException that had interrupted a commit

  /** Id for this commit; only written starting with Lucene 5.0 */
  private byte[] id;

  /** Which Lucene version wrote this commit, or null if this commit is pre-5.3. */
  private Version luceneVersion;

  /** Counts how often the index has been changed.  */
  public long version;

  /** Used to name new segments. */
  // TODO: should this be a long ...?
  public int counter;

  /** Version of the oldest segment in the index, or null if there are no segments. */
  private Version minSegmentLuceneVersion;

  private List<SegmentCommitInfo> segments = new ArrayList<>();

  /** Opaque Map&lt;String, String&gt; that user can specify during IndexWriter.commit */
  public Map<String,String> userData = Collections.emptyMap();
}

/** Embeds a [read-only] SegmentInfo and adds per-commit
 *  fields.
 *
 *  @lucene.experimental */
public class SegmentCommitInfo {

  /** The {@link SegmentInfo} that we wrap. */
  public final SegmentInfo info;

  // How many deleted docs in the segment:
  private int delCount;

  // Generation number of the live docs file (-1 if there
  // are no deletes yet):
  private long delGen;

  // Normally 1+delGen, unless an exception was hit on last
  // attempt to write:
  private long nextWriteDelGen;

  // Generation number of the FieldInfos (-1 if there are no updates)
  private long fieldInfosGen;

  // Normally 1+fieldInfosGen, unless an exception was hit on last attempt to
  // write
  private long nextWriteFieldInfosGen; //fieldInfosGen == -1 ? 1 : fieldInfosGen + 1;

  // Generation number of the DocValues (-1 if there are no updates)
  private long docValuesGen;

  // Normally 1+dvGen, unless an exception was hit on last attempt to
  // write
  private long nextWriteDocValuesGen; //docValuesGen == -1 ? 1 : docValuesGen + 1;

  // TODO should we add .files() to FieldInfosFormat, like we have on
  // LiveDocsFormat?
  // track the fieldInfos update files
  private final Set<String> fieldInfosFiles = new HashSet<>();

  // Track the per-field DocValues update files
  private final Map<Integer,Set<String>> dvUpdatesFiles = new HashMap<>();

  // Track the per-generation updates files
  @Deprecated
  private final Map<Long,Set<String>> genUpdatesFiles = new HashMap<>();

  private volatile long sizeInBytes = -1;
}

segment的元信息文件

文件后缀:.si

每个segment都有一个.si文件,记录了该segment的元信息。

segment元信息文件中记录了segment的文档数量,segment对应的文件列表等信息。

文件示例

elasticsearch_store_si.png

具体实现类

/**
 * Information about a segment such as its name, directory, and files related
 * to the segment.
 *
 * @lucene.experimental
 */
public final class SegmentInfo {

  // _bl
  public final String name;

  /** Where this segment resides. */
  public final Directory dir;

  /** Id that uniquely identifies this segment. */
  private final byte[] id;

  private Codec codec;

  // Tracks the Lucene version this segment was created with, since 3.1. Null
  // indicates an older than 3.0 index, and it's used to detect a too old index.
  // The format expected is "x.y" - "2.x" for pre-3.0 indexes (or null), and
  // specific versions afterwards ("3.0.0", "3.1.0" etc.).
  // see o.a.l.util.Version.
  private Version version;

  private int maxDoc;         // number of docs in seg

  private boolean isCompoundFile;

  private Map<String,String> diagnostics;

  private Set<String> setFiles;

  private final Map<String,String> attributes;
}

fields信息文件

文件后缀:.fnm

该文件存储了fields的基本信息。

fields信息中包括field的数量,field的类型,以及IndexOpetions,包括是否存储、是否索引,是否分词,是否需要列存等等。

文件示例

elasticsearch_store_fnm.png

具体实现类

/**
 *  Access to the Field Info file that describes document fields and whether or
 *  not they are indexed. Each segment has a separate Field Info file. Objects
 *  of this class are thread-safe for multiple readers, but only one thread can
 *  be adding documents at a time, with no other reader or writer threads
 *  accessing this object.
 **/
public final class FieldInfo {
  /** Field's name */
  public final String name;

  /** Internal field number */
  //field在内部的编号
  public final int number;

  //field docvalues的类型
  private DocValuesType docValuesType = DocValuesType.NONE;

  // True if any document indexed term vectors
  private boolean storeTermVector;

  private boolean omitNorms; // omit norms associated with indexed fields 

  //index的配置项
  private IndexOptions indexOptions = IndexOptions.NONE;

  private boolean storePayloads; // whether this field stores payloads together with term positions 

  private final Map<String,String> attributes;

  // docvalues的generation
  private long dvGen;
}

数据存储文件

文件后缀:.fdx, .fdt

索引文件为.fdx,数据文件为.fdt,数据存储文件功能为根据自动的文档id,得到文档的内容,搜索引擎的术语习惯称之为正排数据,即doc_id -> content,es的_source数据就存在这

索引文件记录了快速定位文档数据的索引信息,数据文件记录了所有文档id的具体内容。

文件示例

elasticsearch_store_fdt.png

具体实现类

/**
 * Random-access reader for {@link CompressingStoredFieldsIndexWriter}.
 * @lucene.internal
 */
public final class CompressingStoredFieldsIndexReader implements Cloneable, Accountable {
  private static final long BASE_RAM_BYTES_USED = RamUsageEstimator.shallowSizeOfInstance(CompressingStoredFieldsIndexReader.class);

  final int maxDoc;

  //docid索引,快速定位某个docid的数组坐标
  final int[] docBases;

  //快速定位某个docid所在的文件offset的startPointer
  final long[] startPointers;

  //平均一个chunk的文档数
  final int[] avgChunkDocs;

  //平均一个chunk的size
  final long[] avgChunkSizes;

  final PackedInts.Reader[] docBasesDeltas; // delta from the avg

  final PackedInts.Reader[] startPointersDeltas; // delta from the avg
}

/**
 * {@link StoredFieldsReader} impl for {@link CompressingStoredFieldsFormat}.
 * @lucene.experimental
 */
public final class CompressingStoredFieldsReader extends StoredFieldsReader {

  //从fdt正排索引文件中获得
  private final int version;

  // field的基本信息
  private final FieldInfos fieldInfos;

  //fdt正排索引文件reader
  private final CompressingStoredFieldsIndexReader indexReader;

  //从fdt正排索引文件中获得,用于指向fdx数据文件的末端,指向numChunks地址4
  private final long maxPointer;

  //fdx正排数据文件句柄
  private final IndexInput fieldsStream;

  //块大小
  private final int chunkSize;

  private final int packedIntsVersion;

  //压缩类型
  private final CompressionMode compressionMode;

  //解压缩处理对象
  private final Decompressor decompressor;

  //文档数量,从segment元数据中获得
  private final int numDocs;

  //是否正在merge,默认为false
  private final boolean merging;

  //初始化时new了一个BlockState,BlockState记录下当前正排文件读取的状态信息
  private final BlockState state;
  //chunk的数量
  private final long numChunks; // number of compressed blocks written

  //dirty chunk的数量
  private final long numDirtyChunks; // number of incomplete compressed blocks written

  //是否close,默认为false
  private boolean closed;
}

倒排索引文件

索引后缀:.tip,.tim

倒排索引也包含索引文件和数据文件,.tip为索引文件,.tim为数据文件,索引文件包含了每个字段的索引元信息,数据文件有具体的索引内容。

5.5.0版本的倒排索引实现为FST tree,FST tree的最大优势就是内存空间占用非常低 ,具体可以参看下这篇文章:http://www.cnblogs.com/bonelee/p/6226185.html

http://examples.mikemccandless.com/fst.py?terms=&cmd=Build+it 为FST图实例,可以根据输入的数据构造出FST图

输入到 FST 中的数据为:
String inputValues[] = {"mop","moth","pop","star","stop","top"};
long outputValues[] = {0,1,2,3,4,5};

生成的 FST 图为:

elasticsearch_store_tip1.png

elasticsearch_store_tip2.png

文件示例

elasticsearch_store_tip3.png

具体实现类

public final class BlockTreeTermsReader extends FieldsProducer {
  // Open input to the main terms dict file (_X.tib)
  final IndexInput termsIn;
  // Reads the terms dict entries, to gather state to
  // produce DocsEnum on demand
  final PostingsReaderBase postingsReader;
  private final TreeMap<String,FieldReader> fields = new TreeMap<>();

  /** File offset where the directory starts in the terms file. */
  /索引数据文件tim的数据的尾部的元数据的地址
  private long dirOffset;
  /** File offset where the directory starts in the index file. */

  //索引文件tip的数据的尾部的元数据的地址
  private long indexDirOffset;

  //semgent的名称
  final String segment;

  //版本号
  final int version;

  //5.3.x index, we record up front if we may have written any auto-prefix terms,示例中记录的是false
  final boolean anyAutoPrefixTerms;
}

/**
 * BlockTree's implementation of {@link Terms}.
 * @lucene.internal
 */
public final class FieldReader extends Terms implements Accountable {

  //term的数量
  final long numTerms;

  //field信息
  final FieldInfo fieldInfo;

  final long sumTotalTermFreq;

  //总的文档频率
  final long sumDocFreq;

  //文档数量
  final int docCount;

  //字段在索引文件tip中的起始位置
  final long indexStartFP;

  final long rootBlockFP;

  final BytesRef rootCode;

  final BytesRef minTerm;

  final BytesRef maxTerm;

  //longs:metadata buffer, holding monotonic values
  final int longsSize;

  final BlockTreeTermsReader parent;

  final FST<BytesRef> index;
}

倒排链文件

文件后缀:.doc, .pos, .pay

.doc保存了每个term的doc id列表和term在doc中的词频

全文索引的字段,会有.pos文件,保存了term在doc中的位置

全文索引的字段,使用了一些像payloads的高级特性才会有.pay文件,保存了term在doc中的一些高级特性

文件示例

elasticsearch_store_doc.png

具体实现类

/**
 * Concrete class that reads docId(maybe frq,pos,offset,payloads) list
 * with postings format.
 *
 * @lucene.experimental
 */
public final class Lucene50PostingsReader extends PostingsReaderBase {
  private static final long BASE_RAM_BYTES_USED = RamUsageEstimator.shallowSizeOfInstance(Lucene50PostingsReader.class);
  private final IndexInput docIn;
  private final IndexInput posIn;
  private final IndexInput payIn;
  final ForUtil forUtil;
  private int version;

  //不分词的字段使用的是该对象,基于skiplist实现了倒排链
  final class BlockDocsEnum extends PostingsEnum {
  }

  //全文检索字段使用的是该对象
  final class BlockPostingsEnum extends PostingsEnum {
  }

  //包含高级特性的字段使用的是该对象
  final class EverythingEnum extends PostingsEnum {
  }
}

列存文件(docvalues)

文件后缀:.dvm, .dvd

索引文件为.dvm,数据文件为.dvd。

lucene实现的docvalues有如下类型:

  • 1、NONE 不开启docvalue时的状态
  • 2、NUMERIC 单个数值类型的docvalue主要包括(int,long,float,double)
  • 3、BINARY 二进制类型值对应不同的codes最大值可能超过32766字节,
  • 4、SORTED 有序增量字节存储,仅仅存储不同部分的值和偏移量指针,值必须小于等于32766字节
  • 5、SORTED_NUMERIC 存储数值类型的有序数组列表
  • 6、SORTED_SET 可以存储多值域的docvalue值,但返回时,仅仅只能返回多值域的第一个docvalue
  • 7、对应not_anaylized的string字段,使用的是SORTED_SET类型,number的类型是SORTED_NUMERIC类型

其中SORTED_SET 的 SORTED_SINGLE_VALUED类型包括了两类数据 : binary + numeric, binary是按ord排序的term的列表,numeric是doc到ord的映射。

文件示例

elasticsearch_store_dvd.png

具体实现类

/** reader for {@link Lucene54DocValuesFormat} */
final class Lucene54DocValuesProducer extends DocValuesProducer implements Closeable {
  //number类型的field的列存列表
  private final Map<String,NumericEntry> numerics = new HashMap<>();

  //字符串类型的field的列存列表
  private final Map<String,BinaryEntry> binaries = new HashMap<>();

  //有序字符串类型的field的列存列表
  private final Map<String,SortedSetEntry> sortedSets = new HashMap<>();

  //有序number类型的field的列存列表
  private final Map<String,SortedSetEntry> sortedNumerics = new HashMap<>();

  //字符串类型的field的ords列表
  private final Map<String,NumericEntry> ords = new HashMap<>();

  //docId -> address -> ord 中field的ords列表
  private final Map<String,NumericEntry> ordIndexes = new HashMap<>();

  //field的数量
  private final int numFields;

  //内存使用量
  private final AtomicLong ramBytesUsed;

  //数据源的文件句柄
  private final IndexInput data;

  //文档数
  private final int maxDoc;
  // memory-resident structures
  private final Map<String,MonotonicBlockPackedReader> addressInstances = new HashMap<>();
  private final Map<String,ReverseTermsIndex> reverseIndexInstances = new HashMap<>();
  private final Map<String,DirectMonotonicReader.Meta> directAddressesMeta = new HashMap<>();

  //是否正在merge
  private final boolean merging;
}

/** metadata entry for a numeric docvalues field */
  static class NumericEntry {
    private NumericEntry() {}
    /** offset to the bitset representing docsWithField, or -1 if no documents have missing values */
    long missingOffset;

    /** offset to the actual numeric values */
    //field的在数据文件中的起始地址
    public long offset;

    /** end offset to the actual numeric values */
    //field的在数据文件中的结尾地址
    public long endOffset;

    /** bits per value used to pack the numeric values */
    public int bitsPerValue;

    //format类型
    int format;
    /** count of values written */
    public long count;
    /** monotonic meta */
    public DirectMonotonicReader.Meta monotonicMeta;

    //最小的value
    long minValue;

    //Compressed by computing the GCD
    long gcd;

    //Compressed by giving IDs to unique values.
    long table[];
    /** for sparse compression */
    long numDocsWithValue;
    NumericEntry nonMissingValues;
    NumberType numberType;
  }

  /** metadata entry for a binary docvalues field */
  static class BinaryEntry {
    private BinaryEntry() {}
    /** offset to the bitset representing docsWithField, or -1 if no documents have missing values */
    long missingOffset;
    /** offset to the actual binary values */
    //field的在数据文件中的起始地址
    long offset;
    int format;
    /** count of values written */
    public long count;

    //最短字符串的长度
    int minLength;

    //最长字符串的长度
    int maxLength;
    /** offset to the addressing data that maps a value to its slice of the byte[] */
    public long addressesOffset, addressesEndOffset;
    /** meta data for addresses */
    public DirectMonotonicReader.Meta addressesMeta;
    /** offset to the reverse index */
    public long reverseIndexOffset;
    /** packed ints version used to encode addressing information */
    public int packedIntsVersion;
    /** packed ints blocksize */
    public int blockSize;
  }

参考资料

lucene source code

lucene document

lucene字典实现原理——FST

lucene倒排索引关于数据压缩的问题

Lucenezqc0512 回复了问题 • 6 人关注 • 3 个回复 • 4339 次浏览 • 2018-09-17 09:02 • 来自相关话题

条新动态, 点击查看
事实上,给ES分配的内存有一个魔法上限值26GB,这样可以确保启用zero based Compressed Oops,这样性能才是最佳的。参考:
https://www.elastic.co/blog/a-heap-of-trouble
 
  显示全部 »
事实上,给ES分配的内存有一个魔法上限值26GB,这样可以确保启用zero based Compressed Oops,这样性能才是最佳的。参考:
https://www.elastic.co/blog/a-heap-of-trouble
 
 

Lucene源码和历史版本的问题

回复

默认分类pzw9696 回复了问题 • 2 人关注 • 1 个回复 • 1310 次浏览 • 2022-03-13 17:46 • 来自相关话题

lucene的源码在github上怎么只能找到9的版本

回复

默认分类匿名用户 发起了问题 • 1 人关注 • 0 个回复 • 1062 次浏览 • 2022-03-11 19:40 • 来自相关话题

Lucene索引倒排链数据结构为什么采用单链表,是基于什么考虑的?

回复

ElasticsearchOmbres 回复了问题 • 4 人关注 • 3 个回复 • 2079 次浏览 • 2021-02-07 14:35 • 来自相关话题

Lucene中如何获取一个字段中所有term的tf最大的那个值

回复

Lucenesuisuimu 回复了问题 • 2 人关注 • 1 个回复 • 5318 次浏览 • 2020-10-27 12:59 • 来自相关话题

倒排索引删除文档

回复

LuceneCharele 回复了问题 • 2 人关注 • 1 个回复 • 3576 次浏览 • 2020-07-22 15:51 • 来自相关话题

lucene 或者 es中不存储原字段的应用场景是什么呢???

回复

LuceneCharele 回复了问题 • 3 人关注 • 2 个回复 • 2385 次浏览 • 2020-07-22 15:32 • 来自相关话题

关于默认排序的问题。

回复

ElasticsearchWarrenW 回复了问题 • 3 人关注 • 2 个回复 • 3145 次浏览 • 2020-07-10 14:24 • 来自相关话题

修改Lucene源码,重新打包,替换elasticsearch中原有的Lucene-core.jar包,出现问题

回复

ElasticsearchKalasearch 回复了问题 • 2 人关注 • 1 个回复 • 6959 次浏览 • 2019-12-29 04:06 • 来自相关话题

es是构建在lucene之上,中国目前有lucene的社区吗?国外的也行

回复

Elasticsearchmedcl 回复了问题 • 2 人关注 • 1 个回复 • 1931 次浏览 • 2019-11-15 09:18 • 来自相关话题

Lucene doc value结构自己的一点理解

回复

Elasticsearchcode4j 回复了问题 • 5 人关注 • 3 个回复 • 4284 次浏览 • 2019-07-31 14:57 • 来自相关话题

lucence构建索引几分钟后报错

回复

Lucenelvwendong 发起了问题 • 1 人关注 • 0 个回复 • 2161 次浏览 • 2019-03-01 16:43 • 来自相关话题

lucene倒排索引关于数据压缩的问题

回复

Lucenezqc0512 回复了问题 • 6 人关注 • 3 个回复 • 4339 次浏览 • 2018-09-17 09:02 • 来自相关话题

Lucene用LongPoint或者StringField或者IntPoint做主键,哪个效率更高?

回复

Lucenecodepub 发起了问题 • 1 人关注 • 0 个回复 • 2929 次浏览 • 2018-04-23 17:00 • 来自相关话题

Elasticsearch内存配置成系统内存的50%是否合理?

回复

Elasticsearchkennywu76 回复了问题 • 8 人关注 • 4 个回复 • 12991 次浏览 • 2018-04-18 17:49 • 来自相关话题

关于倒排索引中搜索结果合并不太理解的地方

回复

Elasticsearchstrglee 回复了问题 • 3 人关注 • 1 个回复 • 3446 次浏览 • 2018-04-01 11:43 • 来自相关话题

社区日报 第1247期 (2021-11-12)

社区日报kin122 发表了文章 • 0 个评论 • 929 次浏览 • 2021-11-13 23:20 • 来自相关话题

1. Elasticsearch 异步搜索 Async search 实战 https://mp.weixin.qq.com/s/XYXp5Xn2r7Vnqxj3iswi9A 2.TB级微服务海量日志监控平台 https://mp.weixin.qq.com/s/8BgCxczzFBMTweyRINA0rw 3. Lucene源码解析——DocValue存储方式 https://zhuanlan.zhihu.com/p/384487150 编辑:kin122 归档:https://ela.st/cn-daily-all 订阅:https://ela.st/cn-daily-sub 沙龙:https://ela.st/cn-meetup
1. Elasticsearch 异步搜索 Async search 实战 https://mp.weixin.qq.com/s/XYXp5Xn2r7Vnqxj3iswi9A 2.TB级微服务海量日志监控平台 https://mp.weixin.qq.com/s/8BgCxczzFBMTweyRINA0rw 3. Lucene源码解析——DocValue存储方式 https://zhuanlan.zhihu.com/p/384487150 编辑:kin122 归档:https://ela.st/cn-daily-all 订阅:https://ela.st/cn-daily-sub 沙龙:https://ela.st/cn-meetup

社区日报 第1245期 (2021-11-10)

社区日报kin122 发表了文章 • 0 个评论 • 1091 次浏览 • 2021-11-10 09:41 • 来自相关话题

1. Lucene源码解析——StoredField存储方式 https://zhuanlan.zhihu.com/p/384486147 2.如何在 Kibana 的 Discover 界面中让显示图片字段 https://www.bilibili.com/video/BV14P4y1L7XL/ https://elasticstack.blog.csdn ... 19074 3. 几种常见的查询性能问题及优化方式--字节:张超 https://mp.weixin.qq.com/s/w7hEn2JE9ZXUBh3kZ0uOGg 编辑:kin122 归档:https://ela.st/cn-daily-all 订阅:https://ela.st/cn-daily-sub 沙龙:https://ela.st/cn-meetup
1. Lucene源码解析——StoredField存储方式 https://zhuanlan.zhihu.com/p/384486147 2.如何在 Kibana 的 Discover 界面中让显示图片字段 https://www.bilibili.com/video/BV14P4y1L7XL/ https://elasticstack.blog.csdn ... 19074 3. 几种常见的查询性能问题及优化方式--字节:张超 https://mp.weixin.qq.com/s/w7hEn2JE9ZXUBh3kZ0uOGg 编辑:kin122 归档:https://ela.st/cn-daily-all 订阅:https://ela.st/cn-daily-sub 沙龙:https://ela.st/cn-meetup

Day 7 - Elasticsearch中数据是如何存储的

Adventweizijun 发表了文章 • 7 个评论 • 68633 次浏览 • 2018-12-07 13:55 • 来自相关话题

前言

很多使用Elasticsearch的同学会关心数据存储在ES中的存储容量,会有这样的疑问:xxTB的数据入到ES会使用多少存储空间。这个问题其实很难直接回答的,只有数据写入ES后,才能观察到实际的存储空间。比如同样是1TB的数据,写入ES的存储空间可能差距会非常大,可能小到只有300~400GB,也可能多到6-7TB,为什么会造成这么大的差距呢?究其原因,我们来探究下Elasticsearch中的数据是如何存储。文章中我以Elasticsearch 2.3版本为示例,对应的lucene版本是5.5,Elasticsearch现在已经来到了6.5版本,数字类型、列存等存储结构有些变化,但基本的概念变化不多,文章中的内容依然适用。

Elasticsearch索引结构

Elasticsearch对外提供的是index的概念,可以类比为DB,用户查询是在index上完成的,每个index由若干个shard组成,以此来达到分布式可扩展的能力。比如下图是一个由10个shard组成的index。

elasticsearch_store_arc.png

shard是Elasticsearch数据存储的最小单位,index的存储容量为所有shard的存储容量之和。Elasticsearch集群的存储容量则为所有index存储容量之和。

一个shard就对应了一个lucene的library。对于一个shard,Elasticsearch增加了translog的功能,类似于HBase WAL,是数据写入过程中的中间数据,其余的数据都在lucene库中管理的。

所以Elasticsearch索引使用的存储内容主要取决于lucene中的数据存储。

lucene数据存储

下面我们主要看下lucene的文件内容,在了解lucene文件内容前,大家先了解些lucene的基本概念。

lucene基本概念

  • segment : lucene内部的数据是由一个个segment组成的,写入lucene的数据并不直接落盘,而是先写在内存中,经过了refresh间隔,lucene才将该时间段写入的全部数据refresh成一个segment,segment多了之后会进行merge成更大的segment。lucene查询时会遍历每个segment完成。由于lucene* 写入的数据是在内存中完成,所以写入效率非常高。但是也存在丢失数据的风险,所以Elasticsearch基于此现象实现了translog,只有在segment数据落盘后,Elasticsearch才会删除对应的translog。
  • doc : doc表示lucene中的一条记录
  • field :field表示记录中的字段概念,一个doc由若干个field组成。
  • term :term是lucene中索引的最小单位,某个field对应的内容如果是全文检索类型,会将内容进行分词,分词的结果就是由term组成的。如果是不分词的字段,那么该字段的内容就是一个term。
  • 倒排索引(inverted index): lucene索引的通用叫法,即实现了term到doc list的映射。
  • 正排数据:搜索引擎的通用叫法,即原始数据,可以理解为一个doc list。
  • docvalues :Elasticsearch中的列式存储的名称,Elasticsearch除了存储原始存储、倒排索引,还存储了一份docvalues,用作分析和排序。

lucene文件内容

lucene包的文件是由很多segment文件组成的,segments_xxx文件记录了lucene包下面的segment文件数量。每个segment会包含如下的文件。

Name Extension Brief Description
Segment Info .si segment的元数据文件
Compound File .cfs, .cfe 一个segment包含了如下表的各个文件,为减少打开文件的数量,在segment小的时候,segment的所有文件内容都保存在cfs文件中,cfe文件保存了lucene各文件在cfs文件的位置信息
Fields .fnm 保存了fields的相关信息
Field Index .fdx 正排存储文件的元数据信息
Field Data .fdt 存储了正排存储数据,写入的原文存储在这
Term Dictionary .tim 倒排索引的元数据信息
Term Index .tip 倒排索引文件,存储了所有的倒排索引数据
Frequencies .doc 保存了每个term的doc id列表和term在doc中的词频
Positions .pos Stores position information about where a term occurs in the index
全文索引的字段,会有该文件,保存了term在doc中的位置
Payloads .pay Stores additional per-position metadata information such as character offsets and user payloads
全文索引的字段,使用了一些像payloads的高级特性会有该文件,保存了term在doc中的一些高级特性
Norms .nvd, .nvm 文件保存索引字段加权数据
Per-Document Values .dvd, .dvm lucene的docvalues文件,即数据的列式存储,用作聚合和排序
Term Vector Data .tvx, .tvd, .tvf Stores offset into the document data file
保存索引字段的矢量信息,用在对term进行高亮,计算文本相关性中使用
Live Documents .liv 记录了segment中删除的doc

测试数据示例

下面我们以真实的数据作为示例,看看lucene中各类型数据的容量占比。

写100w数据,有一个uuid字段,写入的是长度为36位的uuid,字符串总为3600w字节,约为35M。

数据使用一个shard,不带副本,使用默认的压缩算法,写入完成后merge成一个segment方便观察。

使用线上默认的配置,uuid存为不分词的字符串类型。创建如下索引:

PUT test_field
{
  "settings": {
    "index": {
      "number_of_shards": "1",
      "number_of_replicas": "0",
      "refresh_interval": "30s"
    }
  },
  "mappings": {
    "type": {
      "_all": {
        "enabled": false
      }, 
      "properties": {
        "uuid": {
          "type": "string",
          "index": "not_analyzed"
        }
      }
    }
  }
}

首先写入100w不同的uuid,使用磁盘容量细节如下:


health status index      pri rep docs.count docs.deleted store.size pri.store.size 
green  open   test_field   1   0    1000000            0    122.7mb        122.7mb 

-rw-r--r--  1 weizijun  staff    41M Aug 19 21:23 _8.fdt
-rw-r--r--  1 weizijun  staff    17K Aug 19 21:23 _8.fdx
-rw-r--r--  1 weizijun  staff   688B Aug 19 21:23 _8.fnm
-rw-r--r--  1 weizijun  staff   494B Aug 19 21:23 _8.si
-rw-r--r--  1 weizijun  staff   265K Aug 19 21:23 _8_Lucene50_0.doc
-rw-r--r--  1 weizijun  staff    44M Aug 19 21:23 _8_Lucene50_0.tim
-rw-r--r--  1 weizijun  staff   340K Aug 19 21:23 _8_Lucene50_0.tip
-rw-r--r--  1 weizijun  staff    37M Aug 19 21:23 _8_Lucene54_0.dvd
-rw-r--r--  1 weizijun  staff   254B Aug 19 21:23 _8_Lucene54_0.dvm
-rw-r--r--  1 weizijun  staff   195B Aug 19 21:23 segments_2
-rw-r--r--  1 weizijun  staff     0B Aug 19 21:20 write.lock

可以看到正排数据、倒排索引数据,列存数据容量占比几乎相同,正排数据和倒排数据还会存储Elasticsearch的唯一id字段,所以容量会比列存多一些。

35M的uuid存入Elasticsearch后,数据膨胀了3倍,达到了122.7mb。Elasticsearch竟然这么消耗资源,不要着急下结论,接下来看另一个测试结果。

我们写入100w一样的uuid,然后看看Elasticsearch使用的容量。

health status index      pri rep docs.count docs.deleted store.size pri.store.size 
green  open   test_field   1   0    1000000            0     13.2mb         13.2mb 

-rw-r--r--  1 weizijun  staff   5.5M Aug 19 21:29 _6.fdt
-rw-r--r--  1 weizijun  staff    15K Aug 19 21:29 _6.fdx
-rw-r--r--  1 weizijun  staff   688B Aug 19 21:29 _6.fnm
-rw-r--r--  1 weizijun  staff   494B Aug 19 21:29 _6.si
-rw-r--r--  1 weizijun  staff   309K Aug 19 21:29 _6_Lucene50_0.doc
-rw-r--r--  1 weizijun  staff   7.0M Aug 19 21:29 _6_Lucene50_0.tim
-rw-r--r--  1 weizijun  staff   195K Aug 19 21:29 _6_Lucene50_0.tip
-rw-r--r--  1 weizijun  staff   244K Aug 19 21:29 _6_Lucene54_0.dvd
-rw-r--r--  1 weizijun  staff   252B Aug 19 21:29 _6_Lucene54_0.dvm
-rw-r--r--  1 weizijun  staff   195B Aug 19 21:29 segments_2
-rw-r--r--  1 weizijun  staff     0B Aug 19 21:26 write.lock

这回35M的数据Elasticsearch容量只有13.2mb,其中还有主要的占比还是Elasticsearch的唯一id,100w的uuid几乎不占存储容积。

所以在Elasticsearch中建立索引的字段如果基数越大(count distinct),越占用磁盘空间。

我们再看看存100w个不一样的整型会是如何。

health status index      pri rep docs.count docs.deleted store.size pri.store.size 
green  open   test_field   1   0    1000000            0     13.6mb         13.6mb 

-rw-r--r--  1 weizijun  staff   6.1M Aug 28 10:19 _42.fdt
-rw-r--r--  1 weizijun  staff    22K Aug 28 10:19 _42.fdx
-rw-r--r--  1 weizijun  staff   688B Aug 28 10:19 _42.fnm
-rw-r--r--  1 weizijun  staff   503B Aug 28 10:19 _42.si
-rw-r--r--  1 weizijun  staff   2.8M Aug 28 10:19 _42_Lucene50_0.doc
-rw-r--r--  1 weizijun  staff   2.2M Aug 28 10:19 _42_Lucene50_0.tim
-rw-r--r--  1 weizijun  staff    83K Aug 28 10:19 _42_Lucene50_0.tip
-rw-r--r--  1 weizijun  staff   2.5M Aug 28 10:19 _42_Lucene54_0.dvd
-rw-r--r--  1 weizijun  staff   228B Aug 28 10:19 _42_Lucene54_0.dvm
-rw-r--r--  1 weizijun  staff   196B Aug 28 10:19 segments_2
-rw-r--r--  1 weizijun  staff     0B Aug 28 10:16 write.lock

从结果可以看到,100w整型数据,Elasticsearch的存储开销为13.6mb。如果以int型计算100w数据的长度的话,为400w字节,大概是3.8mb数据。忽略Elasticsearch唯一id字段的影响,Elasticsearch实际存储容量跟整型数据长度差不多。

我们再看一下开启最佳压缩参数对存储空间的影响:

health status index      pri rep docs.count docs.deleted store.size pri.store.size 
green  open   test_field   1   0    1000000            0    107.2mb        107.2mb 

-rw-r--r--  1 weizijun  staff    25M Aug 20 12:30 _5.fdt
-rw-r--r--  1 weizijun  staff   6.0K Aug 20 12:30 _5.fdx
-rw-r--r--  1 weizijun  staff   688B Aug 20 12:31 _5.fnm
-rw-r--r--  1 weizijun  staff   500B Aug 20 12:31 _5.si
-rw-r--r--  1 weizijun  staff   265K Aug 20 12:31 _5_Lucene50_0.doc
-rw-r--r--  1 weizijun  staff    44M Aug 20 12:31 _5_Lucene50_0.tim
-rw-r--r--  1 weizijun  staff   322K Aug 20 12:31 _5_Lucene50_0.tip
-rw-r--r--  1 weizijun  staff    37M Aug 20 12:31 _5_Lucene54_0.dvd
-rw-r--r--  1 weizijun  staff   254B Aug 20 12:31 _5_Lucene54_0.dvm
-rw-r--r--  1 weizijun  staff   224B Aug 20 12:31 segments_4
-rw-r--r--  1 weizijun  staff     0B Aug 20 12:00 write.lock

结果中可以发现,只有正排数据会启动压缩,压缩能力确实强劲,不考虑唯一id字段,存储容量大概压缩到接近50%。

我们还做了一些实验,Elasticsearch默认是开启_all参数的,_all可以让用户传入的整体json数据作为全文检索的字段,可以更方便的检索,但在现实场景中已经使用的不多,相反会增加很多存储容量的开销,可以看下开启_all的磁盘空间使用情况:


health status index      pri rep docs.count docs.deleted store.size pri.store.size 
green  open   test_field   1   0    1000000            0    162.4mb        162.4mb 

-rw-r--r--  1 weizijun  staff    41M Aug 18 22:59 _20.fdt
-rw-r--r--  1 weizijun  staff    18K Aug 18 22:59 _20.fdx
-rw-r--r--  1 weizijun  staff   777B Aug 18 22:59 _20.fnm
-rw-r--r--  1 weizijun  staff    59B Aug 18 22:59 _20.nvd
-rw-r--r--  1 weizijun  staff    78B Aug 18 22:59 _20.nvm
-rw-r--r--  1 weizijun  staff   539B Aug 18 22:59 _20.si
-rw-r--r--  1 weizijun  staff   7.2M Aug 18 22:59 _20_Lucene50_0.doc
-rw-r--r--  1 weizijun  staff   4.2M Aug 18 22:59 _20_Lucene50_0.pos
-rw-r--r--  1 weizijun  staff    73M Aug 18 22:59 _20_Lucene50_0.tim
-rw-r--r--  1 weizijun  staff   832K Aug 18 22:59 _20_Lucene50_0.tip
-rw-r--r--  1 weizijun  staff    37M Aug 18 22:59 _20_Lucene54_0.dvd
-rw-r--r--  1 weizijun  staff   254B Aug 18 22:59 _20_Lucene54_0.dvm
-rw-r--r--  1 weizijun  staff   196B Aug 18 22:59 segments_2
-rw-r--r--  1 weizijun  staff     0B Aug 18 22:53 write.lock

开启_all比不开启多了40mb的存储空间,多的数据都在倒排索引上,大约会增加30%多的存储开销。所以线上都直接禁用。

然后我还做了其他几个尝试,为了验证存储容量是否和数据量成正比,写入1000w数据的uuid,发现存储容量基本为100w数据的10倍。我还验证了数据长度是否和数据量成正比,发现把uuid增长2倍、4倍,存储容量也响应的增加了2倍和4倍。在此就不一一列出数据了。

lucene各文件具体内容和实现

lucene数据元信息文件

文件名为:segments_xxx

该文件为lucene数据文件的元信息文件,记录所有segment的元数据信息。

该文件主要记录了目前有多少segment,每个segment有一些基本信息,更新这些信息定位到每个segment的元信息文件。

lucene元信息文件还支持记录userData,Elasticsearch可以在此记录translog的一些相关信息。

文件示例

elasticsearch_store_segments.png

具体实现类

public final class SegmentInfos implements Cloneable, Iterable<SegmentCommitInfo> {
  // generation是segment的版本的概念,从文件名中提取出来,实例中为:2t/101
  private long generation;     // generation of the "segments_N" for the next commit

  private long lastGeneration; // generation of the "segments_N" file we last successfully read
                               // or wrote; this is normally the same as generation except if
                               // there was an IOException that had interrupted a commit

  /** Id for this commit; only written starting with Lucene 5.0 */
  private byte[] id;

  /** Which Lucene version wrote this commit, or null if this commit is pre-5.3. */
  private Version luceneVersion;

  /** Counts how often the index has been changed.  */
  public long version;

  /** Used to name new segments. */
  // TODO: should this be a long ...?
  public int counter;

  /** Version of the oldest segment in the index, or null if there are no segments. */
  private Version minSegmentLuceneVersion;

  private List<SegmentCommitInfo> segments = new ArrayList<>();

  /** Opaque Map&lt;String, String&gt; that user can specify during IndexWriter.commit */
  public Map<String,String> userData = Collections.emptyMap();
}

/** Embeds a [read-only] SegmentInfo and adds per-commit
 *  fields.
 *
 *  @lucene.experimental */
public class SegmentCommitInfo {

  /** The {@link SegmentInfo} that we wrap. */
  public final SegmentInfo info;

  // How many deleted docs in the segment:
  private int delCount;

  // Generation number of the live docs file (-1 if there
  // are no deletes yet):
  private long delGen;

  // Normally 1+delGen, unless an exception was hit on last
  // attempt to write:
  private long nextWriteDelGen;

  // Generation number of the FieldInfos (-1 if there are no updates)
  private long fieldInfosGen;

  // Normally 1+fieldInfosGen, unless an exception was hit on last attempt to
  // write
  private long nextWriteFieldInfosGen; //fieldInfosGen == -1 ? 1 : fieldInfosGen + 1;

  // Generation number of the DocValues (-1 if there are no updates)
  private long docValuesGen;

  // Normally 1+dvGen, unless an exception was hit on last attempt to
  // write
  private long nextWriteDocValuesGen; //docValuesGen == -1 ? 1 : docValuesGen + 1;

  // TODO should we add .files() to FieldInfosFormat, like we have on
  // LiveDocsFormat?
  // track the fieldInfos update files
  private final Set<String> fieldInfosFiles = new HashSet<>();

  // Track the per-field DocValues update files
  private final Map<Integer,Set<String>> dvUpdatesFiles = new HashMap<>();

  // Track the per-generation updates files
  @Deprecated
  private final Map<Long,Set<String>> genUpdatesFiles = new HashMap<>();

  private volatile long sizeInBytes = -1;
}

segment的元信息文件

文件后缀:.si

每个segment都有一个.si文件,记录了该segment的元信息。

segment元信息文件中记录了segment的文档数量,segment对应的文件列表等信息。

文件示例

elasticsearch_store_si.png

具体实现类

/**
 * Information about a segment such as its name, directory, and files related
 * to the segment.
 *
 * @lucene.experimental
 */
public final class SegmentInfo {

  // _bl
  public final String name;

  /** Where this segment resides. */
  public final Directory dir;

  /** Id that uniquely identifies this segment. */
  private final byte[] id;

  private Codec codec;

  // Tracks the Lucene version this segment was created with, since 3.1. Null
  // indicates an older than 3.0 index, and it's used to detect a too old index.
  // The format expected is "x.y" - "2.x" for pre-3.0 indexes (or null), and
  // specific versions afterwards ("3.0.0", "3.1.0" etc.).
  // see o.a.l.util.Version.
  private Version version;

  private int maxDoc;         // number of docs in seg

  private boolean isCompoundFile;

  private Map<String,String> diagnostics;

  private Set<String> setFiles;

  private final Map<String,String> attributes;
}

fields信息文件

文件后缀:.fnm

该文件存储了fields的基本信息。

fields信息中包括field的数量,field的类型,以及IndexOpetions,包括是否存储、是否索引,是否分词,是否需要列存等等。

文件示例

elasticsearch_store_fnm.png

具体实现类

/**
 *  Access to the Field Info file that describes document fields and whether or
 *  not they are indexed. Each segment has a separate Field Info file. Objects
 *  of this class are thread-safe for multiple readers, but only one thread can
 *  be adding documents at a time, with no other reader or writer threads
 *  accessing this object.
 **/
public final class FieldInfo {
  /** Field's name */
  public final String name;

  /** Internal field number */
  //field在内部的编号
  public final int number;

  //field docvalues的类型
  private DocValuesType docValuesType = DocValuesType.NONE;

  // True if any document indexed term vectors
  private boolean storeTermVector;

  private boolean omitNorms; // omit norms associated with indexed fields 

  //index的配置项
  private IndexOptions indexOptions = IndexOptions.NONE;

  private boolean storePayloads; // whether this field stores payloads together with term positions 

  private final Map<String,String> attributes;

  // docvalues的generation
  private long dvGen;
}

数据存储文件

文件后缀:.fdx, .fdt

索引文件为.fdx,数据文件为.fdt,数据存储文件功能为根据自动的文档id,得到文档的内容,搜索引擎的术语习惯称之为正排数据,即doc_id -> content,es的_source数据就存在这

索引文件记录了快速定位文档数据的索引信息,数据文件记录了所有文档id的具体内容。

文件示例

elasticsearch_store_fdt.png

具体实现类

/**
 * Random-access reader for {@link CompressingStoredFieldsIndexWriter}.
 * @lucene.internal
 */
public final class CompressingStoredFieldsIndexReader implements Cloneable, Accountable {
  private static final long BASE_RAM_BYTES_USED = RamUsageEstimator.shallowSizeOfInstance(CompressingStoredFieldsIndexReader.class);

  final int maxDoc;

  //docid索引,快速定位某个docid的数组坐标
  final int[] docBases;

  //快速定位某个docid所在的文件offset的startPointer
  final long[] startPointers;

  //平均一个chunk的文档数
  final int[] avgChunkDocs;

  //平均一个chunk的size
  final long[] avgChunkSizes;

  final PackedInts.Reader[] docBasesDeltas; // delta from the avg

  final PackedInts.Reader[] startPointersDeltas; // delta from the avg
}

/**
 * {@link StoredFieldsReader} impl for {@link CompressingStoredFieldsFormat}.
 * @lucene.experimental
 */
public final class CompressingStoredFieldsReader extends StoredFieldsReader {

  //从fdt正排索引文件中获得
  private final int version;

  // field的基本信息
  private final FieldInfos fieldInfos;

  //fdt正排索引文件reader
  private final CompressingStoredFieldsIndexReader indexReader;

  //从fdt正排索引文件中获得,用于指向fdx数据文件的末端,指向numChunks地址4
  private final long maxPointer;

  //fdx正排数据文件句柄
  private final IndexInput fieldsStream;

  //块大小
  private final int chunkSize;

  private final int packedIntsVersion;

  //压缩类型
  private final CompressionMode compressionMode;

  //解压缩处理对象
  private final Decompressor decompressor;

  //文档数量,从segment元数据中获得
  private final int numDocs;

  //是否正在merge,默认为false
  private final boolean merging;

  //初始化时new了一个BlockState,BlockState记录下当前正排文件读取的状态信息
  private final BlockState state;
  //chunk的数量
  private final long numChunks; // number of compressed blocks written

  //dirty chunk的数量
  private final long numDirtyChunks; // number of incomplete compressed blocks written

  //是否close,默认为false
  private boolean closed;
}

倒排索引文件

索引后缀:.tip,.tim

倒排索引也包含索引文件和数据文件,.tip为索引文件,.tim为数据文件,索引文件包含了每个字段的索引元信息,数据文件有具体的索引内容。

5.5.0版本的倒排索引实现为FST tree,FST tree的最大优势就是内存空间占用非常低 ,具体可以参看下这篇文章:http://www.cnblogs.com/bonelee/p/6226185.html

http://examples.mikemccandless.com/fst.py?terms=&cmd=Build+it 为FST图实例,可以根据输入的数据构造出FST图

输入到 FST 中的数据为:
String inputValues[] = {"mop","moth","pop","star","stop","top"};
long outputValues[] = {0,1,2,3,4,5};

生成的 FST 图为:

elasticsearch_store_tip1.png

elasticsearch_store_tip2.png

文件示例

elasticsearch_store_tip3.png

具体实现类

public final class BlockTreeTermsReader extends FieldsProducer {
  // Open input to the main terms dict file (_X.tib)
  final IndexInput termsIn;
  // Reads the terms dict entries, to gather state to
  // produce DocsEnum on demand
  final PostingsReaderBase postingsReader;
  private final TreeMap<String,FieldReader> fields = new TreeMap<>();

  /** File offset where the directory starts in the terms file. */
  /索引数据文件tim的数据的尾部的元数据的地址
  private long dirOffset;
  /** File offset where the directory starts in the index file. */

  //索引文件tip的数据的尾部的元数据的地址
  private long indexDirOffset;

  //semgent的名称
  final String segment;

  //版本号
  final int version;

  //5.3.x index, we record up front if we may have written any auto-prefix terms,示例中记录的是false
  final boolean anyAutoPrefixTerms;
}

/**
 * BlockTree's implementation of {@link Terms}.
 * @lucene.internal
 */
public final class FieldReader extends Terms implements Accountable {

  //term的数量
  final long numTerms;

  //field信息
  final FieldInfo fieldInfo;

  final long sumTotalTermFreq;

  //总的文档频率
  final long sumDocFreq;

  //文档数量
  final int docCount;

  //字段在索引文件tip中的起始位置
  final long indexStartFP;

  final long rootBlockFP;

  final BytesRef rootCode;

  final BytesRef minTerm;

  final BytesRef maxTerm;

  //longs:metadata buffer, holding monotonic values
  final int longsSize;

  final BlockTreeTermsReader parent;

  final FST<BytesRef> index;
}

倒排链文件

文件后缀:.doc, .pos, .pay

.doc保存了每个term的doc id列表和term在doc中的词频

全文索引的字段,会有.pos文件,保存了term在doc中的位置

全文索引的字段,使用了一些像payloads的高级特性才会有.pay文件,保存了term在doc中的一些高级特性

文件示例

elasticsearch_store_doc.png

具体实现类

/**
 * Concrete class that reads docId(maybe frq,pos,offset,payloads) list
 * with postings format.
 *
 * @lucene.experimental
 */
public final class Lucene50PostingsReader extends PostingsReaderBase {
  private static final long BASE_RAM_BYTES_USED = RamUsageEstimator.shallowSizeOfInstance(Lucene50PostingsReader.class);
  private final IndexInput docIn;
  private final IndexInput posIn;
  private final IndexInput payIn;
  final ForUtil forUtil;
  private int version;

  //不分词的字段使用的是该对象,基于skiplist实现了倒排链
  final class BlockDocsEnum extends PostingsEnum {
  }

  //全文检索字段使用的是该对象
  final class BlockPostingsEnum extends PostingsEnum {
  }

  //包含高级特性的字段使用的是该对象
  final class EverythingEnum extends PostingsEnum {
  }
}

列存文件(docvalues)

文件后缀:.dvm, .dvd

索引文件为.dvm,数据文件为.dvd。

lucene实现的docvalues有如下类型:

  • 1、NONE 不开启docvalue时的状态
  • 2、NUMERIC 单个数值类型的docvalue主要包括(int,long,float,double)
  • 3、BINARY 二进制类型值对应不同的codes最大值可能超过32766字节,
  • 4、SORTED 有序增量字节存储,仅仅存储不同部分的值和偏移量指针,值必须小于等于32766字节
  • 5、SORTED_NUMERIC 存储数值类型的有序数组列表
  • 6、SORTED_SET 可以存储多值域的docvalue值,但返回时,仅仅只能返回多值域的第一个docvalue
  • 7、对应not_anaylized的string字段,使用的是SORTED_SET类型,number的类型是SORTED_NUMERIC类型

其中SORTED_SET 的 SORTED_SINGLE_VALUED类型包括了两类数据 : binary + numeric, binary是按ord排序的term的列表,numeric是doc到ord的映射。

文件示例

elasticsearch_store_dvd.png

具体实现类

/** reader for {@link Lucene54DocValuesFormat} */
final class Lucene54DocValuesProducer extends DocValuesProducer implements Closeable {
  //number类型的field的列存列表
  private final Map<String,NumericEntry> numerics = new HashMap<>();

  //字符串类型的field的列存列表
  private final Map<String,BinaryEntry> binaries = new HashMap<>();

  //有序字符串类型的field的列存列表
  private final Map<String,SortedSetEntry> sortedSets = new HashMap<>();

  //有序number类型的field的列存列表
  private final Map<String,SortedSetEntry> sortedNumerics = new HashMap<>();

  //字符串类型的field的ords列表
  private final Map<String,NumericEntry> ords = new HashMap<>();

  //docId -> address -> ord 中field的ords列表
  private final Map<String,NumericEntry> ordIndexes = new HashMap<>();

  //field的数量
  private final int numFields;

  //内存使用量
  private final AtomicLong ramBytesUsed;

  //数据源的文件句柄
  private final IndexInput data;

  //文档数
  private final int maxDoc;
  // memory-resident structures
  private final Map<String,MonotonicBlockPackedReader> addressInstances = new HashMap<>();
  private final Map<String,ReverseTermsIndex> reverseIndexInstances = new HashMap<>();
  private final Map<String,DirectMonotonicReader.Meta> directAddressesMeta = new HashMap<>();

  //是否正在merge
  private final boolean merging;
}

/** metadata entry for a numeric docvalues field */
  static class NumericEntry {
    private NumericEntry() {}
    /** offset to the bitset representing docsWithField, or -1 if no documents have missing values */
    long missingOffset;

    /** offset to the actual numeric values */
    //field的在数据文件中的起始地址
    public long offset;

    /** end offset to the actual numeric values */
    //field的在数据文件中的结尾地址
    public long endOffset;

    /** bits per value used to pack the numeric values */
    public int bitsPerValue;

    //format类型
    int format;
    /** count of values written */
    public long count;
    /** monotonic meta */
    public DirectMonotonicReader.Meta monotonicMeta;

    //最小的value
    long minValue;

    //Compressed by computing the GCD
    long gcd;

    //Compressed by giving IDs to unique values.
    long table[];
    /** for sparse compression */
    long numDocsWithValue;
    NumericEntry nonMissingValues;
    NumberType numberType;
  }

  /** metadata entry for a binary docvalues field */
  static class BinaryEntry {
    private BinaryEntry() {}
    /** offset to the bitset representing docsWithField, or -1 if no documents have missing values */
    long missingOffset;
    /** offset to the actual binary values */
    //field的在数据文件中的起始地址
    long offset;
    int format;
    /** count of values written */
    public long count;

    //最短字符串的长度
    int minLength;

    //最长字符串的长度
    int maxLength;
    /** offset to the addressing data that maps a value to its slice of the byte[] */
    public long addressesOffset, addressesEndOffset;
    /** meta data for addresses */
    public DirectMonotonicReader.Meta addressesMeta;
    /** offset to the reverse index */
    public long reverseIndexOffset;
    /** packed ints version used to encode addressing information */
    public int packedIntsVersion;
    /** packed ints blocksize */
    public int blockSize;
  }

参考资料

lucene source code

lucene document

lucene字典实现原理——FST

一个简单的Lucene工具类,通过注释的方式来配置构建索引的字段。提供新建索引、查找、删除、更新方法,支持分页。

Lucenepengshaojie 发表了文章 • 6 个评论 • 3100 次浏览 • 2018-02-12 10:23 • 来自相关话题

代码地址:https://gitee.com/shaojiepeng/wsm-lucene  ### wsm-lucene 一个简单的Lucene工具类,通过注释的方式来配置构建索引的字段。提供新建索引、查找、删除、更新方法,支持分页。 ### 所需jar包 1. lucene-core:2.4.0 2. lucene-analyzers:2.4.1 3. commons-logging:1.2 ### 背景 以前在做某个feature的时候,鉴于存储在DB中的数据量过大,故使用Lucene来优化查找性能。 相信大家在某些场景下会把DB中的数据读出来,建索引来优化查找。那么这个工具类就比较适合这些场景了。 ### 如何使用  **从附件中下载jar包直接导入到项目中,或者下载此Maven项目的源码,使用项目依赖的方式导入你的项目。**  1. 通过注释的方式配置需要构建索引的model类 ```  **@IndexClass** :注释,说明此model类需要构建索引  **indexDirPath** :索引所存放的物理位置,如:"D:/Index"  **@IndexField** :注释,说明此字段需要构建索引  **fieldStore** :Lucene中的Field.Store同义,不懂请自行查询资料  **fieldIndex** :Lucene中的Field.Index同义,不懂请自行查询资料 ```
173117_4fa2ac08_980808.png
2. 创建索引 ``` IndexService indexService = new IndexServiceImpl(); /** 构建索引的接口  * List:model的集合  * Class: model的class  *  * return boolean **/ indexService.buildIndex(List, Class) ```
173148_bb488cf0_980808.png
3.查找 ``` ArrayList<SearchParamModel> searchParams = new ArrayList<>(); /**添加查询的条件,如果有多个查询条件,则添加SearchParamModel  * fieldName:需要查找的字段,即model中的成员变量  * fieldValue:需要查找字段的值,这个不解释  * BooleanType:Lucene中BooleanClause.Occur值,不懂请自行查询资料 **/ searchParams.add(new SearchParamModel(fieldName, fieldValue, BooleanType)); IndexService indexService = new IndexServiceImpl(); /** 查询的接口  * searchParams:不解释  * Class: model的class  *  * return model的集合 **/ List objs = indexService.search(searchParams, Class); ```
173219_367ef1d0_980808.png
IndexService中还支持update, delete和分页查找的方法,请自行查阅代码。 觉得不错,请点个赞吧。

Lucene 6 基于BKD Tree Index 的应用

Elasticsearchkeehang 发表了文章 • 0 个评论 • 5336 次浏览 • 2017-08-04 10:20 • 来自相关话题

BKD Tree  https://www.elastic.co/blog/lucene-points-6.0 Block k-d trees are a simple yet powerful data structure. At index time, they are built by recursively partitioning the full space of N-dimensional points to be indexed into smaller and smaller rectangular cells, splitting equally along the widest ranging dimension at each step of the recursion. However, unlike an ordinary k-d tree, a block k-d tree stops recursing once there are fewer than a pre-specified (1024 in our case, by default) number of points in the cell. At that point, all points within that cell are written into one leaf block on disk and the starting file-pointer for that block is saved into an in-heap binary tree structure. In the 1D case, this is simply a full sort of all values, divided into adjacent leaf blocks. There are k-d tree variants that can support removing values, and rebalancing, but Lucene does not need these operations because of its write-once per-segment design.   At search time, the same recursion takes place, testing at each level whether the requested query shape intersects the left or right sub-tree of each dimensional split, and recursing if so. In the 1D case, the query shape is simply a numeric range whereas in the 2D and 3D cases, it is a geo-spatial shape (circle, ring, rectangle, polygon, cube, etc.).
测试集合:模拟一亿条
0," nnrIuS","raet","lnsr","inu ","saia",83.405273,73.302012,3991,24,"N"," usA","airport","rra i"
1,"omlritp","aaVe","y Mu","AaVV","NMc ",15.459643,-20.826241,2627,54,"a","eemo","airport","MaArp"
2,"kyaneMr","iasm","raAA"," tnt","inls",16.606066,38.663728,2761,53,"o","arIi","airport","uiron"
1. General Multidimensional Space Points    Search for points with exact given values.    Search for points which has one of the value from a given set of values.  Search for points within a given range.  Get the number of points which has exact point. Get the number of points within a given range. (Ranges are multidimensional ranges. In 3D, they are boxes.) Divide points into range-buckets and get the count in each buckets. (Range bucket is a range which has a label in it)   2. Locations on the planet surface. (Latitude, Longitude)   Find closest set of airports to a given town.     Find the set of airports within a given radius from a particular town.   Find the set of airports inside a country. (Country can be given as a polygon)    Find the set of airports within a given range of Latitudes and Longitudes. It is a Latitude, Longitude box query. (For a examples: Airports closer to the equatorial)    Find the set of airports closer to a given path. (Path can be something like a road. Find the airports which are less than 50km away from a given highway)   Count the airports in each country by giving country maps as polygons.   search  result:   Loading Data is finished ---------------------------------------------------------------------- 建索引花费时间:982ms LatLon - Box Query Example------------------------------------------------------------------------------ search_LatLon_Box 花费时间:69ms LatLon - K Nearest------------------------------------------------------------------------------ search_LatLon_Nearest 花费时间:108ms DoublePoint 1D Point Exact------------------------------------------------------------------------------ search_Double_1D_Exact 花费时间:10ms DoublePoint 1D - Range------------------------------------------------------------------------------ search_Double_1D_range 花费时间:8ms DoublePoint 1D - Range Buckets ----------------------------------------------------------------------------- search_Double_1D_range_bucket 花费时间:58ms DoublePoint multi dimensional - Range------------------------------------------------------------------------------ search_Double_MiltiDimensional_Range 花费时间:1ms      

2017年学习内容

Luceneguoshuangjiang 发表了文章 • 4 个评论 • 4180 次浏览 • 2017-01-05 18:05 • 来自相关话题

  • 重新看lucene源码
  • 看es源码
  • 对比lucene和es
  • 基于lucene实现自己的搜索框架
  • 重新看lucene源码
  • 看es源码
  • 对比lucene和es
  • 基于lucene实现自己的搜索框架

Lucene5.5入门第十篇完结篇——使用Highlighter使关键词高亮

Lucenekl 发表了文章 • 0 个评论 • 5810 次浏览 • 2016-06-24 11:27 • 来自相关话题

前言

我们在使用百度和谷歌等搜索引擎的时候,你会发现,搜索引擎会把和我们输入的关键字以红色的字体显示,来突出显示结果的准确性,这就是高亮显示的使用场景

准备

使用Highlighter需要导入相应的jar包,maven项目可以加入如下依赖

<dependency>
            <groupId>org.apache.lucene</groupId>
            <artifactId>lucene-highlighter</artifactId>
            <version>5.5.0</version>
   </dependency>

直接看代码


/**
 * @author kl by 2016/3/19
 * @boke www.kailing.pub
 */
public class FieldSetBoostTest {
    //索引目录
    String indexDir="E:\\LuceneIndex";
    //测试数据
    String theme="中国";
    String []title={"中国是一个伟大的国家","我爱你的的祖国,美丽的中国","是什么,中国令美日等国虎视眈眈"};
    /**
     * Lucence5.5返回IndexWriter实例
     * @param directory
     * @return
     */
    public IndexWriter getIndexWriter(Directory directory){
        Analyzer analyzer=new CJKAnalyzer();//中日韩二元分词
        IndexWriterConfig writerConfig=new IndexWriterConfig(analyzer);
        IndexWriter writer=null;
        try {
            writer =new IndexWriter(directory,writerConfig);
        }catch (Exception e){
            e.printStackTrace();
        }
        return writer;
    }
    public Directory getDirctory(String indexDir){
        Directory directory=null;
        try {
            directory=FSDirectory.open(Paths.get(indexDir));
        }catch (IOException e){
            e.printStackTrace();
        }
        return directory;
    }
    /**
     * 创建索引不加权
     * @throws Exception
     */
    public void Indexer()throws Exception{
       IndexWriter writer=getIndexWriter(getDirctory(indexDir));
        Document doc=null;
        for(String str:title){
            doc=new Document();
            //Lucence5.5 Fileld有多个实现,StringFIeld不分词  TextField分词
            doc.add(new StringField("theme",theme, Field.Store.YES));
            Field field=new TextField("title",str, Field.Store.YES);
            doc.add(field);
            writer.addDocument(doc);
        }
        writer.close();
    }

    /**
     * 关键命中词高亮输出处理
     * @param query
     * @param context
     * @return
     * @throws Exception
     */
    public static String getHighlighterString(Query query,String context)throws Exception{
        //对促成文档匹配的实际项进行评分
        QueryScorer scorer=new QueryScorer(query);
        //设置高亮的HTML标签格式
        Formatter  simpleHTMLFormatter=new SimpleHTMLFormatter("","");
        //实例化高亮分析器
        Highlighter highlighter=new Highlighter(simpleHTMLFormatter,scorer);
        //提供静态方法,支持从数据源中获取TokenStream,进行token处理
        TokenStream tokenStream=new CJKAnalyzer().tokenStream("title", new StringReader(context));
        return highlighter.getBestFragment(tokenStream, context);
    }
    @Test
    public void searcherTest()throws Exception{
        //  Indexer();
        IndexReader reader= DirectoryReader.open(getDirctory(indexDir));
        IndexSearcher is=new IndexSearcher(reader);
        System.out.println("总的文档数:"+reader.numDocs());
        QueryParser qp=new QueryParser("title",new CJKAnalyzer());
        String q="中国";
        Query query=qp.parse(q);
        TopDocs tDocs=is.search(query,11);
        System.out.println("查询-》"+q+"《-总共命中【"+tDocs.totalHits+"】条结果");
        for (ScoreDoc scoredoc:tDocs.scoreDocs){
            Document doc = is.doc(scoredoc.doc);
            String context=doc.get("title");
            if(context!=null){
                System.out.println(getHighlighterString(query,context));
            }

        }
    }
}
查询效果如下:

原文地址:http://www.kailing.pub/article/index/arcid/82.html

Lucene5.5入门第九篇——使用searchafter方法实现分页查询

Lucenekl 发表了文章 • 2 个评论 • 9411 次浏览 • 2016-06-24 11:25 • 来自相关话题

前言

任何数据量大的情况下,取数据的时候都需要做分页的处理,比如我们百度的时候,结果往往有上千万的结果,而当前呈现在的只有几页的内容,这就是分页的场景,lucene也提供了分页查询的支持

认识searchafter

使用IndexSearcher的searchafter方法可以轻松实现分页查询,如下图



searchafter有多个重载的方法,其中有些searchafter方法Lucene已不推荐使用了,用的多的就searchAfter(final ScoreDoc after, Query query, int numHits)

它有三个形参,分别是

after:上一页最后一个ScoreDoc;

query:query接口实现类的对象,query对象可以通过QueryParser类来创建,也可以自己new Query接口的某一个特定接口实现类;

numHits:每页显示的条数

searchafter官方文档说明地址

重点在下面

/**
 * Created by 小陈 on 2016/3/25.
 */
public class IndexerPaging {
    //测试数据,模拟数据库表结构
    private static String[] ids={"1","2","3","4","5","6"}; //用户ID
    private static String [] names={"kl","kl","kl","kl","kl","fds"};
    private static String [] describes={"shi yi ge mei nan zi","Don't know","Is an idiot\n","Is an idiot\n","Is an idiot\n","Is an idiot\n"};
    //索引存储地址
    private static String indexDir="E:\\javaEEworkspace\\LuceneDemo\\LuceneIndex";

    /**
     * 获取操作索引实体,并添加测试数据
     * @param indexDir 索引存储位置
     * @return
     * @throws Exception
     */
    public static void getIndexWriter(String indexDir)throws Exception{
        IndexWriterConfig writerConfig=new IndexWriterConfig(getAnalyzer());
        IndexWriter indexWriter=new IndexWriter(FSDirectory.open(Paths.get(indexDir)),writerConfig);
        Document document=new Document();
        //Field.Store.YES或者NO(存储域选项)
        //设置为YES表示或把这个域中的内容完全存储到文件中,方便进行文本的还原
        //设置为NO表示把这个域的内容不存储到文件中,但是可以被索引,此时内容无法完全还原(doc.get)
        for(int i=0;i1){
                 int pageIndexLast=(pageIndex-1)*pageSize-1;
                 TopDocs hits=searcher.search(query,pageIndexLast);
                 if(hits.totalHits>=pageIndexLast)
                     return hits.scoreDocs[pageIndexLast];

             }
             return null;
    }

    public static void searcher(String indexDir,String q,int pageIndex,int pageSize)throws Exception{
        Directory directory= FSDirectory.open(Paths.get(indexDir));
        IndexReader reader= DirectoryReader.open(directory);
        IndexSearcher indexSearcher=new IndexSearcher(reader);
        QueryParser queryParser=new QueryParser("names",new StandardAnalyzer());
        Query query=queryParser.parse(q);
        //分页查询
        TopDocs hits=  indexSearcher.searchAfter(getPageLastScoreDoc(pageIndex,pageSize,query,indexSearcher),query,pageSize);//查询首次的30条
        System.out.println("匹配 "+q+"查询到"+hits.totalHits+"个记录");
        for (ScoreDoc scoreDoc:hits.scoreDocs){
            Document doc=indexSearcher.doc(scoreDoc.doc);
            System.out.println(doc.get("describes"));//打印Document的fileName属性
        }
        reader.close();
        directory.close();//关闭连接
    }
    /**
     * 得到默认分词器
     * @return
     */
    public static Analyzer getAnalyzer(){
        return new StandardAnalyzer();
    }

    @Test
    public void  Test()throws Exception{
//     getIndexWriter(indexDir);
        searcher(indexDir,"kl",1,10);//查询测试
    }

}
原文地址:http://www.kailing.pub/article/index/arcid/80.html

Lucene5.5入门第八篇——使用QueryParser实现高级查询

Lucenekl 发表了文章 • 0 个评论 • 8566 次浏览 • 2016-06-24 11:23 • 来自相关话题

前言

为了解决复杂的查询业务,Lucene给我们提供了一个查询语义分析器,一套完整的语法规则,能够满足大部分的查询需求,而不用关心底层是使用什么Query实现类,就好比写sql一样。 Lucene推荐我们使用QueryParser,而不是各种Query的实现类。但是,QueryParser不能满足所有的查询有求,比如多文档域联合查询 。有时候还是需要使用到Query的相关实现类,好了,下面我们就来看看QueryParser能够解析什么语法,解决什么问题,以及多文档域的查询 


直接上代码

每个语法都可以多测试一遍,看看结果,能够加深你的理解,因为这边测试的实在是多,测试结果我就不贴了;       

ps:各个查询语义可以交叉使用的,下面代码有部分也用到了,但是这边因为是写的例子,为了能更好的区分每个语义的作用,所有没有做太多的尝试

/**
 * @author kl by 2016/3/20
 * @boke www.kailing.pub
 */
public class QueryTest {
    //索引目录
    String indexDir="E:\\LuceneIndex";
    //测试数据目录
    String dataDir="E:\\LuceneTestData";
    /**
     * Lucence5.5返回IndexWriter实例
     * @param directory
     * @return
     */
    public IndexWriter getIndexWriter(Directory directory){
        Analyzer analyzer=new StandardAnalyzer();
        IndexWriterConfig writerConfig=new IndexWriterConfig(analyzer);
        IndexWriter writer=null;
        try {
            writer =new IndexWriter(directory,writerConfig);
        }catch (Exception e){
            e.printStackTrace();
        }
        return writer;
    }
    public Directory getDirctory(String indexDir){
        Directory directory=null;
        try {
            directory= FSDirectory.open(Paths.get(indexDir));
        }catch (IOException e){
            e.printStackTrace();
        }
        return directory;
    }
    @Test
    public void TestIndexer()throws Exception{
        File[] files= new File(dataDir).listFiles();
        IndexWriter writer=getIndexWriter(getDirctory(indexDir));
        for(File file:files){
            Document doc=new Document();
            doc.add(new TextField("filePath",file.getCanonicalPath(), Field.Store.YES));
            doc.add(new TextField("context",new  FileReader(file)));
            writer.addDocument(doc);
        }
        System.out.println("总共添加了"+writer.numDocs()+"个文档");
        writer.close();
    }
    @Test
    public void testSearcher()throws  Exception{
        IndexReader reader= DirectoryReader.open(getDirctory(indexDir));
        IndexSearcher searcher=new IndexSearcher(reader);
        QueryParser queryParser=new QueryParser("context",new StandardAnalyzer());
       Query queryw=queryParser.parse("Licensor");//完整匹配分词查询
        /**
         * 通配符 ?,*的使用
         */
         Query queryy=queryParser.parse("Lice?sor");//使用?匹配单个字符查询
         Query queryx=queryParser.parse("L*r");//使用*匹配多个字符查询
        /**
         * 布尔运算AND, OR,NOT,+,-的使用,注意:一定要是大写的AND和OR,NOT
         */
        Query queryo=queryParser.parse("Licensor OR ce*");//使用OR联合多关键字查询,也可用空格代替OR
        Query queryoo=queryParser.parse(" Licensor ce*");//这个和使用OR一样的效果
        Query queryjia=queryParser.parse("+Licensor Wildcard");//+代表必须的条件,搜索文档必须包含Licensor 可能有Wildcard
        Query querya=queryParser.parse("Licensor AND ce* AND Licenso?");//使用AND取多个关键字的并集查询
        Query queryNot=queryParser.parse("'Lincensor Apache' NOT 'Apache Licensor'");//搜索Lincensor Apache而不是Apache Licensor
        Query queryjian=queryParser.parse("'Lincensor Apache' - 'Apache Licensor'");//"-"同NOT的效果一样

        /**
         * 使用正则表达式查询
         */
        Query queryRegular=queryParser.parse("/[Lab]icensor/");//这个匹配Lincensor,aicensor,bicensor分词
        Query queryRegularr=queryParser.parse("/[Lab]icenso[a-z]/");//根据需要可以更灵活的使用
        /**
         * 使用~模糊匹配查询
         * 这个要和*号的用法区分下,*号完整通配多个字符查询,而~不是简单的通配,这个模糊匹配和Lucene的评分有关
         */
        Query queryFuzzy=queryParser.parse("icensor~");//可以查到Licensor关键字,而queryParser.parse("icensor*")查不到
        Query queryFuzzyparam=queryParser.parse("Licens~1");//~后面可加0-2的整数来制定模糊匹配度,默认不加为1
        Query queryFuzzyParam=queryParser.parse("Licens cens ~0");//~还可以模糊匹配差异化N字符数的多个关键字
        /**
         * 范围查询,多用于数字和时间的查询
         */
        Query queryRange =queryParser.parse("{abc TO Licens}");//{}abc与Licenszhi间的文件,不包含
        Query queryRangex =queryParser.parse("[abc TO Licens]");//{}abc与Licenszhi间的文件,包含本身
        /**
         * 关键字加权处理查询
         */
        //默认为1,可加权可降权,可通过加权处理给匹配的结果排序
        Query queryBoosting  =queryParser.parse("Licensor Wildcard^4 ");

        /**
         * Grouping组合查询
         */
        Query queryGrouping  =queryParser.parse("(+Licensor  +Wildcard) AND easier");//可使用()组合多个条件查询

         //ps: 查询部分字符需要转义处理,如(+ - && || ! ( ) { } [ ] ^ " ~ * ? : \ /)

        /**
         * 使用MultiFieldQueryParser进行多个文档域查询
         */
        Map boost=new HashMap();
        boost.put("filePath",1.5F);//设置文档域的权值
        boost.put("context",2F);
        QueryParser multiField=new MultiFieldQueryParser(new String[]{"filePath","context"},new StandardAnalyzer(),boost);
        Query queryq=multiField.parse("lucenetestdata");

        TopDocs topDocs= searcher.search(queryq,10);
        System.out.println("查询结果共有"+topDocs.totalHits+"条");
        for(ScoreDoc scoreDoc:topDocs.scoreDocs){
            Document document=searcher.doc(scoreDoc.doc);
            System.out.println(document.get("filePath")+"--评分:"+scoreDoc.score);
        }
    }

} 
ps:代码中有大量注释,有些不一定理解到位了,深入了解 请参考官方说明:

https://lucene.apache.org/core ... rches
原文地址:http://www.kailing.pub/article/index/arcid/79.html

Lucene5.5入门第七篇——Lucene索引文档域加权

Lucenekl 发表了文章 • 0 个评论 • 5547 次浏览 • 2016-06-24 11:22 • 来自相关话题

前言

就拿百度说事吧,使用百度搜索引擎的时候,你会发现,卧槽,这什么玩意,前面的几个结果根本就不是老子要的东西,都是些推广的内容,而结果匹配度高的还排在老后面去了,百度这铲屎的干嘛吃的!这也不能怪百度,毕竟人家靠推广吃饭的,自然把交了钱的结果权值提高了 !这算文档域加权的使用场景吧

说明

所谓索引域加"权",就是根据需求的不同,对不同的关键值或者不同的关键索引分配不同的权值,因为查询的时候Lucene的评分机制和权值的高低是成正比的,这样权值高的内容更容易被用户搜索出来,而且排在前面。在Lucene3.x版本的时候可以给文档加权,到4.x版本后就取消了给文档加权了,就只有给文档域加权了,如果想达到给文档加权的效果,就要该文档的每个域都加权处理                                                                                                                                                  

ps:博主前篇博文谈过IKAnalyzer与paoding中文分词,今天我们使用的是可用于中日韩的二元分词器CJKAnalyzer

闲话少说,直接上代码,看结果


package com.kl.luceneDemo;
import org.apache.lucene.analysis.Analyzer;
import org.apache.lucene.analysis.cjk.CJKAnalyzer;
import org.apache.lucene.document.Document;
import org.apache.lucene.document.Field;
import org.apache.lucene.document.StringField;
import org.apache.lucene.document.TextField;
import org.apache.lucene.index.*;
import org.apache.lucene.queryparser.classic.QueryParser;
import org.apache.lucene.search.IndexSearcher;
import org.apache.lucene.search.Query;
import org.apache.lucene.search.ScoreDoc;
import org.apache.lucene.search.TopDocs;
import org.apache.lucene.store.Directory;
import org.apache.lucene.store.FSDirectory;
import org.junit.Test;
import java.io.IOException;
import java.nio.file.Paths;
/**
 * @author kl by 2016/3/19
 * @boke www.kailing.pub
 */
public class FieldSetBoostTest {
    //索引目录
    String indexDir="E:\\LuceneIndex";
    //测试数据
    String theme="中国";
    String []title={"中国是一个伟大的国家","我爱你的的祖国,美丽的中国","是什么,中国令美日等国虎视眈眈"};
    /**
     * Lucence5.5返回IndexWriter实例
     * @param directory
     * @return
     */
    public IndexWriter getIndexWriter(Directory directory){
        Analyzer analyzer=new CJKAnalyzer();//中日韩二元分词
        IndexWriterConfig writerConfig=new IndexWriterConfig(analyzer);
        IndexWriter writer=null;
        try {
            writer =new IndexWriter(directory,writerConfig);
        }catch (Exception e){
            e.printStackTrace();
        }
        return writer;
    }
    public Directory getDirctory(String indexDir){
        Directory directory=null;
        try {
            directory=FSDirectory.open(Paths.get(indexDir));
        }catch (IOException e){
            e.printStackTrace();
        }
        return directory;
    }
    /**
     * 创建索引不加权
     * @throws Exception
     */
    public void Indexer()throws Exception{
       IndexWriter writer=getIndexWriter(getDirctory(indexDir));
        Document doc=null;
        for(String str:title){
            doc=new Document();
            //Lucence5.5 Fileld有多个实现,StringFIeld不分词  TextField分词
            doc.add(new StringField("theme",theme, Field.Store.YES));
            Field field=new TextField("title",str, Field.Store.YES);
            doc.add(field);
            writer.addDocument(doc);
        }
        writer.close();
    }
    /**
     * 创建索引,指定文档域加权
     * @throws Exception
     */
    public void IndexerSetBoot()throws Exception{
        IndexWriter writer=getIndexWriter(getDirctory(indexDir));
        Document doc=null;
        for(String str:title){
            doc=new Document();
            //Lucence5.5 Fileld有多个实现,StringFIeld不分词  TextField分词
            doc.add(new StringField("theme",theme, Field.Store.YES));
            Field field=new TextField("title",str, Field.Store.YES);
            if(str.indexOf("是什么")!=-1)
                field.setBoost(2);//提高权值
            doc.add(field);
            writer.addDocument(doc);
        }
        writer.close();
    }
    @Test
    public void searcherTest()throws Exception{
        IndexerSetBoot();
//        Indexer();
        IndexReader reader= DirectoryReader.open(getDirctory(indexDir));
        IndexSearcher is=new IndexSearcher(reader);
        System.out.println("总的文档数:"+reader.numDocs());
        QueryParser qp=new QueryParser("title",new CJKAnalyzer());
        Query query=qp.parse("中国");
        TopDocs tDocs=is.search(query,11);//一次查询多少个结果
        System.out.println("总共有【"+tDocs.totalHits+"】条结果");
        for (ScoreDoc scoredoc:tDocs.scoreDocs){
            Document doc = is.doc(scoredoc.doc);
            System.out.println(doc.getField("title").stringValue());
        }
    }
}
加权和不加权的结果如下



原文地址:http://www.kailing.pub/article/index/arcid/77.html

Lucene5.5入门第六篇——Analyzer中文分词

Lucenekl 发表了文章 • 0 个评论 • 10623 次浏览 • 2016-06-24 11:18 • 来自相关话题

前言

对于中文分词这个字眼,百科是这么描述的:

中文分词(Chinese Word Segmentation) 指的是将一个汉字序列切分成一个一个单独的词。分词就是将连续的字序列按照一定的规范重新组合成词序列的过程。我们知道,在英文的行文中,单词之间是以空格作为自然分界符的,而中文只是字、句和段能通过明显的分界符来简单划界,唯独词没有一个形式上的分界符,虽然英文也同样存在短语的划分问题,不过在词这一层上,中文比之英文要复杂的多、困难的多。

简单的说,就是把一个句子拆分成多个词,有废话的赶脚,呵呵

之前几篇博文,笔者都是用的Lucene里的StandardAnalyzer来做的分词处理,虽然在后面的Lucene版本中,

准备工作

这里先把这两个分词器加入到我们的项目中来

IKAnalyzer:IKAnalyzer是一个国人开发的开源的分词工具,下载地址:https://code.google.com/archiv ... e%3D1,GItHub地址:https://github.com/wks/ik-analyzer。推荐到GitHub上下载源码然后自己打包,项目是maven构建的,打成jar,然后在我们的项目中引用。

ps:打包项目的时候记得去掉test

paoding:paoding也是一个开源的i项目,下载地址:https://code.google.com/archiv ... loads,下载下来是一个压缩文件,里面有源码也有打包好可以直接用的jar

ps:下载paoding的时候请自行翻墙吧,这里推荐一个翻墙神器Lantern

进入正文

笔者在测试过程中并不是一番风顺啊,好多坑,下面我们来看看这些坑

IKAnlyzer的问题:

1.最新的项目也是基于Lucene3.0.3版本的,而笔者一直都是使用的最新的Lucene5.5,所以一测试就报了如下的错误

Exception in thread "main" java.lang.VerifyError: class org.wltea.analyzer.lucene.IKAnalyzer overrides final method tokenStream.(Ljava/lang/String;Ljava/io/Reader;)Lorg/apache/lucene/analysis/TokenStream;

解决:笔者有试着将IKAnlyzer项目的Lucene版本换成5.5的重新打包,然后发现行不通,改动的地方太多了,虽然IKAnlyzer项目不大,文件不多。笔者还没达到重写IKAnlyzer项目的能力,有时间可以研究研究源码,最后只有降级自己的Lucene版本了,幸好有maven,降级只要改下pom.xml就行了

paoding的问题

1.项目首先会依赖apache的commons-logging,笔者测试1.1版本通过。

2.然后就是下面的这个了 问题了,其实这个问题paoding自己的使用文档中类似的说明,(Paoding中文分词参考手册.htm)这个文档包含在了下载的压缩包中了

net.paoding.analysis.exception.PaodingAnalysisException: please set a system env PAODING_DIC_HOME or Config paoding.dic.home in paoding-dic-home.properties point to the dictionaries!

解决:就是指定paoding的一个字典文件目录,这个文件在下载下来的压缩包中的dic中,

三种解决方案:

(1).你可以解压缩jar,然后把paoding-dic-home.properties文件中的paoding.dic.home指定你的doc目录,重新压缩,把后缀换成jar就行了。

(2).就是参照官方的说明,把doc目录添加到环境变量中

(3).把doc放在项目目录下

3.paoding还有个问题就是Lucene3.0.3都不兼容了,笔者只好又把Lucene版本降到2.2.0来测试了

越过那些沟沟坎坎终于要见真功夫了,不多说,直接上代码,上图


package com.kl.Lucene;
import net.paoding.analysis.analyzer.PaodingAnalyzer;
import org.apache.lucene.analysis.Analyzer;
import org.apache.lucene.analysis.Token;
import org.apache.lucene.analysis.TokenStream;
import org.apache.lucene.analysis.tokenattributes.TermAttribute;
import org.junit.Test;
import org.wltea.analyzer.lucene.IKAnalyzer;
import java.io.StringReader;
/**
 * @author kl by 2016/3/14
 * @boke www.kailing.pub
 */
public class AnalyzerTest {
    //测试数据
    public static String testData="中文分词(Chinese Word Segmentation) 指的是将一个汉字序列切分成一" +
            "一个单独的词。分词就是将连续的字序列按照一定的规范重新组合成词序列的过程。";
    /**
     * 得到IKAnalyzer分词器
     * @return
     */
    public static Analyzer getIKAnalyzer(){
        return  new IKAnalyzer();
    }
    /**
     * 得到Paoding分词器
     * @return
     */
    public static Analyzer getPaoding(){
        return new PaodingAnalyzer();
    }
    /**
     * 测试IKAnalyzer
     * @throws Exception
     */
    @Test
    public void TestIKAnalyzer()throws Exception{
        Analyzer analyzer =getIKAnalyzer();
        TokenStream tokenStream = analyzer.tokenStream("", new StringReader(testData));
        tokenStream.addAttribute(TermAttribute.class);
        System.out.println("分词数据:"+testData);
        System.out.println("=====IKAnalyzer的分词结果====");
        while (tokenStream.incrementToken()) {
            TermAttribute termAttribute = tokenStream.getAttribute(TermAttribute.class);
            System.out.println(new String(termAttribute.term()));
            termAttribute.termLength();
        }

    }
    /**
     * 测试Paoding
     * @throws Exception
     */
    @Test
    public void TestPaoding()throws  Exception{
        Analyzer analyzer =getPaoding();
        TokenStream ts = analyzer.tokenStream("",  new StringReader(testData));
        System.out.println("分词数据:"+testData);
        System.out.println("=====Paoding的分词结果====");
        Token t;
//        while ((t = ts.next()) != null) {
//            System.out.println(t.termText());
//        }
    }


}
测试数据:中文分词(Chinese Word Segmentation) 指的是将一个汉字序列切分成一个单独的词。分词就是将连续的字序列按照一定的规范重新组合成词序列的过程。

测试结果如下:





从结果上看,IKAnalyzer和paoding的分词相差无几,IKAnlyzer比paoding的分词粒度更细,这个可以查看他们的分词字典文件去分析

后记:除了上面介绍的两种分词,常用的还有中日韩二元分词器CJKAnalyzer,以及lucene基于中科院分词实现的SmartChineseAnalyzer,其中cjk在lucene-common的jar包里了,SmartChineseAnalyzer需要另外引入jar,如下pom依赖

        <!--公共的分词器,包含大多数的语言分词-->
        <dependency>
            <groupId>org.apache.lucene</groupId>
            <artifactId>lucene-analyzers-common</artifactId>
            <version>5.5.0</version>
        </dependency>
        <!--基于中科院的中文分词-->
        <dependency>
            <groupId>org.apache.lucene</groupId>
            <artifactId>lucene-analyzers-smartcn</artifactId>
            <version>5.5.0</version>
        </dependency>


原文地址:http://www.kailing.pub/article/index/arcid/76.html

Lucene5.5入门第五篇——Lucene索引的【增删改查】

Lucenekl 发表了文章 • 0 个评论 • 6746 次浏览 • 2016-06-24 11:15 • 来自相关话题

前言

从入门的demo,到了解原理到了解结构,继而学习工具,现在我们可以用Lucene来做简单的数据增删改查操作了

直接上代码

ps:代码注释比较全,鉴于作者的水平,有些东西可能未理解到位。推荐使用Luke来配合测试,了解Luke可参考我的上一篇博文:http://www.kailing.pub/article/index/arcid/74.html

package com.kl.Lucene;
import org.apache.lucene.analysis.Analyzer;
import org.apache.lucene.analysis.standard.StandardAnalyzer;
import org.apache.lucene.document.Document;
import org.apache.lucene.document.Field;
import org.apache.lucene.document.StringField;
import org.apache.lucene.document.TextField;
import org.apache.lucene.index.*;
import org.apache.lucene.queryparser.classic.QueryParser;
import org.apache.lucene.search.*;
import org.apache.lucene.store.Directory;
import org.apache.lucene.store.FSDirectory;
import org.junit.Test;
import java.nio.file.Paths;
/**
 * @author kl by 2016/3/14
 * @boke www.kailing.pub
 */
public class IndexerCURD {
    //测试数据,模拟数据库表结构
    private static String ids={"1","2","3"}; //用户ID
    private static String  names={"kl","wn","sb"};
    private static String  describes={"shi yi ge mei nan zi","Don't know","Is an idiot\n"};
    //索引存储地址
    private static String indexDir="E:\\javaEEworkspace\\LuceneDemo\\LuceneIndex";
    /**
     * 获取操作索引实体,并添加测试数据
     * @param indexDir 索引存储位置
     * @return
     * @throws Exception
     */
    public static IndexWriter getIndexWriter(String indexDir)throws Exception{
        IndexWriterConfig writerConfig=new IndexWriterConfig(getAnalyzer());
        IndexWriter indexWriter=new IndexWriter(getDirectory(indexDir),writerConfig);
        Document document=new Document();
        //Field.Store.YES或者NO(存储域选项)
        //设置为YES表示或把这个域中的内容完全存储到文件中,方便进行文本的还原
        //设置为NO表示把这个域的内容不存储到文件中,但是可以被索引,此时内容无法完全还原(doc.get)
        for(int i=0;i<ids.length;i++){
            document.add(new StringField("ids",ids[i], Field.Store.YES));
            document.add(new StringField("names",names[i], Field.Store.YES));
            document.add(new TextField("describes",describes[i], Field.Store.YES));
            indexWriter.addDocument(document);
        }
        return indexWriter;
    }
    /**
     * 得到默认分词器
     * @return
     */
    public static Analyzer getAnalyzer(){
        return  new StandardAnalyzer();
    }
    /**
     * 得到索引磁盘存储器
     * @param indexDir 存储位置
     * @return
     */
    public static Directory getDirectory(String indexDir){
        Directory directory=null;
        try {
             directory= FSDirectory.open(Paths.get(indexDir));
        }catch (Exception e){
            e.printStackTrace();
        }
        return  directory;
    }
    /**
     * 获取读索引实体,并打印读到的索引信息
     * @return
     */
    public  static IndexReader getIndexReader(){
        IndexReader reader=null;
        try {
            reader= DirectoryReader.open(getDirectory(indexDir));
            //通过reader可以有效的获取到文档的数量
            System.out.println("当前存储的文档数::"+reader.numDocs());
            System.out.println("当前存储的文档数,包含回收站的文档::"+reader.maxDoc());
            System.out.println("回收站的文档数:"+reader.numDeletedDocs());

        } catch (Exception e) {
            e.printStackTrace();
        }
        return reader;
    }

    /**
     * 写索引测试,借助Luke观察结果
     * @throws Exception
     */
    @Test
    public void Testinsert() throws  Exception{
        IndexWriter writer=getIndexWriter(indexDir);
        writer.close();
        getIndexReader();
    }
    /**
     * 删除索引测试,借助Luke观察结果
     * @throws Exception
     */
    @Test
    public void TestDelete()throws Exception{
        //测试删除前我们先把上次的索引文件删掉,或者换个目录
        IndexWriter writer=getIndexWriter(indexDir);
        QueryParser parser=new QueryParser("ids", getAnalyzer());//指定Document的某个属性
        Query query=parser.parse("2");//指定索引内容,对应某个分词
        Term term=new Term("names","kl");
        //参数是一个选项,可以是一个query,也可以是一个term,term是一个精确查找的值
        writer.deleteDocuments(query);//此时删除的文档并不会被完全删除,而是存储在一个回收站中的,可以恢复
         writer.forceMergeDeletes();//强制合并删除的索引信息,索引量大的时候不推荐使用,真正的删除
       // writer.commit(); //更改索引要提交,和提交数据库事务一个概念,真正的删除
        writer.close();
        getIndexReader();
    }
    /**
     * 更新操作测试,借助Luke观察结果
     * @throws Exception
     */
    @Test
    public void TestUpdate()throws Exception{
       // Lucene并没有提供更新,这里的更新操作相当于新增,他并不会去掉原来的信息
        IndexWriter writer = getIndexWriter(indexDir);
        try {
            Document doc = new Document();
            doc.add(new StringField("id","1",Field.Store.YES));
            doc.add(new StringField("names","ckl",Field.Store.YES));
            doc.add(new StringField("describes","chenkailing",Field.Store.NO));
            writer.updateDocument(new Term("id","1"), doc);

        } catch (Exception e) {
            e.printStackTrace();
        } finally {
                if(writer!=null) writer.close();
        }
    }
    /**
     * 查询测试
     */
    @Test
    public void TestSearchaer(){
        try {
            IndexReader reader = getIndexReader();
            IndexSearcher searcher = new IndexSearcher(reader);
            QueryParser parser=new QueryParser("names", getAnalyzer());//指定Document的某个属性
            Query query=parser.parse("kl");//指定索引内容,对应某个分词
            Term term=new Term("names","kl");
            //参数是一个选项,可以是一个query,也可以是一个term,term是一个精确查找的值
            TopDocs hits = searcher.search(query, 10);
            for(ScoreDoc sd:hits.scoreDocs) {
                Document doc = searcher.doc(sd.doc);
                System.out.println(
                        doc.get("names")+"["+doc.get("describes")+"]-->"+doc.get("ids"));
            }
            reader.close();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}[/i][/i][/i]
[i][i][i]原文地址:http://www.kailing.pub/article/index/arcid/75.html
[/i][/i][/i]

Lucene5.5入门第四篇——Lucene索引查看工具Luke

Lucenekl 发表了文章 • 0 个评论 • 6316 次浏览 • 2016-06-24 11:14 • 来自相关话题

前言

Luke是一个用于Lucene搜索引擎的,方便开发和诊断的第三方工具,它可以访问现有Lucene的索引,并允许您显示和修改。如果我们把Lucene的索引比作数据库数据的话,那么Luke就是一个管理数据的客户端(DBMS)。我们开发Lucene的时候可以借助这个工具来提高我们的开发效率

准备工作

Luke是一个开源的i项目,项目托管在GitHub上,地址https://github.com/DmitryKey/luke,选好我们的Luke分支下载下来

ps:Lucene更新迭代的很快,每个版本的变化也比较大,所以我们在选Luke版本的时候也要选择对应的分支下对应的版本,不然就gg了,笔者这里的Lucene是最新的5.X版本,当然Luke也是选的最新的

Luke是一个maven项目,下载好后,install下,会打成一个jar包,直接双击jar就可以运行

ps:最新的Luke的使用的是jdk1.8,其他版本的不清楚,可以去pom.xml文件看下

运行后的界面如下,我这里已选则好索引目录了



图解

1处信息可以看出这个Document有几个Field,以及每个Field的分词个数,百分占比,编码

2.处信息是一个统计区,列出了有几个Document,Document有几个FIeld,总共多少个分词

3.处可以重新打开索引(当索引变化时),提交更改

4.处是索引的详细信息,可以看出每个分词出现的频次,以及对于的Field

使用Luke

上图可以看到第一个选项卡OverView的功能,接下来就看看其他的选项卡的功能

documents选项卡是用来进行文档的操作和查看的,比如文件的删除、添加。下面一个大listview就可以用来查看文档的详细信息了,是不是和DBMS的查看表数据非常的像呢?上面有两个查找文档的方法,根据文档编号来查找和根据词来查找了,其实这个就是搜索了,详情如下图

search选项卡是我认为最有用的一个界面了,其中我们可以在这里进行索引的搜索测试,可以编写最的lucene搜索语句,然后可以看到语句解析后的query树,这样就可以知道为什么我们有些查询会查询不到我们想要的信息了,然后还可以选择进行搜索的分词器、默认字段和重复搜索次数的(可以通过多次搜索来获取平均一个搜索过程的耗时长短,这个对查询的性能测试时非常有用的),然后下面的listview中就会列出一个搜索的的文档的所有保存的(store)字段的值,下面可以看到查询花费的时间。详情如下图


Commits选项卡就是用来查看每个索引相关文件的一些属性的界面,具体的话,可以通过这个界面分析下索引文件的多少大小,是否需要优化或者合并等等。详情如下图

最后一个plugins选项卡,就是可以看到luke提供的各种插件,我认为比较有用的还是那个分词工具,提供一个分词的类,然后下面文本框输入一段文本,然后就可以让这个工具帮你分词,你可以看到详细的分词信息。详情如下图


原文地址:http://www.kailing.pub/article/index/arcid/74.html

Lucene5.5入门第三篇——Lucene索引文件结构

Lucenekl 发表了文章 • 0 个评论 • 12684 次浏览 • 2016-06-24 11:12 • 来自相关话题

Lucene的索引结构是有层次结构的,主要分以下几个层次:



索引(Index):
在Lucene中一个索引是放在一个文件夹中的。
如上图,同一文件夹中的所有的文件构成一个Lucene索引。
段(Segment):
一个索引可以包含多个段,段与段之间是独立的,添加新文档可以生成新的段,不同的段可以合并。
如上图,具有相同前缀文件的属同一个段,图中共三个段 "_0" 和 "_1"和“_2”。
segments.gen和segments_X是段的元数据文件,也即它们保存了段的属性信息。
文档(Document):
文档是我们建索引的基本单位,不同的文档是保存在不同的段中的,一个段可以包含多篇文档。
新添加的文档是单独保存在一个新生成的段中,随着段的合并,不同的文档合并到同一个段中。
域(Field):
一篇文档包含不同类型的信息,可以分开索引,比如标题,时间,正文,作者等,都可以保存在不同的域里。
不同域的索引方式可以不同,在真正解析域的存储的时候,我们会详细解读。
词(Term):
词是索引的最小单位,是经过词法分析和语言处理后的字符串。
 更多对应的文件后缀

名称
文件拓展名
描述
段文件
segments_N	保存了索引包含的多少段,每个段包含多少文档。
段元数据
.si	保存了索引段的元数据信息
锁文件 
write.lock	防止多个IndexWriter同时写到一份索引文件中。
复合索引文件
.cfs, .cfe	把所有索引信息都存储到复合索引文件中。
索引段的域信息
.fnm
保存此段包含的域,以及域的名称和域的索引类型。
索引段的文档信息
.fdx, .fdt
保存此段包含的文档,每篇文档中包含的域以及每个域的信息。

索引段Term信息
.tim, .tip
.tim文件中存储着每个域中Term的统计信息且保存着指向.doc, .pos, and .pay 索引文件的指针。

.tip文件保存着Term 字典的索引信息,可支持随机访问。

文档中Term词频和跳表信息
.doc
保存此段中每个文档对应的Term频率信息。
文档中Term的位置信息
.pos
保存此段中每个文档对应的Term位置信息。
文档的有效载荷和部分位置信息
.pay
保存此段中每个文档的有效载体(payload) 和 Term的位置信息(offsets)。 其中有一部分的Term位置信息存储在.pos文件中。
索引字段加权因子
.nvd, .nvm	
.nvm 文件保存索引字段加权因子的元数据

.nvd 文件保存索引字段加权数据

索引文档加权因子
.dvd, .dvm
.dvm 文件保存索引文档加权因子的元数据

.dvd 文件保存索引文档加权数据

索引矢量数据
.tvx, .tvd, .tvf
.tvd 存储此段文档的Term、Term频率、位置信息、有效载荷等信息。

.tvx 索引文件,用于把特定的文档加载到内存。

.tvf 保存索引字段的矢量信息。

有效文档
.liv
保存有效文档的索引文件信息
Lucene的索引结构中,即保存了正向信息,也保存了反向信息。

所谓正向信息:

按层次保存了从索引,一直到词的包含关系:索引(Index) –> 段(segment) –> 文档(Document) –> 域(Field) –> 词(Term)
也即此索引包含了那些段,每个段包含了那些文档,每个文档包含了那些域,每个域包含了那些词。
既然是层次结构,则每个层次都保存了本层次的信息以及下一层次的元信息,也即属性信息,比如一本介绍中国地理的书,应该首先介绍中国地理的概况, 以及中国包含多少个省,每个省介绍本省的基本概况及包含多少个市,每个市介绍本市的基本概况及包含多少个县,每个县具体介绍每个县的具体情况。
如上图,包含正向信息的文件有:
segments_N保存了此索引包含多少个段,每个段包含多少篇文档。
XXX.fnm保存了此段包含了多少个域,每个域的名称及索引方式。
XXX.fdx,XXX.fdt保存了此段包含的所有文档,每篇文档包含了多少域,每个域保存了那些信息。
XXX.tvx,XXX.tvd,XXX.tvf保存了此段包含多少文档,每篇文档包含了多少域,每个域包含了多少词,每个词的字符串,位置等信息。
所谓反向信息:

保存了词典到倒排表的映射:词(Term) –> 文档(Document)
如上图,包含反向信息的文件有:
XXX.tis,XXX.tii保存了词典(Term Dictionary),也即此段包含的所有的词按字典顺序的排序。
XXX.frq保存了倒排表,也即包含每个词的文档ID列表。
XXX.prx保存了倒排表中每个词在包含此词的文档中的位置。
在了解Lucene索引的详细结构之前,先看看Lucene索引中的基本数据类型。


二、基本类型

Lucene索引文件中,用以下基本类型来保存信息:

Byte:是最基本的类型,长8位(bit)。
UInt32:由4个Byte组成。
UInt64:由8个Byte组成。
VInt:
变长的整数类型,它可能包含多个Byte,对于每个Byte的8位,其中后7位表示数值,最高1位表示是否还有另一个Byte,0表示没有,1表示有。
越前面的Byte表示数值的低位,越后面的Byte表示数值的高位。
例如130化为二进制为 1000, 0010,总共需要8位,一个Byte表示不了,因而需要两个Byte来表示,第一个Byte表示后7位,并且在最高位置1来表示后面还有一个Byte, 所以为(1) 0000010,第二个Byte表示第8位,并且最高位置0来表示后面没有其他的Byte了,所以为(0) 0000001。




Chars:是UTF-8编码的一系列Byte。
String:一个字符串首先是一个VInt来表示此字符串包含的字符的个数,接着便是UTF-8编码的字符序列Chars。

三、基本规则

Lucene为了使的信息的存储占用的空间更小,访问速度更快,采取了一些特殊的技巧,然而在看Lucene文件格式的时候,这些技巧却容易使我们感到困惑,所以有必要把这些特殊的技巧规则提取出来介绍一下。

在下不才,胡乱给这些规则起了一些名字,是为了方便后面应用这些规则的时候能够简单,不妥之处请大家谅解。

1. 前缀后缀规则(PREFIX+SUFFIX)

Lucene在反向索引中,要保存词典(Term Dictionary)的信息,所有的词(Term)在词典中是按照字典顺序进行排列的,然而词典中包含了文档中的几乎所有的词,并且有的词还是非常的长 的,这样索引文件会非常的大,所谓前缀后缀规则,即当某个词和前一个词有共同的前缀的时候,后面的词仅仅保存前缀在词中的偏移(offset),以及除前 缀以外的字符串(称为后缀)。

[图]前缀后缀规则



比如要存储如下词:term,termagancy,termagant,terminal,

如果按照正常方式来存储,需要的空间如下:

[VInt = 4] [t][e][r][m],[VInt = 10][t][e][r][m][a][g][a][n][c][y],[VInt = 9][t][e][r][m][a][g][a][n][t],[VInt = 8][t][e][r][m][i][n][a][l]

共需要35个Byte.

如果应用前缀后缀规则,需要的空间如下:

[VInt = 4] [t][e][r][m],[VInt = 4 (offset)][VInt = 6][a][g][a][n][c][y],[VInt = 8 (offset)][VInt = 1][t],[VInt = 4(offset)][VInt = 4][i][n][a][l]

共需要22个Byte。

大大缩小了存储空间,尤其是在按字典顺序排序的情况下,前缀的重合率大大提高。

2. 差值规则(DELTA)

在Lucene的反向索引中,需要保存很多整型数字的信息,比如文档ID号,比如词(Term)在文档中的位置等等。

由上面介绍,我们知道,整型数字是以VInt的格式存储的。随着数值的增大,每个数字占用的Byte的个数也逐渐的增多。所谓差值规则(Delta)就是先后保存两个整数的时候,后面的整数仅仅保存和前面整数的差即可。

[图]差值规则



比如要存储如下整数:16386,16387,16388,16389

如果按照正常方式来存储,需要的空间如下:

[(1) 000, 0010][(1) 000, 0000][(0) 000, 0001],[(1) 000, 0011][(1) 000, 0000][(0) 000, 0001],[(1) 000, 0100][(1) 000, 0000][(0) 000, 0001],[(1) 000, 0101][(1) 000, 0000][(0) 000, 0001]

供需12个Byte。

如果应用差值规则来存储,需要的空间如下:

[(1) 000, 0010][(1) 000, 0000][(0) 000, 0001],[(0) 000, 0001],[(0) 000, 0001],[(0) 000, 0001]

共需6个Byte。

大大缩小了存储空间,而且无论是文档ID,还是词在文档中的位置,都是按从小到大的顺序,逐渐增大的。

3. 或然跟随规则(A, B?)

Lucene的索引结构中存在这样的情况,某个值A后面可能存在某个值B,也可能不存在,需要一个标志来表示后面是否跟随着B。

一般的情况下,在A后面放置一个Byte,为0则后面不存在B,为1则后面存在B,或者0则后面存在B,1则后面不存在B。

但这样要浪费一个Byte的空间,其实一个Bit就可以了。

在Lucene中,采取以下的方式:A的值左移一位,空出最后一位,作为标志位,来表示后面是否跟随B,所以在这种情况下,A/2是真正的A原来的值。





如果去读Apache Lucene - Index File Formats这篇文章,会发现很多符合这种规则的:

.frq文件中的DocDelta[, Freq?],DocSkip,PayloadLength?
.prx文件中的PositionDelta,Payload? (但不完全是,如下表分析)
当然还有一些带?的但不属于此规则的:

.frq文件中的SkipChildLevelPointer?,是多层跳跃表中,指向下一层表的指针,当然如果是最后一层,此值就不存在,也不需要标志。
.tvf文件中的Positions?, Offsets?。
在此类情况下,带?的值是否存在,并不取决于前面的值的最后一位。
而是取决于Lucene的某项配置,当然这些配置也是保存在Lucene索引文件中的。
如Position和Offset是否存储,取决于.fnm文件中对于每个域的配置(TermVector.WITH_POSITIONS和TermVector.WITH_OFFSETS)
为什么会存在以上两种情况,其实是可以理解的:

对于符合或然跟随规则的,是因为对于每一个A,B是否存在都不相同,当这种情况大量存在的时候,从一个Byte到一个Bit如此8倍的空间节约还是很值得的。
对于不符合或然跟随规则的,是因为某个值的是否存在的配置对于整个域(Field)甚至整个索引都是有效的,而非每次的情况都不相同,因而可以统一存放一个标志。
文章中对如下格式的描述令人困惑:
Positions -->  Freq
Payload -->
PositionDelta和Payload是否适用或然跟随规则呢?如何标识PayloadLength是否存在呢?
其实PositionDelta和Payload并不符合或然跟随规则,Payload是否存在,是由.fnm文件中对于每个域的配置中有关Payload的配置决定的(FieldOption.STORES_PAYLOADS) 。
当Payload不存在时,PayloadDelta本身不遵从或然跟随原则。
当Payload存在时,格式应该变成如下:Positions -->  Freq
从而PositionDelta和PayloadLength一起适用或然跟随规则。

4. 跳跃表规则(SKIP LIST)  

为了提高查找的性能,Lucene在很多地方采取的跳跃表的数据结构。

跳跃表(Skip List)是如图的一种数据结构,有以下几个基本特征:

元素是按顺序排列的,在Lucene中,或是按字典顺序排列,或是按从小到大顺序排列。
跳跃是有间隔的(Interval),也即每次跳跃的元素数,间隔是事先配置好的,如图跳跃表的间隔为3。
跳跃表是由层次的(level),每一层的每隔指定间隔的元素构成上一层,如图跳跃表共有2层。




需要注意一点的是,在很多数据结构或算法书中都会有跳跃表的描述,原理都是大致相同的,但是定义稍有差别:

对间隔(Interval)的定义: 如图中,有的认为间隔为2,即两个上层元素之间的元素数,不包括两个上层元素;有的认为是3,即两个上层元素之间的差,包括后面上层元素,不包括前面的上 层元素;有的认为是4,即除两个上层元素之间的元素外,既包括前面,也包括后面的上层元素。Lucene是采取的第二种定义。
对层次(Level)的定义:如图中,有的认为应该包括原链表层,并从1开始计数,则总层次为3,为1,2,3层;有的认为应该包括原链表层,并 从0计数,为0,1,2层;有的认为不应该包括原链表层,且从1开始计数,则为1,2层;有的认为不应该包括链表层,且从0开始计数,则为0,1层。 Lucene采取的是最后一种定义。
跳跃表比顺序查找,大大提高了查找速度,如查找元素72,原来要访问2,3,7,12,23,37,39,44,50,72总共10个元素,应用跳 跃表后,只要首先访问第1层的50,发现72大于50,而第1层无下一个节点,然后访问第2层的94,发现94大于72,然后访问原链表的72,找到元 素,共需要访问3个元素即可。

然而Lucene在具体实现上,与理论又有所不同,在具体的格式中,会详细说明。

原文地址:http://www.kailing.pub/article/index/arcid/73.html

Lucene5.5入门第二篇——Lucene全文检索的基本原理

Lucenekl 发表了文章 • 0 个评论 • 6886 次浏览 • 2016-06-24 11:06 • 来自相关话题

前言

上一篇博文,笔者相当于了解了Lucene是干嘛的,然后写了个hello World增进下对Lucene的感觉。个人觉得,学习一个新的东西时,首先从demo入手,能增加你对这个技术的兴趣,然后慢慢的深入其中的原理,就会有种拨开乌云见明月的感觉。当然,有的人喜欢从原理入手,这个见仁见智。总结来说,不管从哪里入手,对一门新的技术而言总归要知道其所有然

正文


Lucene是一个高效的,基于Java的全文检索库。

所以在了解Lucene之前要费一番工夫了解一下全文检索。

那么什么叫做全文检索呢?这要从我们生活中的数据说起。

我们生活中的数据总体分为两种:结构化数据和非结构化数据。

结构化数据:指具有固定格式或有限长度的数据,如数据库,元数据等。
非结构化数据:指不定长或无固定格式的数据,如邮件,word文档等。
当然有的地方还会提到第三种,半结构化数据,如XML,HTML等,当根据需要可按结构化数据来处理,也可抽取出纯文本按非结构化数据来处理。

非结构化数据又一种叫法叫全文数据。


按照数据的分类,搜索也分为两种:

对结构化数据的搜索:如对数据库的搜索,用SQL语句。再如对元数据的搜索,如利用windows搜索对文件名,类型,修改时间进行搜索等。
对非结构化数据的搜索:如利用windows的搜索也可以搜索文件内容,Linux下的grep命令,再如用Google和百度可以搜索大量内容数据。
对非结构化数据也即对全文数据的搜索主要有两种方法:

一种是顺序扫描法(Serial Scanning):所谓顺序扫描,比如 要找内容包含某一个字符串的文件,就是一个文档一个文档的看,对于每一个文档,从头看到尾,如果此文档包含此字符串,则此文档为我们要找的文件,接着看下 一个文件,直到扫描完所有的文件。如利用windows的搜索也可以搜索文件内容,只是相当的慢。如果你有一个80G硬盘,如果想在上面找到一个内容包含 某字符串的文件,不花他几个小时,怕是做不到。Linux下的grep命令也是这一种方式。大家可能觉得这种方法比较原始,但对于小数据量的文件,这种方 法还是最直接,最方便的。但是对于大量的文件,这种方法就很慢了。

有人可能会说,对非结构化数据顺序扫描很慢,对结构化数据的搜索却相对较快(由于结构化数据有一定的结构可以采取一定的搜索算法加快速度),那么把我们的非结构化数据想办法弄得有一定结构不就行了吗?

这种想法很天然,却构成了全文检索的基本思路,也即将非结构化数据中的一部分信息提取出来,重新组织,使其变得有一定结构,然后对此有一定结构的数据进行搜索,从而达到搜索相对较快的目的。

这部分从非结构化数据中提取出的然后重新组织的信息,我们称之索引。

这种说法比较抽象,举几个例子就很容易明白,比如字典,字典的拼音表和部首检字表就相当于字典的索引,对每一个字的解释是非结构化的,如果字典没有 音节表和部首检字表,在茫茫辞海中找一个字只能顺序扫描。然而字的某些信息可以提取出来进行结构化处理,比如读音,就比较结构化,分声母和韵母,分别只有 几种可以一一列举,于是将读音拿出来按一定的顺序排列,每一项读音都指向此字的详细解释的页数。我们搜索时按结构化的拼音搜到读音,然后按其指向的页数, 便可找到我们的非结构化数据——也即对字的解释。


这种先建立索引,再对索引进行搜索的过程就叫全文检索(Full-text Search)。
全文检索大体分两个过程,索引创建(Indexing)和搜索索引(Search)。

索引创建:将现实世界中所有的结构化和非结构化数据提取信息,创建索引的过程。
搜索索引:就是得到用户的查询请求,搜索创建的索引,然后返回结果的过程。
于是全文检索就存在三个重要问题:

1. 索引里面究竟存些什么?(Index)

2. 如何创建索引?(Indexing)

3. 如何对索引进行搜索?(Search)

下面我们顺序对每个个问题进行研究。


二、索引里面究竟存些什么

索引里面究竟需要存些什么呢?

首先我们来看为什么顺序扫描的速度慢:

其实是由于我们想要搜索的信息和非结构化数据中所存储的信息不一致造成的。

非结构化数据中所存储的信息是每个文件包含哪些字符串,也即已知文件,欲求字符串相对容易,也即是从文件到字符串的映射。而我们想搜索的信息是哪些 文件包含此字符串,也即已知字符串,欲求文件,也即从字符串到文件的映射。两者恰恰相反。于是如果索引总能够保存从字符串到文件的映射,则会大大提高搜索 速度。

由于从字符串到文件的映射是文件到字符串映射的反向过程,于是保存这种信息的索引称为反向索引。

反向索引的所保存的信息一般如下:

假设我的文档集合里面有100篇文档,为了方便表示,我们为文档编号从1到100,得到下面的结构

Lucene全文检索的基本原理

左边保存的是一系列字符串,称为词典。

每个字符串都指向包含此字符串的文档(Document)链表,此文档链表称为倒排表(Posting List)。

有了索引,便使保存的信息和要搜索的信息一致,可以大大加快搜索的速度。

比如说,我们要寻找既包含字符串“lucene”又包含字符串“solr”的文档,我们只需要以下几步:

1. 取出包含字符串“lucene”的文档链表。

2. 取出包含字符串“solr”的文档链表。

3. 通过合并链表,找出既包含“lucene”又包含“solr”的文件。

Lucene全文检索的基本原理

看到这个地方,有人可能会说,全文检索的确加快了搜索的速度,但是多了索引的过程,两者加起来不一定比顺序扫描快多少。的确,加上索引的过程,全文检索不一定比顺序扫描快,尤其是在数据量小的时候更是如此。而对一个很大量的数据创建索引也是一个很慢的过程。

然而两者还是有区别的,顺序扫描是每次都要扫描,而创建索引的过程仅仅需要一次,以后便是一劳永逸的了,每次搜索,创建索引的过程不必经过,仅仅搜索创建好的索引就可以了。

这也是全文搜索相对于顺序扫描的优势之一:一次索引,多次使用。


三、如何创建索引

全文检索的索引创建过程一般有以下几步:

第一步:一些要索引的原文档(DOCUMENT)。

为了方便说明索引创建过程,这里特意用两个文件为例:

文件一:Students should be allowed to go out with their friends, but not allowed to drink beer.

文件二:My friend Jerry went to school to see his students but found them drunk which is not allowed.


第二步:将原文档传给分次组件(TOKENIZER)。

分词组件(Tokenizer)会做以下几件事情(此过程称为Tokenize):

1. 将文档分成一个一个单独的单词。

2. 去除标点符号。

3. 去除停词(Stop word)。

所谓停词(Stop word)就是一种语言中最普通的一些单词,由于没有特别的意义,因而大多数情况下不能成为搜索的关键词,因而创建索引时,这种词会被去掉而减少索引的大小。

英语中挺词(Stop word)如:“the”,“a”,“this”等。

对于每一种语言的分词组件(Tokenizer),都有一个停词(stop word)集合。


经过分词(Tokenizer)后得到的结果称为词元(Token)。

在我们的例子中,便得到以下词元(Token):

“Students”,“allowed”,“go”,“their”,“friends”,“allowed”,“drink”,

“beer”,“My”,“friend”,“Jerry”,“went”,“school”,“see”,“his”,“

students”,“found”,“them”,“drunk”,“allowed”。

第三步:将得到的词元(TOKEN)传给语言处理组件(LINGUISTIC PROCESSOR)。

语言处理组件(linguistic processor)主要是对得到的词元(Token)做一些同语言相关的处理。

对于英语,语言处理组件(Linguistic Processor)一般做以下几点:

1. 变为小写(Lowercase)。

2. 将单词缩减为词根形式,如“cars”到“car”等。这种操作称为:stemming。

3. 将单词转变为词根形式,如“drove”到“drive”等。这种操作称为:lemmatization。


Stemming 和 lemmatization的异同:

相同之处:Stemming和lemmatization都要使词汇成为词根形式。
两者的方式不同:
Stemming采用的是“缩减”的方式:“cars”到“car”,“driving”到“drive”。
Lemmatization采用的是“转变”的方式:“drove”到“drove”,“driving”到“drive”。
两者的算法不同:
Stemming主要是采取某种固定的算法来做这种缩减,如去除“s”,去除“ing”加“e”,将“ational”变为“ate”,将“tional”变为“tion”。
Lemmatization主要是采用保存某种字典的方式做这种转变。比如字典中有“driving”到“drive”,“drove”到“drive”,“am, is, are”到“be”的映射,做转变时,只要查字典就可以了。
Stemming和lemmatization不是互斥关系,是有交集的,有的词利用这两种方式都能达到相同的转换。

语言处理组件(linguistic processor)的结果称为词(Term)。

在我们的例子中,经过语言处理,得到的词(Term)如下:

“student”,“allow”,“go”,“their”,“friend”,“allow”,“drink”,

“beer”,“my”,“friend”,“jerry”,“go”,“school”,“see”,“his”,

“student”,“find”,“them”,“drink”,“allow”。
也正是因为有语言处理的步骤,才能使搜索drove,而drive也能被搜索出来。


第四步:将得到的词(TERM)传给索引组件(INDEXER)。

原文地址:http://www.kailing.pub/article/index/arcid/72.html
再次吐槽这个编辑器,想要发个图片并茂的不容易
Lucene是apache软件基金会4 jakarta项目组的一个子项目,是一个开放源代码的全文检索引擎工具包,但它不是一个完整的全文检索引擎,而是一个全文检索引擎的架构,提供了完整的查询引擎和索引引擎,部分文本分析引擎(英文与德文两种西方语言)。Lucene的目的是为软件开发人员提供一个简单易用的工具包,以方便的在目标系统中实现全文检索的功能,或者是以此为基础建立起完整的全文检索引擎。Lucene是一套用于全文检索和搜寻的开源程式库,由Apache软件基金会支持和提供。Lucene提供了一个简单却强大的应用程式接口,能够做全文索引和搜寻。在Java开发环境里Lucene是一个成熟的免费开源工具。就其本身而言,Lucene是当前以及最近几年最受欢迎的免费Java信息检索程序库。人们经常提到信息检索程序库,虽然与搜索引擎有关,但不应该将信息检索程序库与搜索引擎相混淆。