0.学习目标

  • 了解倒排索引原理
  • 了解Lucene的作用
  • 了解Lucene创建索引基本API
  • 了解Lucene查询基本API
  1. 了解搜索技术

1.1 搜索引擎

什么是搜索引擎?

搜索引擎的原理

可以看到搜索引擎的功能主要是三部分:

  • 爬行和抓取数据(爬虫多用Python来编写)
  • 对数据做预处理(提取文字,中文分词、建立倒排索引)
  • 提供搜索功能(用户输入关键词后,去索引库搜索数据)

在上述三个步骤中,java要解决的往往是后两个步骤:数据处理和搜索。

那么,我们之前学习的mysql知识也能实现数据的存储和搜索,为什么还要学新的东西呢?

1.2 数据库搜索的问题

要实现类似百度的复杂搜索,或者京东的商品搜索,如果使用传统的数据库存储数据,那么会存在一系列的问题:

  • 数据库数据单表存储能力有限,无法存储海量数据
    • 解决大数据,可以进行分库分表。但是分库分表会增加业务复杂度
  • 搜索只能通过模糊匹配,效率极低
    • 模糊搜索可能导致全表扫描,效率非常差

在这里,比较棘手的其实是第二个问题:查询效率低,类似百度和京东这样的网站,对性能要求极高。如果用户点击搜索需要很久才能拿到数据,没人愿意一直等待下去。

那么问题来了:如何才能提高模糊搜索时的效率呢?

答案是:倒排索引技术

1.3 什么是倒排索引

倒排索引是一种存储数据的方式,与传统查找有很大区别:

- 分词:最近 天气 不,气不,好,不好,预计,又要,天天,下,天下,下雨,雨
- 文档:相当于一条数据库的一行记录,任何数据文本都可以叫文档
  • 传统查找:采用数据按行存储,查找时逐行扫描,或者根据索引查找,然后匹配搜索条件,效率较差。概括来讲是先找到文档,然后看是否匹配。
  • 倒排索引:首先对文档数据按照id进行索引存储,然后对文档中的数据分词,记录对词条进行索引,并记录词条在文档中出现的位置。这样查找时只要找到了词条,就找到了对应的文档。概括来讲是先找到词条,然后看看哪些文档包含这些词条。

例如数据库中有这样一批数据:

id title url
1 谷歌地图之父跳槽FaceBook http://www.jd.com/434
2 谷歌地图之父加盟FaceBook
3 谷歌地图创始人拉斯离开谷歌加盟Facebook
4 谷歌地图之父跳槽Facebook与Wave项目取消有关
5 谷歌地图之父拉斯加盟社交网站Facebook

1.3.1 创建倒排索引流程

当我们需要把这些数据创建倒排索引时,会分为两步:

1)创建文档列表(Document)

首先给每一条原始的文档数据创建文档编号(docID),创建索引,形成文档列表:

2)创建倒排索引列表

然后对文档中的数据进行分词,得到词条。对词条进行编号,并以词条创建索引。然后记录下包含该词条的所有文档编号(及其它信息)。

1.3.2 搜索流程

搜索的基本流程:

  • 当用户输入任意的内容时,首先对用户输入的内容进行分词,得到用户要搜索的所有词条
  • 然后拿着这些词条去倒排索引列表中进行匹配。找到这些词条就能找到包含这些词条的所有文档的编号。
  • 然后根据这些编号去文档列表中找到文档

举例:

例如用户要搜索关键词:拉斯跳槽

  • 首先对这句话进行分词,得到2个词条:拉斯、跳槽
  • 然后去倒排索引列表搜索(有索引,速度快),得到词条所在的文档编号:0、2、3、4
  • 然后根据编号到文档列表查找(有索引,速度快),即可得到原始文档信息了。
  1. Lucene概述

2.1 什么是Lucene

在java语言中,对倒排索引的实现中最广为人知的就是Lucene了,目前主流的java搜索框架都是依赖Lucene来实现的。

其LOGO:

  • Lucene是一套用于全文检索和搜寻的开源程序库,由Apache软件基金会支持和提供
  • Lucene提供了一个简单却强大的应用程序接口(API),能够做全文索引和搜寻,在Java开发环境里Lucene是一个成熟的免费 开放源代码 工具
  • Lucene并不是现成的搜索引擎产品,但可以用来制作搜索引擎产品
  • 官网:http://lucene.apache.org/

