Lucene全文检索技术

什么是全文检索

数据分类

  • 结构化数据:指具有固定格式或有限长度的数据,如数据、元数据等
  • 非结构化数据:指不定长或无固定格式的数据。邮件、word等

结构化数据搜索

数据库中存储的数据都是结构化的数据,结构化数据在搜索中很容易

非结构化数据查询方法

  • 顺序扫描法
    一个一个的寻找
  • 全文检索
    将非结构化数据中的一部分信息提取出来,重新组织。使其变得有一定的结构,然后对此有一定结构的数据进行搜索,从而达到搜索相对较快的目的,这部分从非结构化数据中提取出的然后重新组织的信息 我们称之为索引
    对于这种先建立索引,再对索引进行搜索的过程就叫做全文检索
    创建索引的过程比较耗时,但是索引一旦创建就可以多次使用。

实现全文索引

可以使用Lucene实现全文检索。Lucene是apach下的一个开源的全文检索工具包,提供了完整的查询引擎和索引引擎。

全文检索的应用场景

对于数据量大、数据结构不固定的数据可以使用全文检索方式进行搜索,如搜索引站内搜索、电商搜索等

Lucene实现全文检索的流程

全文检索过程
  • 绿色表示创建索引的过程:确定原始内容->采集文档->创建文档->分析文档->索引文档
  • 红色表示搜索过程:用户通过搜索界面->创建查询->执行搜索,从索引库搜索->渲染搜索结果

创建索引

获得原始文档

原始文档是指要索引和搜索的内容,也就是从互联网、数据库、文件系统中获取的原始信息

创建文档对象

获取原始内容的目的就是为了创建索引,在创建索引前需要将原始内容创建成文档(Document),文档中包括一个一个的域(Field),内容存储在域中

Field域对象:相当于数据库表中的列,相当于实体类中的属性


文档结构

每个Document可以有多个Field,不同的Document可以有不同的Field,同一个Document可以有相同的Field
每个文档都有一个唯一的编号,就是文档id

分析文档

将原始内容创建为包含域(Field)的文档(document),需要再对域中的内容进行分析,分析的过程是经过对原始文档提取单词、将字母转为小写、去除标点符号、去除停用词等过程后生成最终的语汇单元。
每一个语汇单元叫做一个Term,不同的域中拆分出来的相同的单词是不同的term,term中包含两部分:

  • 文档和域名
  • 另一部分是单词的内容

创建索引

创建索引是对语汇单元索引,通过词语找文档,这种索引的结构叫倒排索引结构

倒排索引结构

倒排索引结构也叫反向索引结构,包括索引和文档两部分:索引即词汇表

查询索引

用户查询接口

需要提供一个用户查询接口,用户使用搜索界面提交搜索关键字


用户搜索界面

创建查询

用户输入查询关键字执行搜索之前要先构建一个查询对象,查询对象中可以指定查询要搜索的Field文档域、查询关键字等,查询对象会生成具体的语法

执行查询

执行搜索时,根据查询对象创建的查询语法在倒排索引词典表中分别找出对应搜索词的索引,从而找到索引所链接的文档链表
例如:搜索语法为:fileName:lucene 表示搜索出fileName域中包含Lucene的文档。
搜索过程就是在索引上查询域为fileName,并且关键字为Lucene的term,并且根据term找到文档id列表

渲染结果

渲染结果

上面介绍完基本的使用方式,接下来我们来撸一次吧

配置开发环境

Lucene下载

官方网站:http://lucene.apahce.org/

使用jar包

  • Lucene包:
    • lucene-core-xx.jar lucene核心包
    • lucene-analyzers-common-xxx.jar lucene分析包
  • 其他包:
    • commons-io-xxx.jar
    • junit-xxx.jar
  • pom坐标文件
<dependencies>
<dependency>
<groupId>org.apache.lucene</groupId>
<artifactId>lucene-core</artifactId>
<version>4.10.3</version>
</dependency>
 <dependency>
            <groupId>org.apache.lucene</groupId>
            <artifactId>lucene-analyzers-common</artifactId>
            <version>4.10.3</version>
        </dependency>
