Lucene索引讲解

1、IndexWriter详解

问题1:索引创建过程完成什么事?

分词、存储到反向索引中。

Lucene索引创建API图示
Lucene索引创建
Lucene索引创建代码示例
public static void main(String[] args) throws IOException {
  // 创建使用的分词器
  Analyzer analyzer = new IKAnalyzer4Lucene7(true);
  // 索引配置对象
  IndexWriterConfig config = new IndexWriterConfig(analyzer);
  // 设置索引库的打开模式:新建、追加、新建或追加
  config.setOpenMode(OpenMode.CREATE_OR_APPEND);

  // 索引存放目录
  // 存放到文件系统中
  Directory directory = FSDirectory.open((new File("f:/test/indextest")).toPath());

  // 存放到内存中
  // Directory directory = new RAMDirectory();

  // 创建索引写对象
  IndexWriter writer = new IndexWriter(directory, config);

  // 创建document
  Document doc = new Document();
  // 往document中添加 商品id字段
  doc.add(new StoredField("prodId", "p0001"));

  // 往document中添加 商品名称字段
  String name = "ThinkPad X1 Carbon 20KH0009CD/25CD 超极本轻薄笔记本电脑联想";
  doc.add(new TextField("name", name, Store.YES));

  // 将文档添加到索引
  writer.addDocument(doc);

  //继续添加文档...

  //刷新
  writer.flush();

  //提交  
  writer.commit();

  //关闭 会提交
  writer.close();
  directory.close();
}
IndexWrite涉及类图
IndexWrite涉及类图
IndexWriterConfig 写索引配置:
  • 使用的分词器。
  • 如何打开索引(是新建,还是追加)。
  • 还可配置缓冲区大小、或缓存多少个文档,再刷新到存储中。
  • 还可配置合并、删除等的策略。

注意:用这个配置对象创建好 IndexWriter 对象后,在修改这个配置的对象信息不会对 IndexWriter对象起作用。
如果要在 IndexWriter 使用过程中修改它的配置信息,通过 IndxWriter 的getConfig() 方法获得 LiveIndexWriterConfig 对象,在这个对象中可查看该 IndexWriter 使用的配置信息,可进行少量的配置修改(看它的setter方法)

Directory 指定索引数据存放的位置:
  • 内存
  • 文件系统
  • 数据库(需要编写代码指定)
保存文件系统的用法
Directory directory = FSDirectory.open(Path path);//path指定目录
Directory关系图
IndexWriter 用来创建、维护一个索引。它的API使用流程
// 创建索引写对象
IndexWriter writer = new IndexWriter(directory, config);

// 创建document

// 将文档添加到索引
writer.addDocument(doc);

// 删除文档
//writer.deleteDocuments(terms);

//修改文档
//writer.updateDocument(term, doc);

// 刷新
writer.flush();

// 提交
writer.commit();

//回滚
//writer.rollback();

//关闭 会提交
writer.close();

注意:IndexWriter是线程安全的。如果你的业务代码中有其他的总部控制,请不要使用IndexWriter作为锁对象,以免死锁。

IndexWriter中还有一些 add方法 update方法 delete方法(可在源码中查看)

IndexWriter 涉及类图示
IndexWriter 涉及类图示
问题2:索引库会存储反向索引数据,会存储document吗?

会存储在索引库中一些需要展示或使用的信息

问题3: document会以什么结构存储?

是以正向索引来存储的

正向索引与反向索引

2、Document详解

要索引的数据记录、文档在lucene中的表示,是索引、搜索的基本单元。一个Document由多个字段Field构成。就像数据库的记录-字段。

IndexWriter按加入的顺讯为Document指定一个递增的id(从0开始),称为文档id。反向索引中存储的是这个id,文档存储中正向索引也是这个id。业务数据的主键id只是文档的一个字段。

Document API图示
Document类关系

Document方法
Field

字段:由字段名name、字段值value(fieldsData)、字段类型 type(IndexableField) 三部分构成。

字段值可以是文本(String、Reader 或 预分析的 Tokenstream)、二进制(byte)或数组

IndexableField Field API图示
IndexableField Field关系图
IndexableField API
Field API
Duocumnt - Field 数据举例
  • 新闻:新闻id,新闻标题、新闻内容、作者、所属分类、发表时间
  • 网页搜索的网页:标题、内容、链接地址
  • 商品: id、名称、图片链接、类别、价格、库存、商家、品牌、月销量、详情…