什么是全文检索?

这里有一个陌生的词语:全文检索。

其实全文检索就是利用倒排索引技术对需要搜索的数据进行处理,然后提供快速的全文匹配的技术。

2.2 Lucene版本

  • 目前最新的版本是7.x系列,但是大多数企业中依旧使用4.x版本,比较稳定。本次课程我们使用4.10.2版本,老版本下载地址:http://archive.apache.org/dist/lucene/java/
  1. Lucene的基本使用

下面我们来看下Lucene对于索引的增(创建索引)、删(删除索引)、改(修改索引)、查(搜索数据)。

3.1 创建索引

3.1.1 基本流程:

流程:

  • 准备要添加的文档数据:Document
  • 初始化索引写出工具:IndexWriter
    • 设定索引存储目录Directory
    • 设定其他配置:IndexWriterConfig
      • 设定分词器:Analyzer
      • 设定Lucene版本
  • 写出索引

3.1.2 添加依赖

<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
	<modelVersion>4.0.0</modelVersion>
	<groupId>cn.itcast.demo</groupId>
	<artifactId>lucene-demo</artifactId>
	<version>1.0.0-SNAPSHOT</version>
	<dependencies>
		<!-- Junit单元测试 -->
		<dependency>
			<groupId>junit</groupId>
			<artifactId>junit</artifactId>
			<version>4.12</version>
		</dependency>
		<!-- lucene核心库 -->
		<dependency>
			<groupId>org.apache.lucene</groupId>
			<artifactId>lucene-core</artifactId>
			<version>4.10.2</version>
		</dependency>
		<!-- Lucene的查询解析器 -->
		<dependency>
			<groupId>org.apache.lucene</groupId>
			<artifactId>lucene-queryparser</artifactId>
			<version>4.10.2</version>
		</dependency>
		<!-- lucene的默认分词器库 -->
		<dependency>
			<groupId>org.apache.lucene</groupId>
			<artifactId>lucene-analyzers-common</artifactId>
			<version>4.10.2</version>
		</dependency>
		<!-- lucene的高亮显示 -->
		<dependency>
			<groupId>org.apache.lucene</groupId>
			<artifactId>lucene-highlighter</artifactId>
			<version>4.10.2</version>
		</dependency>
	</dependencies>
    <build>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <configuration>
                    <source>1.8</source>
                    <target>1.8</target>
                    <encoding>UTF-8</encoding>
                </configuration>
            </plugin>
        </plugins>
    </build>
</project>

3.1.3 代码实现

// 创建索引
@Test
public void testCreate() throws Exception{
    // 创建文档对象
    Document document = new Document();
    document.add(new StringField("", "", Store.NO));
    // 创建并添加字段信息。参数:字段的名称、字段的值、是否存储,这里选Store.YES代表存储到文档列表。Store.NO代表不存储
    document.add(new StringField("id", "1", Store.YES));
    // 这里我们title字段需要用TextField,即创建索引又会被分词。StringField会创建索引,但是不会被分词
    document.add(new TextField("title", "谷歌地图之父跳槽facebook", Field.Store.YES));

    // 索引目录类,指定索引在硬盘中的位置
    Directory directory = FSDirectory.open(new File("C:\\lesson\\indexDir"));
    // 创建分词器对象
    Analyzer analyzer = new StandardAnalyzer();
    // 索引写出工具的配置对象
    IndexWriterConfig conf = new IndexWriterConfig(Version.LATEST, analyzer);
    // 创建索引的写出工具类。参数:索引的目录和配置信息
    IndexWriter indexWriter = new IndexWriter(directory, conf);

    // 把文档交给IndexWriter
    indexWriter.addDocument(document);
    // 提交
    indexWriter.commit();
    // 关闭
    indexWriter.close();
}

查看目标目录:

3.1.4. 索引查看工具

在课前资料中有工具,帮我们查看生成的索引:

双击run.bat即可运行,需要填写索引所在目录

首页:

可以看到分词的方式不太正确,一个字作为一个词,这是分词方式的问题,我们后续会解决。

文档信息:

3.2 创建索引时的细节

创建索引的API有一些细节需要我们注意。

