全文檢索場(chǎng)景
當(dāng)你在使用百度、Google搜索信息時(shí),當(dāng)你在淘寶、京東搜索商品時(shí)你知道這些都是使用的什么技術(shù)可以很快搜索你想要的東東嗎?正是全文檢索技術(shù)。
全文檢索概念
全文檢索是將整本書(shū)、整篇文章中的任意內(nèi)容信息查找出來(lái)的檢索。它可以根據(jù)需要獲得全文中有關(guān)章、節(jié)、段、句、詞等信息,計(jì)算機(jī)程序通過(guò)掃描文章中的每一個(gè)詞,對(duì)每一個(gè)詞建立一個(gè)索引,指明該詞在文章中出現(xiàn)的次數(shù)和位置,當(dāng)用戶(hù)查詢(xún)時(shí)根據(jù)建立的索引查找,類(lèi)似于通過(guò)字典的檢索字表查字的過(guò)程。
經(jīng)過(guò)幾年的發(fā)展,全文檢索從最初的字符串匹配程序已經(jīng)演進(jìn)到能對(duì)超大文本、語(yǔ)音、圖像、活動(dòng)影像等非結(jié)構(gòu)化數(shù)據(jù)進(jìn)行綜合管理的大型軟件。
什么是Lucene
Lucene是apache下的一個(gè)開(kāi)放源代碼的全文檢索引擎工具包。提供了完整的搜索引擎和索引引擎。Lucene的目的是為軟件開(kāi)發(fā)人員提供一個(gè)簡(jiǎn)單易用的工具包,以方便的在目標(biāo)系統(tǒng)中實(shí)現(xiàn)全文檢索的功能。
案例描述
我們以一個(gè)案例來(lái)研究全文檢索過(guò)程:實(shí)現(xiàn)一個(gè)資源管理器的搜索功能,通過(guò)關(guān)鍵字搜索文件,凡是文件名或文件內(nèi)容包括關(guān)鍵字的文件都需要找出來(lái)。
開(kāi)發(fā)環(huán)境
從Lucene官方網(wǎng)站(
http://lucene.apache.org/)下載Lucene4.3.10,并解壓。
Lucene4.3.10要求Jdk使用1.7以上,本教程使用1.7.0_72版本。
開(kāi)發(fā)工具:eclipse indigo
Lucene包:
lucene-core-4.10.3.jar---Lucene核心包
lucene-analyzers-common-4.10.3.jar----Lucene分析包
lucene-queryparser-4.10.3.jar ---Lucene查詢(xún)包
其它:
commons-io-2.4.jar ---用于讀取磁盤(pán)文件內(nèi)容
junit-4.9.jar---用于單元測(cè)試
Lucene全文檢索過(guò)程
全文檢索包括索引和搜索兩個(gè)過(guò)程,先對(duì)要搜索的信息創(chuàng)建索引,再?gòu)乃饕兴阉餍畔ⅰ?br />
如下圖:
1、黃色表示索引過(guò)程,對(duì)要搜索的原始內(nèi)容進(jìn)行索引構(gòu)建一個(gè)索引庫(kù),索引過(guò)程包括:
確定原始內(nèi)容即要搜索的內(nèi)容--》采集文檔--》創(chuàng)建文檔--》分析文檔--》索引文檔
2、藍(lán)色表示搜索過(guò)程,從索引庫(kù)中搜索內(nèi)容,搜索過(guò)程包括:
用戶(hù)通過(guò)搜索界面--》創(chuàng)建查詢(xún)--》執(zhí)行搜索,從索引庫(kù)搜索--》渲染搜索結(jié)果
第一步:確定原始內(nèi)容
原始內(nèi)容是指要索引和搜索的內(nèi)容。原始內(nèi)容包括互聯(lián)網(wǎng)上的網(wǎng)頁(yè)、數(shù)據(jù)庫(kù)中的數(shù)據(jù)、磁盤(pán)上的文件等。
本案例中的原始內(nèi)容就是磁盤(pán)上的文件(本教程只搜索.txt文件),如下圖:
第二步:獲取原始內(nèi)容
從互聯(lián)網(wǎng)上、數(shù)據(jù)庫(kù)、文件系統(tǒng)中等獲取需要搜索的原始信息,這個(gè)過(guò)程就是信息采集,信息采集的目的是為了對(duì)原始內(nèi)容進(jìn)行索引。
Lucene本身不提供信息采集的功能,這里我們通過(guò)Java流程讀取磁盤(pán)文件的內(nèi)容。
第三步:創(chuàng)建文檔
獲取原始內(nèi)容的目的是為了索引,在索引前需要將原始內(nèi)容創(chuàng)建成文檔(Document),文檔中包括一個(gè)一個(gè)的域(Field),域中存儲(chǔ)內(nèi)容。
這里我們可以將磁盤(pán)上的一個(gè)文件當(dāng)成一個(gè)document,Document中包括一些Field(file_name文件名稱(chēng)、file_path文件路徑、file_size文件大小、file_content文件內(nèi)容),如下圖:
注意:每個(gè)Document可以有多個(gè)Field,不同的Document可以有不同的Field,同一個(gè)Document可以有相同的Field(域名和域值都相同)
下邊代碼實(shí)現(xiàn)了從磁盤(pán)讀取文件并創(chuàng)建文檔的過(guò)程:
// 從文件創(chuàng)建Document
public static List<Document> file2Document(String folderPath)
throws IOException {
List<Document> list =
new ArrayList<Document>();
File folder =
new File(folderPath);
if (!folder.isDirectory()) {
return null;
}
// 獲取目錄 中的所有文件
File[] files = folder.listFiles();
for (File file : files) {
//文件名稱(chēng)
String fileName = file.getName();
System.
out.println(fileName);
if (fileName.lastIndexOf(".txt") > 0) {
// 文件內(nèi)容
String fileContent = FileUtils.
readFileToString(file);
//文件路徑
String
filePath = file.getAbsolutePath();
//文件大小
long fileSize = FileUtils.
sizeOf(file);
//創(chuàng)建文檔
Document doc =
new Document();
//創(chuàng)建各各Field域
//文件名
Field field_fileName =
new StringField("fileName", fileName, Store.
YES);
//文件內(nèi)容
Field field_fileContent =
new TextField("fileContent", fileContent, Store.
NO);
//文件大小
Field field_fileSize =
new LongField("fileSize", fileSize, Store.
YES);
//文件路徑
Field field_filePath =
new StoredField("filePath", filePath, Store.
YES);
//將各各Field添加到文檔中
doc.add(field_fileName);
doc.add(field_fileContent);
doc.add(field_fileSize);
doc.add(field_filePath);
list.add(doc);
}
}
return list;
}
第四步:分析文檔
將原始內(nèi)容創(chuàng)建為包含域(Field)的文檔(document),需要再對(duì)域中的內(nèi)容進(jìn)行分析,分析的過(guò)程是經(jīng)過(guò)對(duì)原始文檔提取單詞、將字母轉(zhuǎn)為小寫(xiě)、去除標(biāo)點(diǎn)符號(hào)、去除常用詞等過(guò)程生成最終的語(yǔ)匯單元,可以將語(yǔ)匯單元理解為一個(gè)一個(gè)的單詞。
比如下邊的文檔經(jīng)過(guò)分析如下:
原文檔內(nèi)容:
Lucene is a Java full-text search engine. Lucene is not a complete
application, but rather a code library and API that can easily be used
to add search capabilities to applications.
分析后得到的語(yǔ)匯單元:
lucene、java、full、search、engine。。。。
第五步:創(chuàng)建索引
對(duì)所有文檔分析得出的語(yǔ)匯單元進(jìn)行索引,索引的目的是為了搜索,最終要實(shí)現(xiàn)只搜索被索引的語(yǔ)匯單元從而找到Document(文檔)。
注意:創(chuàng)建索引是對(duì)語(yǔ)匯單元索引,通過(guò)詞語(yǔ)找文檔,這種索引的結(jié)構(gòu)叫
倒排索引結(jié)構(gòu)。
傳統(tǒng)方法是根據(jù)文件找到該文件的內(nèi)容,在文件內(nèi)容中匹配搜索關(guān)鍵字,這種方法是順序掃描方法,數(shù)據(jù)量大、搜索慢。
倒排索引結(jié)構(gòu)是根據(jù)內(nèi)容(詞語(yǔ))找文檔,如下圖:
根據(jù)左邊的索引詞典可以找到詞對(duì)應(yīng)的文檔。“springmvc.txt”這個(gè)詞在Document1(springmvc.txt) ,“web”和“spring”在Document1、Document2中都存在,詞是通過(guò)Field和Document文檔聯(lián)系起來(lái)的。
倒排索引結(jié)構(gòu)也叫反向索引結(jié)構(gòu),包括索引和文檔兩部分,索引即詞匯表,它的規(guī)模較小,而文檔集合較大。
使用分析器分析并創(chuàng)建索引過(guò)程代碼如下:
public class IndexTest {
// 索引源,即源數(shù)據(jù)目錄
private static String
searchSource = "F:\\develop\\lucene\\searchsource";
// 索引目標(biāo)地址
private static String
indexFolder = "F:\\develop\\lucene\\indexdata";
@Test
public void testCreateIndex() {
try {
//從目錄中讀取文件內(nèi)容并創(chuàng)建Document文檔
List<Document> docs = IndexUtils.
file2Document(
searchSource);
//創(chuàng)建分析器,standardAnalyzer標(biāo)準(zhǔn)分析器
Analyzer standardAnalyzer =
new IKAnalyzer();
// 指定索引存儲(chǔ)目錄
Directory directory = FSDirectory.
open(
new File(
indexFolder));
//創(chuàng)建索引操作配置對(duì)象
IndexWriterConfig indexWriterConfig =
new IndexWriterConfig(Version.
LUCENE_4_10_3,
standardAnalyzer);
// 定義索引操作對(duì)象indexWriter
IndexWriter indexWriter =
new IndexWriter(directory,indexWriterConfig);
// 遍歷目錄 下的文件生成的文檔,調(diào)用indexWriter方法創(chuàng)建索引
for (Document document : docs) {
indexWriter.addDocument(document);
}
// 索引操作流關(guān)閉
indexWriter.close();
}
catch (IOException e) {
e.printStackTrace();
}
}
}
第六步:搜索文件
根據(jù)文件名稱(chēng)搜索文件,需要經(jīng)過(guò)以下步驟:
1)指定索引目錄地址,搜索就是從索引中搜索匹配的詞語(yǔ)
// 指定索引目錄地址,
private static String
indexFolder = "F:\\develop\\lucene\\indexdata";
2)創(chuàng)建Query構(gòu)建查詢(xún)語(yǔ)法 (可以理解為和關(guān)系數(shù)據(jù)庫(kù)的Sql作用一樣)
// 創(chuàng)建查詢(xún)對(duì)象,根據(jù)文件名稱(chēng)域搜索匹配文件名稱(chēng)的文檔
Query query =
new TermQuery(
new Term("fileName", "springmvc_test.txt"));
3)創(chuàng)建IndexReader讀取索引文件
// 指定索引目錄
Directory directory = FSDirectory.
open(
new File(
indexFolder));
// 定義IndexReader
IndexReader reader = DirectoryReader.
open(directory);
4)創(chuàng)建IndexSearcher執(zhí)行搜索
// 創(chuàng)建indexSearcher
IndexSearcher indexSearcher =
new IndexSearcher(reader);
// 執(zhí)行搜索
TopDocs topDocs = indexSearcher.search(query, 100);
5)通過(guò)TopDocs獲取搜索結(jié)果
// 提取搜索結(jié)果
ScoreDoc[] scoreDocs = topDocs.scoreDocs;
6)遍歷結(jié)果,從Document中獲取Field內(nèi)容
for (ScoreDoc scoreDoc : scoreDocs) {
// 文檔id
int docID = scoreDoc.doc;
// 得到文檔
Document doc = indexSearcher.doc(docID);
// 輸出 文件內(nèi)容
System.
out.println("------------------------------");
System.
out.println("文件名稱(chēng) =" + doc.get("fileName"));
System.
out.println("文件大小 =" + doc.get("fileSize"));
System.
out.println("文件內(nèi)容 =" + doc.get("fileContent"));
}
完整代碼如下:
public class SearchTest {
// 指定索引目錄地址,
private static String
indexFolder = "F:\\develop\\lucene\\indexdata";
//查詢(xún)方法
@Test
public void testTermQuery()
throws IOException {
// 創(chuàng)建查詢(xún)對(duì)象,根據(jù)文件名稱(chēng)域搜索匹配文件名稱(chēng)的文檔
Query query =
new TermQuery(
new Term("fileName", "springmvc_test.txt"));
// 指定索引目錄
Directory directory = FSDirectory.
open(
new File(
indexFolder));
// 定義IndexReader
IndexReader reader = DirectoryReader.
open(directory);
// 創(chuàng)建indexSearcher
IndexSearcher indexSearcher =
new IndexSearcher(reader);
// 執(zhí)行搜索
TopDocs topDocs = indexSearcher.search(query, 100);
// 提取搜索結(jié)果
ScoreDoc[] scoreDocs = topDocs.scoreDocs;
System.
out.println("共搜索到總記錄數(shù):" + topDocs.totalHits);
for (ScoreDoc scoreDoc : scoreDocs) {
// 文檔id
int docID = scoreDoc.doc;
// 得到文檔
Document doc = indexSearcher.doc(docID);
// 輸出 文件內(nèi)容
System.
out.println("------------------------------");
System.
out.println("文件名稱(chēng) =" + doc.get("fileName"));
System.
out.println("文件大小 =" + doc.get("fileSize"));
System.
out.println("文件內(nèi)容 =" + doc.get("fileContent"));
}
}