在这里我们收集数据创建document对象来为其创建索引,数据的所有属性不需要都加入到document中。
需要被用做搜索条件的和要在搜索结果列表中使用的字段才需要加入到document中。

在这里我们查询条件中使用的字段才该被索引,只有在搜索结果列表中要使用的字段才应该被存储。
网页的标题、内容会被作为搜索条件,需要索引。网页的标题、链接需要显示或使用则需要存储。全部的内容做索引,但是不需要全部内容做展示所以只存储部分内容即可。

在这里我们需要模糊查询的进行分词精确查询或范围查询的则不需要分词

IndexableFieldType
  • 字段类型:描述该如何索引存储该字段

字段可选择性地保存在索引中,这样在搜索结果中,这些字段值就可以获得。

一个Document应该包含一个或多个存储字段来查询(唯一标识)数据库(文档)中对应的一条数据。

注意:未存储的字段,从索引中取得的document中是没有这些字段的。

IndexableFieldType关系图

标准化说明:比如一个单词为复数的形式:Bikes。标准化过后则为:bike。(中文基本不需要标准化)

IndexOptions 索引说明
  • NONE
    • Not indexed 不索引
  • DOCS
    • 反向索引中只存储了包含该词的 文档id,没有词频、位置
  • DOCS_AND_FREQS
    • 反向索引中会存储 文档id、词频
  • DOC_AND_FREQS_AND_POSITIONS
    • 反向索引中存储 文档id、词频、位置
  • DOC_AND_FREQS_AND_POSITIONS_AND_OFFSETS
    • 反向索引中存储 文档id、词频、位置、偏移量

github地址

注意:

  1. 如果要在搜索结果中做关键字高亮,那么就需要词频
  2. 如果要实现短语查询、临近查询(跨度查询),那么就需要出现的位置和偏移量
  3. 但是在反向索引中 位置、偏移数据 存储量非常大
  4. 为了提升反向索引的效率短语查询、临近查询这样的字段的位置、偏移数据应该保存反向索引中的。这也你前面看到 IndexOptions为什么有那些选项的原因。
反向索引存储的数据

在lucene4.0以前,反向索引中总会存储这些数据,4.0后改进为可选择的。

一个字段分词器分词后,每个词项会得到一系列属性信息,如 出现频率、位置、偏移量等,这些信息构成一个词项向量 termVectors。

storeTermVectors

对于不需要在搜索反向索引时用到,但在搜索结果处理时需要位置、偏移量、附加数据(payLoad)的字段,我们可以单独为该字段存储(文档id→词项向量)的正向索引。

storeTermVectors
  • boolean storeTermVectors() 是否存储词项向量
  • boolean storeTermVectorPositions() 是否在词项向量中存储位置
  • boolean storeTermVectorOffsets() 是否在词项向量中存储偏移量
  • boolean storeTermVectorPayloads() 是否在词项向量中存储附加信息

FieldType 实现类中有对应的set方法
概念说明:Token trem 词条:三个词都是同一个意思: 分词得到的词项

代码:IndexTermVectorsDemo

什么是附加信息Payloads
附加信息

比如:a 词语 出现在第二个文档(D2) 出现了一次 出现的位置为2
is 词语 出现在第零个文档(D0)出现了二次 位置为1和4,出现在第一个文档(D1)出现了1次 位置为1 并且有下划线,出现在第二个文档(D3)出现了1次 位置为1。

练习1
商品id:字符串,不索引、但存储
String prodId = "p0001";
商品名称:字符串,分词索引(存储词频、位置、偏移量)、存储
String name = "ThinkPad X1 Carbon 20KH0009CD/25CD 超极本轻薄笔记本电脑";
图片链接:仅存储
String imgUrl = "http://www.dongnao.com/aaa";
商品简介:字符串,分词索引(不需要支持短语、临近查询)、存储,结果中
支持高亮显示
String simpleIntro = "集成显卡 英特尔 酷睿 i5-8250U 14英寸";
品牌:字符串,不分词索引,存储
String brand = "ThinkPad";
docValuesType