3.2.1 覆盖或追加

我们在写索引时,可以在IndexConfigWriter中配置写入模式:覆盖或者追加:

可以有3种模式:

  • CREATE:每次写入都覆盖以前的数据
  • APPEND:不覆盖数据,而是使用以前的索引数据后追加
  • CREATE_OR_APPEND:如果不存在则创建新的,如果存在则追加数据

3.2.2 Field字段类型

刚才创建Document的时候,我们添加了两个字段,StringField和TextField,其实Field还有很多其它实现类:

他们有一些不同的特性:

  • DoubleField、FloatField、IntField、LongField、StringField、TextField这些子类创建的字段一定会被创建索引。但是不一定会被存储到文档列表。要通过构造函数中的参数Store来指定:

    • Store.YES代表存储,在搜索结果中也会展示出来
    • Store.NO代表不存储,在搜索结果中无法展示
      这些字段虽然会创建索引,但是不一定会分词。不分词的字段,会作为一个整体词条存入索引,其中:
  • TextFiel既即创建索引,又会被分词。其它Field会创建索引,但是不会被分词。
    如果不分词,会造成整个字段作为一个词条,除非用户完全匹配,否则搜索不到:

    我们一般,需要搜索的字段,都会做分词:

  • 上述所有字段都会创建索引,有一个例外:StoreField一定会被存储,但是一定不创建索引
    StoredField可以创建各种数据类型的字段:

    一般,一些不需要进行搜索的字段我们无需创建索引,就可以使用StoreField类型

到底该使用哪个字段?我们需要思考下面的问题:

  • 问题1:这个字段是否需要创建索引?
    • 如果需要根据这个字段搜索,则这个字段需要创建索引。
      • 无需创建索引:使用StoreField类型
      • 需要创建索引:使用除了StoreField外的其它类型
  • 问题2:这个字段是否需要存储?
    • 如果一个字段要显示到最终的结果中,那么一定要存储,否则就不存储
      • 存储,则使用StoreField或者其它类型字段,但是构造函数第三个参数必须是Store.YES
      • 不存储,必须使用StoreField以外的字段,并且构造函数第三个参数必须是Store.NO
  • 问题3:这个字段是否需要分词?
    这个字段首先要需要被搜索,因此剔除了StoreField。然后如果这个字段的值是不可分割的,那么就不需要分词,例如:ID;否则就需要分词
    • 需要分词:使用TextField
    • 不需要分词:使用除TextField外的其它几个类型

其实,这里最关键的是弄清楚一个字段:是否需要存储、是否需要索引、是否需要分词。弄清楚这个,就能知道怎么选择API了。

3.2.3 分词器

刚才的案例中,我们使用了StandardAnalyzer分词器,不过这个分词器对中文的解析能力很差。我们需要使用中文分词器。

一些比较流行的中文分词器:

这里我们使用IK分词器。

3.2.3.1 IK分词器

IK分词器官方版本是不支持Lucene4.X的,有人基于IK的源码做了改造,支持了Lucene4.X,我们可以通过maven引入其依赖:

<dependency>
  <groupId>com.janeluo</groupId>
  <artifactId>ikanalyzer</artifactId>
  <version>2012_u6</version>
</dependency>

然后修改代码中的分词器类型:

再次测试:

3.2.3.2 停用词典和扩展词典

IK分词器的词库有限,如果是词库中没有出现的词条,不会被正确分词,例如这样一句话:

谷歌地图之父跳槽facebook, 加入了传智黑马,屌爆了啊

分词结果:

如图:红色的词条是没有正确分词的;蓝色的词条是没有意义的词语。

我们期待:传智、屌爆了能作为一个完整词条;并且一些无关词语如:了、啊、额、入了可以不被分词。

新增加的词条可以通过配置文件添加到IK的词库中,也可以把一些不用的词条去除。

  • 首先,我们需要在配置文件目录新建一个文件,编写扩展词条和停用词条:

  • 然后,在classpath下创建一个配置文件,名为:IKAnalyzer.cfg.xml,把刚刚填写的词典配置进去:
    <?xml version="1.0" encoding="UTF-8"?>


    IK Analyzer 扩展配置

    ext.dic;

    stopword.dic;

结构:

再次测试后,查看结果:

