Lucene7.4 初體驗
前言
本文的簡要內容:
- Lucene簡介
- 體驗Lucene Demo
- Lucene 核心類介紹
- Lucene 索引檔案格式
Lucene簡介
Lucene是目前最流行的Java開源搜尋引擎類庫,最新版本為7.4.0。Lucene通常用於全文檢索,Lucene具有簡單高效跨平臺等特點,因此有不少搜尋引擎都是基於Lucene構建的,例如:Elasticsearch,Solr等等。
現代搜尋引擎的兩大核心就是索引和搜尋,建立索引的過程就是對源資料進行處理,例如過濾掉一些特殊字元或詞語,單詞大小寫轉換,分詞,建立倒排索引等支援後續高效準確的搜尋。而搜尋則是直接提供給使用者的功能,儘管面向的使用者不同,諸如百度,谷歌等網際網路公司以及各種企業都提供了各自的搜尋引擎。搜尋過程需要對搜尋關鍵詞進行分詞等處理,然後再引擎內部構建查詢,還要根據相關度對搜尋結果進行排序,最終把命中結果展示給使用者。
Lucene只是一個提供索引和查詢的類庫,並不是一個應用,程式設計師需要根據自己的應用場景進行如資料獲取、資料預處理、使用者介面提供等工作。
搜尋程式的典型元件如下所示:
下圖為Lucene與應用程式的關係:
體驗Lucene Demo
接下來先來看一個簡單的demo
引入 Maven 依賴
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<maven.compiler.source >1.8</maven.compiler.source>
<maven.compiler.target>1.8</maven.compiler.target>
<lucene.version>7.4.0</lucene.version>
</properties>
<dependencies>
<dependency>
<groupId>junit</groupId>
<artifactId >junit</artifactId>
<version>4.11</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.apache.lucene</groupId>
<artifactId>lucene-core</artifactId>
<version>${lucene.version}</version>
</dependency>
<dependency>
<groupId>org.apache.lucene</groupId>
<artifactId>lucene-queryparser</artifactId>
<version>${lucene.version}</version>
</dependency>
</dependencies>
索引類 IndexFiles.java
import org.apache.lucene.analysis.*;
import org.apache.lucene.analysis.standard.*;
import org.apache.lucene.document.*;
import org.apache.lucene.index.*;
import org.apache.lucene.store.*;
import java.io.*;
import java.nio.charset.*;
import java.nio.file.*;
import java.nio.file.attribute.*;
public class IndexFiles {
public static void main(String[] args) {
String indexPath = "D:/lucene_test/index"; // 建立索引檔案的目錄
String docsPath = "D:/lucene_test/docs"; // 讀取文字檔案的目錄
Path docDir = Paths.get(docsPath);
IndexWriter writer = null;
try {
// 儲存索引資料的目錄
Directory dir = FSDirectory.open(Paths.get(indexPath));
// 建立分析器
Analyzer analyzer = new StandardAnalyzer();
IndexWriterConfig iwc = new IndexWriterConfig(analyzer);
iwc.setOpenMode(IndexWriterConfig.OpenMode.CREATE);
writer = new IndexWriter(dir, iwc);
indexDocs(writer, docDir);
writer.close();
} catch (IOException e) {
e.printStackTrace();
}
}
private static void indexDocs(final IndexWriter writer, Path path) throws IOException {
if (Files.isDirectory(path)) {
Files.walkFileTree(path, new SimpleFileVisitor<Path>() {
@Override
public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) {
try {
indexDoc(writer, file);
} catch (IOException ignore) {
// 不索引那些不能讀取的檔案,忽略該異常
}
return FileVisitResult.CONTINUE;
}
});
} else {
indexDoc(writer, path);
}
}
private static void indexDoc(IndexWriter writer, Path file) throws IOException {
try (InputStream stream = Files.newInputStream(file)) {
// 建立一個新的空文件
Document doc = new Document();
// 新增欄位
Field pathField = new StringField("path", file.toString(), Field.Store.YES);
doc.add(pathField);
Field contentsField = new TextField("contents",
new BufferedReader(new InputStreamReader(stream, StandardCharsets.UTF_8)));
doc.add(contentsField);
System.out.println("adding " + file);
// 寫文件
writer.addDocument(doc);
}
}
}
查詢類 SearchFiles.java
import org.apache.lucene.analysis.*;
import org.apache.lucene.analysis.standard.*;
import org.apache.lucene.document.*;
import org.apache.lucene.index.*;
import org.apache.lucene.queryparser.classic.*;
import org.apache.lucene.search.*;
import org.apache.lucene.store.*;
import java.io.*;
import java.nio.charset.*;
import java.nio.file.*;
public class SearchFiles {
public static void main(String[] args) throws Exception {
String indexPath = "D:/lucene_test/index"; // 建立索引檔案的目錄
String field = "contents";
IndexReader reader = DirectoryReader.open(FSDirectory.open(Paths.get(indexPath)));
IndexSearcher searcher = new IndexSearcher(reader);
Analyzer analyzer = new StandardAnalyzer();
BufferedReader in = null;
in = new BufferedReader(new InputStreamReader(System.in, StandardCharsets.UTF_8));
QueryParser parser = new QueryParser(field, analyzer);
System.out.println("Enter query:");
// 從Console讀取要查詢的語句
String line = in.readLine();
if (line == null || line.length() == -1) {
return;
}
line = line.trim();
if (line.length() == 0) {
return;
}
Query query = parser.parse(line);
System.out.println("Searching for:" + query.toString(field));
doPagingSearch(searcher, query);
in.close();
reader.close();
}
private static void doPagingSearch(IndexSearcher searcher, Query query) throws IOException {
// TopDocs儲存搜尋結果
TopDocs results = searcher.search(query, 10);
ScoreDoc[] hits = results.scoreDocs;
int numTotalHits = Math.toIntExact(results.totalHits);
System.out.println(numTotalHits + " total matching documents");
for (ScoreDoc hit : hits) {
Document document = searcher.doc(hit.doc);
System.out.println("文件:" + document.get("path"));
System.out.println("相關度:" + hit.score);
System.out.println("================================");
}
}
}
測試
首先建立資料夾 D:\lucene_test
,在 lucene_test
下再建立 docs
資料夾,用來儲存要索引的測試檔案
在 docs
下建立3個檔案 test1.txt, test2.txt, test3.txt,分別寫入 hello world、 hello lucene、 hello elasticsearch
執行索引類 IndexFiles.java,可看到Console輸出
adding D:\lucene_test\docs\test1.txt
adding D:\lucene_test\docs\test2.txt
adding D:\lucene_test\docs\test3.txt
執行查詢類 SearchFiles.java,搜尋 hello ,三個檔案相關度一樣
Enter query:
hello
Searching for:hello
3 total matching documents
文件:D:\lucene_test\docs\test1.txt
相關度:0.13353139
================================
文件:D:\lucene_test\docs\test2.txt
相關度:0.13353139
================================
文件:D:\lucene_test\docs\test3.txt
相關度:0.13353139
================================
搜尋 hello lucene,test2.txt的相關度比其他兩個高
Enter query:
hello lucene
Searching for:hello lucene
3 total matching documents
文件:D:\lucene_test\docs\test2.txt
相關度:1.1143606
================================
文件:D:\lucene_test\docs\test1.txt
相關度:0.13353139
================================
文件:D:\lucene_test\docs\test3.txt
相關度:0.13353139
================================
Lucene 核心類介紹
核心索引類
IndexWriter
進行索引寫操作的一箇中心元件
不能進行讀取和搜尋
Directory
Directory代表Lucene索引的存放位置
常用的實現:
FSDerectory:表示一個儲存在檔案系統中的索引的位置
RAMDirectory:表示一個儲存在記憶體當中的索引的位置
作用:
IndexWriter通過獲取Directory的一個具體實現,在Directory指向的位置中操作索引
Analyzer
Analyzer,分析器,相當於篩子,對內容進行過濾,分詞,轉換等
作用:把過濾之後的資料交給indexWriter進行索引
Document
用來存放文件(資料),該文件為非結構化資料中抓取的相關資料
通過Field(域)組成Document,類似於mysql中的一個個欄位組成的一條記錄
Field
Document中的一個欄位
核心搜尋類
IndexSearcher
IndexSearcher在建立好的索引上進行搜尋
它只能以 只讀 的方式開啟一個索引,所以可以有多個IndexSearcher的例項在一個索引上進行操作
Term
Term是搜尋的基本單元,一個Term由 key:value 組成(類似於mysql中的 欄位名稱=查詢的內容)
例子: Query query = new TermQuery(new Term("filename", "lucene"));
Query
Query是一個抽象類,用來將使用者輸入的查詢字串封裝成Lucene能夠識別的Query
TermQuery
Query子類,Lucene支援的最基本的一個查詢類
例子:TermQuery termQuery = new TermQuery(new Term("filename", "lucene"));
BooleanQuery
BooleanQUery,布林查詢,是一個組合Query(多個查詢條件的組合)
BooleanQuery是可以巢狀的
栗子:
BooleanQuery query = new BooleanQuery();
BooleanQuery query2 = new BooleanQuery();
TermQuery termQuery1 = new TermQuery(new Term("fileName", "lucene"));
TermQuery termQuery2 = new TermQuery(new Term("fileName", "name"));
query2.add(termQuery1, Occur.SHOULD);
query.add(termQuery2, Occur.SHOULD);
query.add(query2, Occur.SHOULD);; //BooleanQuery是可以巢狀的
Occur列舉:
MUST
SHOULD
FILTER
MUST_NOT
NumericRangeQuery
數字區間查詢
栗子:
Query newLongRange = NumericRangeQuery.newLongRange("fileSize",0l, 100l, true, true);
PrefixQuery
字首查詢,查詢分詞中含有指定字元開頭的內容
栗子:
PrefixQuery query = new PrefixQuery(new Term("fileName","hell"));
PhraseQuery
短語查詢
栗子1:
PhraseQuery query = new PhraseQuery();
query.add(new Term("fileName","lucene"));
FuzzyQuery
模糊查詢
栗子:
FuzzyQuery query = new FuzzyQuery(new Term("fileName","lucene"));
WildcardQuery
萬用字元查詢:
* :任意字元(0或多個)
? : 一個字元
栗子:
WildcardQuery query = new WildcardQuery(new Term("fileName","*"));
RegexQuery
正則表示式查詢
栗子:搜尋含有最少1個字元,最多6個字元的
RegexQuery query = new RegexQuery(new Term("fileName","[a-z]{1,6}"));
MultiFieldQueryParser
查詢多個field
栗子:
String[] fields = {"fileName","fileContent"};
MultiFieldQueryParser queryParser = new MultiFieldQueryParser(fields, new StandardAnalyzer());
Query query = queryParser.parse("fileName:lucene AND filePath:a");
TopDocs
TopDocs類是一個簡單的指標容器,指標一般指向前N個排名的搜尋結果,搜尋結果即匹配條件的文件
TopDocs會記錄前N個結果中每個結果的int docID和浮點數型分數(反映相關度)
栗子:
TermQuery searchingBooks = new TermQuery(new Term("subject","search"));
Directory dir = TestUtil.getBookIndexDirectory();
IndexSearcher searcher = new IndexSearcher(dir);
TopDocs matches = searcher.search(searchingBooks, 10);
Lucene 6.0 索引檔案格式
倒排索引
談到倒排索引,那麼首先看看正排是什麼樣子的呢?假設文件1包含【中文、英文、日文】,文件2包含【英文、日文、韓文】,文件3包含【韓文,中文】,那麼根據文件去查詢內容的話
文件1->【中文、英文、日文】
文件2->【英文、日文、韓文】
文件3->【韓文,中文】
反過來,根據內容去查詢文件
中文->【文件1、文件3】
英文->【文件1、文件2】
日文->【文件1、文件2】
韓文->【文件2、文件3】
這就是倒排索引,而Lucene擅長的也正在於此
段(Segments)
Lucene的索引可能是由多個子索引或Segments組成。每個Segment是一個完全獨立的索引,可以單獨用於搜尋,索引涉及
- 為新新增的documents建立新的segments
- 合併已經存在的segments
搜尋可能涉及多個segments或多個索引,每個索引可能由一組segments組成
文件編號
Lucene通過一個整型的文件編號指向每個文件,第一個被加入索引的文件編號為0,後續加入的文件編號依次遞增。
注意文件編號是可能發生變化的,所以在Lucene外部儲存這些值時需要格外小心。
索引結構概述
每個segment索引包括資訊
- Segment info:包含有關segment的元資料,例如文件編號,使用的檔案
- Field names:包含索引中使用的欄位名稱集合
- Stored Field values:對於每個document,它包含屬性-值對的列表,其中屬性是欄位名稱。這些用於儲存有關文件的輔助資訊,例如其標題、url或訪問資料庫的識別符號
- Term dictionary:包含所有文件的所有索引欄位中使用的所有terms的字典。字典還包括包含term的文件編號,以及指向term的頻率和接近度的指標
- Term Frequency data:對於字典中的每個term,包含該term的所有文件的數量以及該term在該文件中的頻率,除非省略頻率(IndexOptions.DOCS)
- Term Proximity data:對於字典中的每個term,term在每個文件中出現的位置。注意,如果所有文件中的所有欄位都省略位置資料,則不會存在
- Normalization factors:對於每個文件中的每個欄位,儲存一個值,該值將乘以該欄位上的匹配的分數
- Term Vectors:對於每個文件中的每個欄位,可以儲存term vector,term vector由term文字和term頻率組成
- Per-document values:與儲存的值類似,這些也以文件編號作為key,但通常旨在被載入到主儲存器中以用於快速訪問。儲存的值通常用於彙總來自搜尋的結果,而每個文件值對於諸如評分因子是有用的
- Live documents:一個可選檔案,指示哪些文件是活動的
- Point values:可選的檔案對,記錄索引欄位尺寸,以實現快速數字範圍過濾和大數值(例如BigInteger、BigDecimal(1D)、地理形狀交集(2D,3D))
檔案命名
屬於一個段的所有檔案具有相同的名稱和不同的副檔名。當使用複合索引檔案,這些檔案(除了段資訊檔案、鎖檔案和已刪除的文件檔案)將壓縮成單個.cfs檔案。當任何索引檔案被儲存到目錄時,它被賦予一個從未被使用過的檔名字
副檔名摘要
名稱 | 副檔名 | 簡短描述 |
---|---|---|
Segments File | segments_N | 儲存了一個提交點(a commit point)的資訊 |
Lock File | write.lock | 防止多個IndexWriter同時寫到一份索引檔案中 |
Segment Info | .si | 儲存了索引段的元資料資訊 |
Compound File | .cfs,.cfe | 一個可選的虛擬檔案,把所有索引資訊都儲存到複合索引檔案中 |
Fields | .fnm | 儲存fields的相關資訊 |
Field Index | .fdx | 儲存指向field data的指標 |
Field Data | .fdt | 文件儲存的欄位的值 |
Term Dictionary | .tim | term詞典,儲存term資訊 |
Term Index | .tip | 到Term Dictionary的索引 |
Frequencies | .doc | 由包含每個term以及頻率的docs列表組成 |
Positions | .pos | 儲存出現在索引中的term的位置資訊 |
Payloads | .pay | 儲存額外的per-position元資料資訊,例如字元偏移和使用者payloads |
Norms | .nvd,.nvm | .nvm檔案儲存索引欄位加權因子的元資料,.nvd檔案儲存索引欄位加權資料 |
Per-Document Values | .dvd,.dvm | .dvm檔案儲存索引文件評分因子的元資料,.dvd檔案儲存索引文件評分資料 |
Term Vector Index | .tvx | 將偏移儲存到文件資料檔案中 |
Term Vector Documents | .tvd | 包含有term vectors的每個文件資訊 |
Term Vector Fields | .tvf | 欄位級別有關term vectors的資訊 |
Live Documents | .liv | 哪些是有效檔案的資訊 |
Point values | .dii,.dim | 保留索引點,如果有的話 |
鎖檔案
預設情況下,儲存在索引目錄中的鎖檔名為 write.lock
。如果鎖目錄與索引目錄不同,則鎖檔案將命名為“XXXX-write.lock”,其中XXXX是從索引目錄的完整路徑匯出的唯一字首。此鎖檔案確保每次只有一個寫入程式在修改索引。