
寫在前面:本文中用到的 Apache Lucene 版本號是 4.10.2 截止到文章發(fā)布時官方的最新版本是 6.5.1 因不同的版本差異較大,請大家在學習過程中確認下版本號是否一致。本文中的所涉及到的源碼分享在在 Gighub 鏈接地址:Part01_Lucene
1.搜索引擎
1.1 - 概述
- 概述:根據(jù)一定的策略、運用特定的計算機程序從互聯(lián)網(wǎng)上搜集信息,再對信息進行 組織(分詞) 和 處理(添加索引) 后,為用戶提供檢索服務(wù),將用戶檢索相關(guān)的信息展示給用戶的系統(tǒng)。搜索引擎包括 全文索引、目錄索引、 元搜索引擎、垂直搜索引擎、集合式搜索引擎 、門戶搜索引擎 與 免費鏈接列表等。
1.2 - 搜索原理

1.3 - 應(yīng)用場景
- 大型綜合搜索網(wǎng)站
- 站內(nèi)搜索
- 垂直領(lǐng)域搜索
- 軟甲內(nèi)部搜索
1.4 - 搜索技術(shù)
- SQL 進行模糊查詢:如果沒有前置
%可以執(zhí)行索引,如果添加前置%則全文檢索。 - Lucene:解決在海量數(shù)據(jù)的情況下,利用 倒排索引 技術(shù),實現(xiàn)快速的 搜索 、 打分 、 排序 等功能
1.5 - 倒排索引
根據(jù)詞條查找文檔
-
名詞解釋:
- 文檔(Document):索引庫中的每一條原始數(shù)據(jù)。
- 詞條(Term):原始數(shù)據(jù)按照算法進行 分詞,得到的每一個詞語。
- 文檔列表:Lucene 對原始文檔進行編號(DocID),形成的列表就是文檔列表。
創(chuàng)建文檔列表:Lucene 首先對原始文檔數(shù)據(jù)進行編號(DocId),形成文檔列表
| 文檔編號 | ID | Title |
|---|---|---|
| 0 | 1 | 谷歌地圖之父跳槽Facebook |
| 1 | 2 | 谷歌地圖之父加盟Facebook |
| 2 | 3 | 谷歌地圖創(chuàng)始人拉斯離開谷歌加盟Facebook |
| 3 | 4 | 谷歌地圖之父跳槽Facebook與Wave項目取消有關(guān) |
| 4 | 5 | 谷歌地圖之父拉斯加盟社交網(wǎng)站Facebook |
- 創(chuàng)建倒排索引列表:對文檔中數(shù)據(jù)進行分詞,得到 詞條(Term)。對詞條添加編號并創(chuàng)建索引,并在詞條中記錄包含該詞條的所有文檔編號及其他信息。
| 詞條ID | 詞條 | 倒排列表(包含該詞條文檔 ID) |
|---|---|---|
| 1 | 谷歌 | 0,1,2,3,4 |
| 2 | 地圖 | 0,1,2,3,4 |
| 3 | 之父 | 0,1,3,4 |
| 4 | 跳槽 | 0,3 |
| 5 | 0,1,2,3,4 | |
| 6 | 加盟 | 1,2,4 |
| 7 | 創(chuàng)始人 | 2 |
| 8 | 拉斯 | 2,4 |
| 9 | 離開 | 2 |
| 10 | 與 | 3 |
| 11 | wave | 3 |
| 12 | 項目 | 3 |
| 13 | 取消 | 3 |
| 14 | 有關(guān) | 3 |
| 15 | 社交 | 4 |
| 16 | 網(wǎng)站 | 4 |
- 搜索過程:
- 獲得用戶搜索內(nèi)容,對搜索內(nèi)容進行分詞,得到用戶搜索的所有詞條。
- 將詞條在倒排索引列表中進行匹配,得到包含該詞條的所有文檔編號。
2.Lucene

2.1 - 概述
- 用于全文檢索和搜尋的開源程序庫,由 Apache 軟件基金會支持和提供。Lucene 提供了簡單強大應(yīng)用程序接口(API),可以進行全文索引和搜索,可以用來制作搜索引擎產(chǎn)品。
2.2 - 全文檢索
- 計算機索引程序通過掃描文章中的每一個詞,對每一個詞建立一個索引,指明該詞在文章中出現(xiàn)的 次數(shù) 和 位置 ,當用戶查詢時,檢索程序就根據(jù)事先建立的索引進行查找,并將查找的結(jié)果反饋給用戶的檢索方式。
- 總結(jié):Lucene 全文檢索就是對文檔中全部內(nèi)容進行分詞,然后對所有單詞建立倒排索引的過程。
3.QuickStart
3.1 - 創(chuàng)建索引流程圖