IndexableFieldType 中的 docValuesType 方法 就是让你来为需要排序、分组、聚合的字段指定如何为该字段创建文档→字段值得正向索引的。(以空间换时间)

IndexableFieldType API docValuseType图示
docValuseType
DocValuesType 选项说明
  • NONE 不开启docvalue
  • NUMERIC 单值、数值字段,使用。
  • BINARY 单值、字节数组,使用。
  • SORTED 单值、字符,使用。会预先对值字节进行排序、去重排序。
  • SORTED_NUMERIC 单值、数值数组,使用。会预先对数值数组进行排序
  • SORTED_SET 多字段使用。会预先对值字节进行排序、去重存储。

DocValuesType 是强调类型要求的:字段的值必须保证同类型。

具体使用选择:

  • 字符串+单值 会选择 SORTED 作为docvalue存储
  • 字符串+多值 会选择 SORTED_SET 作为docvalue存储
  • 数值或日期或枚举字段+单值 会选择 NUMERIC 作为docvalue存储
  • 数值或日期或枚举字段+多值 会选择 SORTED_SET 作为docvalue存储

强调:需要排序、分组、聚合、分类查询(面查询)的字段才创建docValues

练习2

1、修改品牌字段:支持统计查询
2、增加商品类别字段:字符串(类别名),索引不分词,不存储、支持分
类统计,多值(一个商品可能属于多个类别)。
type = {“电脑”,”笔记本电脑”}
3、增加价格字段:整数,单位分,不索引、存储,需要支持排序

多值字段只需要同字段多次加入即可

练习一、二github代码

如何加入数值字段(int)

Field的构造方法和set方法

Field构造方法和set方法

在Field类中没有对应的构造方法设置数值字段,但是在 set 方法中有设置数值字段的功能。

//设置数值的源码
/**
   * Expert: change the value of this field. See 
   * {@link #setStringValue(String)}.
   */
  public void setIntValue(int value) {
    if (!(fieldsData instanceof Integer)) {
      throw new IllegalArgumentException("cannot change value type from " + fieldsData.getClass().getSimpleName() + " to Integer");
    }
    fieldsData = Integer.valueOf(value);
  }

在上面的源码中可以看出在设置数值类型值的时候会先去判断 fieldsData 是否为Integer类型。fieldsData 是通过构造方法设置的,而构造方法并没有设置数值属性的值。(所以需要自己实现)

  • 因为 IndexableField 的实现类为 Field。所以扩展数值字段的Field,首先查看 IndexableField 的 API
IndexableField API

再查看Field类中对应的实现,发现与 Field 类中 set 方法的实现逻辑是一致的,都需要先判断值类型。

  @Override
  public Number numericValue() {
    if (fieldsData instanceof Number) {
      return (Number) fieldsData;
    } else {
      return null;
    }
  }
  @Override
  public BytesRef binaryValue() {
    if (fieldsData instanceof BytesRef) {
      return (BytesRef) fieldsData;
    } else {
      return null;
    }
  }
  @Override
  public String stringValue() {
    if (fieldsData instanceof String || fieldsData instanceof Number) {
      return fieldsData.toString();
    } else {
      return null;
    }
  }
  @Override
  public Reader readerValue() {
    return fieldsData instanceof Reader ? (Reader) fieldsData : null;
  }
  @Override
  public TokenStream tokenStream(Analyzer analyzer, TokenStream reuse) {
    if (fieldType().indexOptions() == IndexOptions.NONE) {
      // Not indexed
      return null;
    }

    if (!fieldType().tokenized()) {
      if (stringValue() != null) {
        if (!(reuse instanceof StringTokenStream)) {
          // lazy init the TokenStream as it is heavy to instantiate
          // (attributes,...) if not needed
          reuse = new StringTokenStream();
        }
        ((StringTokenStream) reuse).setValue(stringValue());
        return reuse;
      } else if (binaryValue() != null) {
        if (!(reuse instanceof BinaryTokenStream)) {
          // lazy init the TokenStream as it is heavy to instantiate
          // (attributes,...) if not needed
          reuse = new BinaryTokenStream();
        }
        ((BinaryTokenStream) reuse).setValue(binaryValue());
        return reuse;
      } else {
        throw new IllegalArgumentException("Non-Tokenized Fields must have a String value");
      }
    }

    if (tokenStream != null) {
      return tokenStream;
    } else if (readerValue() != null) {
      return analyzer.tokenStream(name(), readerValue());
    } else if (stringValue() != null) {
      return analyzer.tokenStream(name(), stringValue());
    }

    throw new IllegalArgumentException("Field must have either TokenStream, String, Reader or Number value; got " + this);
  }

