一、 Lucene概述
1.1 Lucene是什麼
Lucene是apache軟體基金會4 jakarta項目組的一個子項目,是一個開放源代碼的全文檢索引擎工具包,但它不是一個完整的全文檢索引擎,而是一個全文檢索引擎的架構,提供了完整的查詢引擎和索引引擎,部分文本分析引擎(英文與德文兩種西方語言)。
Lucene的目的是為軟體開發人員提供一個簡單易用的工具包,以友善的在目标系統中實作全文檢索的功能,或者是以此為基礎建立起完整的全文檢索引擎。
目前已經有很多應用程式的搜尋功能是基于 Lucene 的,比如 Eclipse 的幫助系統的搜尋功能。Lucene 能夠為文本類型的資料建立索引,是以你隻要能把你要索引的資料格式轉化的文本的,Lucene 就能對你的文檔進行索引和搜尋。比如你要對一些 HTML 文檔,PDF 文檔進行索引的話你就首先需要把 HTML 文檔和 PDF 文檔轉化成文本格式的,然後将轉化後的内容交給 Lucene 進行索引,然後把建立好的索引檔案儲存到磁盤或者記憶體中,最後根據使用者輸入的查詢條件在索引檔案上進行查詢。不指定要索引的文檔的格式也使 Lucene 能夠幾乎适用于所有的搜尋應用程式。
簡單概括:
- Lucene是一套用于全文檢索和搜尋的開源程式庫,由Apache軟體基金會支 持和提供;
- Lucene提供了一個簡單卻強大的應用程式接口,能夠做全文索引和搜尋, 在Java開發環境裡Lucene是一個成熟的免費開放源代碼工具;
- Lucene并不是現成的搜尋引擎産品,但可以用來制作搜尋引擎産品;
官網位址: http://lucene.apache.org/
1.2 Lucene的工作流程
索引:綠色部分表示索引。在搜尋前需要先對原始内容進行索引,建構索引庫。
索引過程:确定原始内容 > 獲得文檔 > 建立文檔 > 分析文檔 > 索引文檔。
搜尋:紅色部分代表搜尋。搜尋即從索引庫中搜尋内容。
搜尋過程:建立查詢 > 執行搜尋 > 從索引庫搜尋 > 渲染搜尋結果。
二、Lucene環境安裝
開發環境:
JDK: 1.8.0_144
IDE: eclipse Luna
資料庫: MySQL5.6.38
2.1 建立Maven工程,引入lucene相關坐标。
<dependencies>
<dependency>
<groupId>org.apache.lucene</groupId>
<artifactId>lucene-core</artifactId>
<version>8.2.0</version>
</dependency>
<dependency>
<groupId>org.apache.lucene</groupId>
<artifactId>lucene-queryparser</artifactId>
<version>8.2.0</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.22</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.16.18</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.12</version>
</dependency>
</dependencies>
2.2 建立資料庫
# 建立資料庫
create database solr;
# 標明資料庫
use solr;
# 建立圖書表
CREATE TABLE `book` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`name` varchar(255) NOT NULL,
`price` int(11) NOT NULL DEFAULT '0',
`pic` varchar(255) DEFAULT NULL,
`remark` varchar(2000) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=5 DEFAULT CHARSET=utf8;
# 初始化資料
INSERT INTO `book` VALUES ('1', 'jvm原理', '49', '/uploads/1.jpg', '深入淺出介紹jvm的實作原理');
INSERT INTO `book` VALUES ('2', '程式員基本功', '149', '/uploads/2.jpg', '學習jvm基本功');
INSERT INTO `book` VALUES ('3', 'lucene搜尋引擎', '79', '/uploads/3.jpg', '深入淺出蜘蛛 爬蟲 lutch 資料分析');
INSERT INTO `book` VALUES ('4', '搜尋引擎内部剖析', '59', '/uploads/4.jpg', '呵呵');
2.3 建立Beans
@Data
public class Book {
// 圖書ID
private Integer id;
// 圖書名稱
private String name;
// 圖書價格
private Float price;
// 圖書圖檔
private String pic;
// 圖書描述
private String remark;
}
2.4 建立Dao接口
public interface BookDao {
List<Book> queryBookList();
}
2.5 建立Dao實作類
public class BookDaoImpl implements BookDao {
@Override
public List<Book> queryBookList() {
// 資料庫連結
Connection connection = null;
// 預編譯statement
PreparedStatement preparedStatement = null;
// 結果集
ResultSet resultSet = null;
// 圖書清單
List<Book> list = new ArrayList<Book>();
try {
// 加載資料庫驅動
Class.forName("com.mysql.jdbc.Driver");
// 連接配接資料庫
connection = DriverManager.getConnection("jdbc:mysql://localhost:3306/solr", "root", "root");
// SQL語句
String sql = "SELECT * FROM book";
// 建立preparedStatement
preparedStatement = connection.prepareStatement(sql);
// 擷取結果集
resultSet = preparedStatement.executeQuery();
// 結果集解析
while (resultSet.next()) {
Book book = new Book();
book.setId(resultSet.getInt("id"));
book.setName(resultSet.getString("name"));
book.setPrice(resultSet.getFloat("price"));
book.setPic(resultSet.getString("pic"));
book.setRemark(resultSet.getString("remark"));
list.add(book);
}
} catch (Exception e) {
e.printStackTrace();
}
return list;
}
}
三、實作索引
第一步:建立Document對象;
第二步:建立解析器對象;
第三步:建立Directory對象,該對象聲明了索引庫的位置;
第四步:建立IndexWriter對象,該對象負責把Document寫入索引庫檔案中;
第五步:釋放資源;
@Test
public void testCreateIndex() throws Exception {
// 采集資料
BookDao bookDao = new BookDaoImpl();
List<Book> bookList = bookDao.queryBookList();
// 建立Document對象,每一個Document對象對應資料庫表的一行記錄
List<Document> documents = new ArrayList<>();
for (Book book : bookList) {
Document document = new Document();
// 添加Field域
document.add(new TextField("id", book.getId().toString(), Store.YES));
// 圖書名稱
document.add(new TextField("name", book.getName().toString(), Store.YES));
// 圖書價格
document.add(new TextField("price", book.getPrice().toString(), Store.YES));
// 圖書圖檔位址
document.add(new TextField("pic", book.getPic().toString(), Store.YES));
// 圖書描述
document.add(new TextField("remark", book.getRemark().toString(), Store.YES));
// 把Document放到list中
documents.add(document);
}
// 建立Analyzer分詞器,分析文檔,對文檔進行分詞
Analyzer analyzer = new StandardAnalyzer();
// 建構建立檔案目錄
Directory directory = FSDirectory.open(Paths.get("d:/lucene/index"));
// 建立IndexWriteConfig對象,寫入索引需要的配置
IndexWriterConfig config = new IndexWriterConfig(analyzer);
// 建立IndexWriter寫入對象
IndexWriter indexWriter = new IndexWriter(directory, config);
// 把Document寫入到索引庫
for (Document doc : documents) {
indexWriter.addDocument(doc);
}
// 8.釋放資源
indexWriter.close();
}
運作程式,可以在d:/lucene/index目錄下看到建立的索引檔案。
四、搜尋索引
4.1 搜尋分詞
和索引過程的分詞一樣,這裡要對使用者輸入的關鍵字進行分詞,一般情況索引和搜尋使用的分詞器一緻。比如搜尋“java教育訓練”,分詞處理後變成了“java”和“教育訓練”兩個詞。這時候與java和教育訓練相關的内容都會被查詢出來。
4.2 使用QueryParser對象實作搜尋
第一步:讀取索引檔案;
第二步:建立搜尋器;
第三步:建立解析器對象;
第四步:建立查詢解析器對象,該對象會使用剛建立的解析器對象對指定查詢内容進行解析;
第五步:執行搜尋;
第六步:處理搜尋結果;
第七部:關閉資源;
@Test
public void testSearchIndex2() throws IOException, ParseException {
// 建立Directory流對象,聲明索引庫位置
Directory directory = FSDirectory.open(Paths.get("d:/lucene/index"));
// 建立索引讀取對象IndexReader
IndexReader reader = DirectoryReader.open(directory);
// 建立索引搜尋對象
IndexSearcher searcher = new IndexSearcher(reader);
// 建立分詞器
Analyzer analyzer = new StandardAnalyzer();
// 建立搜尋解析器,第一個參數:預設Field域,第二個參數:分詞器
QueryParser queryParser = new QueryParser("name", analyzer);
Query query = queryParser.parse("name:jvm");
// 使用索引搜尋對象,執行搜尋,傳回結果集TopDocs
TopDocs topDocs = searcher.search(query, 10);
System.out.println("查詢到的資料總條數是:" + topDocs.totalHits);
// 擷取查詢結果集
ScoreDoc[] docs = topDocs.scoreDocs;
// 6. 解析結果集
for (ScoreDoc scoreDoc : docs) {
System.out.println("=============================");
// 擷取文檔ID
int docID = scoreDoc.doc;
// 根據文檔ID擷取文檔
Document doc = searcher.doc(docID);
// 擷取文檔中的每一個字段内容
System.out.println("bookId:" + doc.get("id"));
System.out.println("name:" + doc.get("name"));
System.out.println("price:" + doc.get("price"));
System.out.println("pic:" + doc.get("pic"));
System.out.println("remark:" + doc.get("remark"));
}
// 7. 釋放資源
reader.close();
}
上面search方法傳回一個TopDocs對象,該對象封裝了查詢結果資訊。 該對象包含兩個常用屬性:
totalHits:比對搜尋條件的總記錄數;
scoreDocs:傳回比對度最高的記錄;
比如:indexSearcher.search(query, 10),那麼最多比對10條記錄。
查詢結果如下圖所示:
五、分詞器
5.1 什麼是分詞
分詞就是把采集到的資料存儲到document對象的Field域中,分詞就是将Document中Field的value值切分成一個一個的詞。
5.2 分詞器的執行過程
分詞(Tokenizer)的主要過程就是先分詞後過濾(TokenFilter)。過濾的工作主要包括:去除标點符号過濾、去除停用詞過濾(的、是、a、an、the等)、大寫轉小寫、詞形還原(複數形式轉成單數形參、過去式轉成現在式)等等。
分詞器的執行過程如下圖所示:
從圖上可以看到,分詞完成後會經過一系列的過濾,最後才得到一個個的Token。我們可以把token了解為一個個的單詞。
例如:
原文:Lucene is a full-text search enginie.
分詞後:lucene、is、a、full、text、search、engine,一共7個詞。
5.3 中文分詞器
雖然lucene自帶了中文分詞器,但是許多人使用後都覺得lucene自帶的中文分詞功能比較弱,分詞的效果不太令人滿意。是以建議使用第三方的分詞器。例如:IkAnalyzer。
下面列出一些常見的分詞器:
分詞器 | 描述 |
---|---|
StandardAnalyzer | 按照中文一個字一個字地進行分詞,比如:中國,效果:中、國 |
CJKAnalyzer | 按兩個字進行切分。如:“我是中國人”,效果:“我是”、“是中”、“中國”“國人” |
SmartChineseAnalyzer | 相對來說對中文支援較好,但擴充性差,擴充詞庫,禁用詞庫等不好處理 |
IK-analyzer | 對中文支援較好,實作了簡單的分詞 ,歧義排除算法,而且版本還不斷更新,推薦使用 |
5.4 使用IkAnalyzer
第一步:引入坐标。
<dependency>
<groupId>com.jianggujin</groupId>
<artifactId>IKAnalyzer-lucene</artifactId>
<version>8.0.0</version>
</dependency>
第二步:建立分詞器;
//Analyzer analyzer = new StandardAnalyzer();
Analyzer analyzer = new IKAnalyzer();
六、Field詳解
Lucene中的Field相當于資料庫表中的字段,一個文檔可以包括多個Field,Document隻是Field的一個承載體,Field值即為要索引的内容,也是要搜尋的内容。
- 是否分詞
如果是,則進行分詞處理。比如:商品名稱、商品描述等,這些内容使用者要輸入關鍵字搜尋,由于搜尋的内容格式大、内容多需要分詞後将語彙單元建立索引。
如果否,則不進行分詞處理。比如:商品id、訂單号、身份證号等。
- 是否索引
如果是,則将分詞後的詞或整個Field值進行索引,存儲到索引域。索引的目的是為了搜尋。比如:商品名稱、商品描述。訂單号、身份證号不用分詞但也要索引,這些将來都要作為查詢條件。
如果否,則不進行索引。比如:圖檔路徑、檔案路徑等,不用作為查詢條件的不用索引。
- 是否存儲
如果是,則将Field值存儲在文檔域中。存儲在文檔域中的Field才可以從Document中擷取。比如:商品名稱、訂單号,凡是将來要從Document中擷取的Field都要存儲。
如果否,則不存儲Field的值。比如:商品描述,内容較大不用存儲。
6.1 常見Field類型
Field | 資料類型 | 是否分詞 | 是否索引 | 是否存儲 | 說明 |
---|---|---|---|---|---|
StringField | 字元串 | N | Y | Y或N | 這個Field用來建構一個字元串Field,但是不會進行分詞,會将整個串存儲在索引中,比如(訂單号,身份證号等),是否存儲在文檔中用Store.YES或Store.NO決定 |
NumericDocValuesField | long | Y | Y | Y或N | 這個Field用來建構一個Long數字型Field,進行分詞和索引,比如(價格),是否存儲在文檔中用Store.YES或Store.NO決定 |
StoredField | 支援多種類型 | N | N | Y | 這個Field用來建構不同類型Field不分析,不索引,但要Field存儲在文檔中 |
TextField | 字元串或流 | Y | Y | Y或N | 如果是一個Reader, lucene猜測内容比較多,會采用Unstored的政策 |
6.2 Field改造
// 圖書ID,不分詞,不索引,儲存
document.add(new StoredField("id", book.getId().toString()));
// 圖書名稱,分詞,索引,儲存
document.add(new TextField("name", book.getName().toString(), Store.YES));
// 圖書價格,分詞,索引,不儲存
document.add(new DoublePoint("price", book.getPrice()));
// 圖檔位址,不分詞,不索引,儲存
document.add(new StoredField("pic", book.getPic().toString()));
// 圖書描述,分詞,索引,不儲存
document.add(new TextField("remark", book.getRemark().toString(), Store.NO));
七、索引維護
7.1 删除索引
IndexWriter提供了兩個方法實作索引的删除:
1)deleteDocuments:根據條件删除索引;
2)deleteAll:删除所有索引;
@Test
public void testIndexDelete() throws Exception {
// 建立Directory流對象
Directory directory = FSDirectory.open(Paths.get("d:/lucene/index"));
// 建立分詞器
Analyzer analyzer = new IKAnalyzer();
// 建立IndexWriterConfig對象,該對象封裝了IndexWriter的配置資訊
IndexWriterConfig config = new IndexWriterConfig(analyzer);
// 建立IndexWriter對象
IndexWriter indexWriter = new IndexWriter(directory, config);
// 根據Term删除索引庫,name:lucene
indexWriter.deleteDocuments(new Term("name", "lucene"));
// 釋放資源
indexWriter.close();
}
删除所有索引:
@Test
public void testIndexAllDelete() throws Exception {
// 建立Directory流對象
Directory directory = FSDirectory.open(Paths.get("d:/lucene/index"));
IndexWriterConfig config = new IndexWriterConfig();
// 建立寫入對象
IndexWriter indexWriter = new IndexWriter(directory, config);
// 根據Term删除索引庫,name:lucene
indexWriter.deleteAll();
// 釋放資源
indexWriter.close();
}
7.2 修改索引
@Test
public void testIndexUpdate() throws Exception {
// 建立分詞器
Analyzer analyzer = new IKAnalyzer();
// 建立Directory流對象
Directory directory = FSDirectory.open(Paths.get("d:/lucene/index"));
// 配置分詞器
IndexWriterConfig config = new IndexWriterConfig(analyzer);
// 建立寫入對象
IndexWriter indexWriter = new IndexWriter(directory, config);
// 建立Document
Document document = new Document();
// 設定要更新的字段
document.add(new StoredField("id", "3"));
document.add(new TextField("name", "全文搜尋", Store.YES));
// 執行更新
indexWriter.updateDocument(new Term("name", "lucene"), document);
// 釋放資源
indexWriter.close();
}
實際上,updateDocument方法會把所有符合條件的Document先删除,再執行添加操作。
八、搜尋
8.1 通過Query子類實作搜尋
8.1.1 TermQuery
詞項查詢,TermQuery不使用分析器,搜尋關鍵詞進行精确比對Field域中的詞,比如訂單号、分類ID号等。
81.2 TermRangeQuery
多條件查詢,通過一組Term來查找索引文檔。
- 參數說明:
參數一:要查詢的Field;
參數二:Field的下限,不支援數值類型;
參數三:Field的上限,不支援數值類型;
參數四:是否包含下限值,如果true代表包含,否則不包含;
參數五:是否包含上限值,如果true代表包含,否則不包含;
8.1.3 BooleanQuery
布爾查詢,實作組合條件查詢。例如:
Query query1 = new TermQuery(new Term("name", "lucene"));
Query query2 = new TermRangeQuery("price", new BytesRef("50"), new BytesRef("99"), false, true);
// 組合多個條件
Query query = new BooleanQuery.Builder()
.add(query1, Occur.SHOULD)
.add(query2, Occur.SHOULD)
.build();
- 組合關系有:
MUST | MUST_NOT | SHOULD | |
---|---|---|---|
MUST | 交集 | 減集 | MUST |
MUST_NOT | 減集 | 沒意義 | 減集 |
SHOULD | MUST | 減集 | 并集 |
1)MUST和MUST表示“與”的關系,即“交集”
2)MUST和MUST_NOT前者包含後者不包含
3)MUST_NOT和MUST_NOT沒意義
4)SHOULD與MUST表示MUST,SHOULD失去意義
5)SHOULD與MUST_NOT相當于MUST與MUST_NOT
6)SHOULD與SHOULD表示“或”的關系,即“并集”
8.2 通過QueryParser實作搜尋
8.2.1 查詢文法
基本查詢:Field + “:” + 搜尋的關鍵字
例如:name:java
範圍查詢:Field + “:” + [最小值 TO 最大值]
例如:size:[1 TO 1000]
組合條件查詢:
+加号:相當于AND
空(沒有字元):相當于OR
-
減号:相當于NOT
8.2.2 QueryParser
// 建立分詞解析器
Analyzer analyzer = new IKAnalyzer();
// 建立搜尋解析器,第一個參數:預設Field域,第二個參數:分詞器
QueryParser queryParser = new QueryParser("remark", analyzer);
// 建立搜尋對象
Query query = queryParser.parse("remark:java AND lucene");
// 列印生成的搜尋語句
System.out.println(query);
8.2.3 MultiFieldQueryParser
String[] fields = {"name", "remark"};
MultiFieldQueryParser multiFieldQueryParser = new MultiFieldQueryParser(fields, analyzer);
Query query = multiFieldQueryParser.parse("jvm");
上面MultiFieldQueryParser解析器生成的查詢條件:
name:jvm remark:jvm
九、相關度排序
9.1 相關度描述
相關度排序是查詢結果按照與查詢關鍵字的相關性進行排序,相關度越高的記錄就越容易被查詢到。比如搜尋“Lucene”關鍵字,與該關鍵字最相關的文章應該排在前邊。
Lucene對查詢關鍵字和索引文檔的相關度進行打分,得分高的就排在前邊。
問題:如何打分呢?
Lucene是在使用者進行檢索時實時根據搜尋的關鍵字計算出來的。計算步驟分為:
1)計算詞(Term)的權重;
2)根據詞的權重值,計算出文檔相關度得分;
影響詞權重的兩個重要因素:
1)Term Frequency(tf):此Term在此文檔中出現的次數。次數越多,tf的值就越大,此詞對于該文檔就越重要;
2)Document Frequency(df):指有多少文檔包含此Term。df的值越大,代表此詞對于該文檔越不重要;
9.2 設定boost
boost是一個權重值(預設權重值為1.0f),它可以影響權重的計算。在索引時對某個文檔中的field設定權重值,設定越高,在搜尋時比對到這個文檔就可能排在前邊。
例如:給id為1的文檔設定權重值。
TextField descField = new TextField("desc", book.getDesc().toString(), Store.NO);
if (book.getId() == 1) {
descField.setBoost(100f);
}
document.add(descField);