<dependency>
<groupId>commons-io</groupId>
<artifactId>commons-io</artifactId>
<version>2.5</version>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.12</version>
</dependency>
<dependency>
<groupId>org.apache.lucene</groupId>
<artifactId>lucene-queryparser</artifactId>
<version>4.10.3</version>
</dependency>
</dependencies>

入门案例

需求

实现一个文件搜索的功能,凡是文件名或文件内容包括搜索的关键字的都要找出来,可以根据中文词语进行查询,并且支持多个条件查询
原始内容:


原始内容

实现功能

创建索引

步骤

  1. 创建工程导入依赖
  2. 创建indexwriter对象
    2.1 指定索引库的存放位置Directory对象
    2.2 指定一个分析器,对文档内容进行分析
  3. 创建document对象
  4. 创建field对象,将field对象添加到document对象中
  5. 使用indexwriter对象将document对象写入索引库,此过程进行索引创建,并将索引和document对象写入索引库
  6. 关闭indexwriter对象

indexwriter 索引创建器

代码实现

package com.probuing.lucenelsn.test;

import org.apache.commons.io.FileUtils;
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.TextField;
import org.apache.lucene.index.IndexWriter;
import org.apache.lucene.index.IndexWriterConfig;
import org.apache.lucene.store.FSDirectory;
import org.apache.lucene.util.Version;
import org.junit.Test;

import java.io.File;
import java.io.IOException;

/**
 * @author wangxin
 * @date 2020/2/6 15:32
 * @description: TODO
 * GOOD LUCK!
 */

public class LuceneTest {