-
創(chuàng)建文檔對象(Document),并添加索引Field字段(Field)
- 索引字段:Field
創(chuàng)建目錄對象(Directory)并指定索引在硬盤中存儲位置
創(chuàng)建分詞器對象(Analyzer)
-
創(chuàng)建索引寫出器配置對象(IndexWriterConfig)
- 索引分詞器:Analyzer
- 版本:Version
-
創(chuàng)建索引寫出器(IndexWriter)
- 目錄:Directory
- 索引寫出器配置:IndexWriterConfig
-
索引寫出器,添加文檔對象
- 文檔:Document
提交并關(guān)閉索引寫出器
3.2 - 創(chuàng)建索引
-
導入依賴 jar:
<dependency> <groupId>org.apache.lucene</groupId> <artifactId>lucene-core</artifactId> <version>${lucene.version}</version> </dependency> <dependency> <groupId>org.apache.lucene</groupId> <artifactId>lucene-analyzers-common</artifactId> <version>${lucene.version}</version> </dependency> -
代碼示例:
public class QuickStartTest { @Test public void createTest() throws IOException { /* 1.創(chuàng)建文檔對象 */ Document document = new Document(); /* * 添加字段 * * StringField: Field.Store.YES 表示存儲到文檔列表 * TestField: 既創(chuàng)建索引 又分詞 * */ document.add(new StringField("id", "1", Field.Store.YES)); document.add(new TextField("title", "谷歌地圖之父跳槽facebook", Field.Store.YES)); /* * 2.創(chuàng)建目錄類 指定索引在硬盤中位置 * */ Directory directory = FSDirectory.open(new File("/Users/zhangsiyao1/Desktop/indexDir")); /* * 3.創(chuàng)建分詞器對象 analyzer * */ Analyzer analyzer = new StandardAnalyzer(); /* * 4.索引寫出工具配置對象 config * */ IndexWriterConfig config = new IndexWriterConfig(Version.LATEST, analyzer); /* * 5.創(chuàng)建索引寫出工具類 * */ IndexWriter writer = new IndexWriter(directory, config); /* * 6.將文檔添加到寫出器工具類中 * */ writer.addDocument(document); /* * 7.提交 & 關(guān)閉 寫出工具 * */ writer.commit(); writer.close(); } }
3.3 - 使用 lukeall 工具查看索引
- Google 下載地址:lukeall

4.創(chuàng)建索引詳解
4.1 - Document

- Document:代表一行數(shù)據(jù)
- Field:代表 Document 中的一個字段
4.2 - Field

- 存儲 :StoreField 支持(byte[]、BytesRef、double、float、int、long、String)
- 創(chuàng)建索引 + 可選存儲 :DoubleField、FloatField、IntField、LongField、StringField
- 創(chuàng)建索引 + 可選存儲 + 分詞 :TestField
- 是否存儲?:如果字段需要顯示到最終結(jié)果中,則需要存儲。
- 是否創(chuàng)建索引?:如果根據(jù)該字段進行索引,則需要創(chuàng)建索引。
- 是否分詞?:前提需要創(chuàng)建索引,如果字段是不可分割的則不需要分詞。
4.3 - Directory

- FSDirectory:文件系統(tǒng)目錄,將索引庫指向本地磁盤。
- 特點:速度略慢,較安全,節(jié)約內(nèi)存。
- RAMDirectory:內(nèi)存目錄,將索引保存在內(nèi)存中。
- 特點:速度快,不安全,占用內(nèi)存。
4.4 - Analyzer

