基于Lucene8.0搭建有纠错检查的简易文本检索器
文章目录
基本要求
实现简单的搜索引擎功能,要求实现以下基本功能:
- 拼写检查(参考最小编辑距离原理)
- 倒排索引
- 使用TF/IDF或者VSM进行文档排序
实现流程
1. 搭建Java开发环境,使用Eclipse进行项目开发
2. 安装最新版Lucene,目前最新版本是Lucene 8.0
Lucene安装配置教程
我个人觉得只需要把那Lucene的包下载下来后解压就可以了,不需要配置环境变量。不过也可以配置环境变量然后在命令行窗口试运行一下demo项目,来测试是否安装配置成功。
3. 创建倒排索引
1. Lucene 创建倒排索引需要用到的类的关系图
2. 实现步骤和代码展示
- 创建存储索引的类的实例,我选择了FSDirectory,代表创建的索引会被存储在磁盘上,还有把索引存储在RAM也就是内存上的类RAMDirectory;
- 创建中文分析器,实例化SmartChineseAnalyzer;
- 如上面的图所示,实例化IndexWriterConfig类,并把中文分析器作为参数传入,这个类是用于配置IndexWriter的;
- 上述三个步骤实现了就代表IndexWriter的配置参数设置完成,紧接着需要把要创建索引的文件转变成Document;
- 把文本文件读取到string对象中,然后实例化Document;
- 用FieldType类配置Document中多个FieldType参数,这里我们设置了要把索引的内容存储到索引中并且把索引类型设置为(字段-频数-文档位置)
import java.io.BufferedReader;
import java.io.File;
import java.io.FileFilter;
import java.io.FileReader;
import java.io.IOException;
import java.nio.file.FileSystems;
import org.apache.lucene.analysis.Analyzer;
import org.apache.lucene.analysis.cn.smart.SmartChineseAnalyzer;
import org.apache.lucene.document.Document;
import org.apache.lucene.document.Field;
import org.apache.lucene.document.FieldType;
import org.apache.lucene.index.CorruptIndexException;
import org.apache.lucene.index.IndexOptions;
import org.apache.lucene.index.IndexWriter;
import org.apache.lucene.index.IndexWriterConfig;
import org.apache.lucene.store.Directory;
import org.apache.lucene.store.FSDirectory;
public class Indexer {
private IndexWriter indexWriter;
public Indexer(String indexDirectoryPath) throws IOException{
//创建储存索引的类,储存在磁盘上
Directory indexDirectory = FSDirectory.open(FileSystems.getDefault().getPath(indexDirectoryPath));
//中文分析器
Analyzer analyzer = new SmartChineseAnalyzer();
//配置IndexWriter
IndexWriterConfig config = new IndexWriterConfig(analyzer);
//创建IndexWriter,可以对索引进行写操作,负责创建索引和打开已经存在的索引
indexWriter = new IndexWriter(indexDirectory, config);
}
public void close() throws CorruptIndexException, IOException{
indexWriter.close();
}
private Document getDocument(File file) throws IOException{
//把file转化为string,读取文本,把文本一行行读取到buffer
StringBuffer buffer = new StringBuffer();
BufferedReader bf= new BufferedReader(new FileReader(file));
String s = null;
while((s = bf.readLine())!=null){
buffer.append(s.trim());
}
String content = buffer.toString();
//读取完成记得把缓存读取关掉
bf.close();
//创建document
Document document = new Document();
//配置document中fieldType的参数
FieldType fieldType = new FieldType();
//表示要存储到索引中,因为比较短所以将内容,文件名和绝对路径都存储到索引里了
fieldType.setStored(true);
//表示文档、词频和 位置都被索引
fieldType.setIndexOptions(IndexOptions.DOCS_AND_FREQS_AND_POSITIONS);
//向document中添加内容,文件名和绝对路径这三个域,才能创建这三者的索引
document.add(new Field(LuceneConstants.CONTENTS, content,fieldType));
document.add(new Field(LuceneConstants.FILE_NAME,file.getName(),fieldType));
document.add(new Field(LuceneConstants.FILE_PATH,file.getCanonicalPath(),fieldType));
return document;
}
private void indexFile(File file) throws IOException{
System.out.println("Indexing " + file.getCanonicalPath());
Document document = getDocument(file);
indexWriter.addDocument(document);
}
public int createIndex(String dataDirPath, FileFilter filter) throws IOException{
//获取data文件夹中的所有文件
File[] files = new File(dataDirPath).listFiles();
int count = 0;
//对每个file创建索引
for (File file : files) {
//判断这个file不是目录名,存在,可读且是txt文件
if(!file.isDirectory() && !file.isHidden() && file.exists() && file.canRead() && filter.accept(file)) {
indexFile(file);
count++;
}
}
return count;
}
}
测试用例
import java.io.IOException;
import xujj.Indexer;
import xujj.LuceneTester;
import xujj.TextFileFilter;
public class LuceneTester {
String indexDir = "index";
String dataDir = "data";
Indexer indexer;
public static void main(String[] args) {
LuceneTester tester;
try {
tester = new LuceneTester();
tester.createIndex();
} catch (IOException e) {
e.printStackTrace();
}
}
private void createIndex() throws IOException{
indexer = new Indexer(indexDir);
int numIndexed;
long startTime = System.currentTimeMillis();
numIndexed = indexer.createIndex(dataDir, new TextFileFilter());
long endTime = System.currentTimeMillis();
indexer.close();
System.out.println(numIndexed+" File indexed, time taken: "
+(endTime-startTime)+" ms");
}
}
3. 运行结果截图
结果会在index目录下生成:_0.cfe, _0.cfs, _0.si, segments_1, write.lock等文件。截图上是运行多次后的结果。
4. 实现带有拼写纠正功能的搜索器
1. Lucene 实现搜索功能的类图
2. 实现步骤和参考代码
1. 查询功能
- 如上图所示,先得到通过索引文件存储目录实例化Directory类,要与前面创建的保持一致
- 用得到的Directory创建索引读取器,将Directory作为参数实例化IndexReader;
- 创建搜索器,将IndexReader作为参数传入实例化IndexSearcher;
- 实例化ClassicSimilarity类并作为参数传入IndexSearcher的setSimilarity方法,设置相关性排序为TF/IDF 排序;
- 上述步骤实现了IndexSearcher的创建和配置,接下来就要对输入的查询字符串进行处理。首先实例化一个中文分析器SmartChineseAnalyzer,注意要和前面创建索引的分析器保持一致,不然会出错。
- 实例化查询解析器QueryParser,第一参数是查询的field, 第二个参数是分析器;
- 通过查询字符串创建一个Query;
- 使用IndexSearcher的search,把Query作为第一个参数,第二个参数是得到的top-n 的TopDocs的数目;
- 遍历TopDocs中的top-n文件集,也就是ScoreDocs,通过ScoreDocs获取Document,获取文件路径。
2. 拼写检查纠错功能
- 先得到通过索引文件存储目录实例化Directory类;
- 利用原本有的索引,实例化拼写检查器DirectSpellChecker,也可以用其它拼写检查器;
- 用得到的Directory创建索引读取器,将Directory作为参数实例化IndexReader;
- 从输入的string创建查询条目Term;
- 使用拼写检查器DirectSpellChecker的suggestSimilar方法,传入Term希望获取的最大建议数和索引读取器IndexReader,获得SuggestWord的数组。
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.nio.file.FileSystems;
import org.apache.lucene.analysis.Analyzer;
import org.apache.lucene.analysis.cn.smart.SmartChineseAnalyzer;
import org.apache.lucene.document.Document;
import org.apache.lucene.index.DirectoryReader;
import org.apache.lucene.index.IndexReader;
import org.apache.lucene.index.Term;
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.search.similarities.ClassicSimilarity;
import org.apache.lucene.search.spell.DirectSpellChecker;
import org.apache.lucene.search.spell.SuggestWord;
import org.apache.lucene.store.Directory;
import org.apache.lucene.store.FSDirectory;
/**
*
* 通过索引字段来读取文档
*
*/
public class Searcher {
public static void search(String indexDir,String q)throws Exception{
//得到索引文件存储目录
Directory dict = FSDirectory.open(FileSystems.getDefault().getPath(indexDir));
//创建索引读取器
IndexReader reader = DirectoryReader.open(dict);
//创建索引搜索器
IndexSearcher searcher = new IndexSearcher(reader);
//设置相关性排序为TF/IDF 排序
ClassicSimilarity tf_idf = new ClassicSimilarity();
searcher.setSimilarity(tf_idf);
//创建分析器,要跟前面创建索引用到的分析器相同,否则会出错
Analyzer analyzer = new SmartChineseAnalyzer();
//建立查询解析器,第一参数是查询的field, 第二个参数是分析器
QueryParser parser = new QueryParser("contents", analyzer);
//根据传进来的字符串q查找
Query query = parser.parse(q);
//检索开始时间
long start = System.currentTimeMillis();
//查询得到搜索命中的结果集,第一个参数是QueryParser生成的Parser,第二个参数是ScoreDos的最大文件数
TopDocs hits = searcher.search(query, 10);
//检索结束时间
long end=System.currentTimeMillis();
System.out.println("检索 " + q + " ,总共花费" + (end - start) + "毫秒" + "查询到" + hits.totalHits + "个记录");
//遍历hits中的top-n文件集,也就是ScoreDoc
for(ScoreDoc scoreDoc:hits.scoreDocs){
//通过文件的序号找到文件所在路径
Document doc = searcher.doc(scoreDoc.doc);
System.out.println(doc.get(LuceneConstants.FILE_PATH) + " 相关性是:" + String.valueOf(scoreDoc.score));
}
//关闭reader
reader.close();
}
public static SuggestWord[] checkWord(String queryWord){
//索引目录
String indexDir = "index";
//拼写检查
try {
//目录
Directory directory = FSDirectory.open(FileSystems.getDefault().getPath(indexDir));
//创建拼写检查器,利用原本有的索引
DirectSpellChecker checker = new DirectSpellChecker();
//以下几步用来初始化索引
IndexReader reader = DirectoryReader.open(directory);
//从输入的string创建查询条目
Term term = new Term(LuceneConstants.CONTENTS, queryWord);
//获取最相近的前5个词
int numSug = 5;
SuggestWord[] suggestions = checker.suggestSimilar(term, numSug, reader);
reader.close();
directory.close();
return suggestions;
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
return null;
}
//测试
public static void main(String[] args) throws IOException {
String indexDir="index";
// 处理输入
BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
String str = null;
System.out.println("请输入你要搜索的关键词:");
try {
str = br.readLine();
System.out.println();
} catch (IOException e1) {
// TODO Auto-generated catch block
e1.printStackTrace();
}
// 拼写检查
String temp = str;
SuggestWord[] suggestions = checkWord(str);
if (suggestions != null && suggestions.length != 0){
System.out.println("你想输入的可能是:");
for(int i = 0; i < suggestions.length; i++){
System.out.println((i+1) + " : " + suggestions[i].string);
}
System.out.println("请选择上面的一个正确的关键词(输入 1 ~ 5),或继续原词(输入0)进行搜索:");
str = br.readLine();
System.out.println();
//判断如果输入0,就按原词搜索;如果输入其它,判断是否在1~5范围内
if (str == "0"){
str = temp;
}
else {
boolean right = false;
for (int i = 1; i <= 5; i++) {
if (str == String.valueOf(i)) {
right = true;
}
}
if (right) {
str = suggestions[str.charAt(0) - '1'].string;
}
else {
System.out.println("请输入 1 ~ 5之间的数字进行搜索");
}
}
}
try {
search(indexDir, str);
} catch (Exception e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}