3.2.4.批量创建索引

我们刚才使用IndexWriter中的addDocument方法来添加文档,然后写出到索引库,是一次添加一个文档。事实上这里也支持批量添加:

可以看到这个API接收的是一个Interable类型,即迭代器类型,完全可以接收一个集合:

 // 批量创建索引
    @Test
    public void testBatchCreateIndex() throws Exception{
        // 创建文档的集合
        Collection<Document> docs = new ArrayList<>();
        // 创建文档对象
        Document document1 = new Document();
        document1.add(new StringField("id", "1", Field.Store.YES));
        document1.add(new TextField("title", "谷歌地图之父跳槽facebook", Field.Store.YES));
        docs.add(document1);
        // 创建文档对象
        Document document2 = new Document();
        document2.add(new StringField("id", "2", Field.Store.YES));
        document2.add(new TextField("title", "谷歌地图之父加盟FaceBook", Field.Store.YES));
        docs.add(document2);
        // 创建文档对象
        Document document3 = new Document();
        document3.add(new StringField("id", "3", Field.Store.YES));
        document3.add(new TextField("title", "谷歌地图创始人拉斯离开谷歌加盟Facebook", Field.Store.YES));
        docs.add(document3);
        // 创建文档对象
        Document document4 = new Document();
        document4.add(new StringField("id", "4", Field.Store.YES));
        document4.add(new TextField("title", "谷歌地图之父跳槽Facebook与Wave项目取消有关", Field.Store.YES));
        docs.add(document4);
        // 创建文档对象
        Document document5 = new Document();
        document5.add(new StringField("id", "5", Field.Store.YES));
        document5.add(new TextField("title", "谷歌地图之父拉斯加盟社交网站Facebook", Field.Store.YES));
        docs.add(document5);

        // 索引目录类,指定索引在硬盘中的位置
        Directory directory = FSDirectory.open(new File("C:\\lesson\\indexDir"));
        // 引入IK分词器
        Analyzer analyzer = new IKAnalyzer();
        // 索引写出工具的配置对象
        IndexWriterConfig conf = new IndexWriterConfig(Version.LATEST, analyzer);
        // 设置打开方式:OpenMode.APPEND 会在索引库的基础上追加新索引。
        // OpenMode.CREATE会先清空原来数据,再提交新的索引
        conf.setOpenMode(IndexWriterConfig.OpenMode.CREATE);

        // 创建索引的写出工具类。参数:索引的目录和配置信息
        IndexWriter indexWriter = new IndexWriter(directory, conf);
        // 把文档集合交给IndexWriter
        indexWriter.addDocuments(docs);
        // 提交
        indexWriter.commit();
        // 关闭
        indexWriter.close();
    }

3.3.索引的基本查询

基本流程:

  • 创建索引搜索工具
    • 指定索引目录
    • 创建读取流工具
    • 创建搜索工具
  • 创建查询条件
    • 创建查询解析器
    • 解析用户搜索语句,得到查询条件对象
  • 搜索并解析结果

代码如下:

@Test
public void testSearch() throws Exception {
    // 索引目录对象
    Directory directory = FSDirectory.open(new File("C:\\lesson\\indexDir"));
    // 索引读取工具
    IndexReader reader = DirectoryReader.open(directory);
    // 索引搜索工具
    IndexSearcher searcher = new IndexSearcher(reader);

    // 创建查询解析器,两个参数:默认要查询的字段的名称,分词器
    QueryParser parser = new QueryParser("title", new IKAnalyzer());
    // 创建查询对象
    Query query = parser.parse("谷歌地图之父拉斯");

    // 搜索数据,两个参数:查询条件对象要查询的最大结果条数
    // 返回的结果是 按照匹配度排名得分前N名的文档信息(包含查询到的总条数信息、所有符合条件的文档的编号信息)。
    TopDocs topDocs = searcher.search(query, 10);
    // 获取总条数
    System.out.println("本次搜索共找到" + topDocs.totalHits + "条数据");
    // 获取得分文档对象(ScoreDoc)数组.SocreDoc中包含:文档的编号、文档的得分
    ScoreDoc[] scoreDocs = topDocs.scoreDocs;
    for (ScoreDoc scoreDoc : scoreDocs) {
        // 取出文档编号
        int docID = scoreDoc.doc;
        // 根据编号去找文档
        Document doc = reader.document(docID);
        System.out.println("id: " + doc.get("id"));
        System.out.println("title: " + doc.get("title"));
        // 取出文档得分
        System.out.println("得分: " + scoreDoc.score);
    }
}

