天天看點

lucene&solr入門(一)一、 Lucene概述二、Lucene環境安裝五、分詞器六、Field詳解八、搜尋九、相關度排序

一、 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/

lucene&solr入門(一)一、 Lucene概述二、Lucene環境安裝五、分詞器六、Field詳解八、搜尋九、相關度排序

1.2 Lucene的工作流程

lucene&solr入門(一)一、 Lucene概述二、Lucene環境安裝五、分詞器六、Field詳解八、搜尋九、相關度排序

索引:綠色部分表示索引。在搜尋前需要先對原始内容進行索引,建構索引庫。

索引過程:确定原始内容 > 獲得文檔 > 建立文檔 > 分析文檔 > 索引文檔。

搜尋:紅色部分代表搜尋。搜尋即從索引庫中搜尋内容。

搜尋過程:建立查詢 > 執行搜尋 > 從索引庫搜尋 > 渲染搜尋結果。

二、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目錄下看到建立的索引檔案。

lucene&amp;solr入門(一)一、 Lucene概述二、Lucene環境安裝五、分詞器六、Field詳解八、搜尋九、相關度排序

四、搜尋索引

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條記錄。

查詢結果如下圖所示:

lucene&amp;solr入門(一)一、 Lucene概述二、Lucene環境安裝五、分詞器六、Field詳解八、搜尋九、相關度排序

五、分詞器

5.1 什麼是分詞

分詞就是把采集到的資料存儲到document對象的Field域中,分詞就是将Document中Field的value值切分成一個一個的詞。

5.2 分詞器的執行過程

分詞(Tokenizer)的主要過程就是先分詞後過濾(TokenFilter)。過濾的工作主要包括:去除标點符号過濾、去除停用詞過濾(的、是、a、an、the等)、大寫轉小寫、詞形還原(複數形式轉成單數形參、過去式轉成現在式)等等。

分詞器的執行過程如下圖所示:

lucene&amp;solr入門(一)一、 Lucene概述二、Lucene環境安裝五、分詞器六、Field詳解八、搜尋九、相關度排序

從圖上可以看到,分詞完成後會經過一系列的過濾,最後才得到一個個的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);