我们可以发现上面源码如果类型不匹配我们返回的都是null,而且五个方法对应了五种不同的值类型,文本(String、Reader 或 预分析的 TokenStream)、二进值(byte[])或数组。
而它返回null则表示它不是该类型的值。

在IndexWriter(创建索引)中这五个方法是按照怎样的顺序去执行的?

org.apache.lucene.index.DefaultIndexingChain lucene源码得出

问1:反向索引时是如何使用这五个方法的?

  • 不分词:stringValue() [Sring Number],binaryValue();
  • 分词:tokenStream、readerValue、stringValue,异常

问2:存储时是如何使用这五个方法的?
numericValue、binaryValue、stringValue

问3:docValues时是如何使用这五个方法的?
docValues:number、sort_number、numeric
其他:binry

  • 上面的答案顺序就是代码中会执行的先后顺序

因为如果 docValues 中没有指定为 number 类型,那么也可以通过binry字节类型来加载。所以我们即使不是binary类型值,也要重写binayValue()获取。

加入数值字段方式:

  • 扩展Field,提供构造方法传入数值类型值,赋给字段值字段;
  • 改写binaryValue() 方法,返回数值的字节引用。

代码示例

在IndexableFieldType中最后定义的pointDimensionCount与pointNumBytes的作用是

Lucene6以后引入了点的概念来表示数值字段,废除了原来的IntField等。在Point
字段类中提供了精确、范围查询的便捷方法。

注意:只是引入点的概念,并未改变数值字段的本质。
既然是点,就有空间概念:维度。一维:一个值,二维:两个值的;……

pointDimensionCount() 返回点的维数
pointNumBytes() 返回点中数值类型的字节数。

一般适用于经纬度定位,地图等。

Lucene预定义的字段子类,你可灵活选用
  • TextField: Reader or String indexed for full-text search
  • StringField: String indexed verbatim as a single token
  • IntPoint: int indexed for exact/range queries.
  • LongPoint: long indexed for exact/range queries.
  • FloatPoint: float indexed for exact/range queries.
  • DoublePoint: double indexed for exact/range queries.
  • SortedDocValuesField: byte[] indexed column-wise for sorting/faceting
  • SortedSetDocValuesField: SortedSet<byte[]> indexed column-wise for
    sorting/faceting
  • NumericDocValuesField: long indexed column-wise for sorting/faceting
  • SortedNumericDocValuesField: SortedSet<long> indexed column-wise for
    sorting/faceting
  • StoredField: Stored-only value for retrieving in summary results
  1. 请仔细看它们的源码是怎么设置字段的值、类别的。

注意:这里没有设置存储、词项向量的。

  1. 如果单个子类不满足需要,可多个组合。请参考 示例代码:IndexWriteDemo

  2. 如果组合不了,就直接用Field + FieldType

Field中提供那么多的setXXValue()方法,是什么意图?

setXXXValue()方法主要是为了重用Filed对象的,因为我们去给文档创建索引的时候是去循环创建很多的文档那么里面的字段是可以重复利用的。使用setXXXValue()就只需要改变里面的字段即可。

加入索引时,每个数据记录需要都创建一个Document吗?

Document只是一个容器里面放的是要索引或者存储的字段,那么创建之后就是可以重复利用的。所以只需要一个字段就可以了。

IndexWriter 索引更新API

IndexWriter 索引更新API

说明:

  1. Term 词项 指定字段的词项
  2. 删除流程:根据Term、Query找到相关的文档id、同时删除索引信息,再根据文档id删除对应的文档存储。
  3. 更新流程:先删除、再加入新的doc。
  4. 注意:只可根据索引的字段进行更新。

示例代码:IndexUpdateDemo

总结

总结
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,602评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,442评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,878评论 0 344
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,306评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,330评论 5 373
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,071评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,382评论 3 400
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,006评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,512评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,965评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,094评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,732评论 4 323
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,283评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,286评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,512评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,536评论 2 354
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,828评论 2 345

推荐阅读更多精彩内容