    @Test
    public void createIndex() {
        try {
            //指定索引库创建位置
            //指定索引库到硬盘位置
            FSDirectory directory = FSDirectory.open(new File("/Users/wangxin/temp/luceneIndex"));
            //指定索引库到内存
//            RAMDirectory ramDirectory = new RAMDirectory();
            //创建一个标准分析器
            Analyzer analyzer = new StandardAnalyzer();
            //创建索引创建器配置
            IndexWriterConfig writerConfig = new IndexWriterConfig(Version.LATEST, analyzer);
            //创建索引创建器
            //参数一:索引库位置
            //参数二:配置类
            IndexWriter indexWriter = new IndexWriter(directory, writerConfig);
            //指定原始文档路径
            File dir = new File("/Users/wangxin/temp/tempfiles");
            for (File file : dir.listFiles()) {
                //获取文件名
                String fileName = file.getName();
                //文件内容
                String fileContent = FileUtils.readFileToString(file, "utf-8");
                //文件路径
                String filePath = file.getPath();
                //文件大小
                long fileSize = FileUtils.sizeOf(file);
                //创建文件名域field
                //参数一:域名称 参数二 内容 值 参数三:是否存储
                TextField filenameField = new TextField("filename", fileName, Field.Store.YES);
                //文件内容域
                TextField contentField = new TextField("content", fileContent, Field.Store.YES);
                //文件路径域
                TextField pathField = new TextField("path", filePath, Field.Store.YES);
                //文件大小域
                TextField sizeField = new TextField("size", String.valueOf(fileSize), Field.Store.YES);
                /**
                 * 域Field中包括域名称:内容
                 * 域创建完了就要组装Document了
                 */
                Document document = new Document();
                //document添加field
                document.add(filenameField);
                document.add(contentField);
                document.add(pathField);
                document.add(sizeField);
                //使用索引创建器创建索引并写入索引库
                indexWriter.addDocument(document);
            }
            //关闭索引创建器
            indexWriter.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

使用Luke工具查看索引库文件

image.png

查询索引

实现步骤

  1. 创建Directory对象,指定索引库存放位置
  2. 创建一个indexReader对象,需要指定Directory对象
  3. 创建一个indexSearcher对象,需要指定indexReader对象
  4. 创建TermQuery对象,指定查询的域和查询的关键词
  5. 执行查询
  6. 返回查询结果。遍历查询并输出
  7. 关闭IndexReader对象

代码实现

 @Test
    public void searchIndex() {
        try {
            //使用Directory指定索引库位置
            FSDirectory directory = FSDirectory.open(new File("/Users/wangxin/temp/luceneIndex"));
            //创建indexReader对象
            IndexReader reader = DirectoryReader.open(directory);
            //创建indexSearch对象
            IndexSearcher searcher = new IndexSearcher(reader);
            //创建查询
            TermQuery query = new TermQuery(new Term("filename", "apache"));
            //执行查询 第一个参数:查询对象 第二个参数:返回结果的最大值
            TopDocs topDocs = searcher.search(query, 10);
            System.out.println("查询到的条数" + topDocs.totalHits);
            //遍历查询结果
            for (ScoreDoc score : topDocs.scoreDocs) {
                //score的doc就是document的id
                Document document = searcher.doc(score.doc);
                //输出结果
                String filename = document.get("filename");
                String content = document.get("content");
                String path = document.get("path");
                String size = document.get("size");
                System.out.println(filename + "----" + content + "----" + path + "----" + size);
            }
            //关闭对象
            reader.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

TopDocs

Lucene搜索结果可以通过TopDocs遍历,TopDocs提供了少量的属性

方法或属性 说明
totalHits 匹配搜索条件的总记录数
scoreDocs 顶部匹配记录

分析器

分析器的执行过程

语汇单元的生成过程


语汇单元的生成过程

从一个Reader字符流开始,创建一个基于Reader的Tokenizer分词骑,经过三个TokenFilter生成语汇单元Token
每一个分析器都有一个方法tokenStream,返回一个tokenStream对象

分析器的分词效果

 @Test
    public void testTokenStream() {
        try {
            //创建一个标准分析器对象
            Analyzer analyzer = new StandardAnalyzer();
            //获得tokenStream对象
            TokenStream tokenStream = analyzer.tokenStream("test", "The Spring Framework provides a comprehensive programming and configuration model.");
            //添加一个引用 获得每个关键词
            CharTermAttribute attribute = tokenStream.addAttribute(CharTermAttribute.class);
            //调整指针到列表的头部
            tokenStream.reset();
            //遍历结果
            while (tokenStream.incrementToken()) {
                System.out.println(attribute);
            }
            //关闭tokenStream
            tokenStream.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
  • 运行结果

spring
framework
provides
comprehensive
programming
configuration
model

中文分析器

Lucene自带的中文分词器

  • StandardAnalyzer
    单字分词:按照中文一个字一个字地进行分词,"我爱中国"->"我","爱","中","国"
  • CJKAnalyzer
    二分法分词:按两个字进行切分。“我是中国人”->"我是"、是中、中国、国人

第三方中文分词器

  • pading
    庖丁解牛最新版在 https://code.google.com/p/paoding/ 中最多支持
    Lucene 3.0,且最新提交的代码在 2008-06-03,在 svn 中最新也是 2010 年提交,已经过时,
    不予考虑。
  • mmseg4j
    最新版已从 https://code.google.com/p/mmseg4j/
    https://github.com/chenlb/mmseg4j-solr,支持 Lucene 4.10,且在 github 中最新
    提交代码是 2014 年 6 月,从 09 年~14 年一共有: 18 个版本,也就是一年几乎有 3 个大小
    版本,有较大的活跃度,用了 mmseg 算法。
  • IK-analyzer
    最新版在 https://code.google.com/p/ik-analyzer/上,支持 Lucene
    4.10 从 2006 年 12 月推出 1.0 版开始, IKAnalyzer 已经推出了 4 个大版本。最初,它是以
    开源项目 Luence 为应用主体的,结合词典分词和文法分析算法的中文分词组件。从 3.0 版
    本开 始, IK 发展为面向 Java 的公用分词组件,独立于 Lucene 项目,同时提供了对 Lucene
    的默认优化实现。在 2012 版本中, IK 实现了简单的分词 歧义排除算法,标志着 IK 分词器
    从单纯的词典分词向模拟语义分词衍化。 但是也就是 2012 年 12 月后没有在更新。
  • ansj_seg
    最新版本在 https://github.com/NLPchina/ansj_seg tags 仅有 1.1 版本,
    从 2012 年到 2014 年更新了大小 6 次,但是作者本人在 2014 年 10 月 10 日说明: “可能我
    以后没有精力来维护 ansj_seg 了” ,现在由”nlp_china” 管理。 2014 年 11 月有更新。并
    未说明是否支持 Lucene,是一个由 CRF(条件随机场)算法所做的分词算法。
  • imdict-chinese-analyzer
    https://code.google.com/p/imdict-chinese-analyzer/ , 最新更新也在 2009 年 5
    月,下载源码,不支持 Lucene 4.10 。是利用 HMM(隐马尔科夫链)算法。
  • Jcseg
    最新版本在 git.oschina.net/lionsoul/jcseg,支持 Lucene 4.10,作者有较高
    的活跃度。利用 mmseg 算法。

IKAnalyzer

使用方法:

添加pom坐标

<dependency>
<groupId>com.janeluo</groupId>
<artifactId>ikanalyzer</artifactId>
<version>2012_u6</version>
</dependency>
添加配置文件

添加*.dic文件到classpath 注意格式为无BOM的UTF-8编码

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE properties SYSTEM "http://java.sun.com/dtd/properties.dtd">
<properties>
    <comment>IK Analyzer 扩展配置</comment>
    <!--用户可以在这里配置自己的扩展字典 -->
    <entry key="ext_dict">ext.dic</entry>
    <!--用户可以在这里配置自己的停止词字典-->
    <entry key="ext_stopwords">stopword.dic</entry>

</properties>
  • 扩展词典:为的是让需要切分的字符串的词语 根据扩展词典里的词,不要切分开来。不要切分
  • 停止词典:对比停止词典,直接删掉停止词典中出现的词语
    直接删除
@Test
    public void testIKAnalyzer() {
        try {
            //创建一个标准分析器对象
//            Analyzer analyzer = new StandardAnalyzer();
            IKAnalyzer analyzer = new IKAnalyzer(true);
            //获得tokenStream对象
            TokenStream tokenStream = analyzer.tokenStream("test",
                    "这是一个粗糙的栅栏,浪费钱,我想要一堵巨大的墙!”网友Mary说,还附上了“理想”中的边境墙照片");
            //添加一个引用 获得每个关键词
            CharTermAttribute attribute = tokenStream.addAttribute(CharTermAttribute.class);
            //调整指针到列表的头部
            tokenStream.reset();
            //遍历结果
            while (tokenStream.incrementToken()) {
                System.out.println(attribute);
            }
            //关闭tokenStream
            tokenStream.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

Lucene高级查询

对要搜索的信息创建Query查询对象,Lucene会根据Query查询对象生成最终的查询语法。

可通过两种方法创建查询对象

1. 使用Lucene提供的Query子类

使用MatchAllDocsQuery查询索引目录中的所有文档

 @Test
    public void testMatchAllDocsQuery() {
        try {
            //使用Directory指定索引库位置
            FSDirectory directory = FSDirectory.open(new File("/Users/wangxin/temp/luceneIndex"));
            //创建indexReader对象
            IndexReader reader = null;
            reader = DirectoryReader.open(directory);
            //创建indexSearch对象
            IndexSearcher searcher = new IndexSearcher(reader);
            Query query = new MatchAllDocsQuery();
            TopDocs topDocs = searcher.search(query, 10);
            for (ScoreDoc scoreDoc : topDocs.scoreDocs) {
                int doc = scoreDoc.doc;
                Document document = searcher.doc(doc);
                System.out.println(document);
            }
        } catch (IOException e) {
            e.printStackTrace();
        }

    }

使用TermQuery

通过项查询,TermQuery不使用分析器所以建议匹配部分词的Field域查询,指定要查询的域和要查询的关键词

 @Test
    public void testTermQuery() {
        try {
            //指定索引库位置
            FSDirectory directory = FSDirectory.open(new File("/Users/wangxin/temp/luceneIndex"));
            //创建indexReader
            IndexReader reader = DirectoryReader.open(directory);
            //通过indexReader创建IndexSearch
            IndexSearcher searcher = new IndexSearcher(reader);
            ////创建查询对象
            TermQuery termQuery = new TermQuery(new Term("content", "lucene"));
            //使用searcher查询
            TopDocs topDocs = searcher.search(termQuery, 10);
            for (ScoreDoc scoreDoc : topDocs.scoreDocs) {
                Document document = searcher.doc(scoreDoc.doc);
                System.out.println(document.get("filename"));
//System.out.println(document.get("content"));
                System.out.println(document.get("path"));
                System.out.println(document.get("size"));
            }
            reader.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

NumericRangeQuery

根据数值范围查询

@Test
    public void testNumericRangeQuery() {
        try {
            //指定索引库位置
            FSDirectory directory = FSDirectory.open(new File("/Users/wangxin/temp/luceneIndex"));
            //创建indexReader
            IndexReader reader = DirectoryReader.open(directory);
            //通过indexReader创建IndexSearch
            IndexSearcher searcher = new IndexSearcher(reader);
            /*
                参数解释
                1. 域名
                2. 最小值
                3. 最大值
                4. 是否包含最小值
                5. 是否包含最大值
             */
            Query rangeQuery = NumericRangeQuery.newIntRange("size", 1, 100000, true, true);
            //使用searcher查询
            TopDocs topDocs = searcher.search(rangeQuery, 10);
            for (ScoreDoc scoreDoc : topDocs.scoreDocs) {
                Document document = searcher.doc(scoreDoc.doc);
                System.out.println(document.get("filename"));
//System.out.println(document.get("content"));
                System.out.println(document.get("path"));
                System.out.println(document.get("size"));
            }
            reader.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

BooleanQuery

组合查询条件

 @Test
    public void testBooleanQuery(){
        try {
            //指定索引库位置
            FSDirectory directory = FSDirectory.open(new File("/Users/wangxin/temp/luceneIndex"));
            //创建indexReader
            IndexReader reader = DirectoryReader.open(directory);
            //通过indexReader创建IndexSearch
            IndexSearcher searcher = new IndexSearcher(reader);

            BooleanQuery query = new BooleanQuery();
            //创建第一个查询条件
            Query query1 = new TermQuery(new Term("filename", "apache"));
            Query query2 = new TermQuery(new Term("content", "apache"));
//组合查询条件
            query.add(query1, BooleanClause.Occur.MUST);
            query.add(query2, BooleanClause.Occur.MUST);
            TopDocs topDocs = searcher.search(query, 10);
            for (ScoreDoc scoreDoc : topDocs.scoreDocs) {
                Document document = searcher.doc(scoreDoc.doc);
                System.out.println(document.get("filename"));
//System.out.println(document.get("content"));
                System.out.println(document.get("path"));
                System.out.println(document.get("size"));
            }
            reader.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
  • Occur.MUST:必须满足此条件,相当于and
  • Occur.SHOULD:应该满足,相当于or
  • Occur.MUST_NOT:必须不满足 相当于not

使用 queryparser查询

通过QueryParser也可以创建Query,QueryParser提供一个Parse方法,此方法可以直接根据语法来查询。Query对象的查询语法可以使用打印语句类打印查看
使用QueryParser需要使用分析器,
建议创建索引时使用的分析器和查询索引时使用的分析器要一致

QueryParser

  • 加入pom依赖
 <dependency>
            <groupId>org.apache.lucene</groupId>
            <artifactId>lucene-queryparser</artifactId>
            <version>8.4.1</version>
        </dependency>
  • 代码实现
 @Test
    public void testQueryParser() {
        try {
            //指定索引库位置
            FSDirectory directory = FSDirectory.open(new File("/Users/wangxin/temp/luceneIndex"));
            //创建indexReader
            IndexReader reader = DirectoryReader.open(directory);
            //通过indexReader创建IndexSearch
            IndexSearcher searcher = new IndexSearcher(reader);
            QueryParser queryParser = new QueryParser("content", new StandardAnalyzer());
            Query query = queryParser.parse("lucene是java开发的");
            TopDocs topDocs = searcher.search(query, 10);
            for (ScoreDoc scoreDoc : topDocs.scoreDocs) {
                Document document = searcher.doc(scoreDoc.doc);
                System.out.println(document.get("filename"));
//System.out.println(document.get("content"));
                System.out.println(document.get("path"));
                System.out.println(document.get("size"));
            }
            reader.close();
        } catch (IOException e) {
            e.printStackTrace();
        } catch (ParseException e) {
            e.printStackTrace();
        }
    }
查询语法
  • 基础查询语法:关键字查询
域名+“:”+搜索的关键字
例如:content:java
  • 范围查询
域名+“:”+[最小值 TO 最大值]
例如:size:[1 TO 1000]
  • 组合条件查询
  1. +条件1+条件2 两个条件之间是并且的关系and
    例如:+filename:apache+content:apache
  2. +条件1+条件2 必须满足一个条件,应该满足第二个条件
    例如:+filename:apache content:apache
  3. 条件1 条件2:两个条件满足其一即可
    例如:filename:apahce content:apache
  4. -条件1 条件2 必须不,满足条件1 要满足条件2
    例如:-filename:apache content:apache
    |Occur.MUST 查询条件必须满足,相当于and|+|
    |Occur.SHOULD 查询条件可选,相当于or|空(不用符号)|
    |Occur.MUST_NOT 查询条件不能满足,相当于not非|-(减号)|

MulitFiledQueryParser

可以指定多个默认搜索域

 @Test
    public void testMulitFieldQueryParser() {
        try {
            //指定索引库位置
            FSDirectory directory = FSDirectory.open(new File("/Users/wangxin/temp/luceneIndex"));
            //创建indexReader
            IndexReader reader = DirectoryReader.open(directory);
            //通过indexReader创建IndexSearch
            IndexSearcher searcher = new IndexSearcher(reader);
            //指定多个搜索域
            String[] fields = {"filename", "content"};
            //创建mulitFieldQueryParser对象
            MultiFieldQueryParser queryParser = new MultiFieldQueryParser(fields, new StandardAnalyzer());
            Query query = queryParser.parse("java and apache");
            TopDocs topDocs = searcher.search(query, 10);
            System.out.println("查询语句为" + query);
            for (ScoreDoc scoreDoc : topDocs.scoreDocs) {
                Document document = searcher.doc(scoreDoc.doc);
                System.out.println(document.get("filename"));
                System.out.println(document.get("path"));
                System.out.println(document.get("size"));
            }
            reader.close();
        } catch (IOException e) {
            e.printStackTrace();
        } catch (ParseException e) {
            e.printStackTrace();
        }
    }

推荐阅读更多精彩内容

  • 1. 案例分析:什么时全文检索,如何实现全文检索   1.1 案例   实现一个文件的搜索功能,通过关键字搜索文件...
    东方舵手阅读 289评论 0 1
  • 目录结构:1.全文检索 2.Lucene入门3.Lucene进阶 全文检索 一, 生活中的搜索:1.Win...
    CoderZS阅读 571评论 0 12
  • 1.什么是全文检索技术是一种搜索技术,一般对于结构化数据,有固定格式和长度的数据,我们使用sql进行查询,而对于非...
    亭台雨榭111阅读 216评论 0 2
  • 1. Lucene 官网 1). 概述 Lucene是一款高性能的、可扩展的信息检索(IR)工具库。信息检索是指文...
    _凌浩雨阅读 511评论 0 1
  • 1.1. 数据分类 结构化数据:指具有固定格式或有限长度的数据,如数据库,元数据等。非结构化数据:指不定长或无...
    Zephyr_07阅读 1,054评论 0 0