结果:

3.4.索引的高级查询

在刚才的基本查询中,我们使用QueryParser来解析并获取查询条件对象Query。事实上,Query有很多的子类,代表各种不同的特殊查询方式:

我们创建不同的Query子类,就会实现不同的查询功能。

3.4.1 抽取通用查询方法

当我们使用各种不同查询时,其它代码几乎不动,就是查询条件在发生变化,因此我们可以把查询代码进行抽取:

public void search(Query query) throws Exception{
    // 索引目录对象
    Directory directory = FSDirectory.open(new File("C:\\lesson\\indexDir"));
    // 索引读取工具
    IndexReader reader = DirectoryReader.open(directory);
    // 索引搜索工具
    IndexSearcher searcher = new IndexSearcher(reader);

    // 搜索数据,两个参数:查询条件对象要查询的最大结果条数
    // 返回的结果是 按照匹配度排名得分前N名的文档信息(包含查询到的总条数信息、所有符合条件的文档的编号信息)。
    TopDocs topDocs = searcher.search(query, 10);
    // 获取总条数
    System.out.println("本次搜索共找到" + topDocs.totalHits + "条数据");
    // 获取得分文档对象(ScoreDoc)数组.SocreDoc中包含:文档的编号、文档的得分
    ScoreDoc[] scoreDocs = topDocs.scoreDocs;
    for (ScoreDoc scoreDoc : scoreDocs) {
        // 取出文档编号
        int docID = scoreDoc.doc;
        // 根据编号去找文档
        Document doc = reader.document(docID);
        System.out.println("id: " + doc.get("id"));
        System.out.println("title: " + doc.get("title"));
        // 取出文档得分
        System.out.println("得分: " + scoreDoc.score);
    }
}

改造之前的查询:

@Test
public void testSearch() throws Exception {
    // 创建查询解析器,两个参数:默认要查询的字段的名称,分词器
    QueryParser parser = new QueryParser("title", new IKAnalyzer());
    // 创建查询对象
    Query query = parser.parse("谷歌地图之父拉斯");
	// 查询并解析
    search(query);
}

3.4.2 词条查询

词条,英文是Term,代表对原始数据进行分词后得到的每一个词语。是搜索匹配时的最小单位,不可再分词。

因此词条查询必须是精确匹配查询。用户输入的查询条件必须是完整的词条。

示例:

/*
 * 注意:Term(词条)是搜索的最小单位,不可再分词。值必须是字符串!
 */
@Test
public void testTermQuery() throws Exception {
    // 创建词条查询对象
    Query query = new TermQuery(new Term("title", "谷歌地图"));
    search(query);
}

3.4.3 通配符查询

查询条件可以包含通配符:

/* 通配符查询:
 * 	? 可以代表任意一个字符
 * 	* 可以任意多个任意字符
 */
@Test
public void testWildCardQuery() throws Exception {
    // 创建查询对象
    Query query = new WildcardQuery(new Term("title", "*歌*"));
    search(query);
}

3.4.4 FuzzyQuery(模糊查询)

/*
	 * 测试模糊查询
	 */
@Test
public void testFuzzyQuery() throws Exception {
    // 创建模糊查询对象:允许用户输错。但是要求错误的最大编辑距离不能超过2
    // 编辑距离:一个单词到另一个单词最少要修改的次数 facebool --> facebook 需要编辑1次,编辑距离就是1
    // Query query = new FuzzyQuery(new Term("title","fscevool"));
    // 可以手动指定编辑距离,但是参数必须在0~2之间
    Query query = new FuzzyQuery(new Term("title","facevool"),1);
    search(query);
}

3.4.5 数值范围查询

/*
	 * 测试:数值范围查询
	 * 注意:数值范围查询,可以用来对非String类型的ID进行精确的查找
	 */
@Test
public void testNumericRangeQuery() throws Exception{
    // 数值范围查询对象,参数:字段名称,最小值、最大值、是否包含最小值、是否包含最大值
    Query query = NumericRangeQuery.newLongRange("id", 2L, 2L, true, true);
    search(query);
}