沒有適合中文的分詞器,ChineseAnalyzer(棄用),需要使用第三方分詞器
-
maven 導入 IKAnalyzer
<dependency> <groupId>com.janeluo</groupId> <artifactId>ikanalyzer</artifactId> <version>2012_u6</version> </dependency> -
擴展詞典和停用詞典:在 resources 下創(chuàng)建 IKAnalyzer.cfg.xml
<?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>
4.5 - IndexWriterConfig
- 設(shè)置寫出時,是否先清除索引庫中數(shù)據(jù):
public IndexWriterConfig setOpenMode(OpenMode openMode)
- 打開索引庫類型:
public static enum OpenMode {
/**
* Creates a new index or overwrites an existing one.
*/
CREATE,
/**
* Opens an existing index.
*/
APPEND,
/**
* Creates a new index if one does not exist,
* otherwise it opens the index and documents will be appended.
*/
CREATE_OR_APPEND
}
4.6 - IndexWriter
- 批量創(chuàng)建索引:
public void addDocuments(Iterable<? extends Iterable<? extends IndexableField>> docs)
5.查詢索引
5.1 - 基本查詢
public class QueryTest {
private static final File INDEX_DIR_FILE = new File("/Users/zhangsiyao1/Desktop/indexDir");
@Test
public void baseSearchTest() throws IOException, ParseException {
/* 索引目錄對象 */
Directory directory = FSDirectory.open(INDEX_DIR_FILE);
/* 索引讀取工具 */
DirectoryReader directoryReader = DirectoryReader.open(directory);
/* 索引搜索工具 */
IndexSearcher indexSearcher = new IndexSearcher(directoryReader);
/*
* 創(chuàng)建查詢解析器
* 1.查詢字段名稱
* 2.分詞解析器
* */
QueryParser queryParser = new QueryParser("title", new IKAnalyzer());
/* 獲取查詢對象 */
Query query = queryParser.parse("谷歌地圖之父拉斯");
/*
* 搜索數(shù)據(jù)
* 1.查詢解析器解析后的查詢結(jié)果
* 2.查詢結(jié)果的最大條數(shù)
* */
TopDocs topDocs = indexSearcher.search(query, 10);
/* 獲取總條數(shù) */
int totalHits = topDocs.totalHits;
System.out.println("本地搜索共查詢到 " + totalHits + " 匹配記錄");
/*
* 得分文檔數(shù)組
* 1.doc 文檔編號(ID)
* 2.score 文檔得分數(shù)
* */
ScoreDoc[] scoreDocs = topDocs.scoreDocs;
for (ScoreDoc scoreDoc : scoreDocs) {
/* 文檔編號 */
int docID = scoreDoc.doc;
/* 通過索引讀取器 根據(jù)文檔編號獲取文檔 */
Document document = directoryReader.document(docID);
System.out.println("DocID: " + document.get("id"));
System.out.println("Title: " + document.get("title"));
/* 文檔得分 */
System.out.println("Score: " + scoreDoc.score);
}
}
}
6.查詢索引詳解
- 封裝 Lucene 查詢工具類 LuceneQueryUtils
public class LuceneQueryUtils {
private static final File INDEX_DIR_FILE = new File("/Users/zhangsiyao1/Desktop/indexDir");
public static void queryByQuery(Query query, int maxResult) throws IOException {
/* 索引目錄對象 */
Directory directory = FSDirectory.open(INDEX_DIR_FILE);
/* 索引讀取工具 */
DirectoryReader directoryReader = DirectoryReader.open(directory);
/* 索引搜索工具 */
IndexSearcher indexSearcher = new IndexSearcher(directoryReader);
/* 搜索數(shù)據(jù) */
TopDocs topDocs = indexSearcher.search(query, maxResult);
int totalHits = topDocs.totalHits;
System.out.println("本地搜索共查詢到 " + totalHits + " 匹配記錄");
System.out.println("=======================================");
/*
* 得分文檔數(shù)組
* */
ScoreDoc[] scoreDocs = topDocs.scoreDocs;
for (ScoreDoc scoreDoc : scoreDocs) {
/* 文檔編號 */
int docID = scoreDoc.doc;
/* 通過索引讀取器 根據(jù)文檔編號獲取文檔 */
Document document = directoryReader.document(docID);
System.out.println("DocID: " + document.get("id"));
System.out.println("Title: " + document.get("title"));
System.out.println("Score: " + scoreDoc.score);
System.out.println("=======================================");
}
}
}
6.1 - MultiFieldQueryParser
-
根據(jù)多字段查詢:MultiFieldQueryParser
MultiFieldQueryParser queryParser = new MultiFieldQueryParser( new String[]{"id", "title"}, new IKAnalyzer() );
6.2 - Query
- 方式一:通過 QueryParser 解析關(guān)鍵字,得到查詢對象。
- 方式二:自定義查詢對象,通過 Query 子類,創(chuàng)建查詢對象,實現(xiàn)高級查詢。
6.3 - IndexSearch
功能:快速搜索、排序、打分等功能,其依賴于 IndexReader 對象。
-
基本創(chuàng)建過程:
/* 索引目錄對象 */ Directory directory = FSDirectory.open(INDEX_DIR_FILE); /* 索引讀取工具 */ DirectoryReader directoryReader = DirectoryReader.open(directory); /* 索引搜索工具 */ IndexSearcher indexSearcher = new IndexSearcher(directoryReader); -
根據(jù)打分排序指定位置結(jié)果:
TopDocs topDocs = indexSearcher.search(query, 10);
6.4 - TopDocs
-
獲取對象:
TopDocs topDocs = indexSearcher.search(query, 10); -
包含內(nèi)容:
-
int totalHints:查詢的總條數(shù) -
ScoreDoc[] scoreDocs:得分文檔對象數(shù)組
-
6.5 - ScoreDoc
- 包含內(nèi)容:
-
int doc:文檔編號(ID),根據(jù)文檔 ID 獲取指定文檔Document document = directoryReader.document(docID); float score:文檔得分
-
7.高級查詢
7.1 - 詞條查詢
- 概述:詞條是搜索的最小單位 不可再分割 且值必須是字符串
public void termQueryTest() throws IOException {
TermQuery termQuery = new TermQuery(new Term("title", "谷歌地圖"));
LuceneQueryUtils.queryByQuery(termQuery, 10);
}
7.2 - 通配符查詢
-
?:任意 1 個字符 -
*:任意 n 字符
public void wildcardQuery() throws IOException {
WildcardQuery query = new WildcardQuery(new Term("title", "*歌"));
LuceneQueryUtils.queryByQuery(query, 10);
}
7.3 - 模糊查詢
-
maxEdits:最大編輯距離 一個單詞到另一個單詞最少修改次數(shù) [0,2]
public void fuzzyQueryTest() throws IOException {
FuzzyQuery fuzzyQuery = new FuzzyQuery(new Term("title", "facebool"), 1);
LuceneQueryUtils.queryByQuery(fuzzyQuery, 10);
}
7.4 - 數(shù)值范圍查詢
- 應(yīng)用:
id[2L,2L]對非 String 類型的 ID 進行精確查找
public void numericRangeQueryTest() throws IOException {
NumericRangeQuery<Long> numericRangeQuery = NumericRangeQuery.newLongRange("id", 2L, 2L, true, true);
LuceneQueryUtils.queryByQuery(numericRangeQuery, 10);
}
7.5 - 組合查詢
- 交集:
Occur.MUST+Occur.MUST - 并集:
Occur.SHOULD+Occur.SHOULD - 補集:
Occur.MUST_NOT
public void booleanQueryTest() throws IOException {
NumericRangeQuery<Long> numericRangeQuery1 = NumericRangeQuery.newLongRange("id", 1L, 3L, true, true);
NumericRangeQuery<Long> numericRangeQuery2 = NumericRangeQuery.newLongRange("id", 2L, 4L, true, true);
BooleanQuery booleanQuery = new BooleanQuery();
booleanQuery.add(numericRangeQuery1, BooleanClause.Occur.MUST_NOT);
booleanQuery.add(numericRangeQuery2, BooleanClause.Occur.SHOULD);
LuceneQueryUtils.queryByQuery(booleanQuery, 10);
}
8.修改索引
- 問題:修改索引時,只能指定詞條(Term)進行更新,詞條只能是 String 類型,如果 id 為數(shù)值類型怎么更新?
- 答案:先刪除,再更新
public class UpdateIndexTest {
private static final File INDEX_DIR_FILE = new File("/Users/zhangsiyao1/Desktop/indexDir");
/*
* 1.Lucene 底層先刪除所有匹配的索引 再添加新文檔
* 2.一般修改功能會根據(jù) Term 詞條進行匹配
* 3.根據(jù)一個唯一不重復(fù)字段進行匹配(ID)
*
* 問題: update 時 Term 詞條搜索 要求 ID 必須是字符串 如果不是則不能使用這個方法
* 解決: 先刪除該詞條 再添加更新后的詞條
* */
@Test
public void updateTest() throws IOException {
/* 創(chuàng)建目錄對象 */
FSDirectory directory = FSDirectory.open(INDEX_DIR_FILE);
/* 創(chuàng)建索引寫出器配置對象 */
IndexWriterConfig config = new IndexWriterConfig(Version.LATEST, new IKAnalyzer());
/* 創(chuàng)建索引寫出器 */
IndexWriter writer = new IndexWriter(directory, config);
Document document = new Document();
document.add(new StringField("id", "1", Field.Store.YES));
document.add(new TextField("title", "谷歌地圖之父跳槽facebook為了加入Amazon", Field.Store.YES));
writer.updateDocument(new Term("id", "1"), document);
writer.commit();
writer.close();
}
}
9.刪除索引
- 方式一:根據(jù) Term 刪除,只能根據(jù) String 類型的詞條進行匹配刪除。
- 方式二:根據(jù) Query 刪除,可以是任意類型的詞條進行匹配(更新 ID 非 String 類型文檔的解決方案)。
- 方式三:刪除所有。
public class UpdateIndexTest {
/*
* 刪除索引
* */
@Test
public void deleteTest() throws IOException {
/* 創(chuàng)建目錄對象 */
FSDirectory directory = FSDirectory.open(INDEX_DIR_FILE);
/* 創(chuàng)建索引寫出器配置對象 */
IndexWriterConfig config = new IndexWriterConfig(Version.LATEST, new IKAnalyzer());
/* 創(chuàng)建索引寫出器 */
IndexWriter writer = new IndexWriter(directory, config);
/*
* 1.根據(jù)詞條 Term 進行刪除 只能匹配 字符串類型 字段
* */
writer.deleteDocuments(new Term("id", "1"));
/*
* 2.根據(jù) Query 刪除 可以匹配任何類型的字段
* */
NumericRangeQuery<Long> numericRangeQuery = NumericRangeQuery.newLongRange("id", 2L, 2L, true, true);
writer.deleteDocuments(numericRangeQuery);
/* 3.刪除所有 */
writer.deleteAll();
writer.commit();
writer.close();
}
}
10.Lucene 高級使用
10.1 - 高亮顯示
-
SimpleHTMLFormatter:HTML 格式化工具 -
Highlighter:高亮工具
@Test
public void highLightTest() throws IOException, ParseException, InvalidTokenOffsetsException {
/* 目錄對象 */
FSDirectory directory = FSDirectory.open(INDEX_DIR_FILE);
/* 讀取工具 */
DirectoryReader reader = DirectoryReader.open(directory);
/* 搜索工具 */
IndexSearcher searcher = new IndexSearcher(reader);
/* parse 方式獲得 Query 對象 */
QueryParser queryParser = new QueryParser("title", new IKAnalyzer());
Query query = queryParser.parse("谷歌地圖");
/* HTML 格式化器 */
Formatter formatter = new SimpleHTMLFormatter("<em>", "</em>");
QueryScorer queryScorer = new QueryScorer(query);
/* 準備高亮工具 */
Highlighter highlighter = new Highlighter(formatter, queryScorer);
/* 搜索 */
TopDocs topDocs = searcher.search(query, 10);
System.out.println("TotalHits: " + topDocs.totalHits);
for (ScoreDoc scoreDoc : topDocs.scoreDocs) {
Document document = reader.document(scoreDoc.doc);
/*
* 高亮工具處理普通查詢結(jié)果
* 參數(shù)一: 分詞器
* 參數(shù)二: 高亮字段名
* 參數(shù)三: 高亮字段原始值
* */
String highLightTitle = highlighter.getBestFragment(new IKAnalyzer(), "title", document.get("title"));
System.out.println(highLightTitle);
}
}
- 導入依賴 jar:
<dependency>
<groupId>org.apache.lucene</groupId>
<artifactId>lucene-highlighter</artifactId>
<version>${lucene.version}</version>
</dependency>
10.2 - 排序
Sort sortArray = new Sort(new SortField("id", SortField.Type.LONG, true));
TopDocs topDocs = searcher.search(query, 10, sortArray);
悄悄話 ??
- 最近項目進度比較緊,基本是有時間學習技術(shù),沒時間寫出來的樣子,這兩天趁著休息時間將之前的一些學習內(nèi)容按照先后順序陸續(xù)整理一下與大家分享。
彩蛋 ??
-
最近開通了文集的同名專題 《Java大數(shù)據(jù)開發(fā)》 并會從大數(shù)據(jù)開發(fā)的基礎(chǔ)技術(shù)向下延伸至云服務(wù),有興趣的朋友可以來一同交流進步。
如果你覺得我的分享對你有幫助的話,請在下面??隨手點個喜歡 ??,你的肯定才是我最大的動力,感謝。