JavaWeb框架&JavaWeb工具学习

Lucene全文检索技术

2020-02-06  本文已影响0人  So_ProbuING

什么是全文检索

数据分类

结构化数据搜索

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

非结构化数据查询方法

实现全文索引

可以使用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包

<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自带的中文分词器

第三方中文分词器

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();
        }
    }

使用 queryparser查询

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

QueryParser

 <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();
        }
    }
上一篇下一篇

猜你喜欢

热点阅读