3.4.6 布尔查询

/*
 * 布尔查询:
 * 	布尔查询本身没有查询条件,可以把其它查询通过逻辑运算进行组合!
 * 交集:Occur.MUST + Occur.MUST
 * 并集:Occur.SHOULD + Occur.SHOULD
 * 非:Occur.MUST_NOT
 */
@Test
public void testBooleanQuery() throws Exception{

    Query query1 = NumericRangeQuery.newLongRange("id", 1L, 3L, true, true);
    Query query2 = NumericRangeQuery.newLongRange("id", 2L, 4L, true, true);
    // 创建布尔查询的对象
    BooleanQuery query = new BooleanQuery();
    // 组合其它查询
    query.add(query1, Occur.MUST_NOT);
    query.add(query2, Occur.SHOULD);

    search(query);
}

3.5 修改索引

基本流程:

  • 创建索引写出对象

    • 指定目录
    • 配置
  • 创建文档

  • 更新数据

    /**
    * 注意,这里的更新接收的条件时Term,即词条。需要注意两点:
    * 1)搜索条件最好唯一,例如ID,否则后果很严重
    * 2)之前说过,词条要求必须是字符串类型,那如果我们的id是Long类型怎么办?
    * @throws Exception
    */
    @Test
    public void testUpdate() throws Exception{
    // 创建目录对象
    Directory directory = FSDirectory.open(new File(“indexDir”));
    // 创建配置对象
    IndexWriterConfig conf = new IndexWriterConfig(Version.LATEST, new IKAnalyzer());
    // 创建索引写出工具
    IndexWriter writer = new IndexWriter(directory, conf);

      // 创建新的文档数据
      Document doc = new Document();
      doc.add(new StringField("id","1",Store.YES));
      doc.add(new TextField("title","谷歌地图之父跳槽facebook 为了加入传智播客 屌爆了啊",Store.YES));
      /* 修改索引。参数:
           * 	词条:根据这个词条匹配到的所有文档都会被修改
           * 	文档信息:要修改的新的文档数据
           */
      writer.updateDocument(new Term("id","1"), doc);
      // 提交
      writer.commit();
      // 关闭
      writer.close();
    

    }

3.6. 删除索引

基本流程:

  • 创建索引写出工具

  • 创建删除条件

  • 删除

    /**
    * 删除的方式有多样:
    * 1)根据Term删除,需要注意:
    * a. 词条的数据类型必须是字符串
    * b. 最好根据id进行唯一匹配删除,如果id不是字符串类型怎么办?
    * 2)根据Query删除
    * @throws Exception
    */
    @Test
    public void testDelete() throws Exception {
    // 创建目录对象
    Directory directory = FSDirectory.open(new File(“indexDir”));
    // 创建配置对象
    IndexWriterConfig conf = new IndexWriterConfig(Version.LATEST, new IKAnalyzer());
    // 创建索引写出工具
    IndexWriter writer = new IndexWriter(directory, conf);

      // 根据词条进行删除
      //		writer.deleteDocuments(new Term("id", "1"));
    
      // 根据query对象删除,如果ID是数值类型,那么我们可以用数值范围查询锁定一个具体的ID
      //		Query query = NumericRangeQuery.newLongRange("id", 2L, 2L, true, true);
      //		writer.deleteDocuments(query);
    
      // 删除所有
      writer.deleteAll();
      // 提交
      writer.commit();
      // 关闭
      writer.close();
    

    }

  1. Lucene的高级使用

4.1 高亮显示

原理:

1)给所有关键字加上一个HTML标签

2)给这个特殊的标签设置CSS样式

代码实现:

@Test
public void testHighlighter() throws Exception {
    // 目录对象
    Directory directory = FSDirectory.open(new File("C:\\lesson\\indexDir"));
    // 创建读取工具
    IndexReader reader = DirectoryReader.open(directory);
    // 创建搜索工具
    IndexSearcher searcher = new IndexSearcher(reader);

    QueryParser parser = new QueryParser("title", new IKAnalyzer());
    Query query = parser.parse("谷歌地图");

    // 格式化器
    Formatter formatter = new SimpleHTMLFormatter("<em>", "</em>");
    Scorer scorer = new QueryScorer(query);
    // 准备高亮工具
    Highlighter highlighter = new Highlighter(formatter, scorer);
    // 搜索
    TopDocs topDocs = searcher.search(query, 10);
    System.out.println("本次搜索共" + topDocs.totalHits + "条数据");

    ScoreDoc[] scoreDocs = topDocs.scoreDocs;
    for (ScoreDoc scoreDoc : scoreDocs) {
        // 获取文档编号
        int docID = scoreDoc.doc;
        Document doc = reader.document(docID);
        System.out.println("id: " + doc.get("id"));

        String title = doc.get("title");
        // 用高亮工具处理普通的查询结果,参数:分词器,要高亮的字段的名称,高亮字段的原始值
        String hTitle = highlighter.getBestFragment(new IKAnalyzer(), "title", title);

        System.out.println("title: " + hTitle);
        // 获取文档的得分
        System.out.println("得分:" + scoreDoc.score);
    }

}

4.2. 排序

@Test
public void testSortQuery() throws Exception {
    // 目录对象
    Directory directory = FSDirectory.open(new File("C:\\lesson\\indexDir"));
    // 创建读取工具
    IndexReader reader = DirectoryReader.open(directory);
    // 创建搜索工具
    IndexSearcher searcher = new IndexSearcher(reader);

    QueryParser parser = new QueryParser("title", new IKAnalyzer());
    Query query = parser.parse("谷歌地图");

    // 创建排序对象,需要排序字段SortField,参数:字段的名称、字段的类型、是否反转如果是false,升序。true降序
    Sort sort = new Sort(new SortField("id", SortField.Type.STRING, true));
    // 搜索
    TopDocs topDocs = searcher.search(query, 10,sort);
    System.out.println("本次搜索共" + topDocs.totalHits + "条数据");

    ScoreDoc[] scoreDocs = topDocs.scoreDocs;
    for (ScoreDoc scoreDoc : scoreDocs) {
        // 获取文档编号
        int docID = scoreDoc.doc;
        Document doc = reader.document(docID);
        System.out.println("id: " + doc.get("id"));
        System.out.println("title: " + doc.get("title"));
    }
}

4.3 分页

Lucene本身没有分页功能,分页需要我们自己用代码来模拟实现:

100页,500个,500个全部查,第5-10

@Test
public void testPageQuery() throws Exception {
    // 实际上Lucene本身不支持分页。因此我们需要自己进行逻辑分页。我们要准备分页参数:
    int pageSize = 2;// 每页条数
    int pageNum = 3;// 当前页码
    int start = (pageNum - 1) * pageSize;// 当前页的起始条数
    int end = start + pageSize;// 当前页的结束条数(不能包含)

    // 目录对象
    Directory directory = FSDirectory.open(new File("C:\\lesson\\indexDir"));
    // 创建读取工具
    IndexReader reader = DirectoryReader.open(directory);
    // 创建搜索工具
    IndexSearcher searcher = new IndexSearcher(reader);

    QueryParser parser = new QueryParser("title", new IKAnalyzer());
    Query query = parser.parse("谷歌地图");

    // 创建排序对象,需要排序字段SortField,参数:字段的名称、字段的类型、是否反转如果是false,升序。true降序
    Sort sort = new Sort(new SortField("id", SortField.Type.STRING, false));
    // 搜索数据,查询0~end条
    TopDocs topDocs = searcher.search(query, end,sort);
    System.out.println("本次搜索共" + topDocs.totalHits + "条数据");

    ScoreDoc[] scoreDocs = topDocs.scoreDocs;
    for (int i = start; i < end && i < scoreDocs.length; i++) {
        ScoreDoc scoreDoc = scoreDocs[i];
        // 获取文档编号
        int docID = scoreDoc.doc;
        Document doc = reader.document(docID);
        System.out.println("id: " + doc.get("id"));
        System.out.println("title: " + doc.get("title"));
    }
}

4.4、得分算法

Lucene会对搜索结果打分,用来表示文档数据与词条关联性的强弱,得分越高,表示查询的匹配度就越高,排名就越靠前!其算法公式是:

这里是引用

Logo

CSDN联合极客时间,共同打造面向开发者的精品内容学习社区,助力成长!

更多推荐