lucene ---- 黑马教程
https://www.bilibili.com/video/BV1eJ411q7nw/?spm_id_from=333.337.search-card.all.click
文章目錄
- 全文檢索Lucene
- 課程計劃
- 1.搜索技術理論基礎
- 1.1. 為什么要學習Lucene
- 1.2. 數據查詢方法
- 1.2.1. 順序掃描法
- 1.2.2. 倒排索引
- 1.3. 全文檢索技術應用場景
- 2.Lucene介紹
- 2.1. 什么是全文檢索
- 2.2.什么是Lucene
- 2.3. Lucene官網
- 3. Lucene全文檢索的流程
- 3.1.索引和搜索流程圖
- 3.2. 索引流程
- 3.2.1. 原始內容
- 3.2.2. 獲得文檔(采集數據)
- 3.2.3. 創建文檔
- 3.2.4. 分析文檔
- 3.2.5. 索引文檔
- 3.2.6 Lucene底層存儲結構
- 3.3. 搜索流程
- 3.3.1. 用戶
- 3.3.2. 用戶搜索界面
- 3.3.3. 創建查詢
- 3.3.4. 執行搜索
- 4. Lucene入門
- 4.1. Lucene準備
- 4.2. 開發環境
- 4.3. 創建Java工程
- 4.4. 索引流程
- 4.4.1. 數據采集
- 4.4.1.1. 創建pojo
- 4.4.1.2. 創建dao
- 4.4.1.3. 測試類創建索引文件
- 4.4.1.4. 使用luke 查看索引文件
- 4.5. 代碼實現查找索引文件
- 5. Field域類型
- 5.1. Field屬性
- 6. 索引維護
- 6.1. 需求
- 6.2. 添加索引
- 6.3. 修改索引
- 6.4. 刪除索引
- 6.4.1. 刪除指定索引
- 6.4.2. 刪除全部索引(慎用)
- 7. 分詞器
- 7.1. 分詞理解
- 7.2. Analyzer使用時機
- **7.2.2.** 搜索時使用Analyzer
- 7.3 原生分詞器
- 7.3.1. StandardAnalyzer
- 7.3.2. WhitespaceAnalyzer
- 7.3.3. SimpleAnalyzer
- 7.3.4. CJKAnalyzer
- 7.3.5. SmartChineseAnalyzer
- **7.4.** **第三方中文分詞器**
- **7.4.1.** **什么是中文分詞器**
- **7.4.2.** **第三方中文分詞器簡介**
- **7.4.3.** 使用中文分詞器IKAnalyzer
- **7.4.4.** 擴展中文詞庫
- 8. Lucene高級搜索
- 8.1.文本搜索
- 8.2.數值范圍搜索
- 8.3.組合搜索
- **9.** 搜索案例
- **9.1.** 引入依賴
- 9.2. 項目加入頁面和資源
- **9.3.** 創建包和啟動類
- **9.4.** 配置文件
- **9.5.** 業務代碼:
- **9.5.1.** 封裝pojo
- 9.5.2. controller代碼
- 9.5.3. service代碼
- 10. Lucene底層儲存結構(高級)
- 10.1. 詳細理解lucene存儲結構
- **10.2.** 索引庫物理文件
- **10.3.** 索引庫文件擴展名對照表
- **10.4.** 詞典的構建
- **10.4.1.** 詞典數據結構對比
- **10.4.2.** 跳躍表原理
- 10.4.3. FST原理簡析
- 11. Lucene優化(高級)
- 11.1. 解決大量磁盤IO
- 11.2. 選擇合適的分詞器
- 11.3. 選擇合適的位置存放索引庫
- 11.4. 搜索api的選擇
- 12. Lucene相關度排序(高級)
- 12.1. 什么是相關度排序
- 12.1.1. 如何打分
- **12.1.2.** **什么是詞的權重**
- **12.1.3.** **怎樣影響相關度排序**
- 12.2.人為影響相關度排序
- 13. Lucene使用注意事項(高級)
全文檢索Lucene
課程計劃
-
lucene入門
什么是lucene
Lucene的作用
使用場景
優點和缺點
-
lucene應用
索引流程
搜索流程
fifield域的使用
索引庫維護
分詞器
高級搜索實戰案例
-
Lucene高級
Lucene底層存儲結構
詞典排序算法
Lucene優化
Lucene使用的一些注意事項
1.搜索技術理論基礎
1.1. 為什么要學習Lucene
原來的方式實現搜索功能,我們的搜索流程如下圖:
上圖就是原始搜索引擎技術,如果用戶比較少而且數據庫的數據量比較小,那么這種方式實現搜索功能在企業中是比較常見的。
現在的方案(使用Lucene),如下圖
為了解決數據庫壓力和速度的問題,我們的數據庫就變成了索引庫,我們使用Lucene的API的來操作服務器上的索引庫。這樣完全和數據庫進行了隔離。
1.2. 數據查詢方法
1.2.1. 順序掃描法
算法描述:
所謂順序掃描,例如要找內容包含一個字符串的文件,就是一個文檔一個文檔的看,對于每一個文檔,從頭看到尾,如果此文檔包含此字符串,則此文檔為我們要找的文件,接著看下一個文件,直到掃描完所有的文件。
優點:
查詢準確率高
缺點:
查詢速度會隨著查詢數據量的增大, 越來越慢
使用場景:
-
數據庫中的like關鍵字模糊查詢
-
文本編輯器的Ctrl + F 查詢功能
1.2.2. 倒排索引
先舉一個栗子:
例如我們使用新華字典查詢漢字,新華字典有偏旁部首的目錄(索引),我們查字首先查這個目錄,找到這個目錄中對應的偏旁部首,就可以通過這個目錄中的偏旁部首找到這個字所在的位置(文檔)。
Lucene會對文檔建立倒排索引
1、 提取資源中關鍵信息, 建立索引 (目錄)
2、 搜索時,根據關鍵字(目錄),找到資源的位置
算法描述:
查詢前會先將查詢的內容提取出來組成文檔(正文), 對文檔進行切分詞組成索引(目錄), 索引和文檔有關聯關系, 查詢的時候先查詢索引, 通過索引找文檔的這個過程叫做全文檢索。
為什么倒排索引比順序掃描快**?**
理解 : 因為索引可以去掉重復的詞, 漢語常用的字和詞大概等于, 字典加詞典, 常用的英文在牛津詞典也有收錄.如果用計算機的速度查詢, 字典+詞典+牛津詞典這些內容是非常快的. 但是用這些字典, 詞典組成的文章卻是千千萬萬不計其數. 索引的大小最多也就是字典+詞典. 所以通過查詢索引, 再通過索引和文檔的關聯關系找到文檔速度比較快. 順序掃描法則是直接去逐個查詢那些不計其數的文章就算是計算的速度也會很慢.
優點:
查詢準確率高
查詢速度快, 并且不會因為查詢內容量的增加, 而使查詢速度逐漸變慢
缺點:
索引文件會占用額外的磁盤空間, 也就是占用磁盤量會增大。
使用場景:
海量數據查詢
1.3. 全文檢索技術應用場景
應用場景 :
1、 站內搜索 (baidu貼吧、論壇、 京東、 taobao)
2、 垂直領域的搜索 (818工作網)
3、 專業搜索引擎公司 (google、baidu)
2.Lucene介紹
2.1. 什么是全文檢索
計算機索引程序通過掃描文章中的每一個詞,對每一個詞建立一個索引,指明該詞在文章中出現的次數和位置,當用戶查詢時,檢索程序就根據事先建立的索引進行查找,并將查找的結果反饋給用戶的檢索方式
2.2.什么是Lucene
他是Lucene、Nutch 、Hadoop等項目的發起人Doug Cutting
Lucene是apache軟件基金會4 jakarta項目組的一個子項目,是一個開放源代碼的全文檢索引擎工具包,但它不是一個完整的全文檢索引擎,而是一個全文檢索引擎的架構,提供了完整的查詢引擎和索引引擎,部分文本分析引擎(英文與德文兩種西方語言)。
Lucene的目的是為軟件開發人員提供一個簡單易用的工具包,以方便的在目標系統中實現全文檢索的功能,或者是以此為基礎建立起完整的全文檢索引擎。
目前已經有很多應用程序的搜索功能是基于 Lucene 的,比如 Eclipse 的幫助系統的搜索功能。Lucene能夠為文本類型的數據建立索引,所以你只要能把你要索引的數據格式轉化的文本的,Lucene 就能對你的文檔進行索引和搜索。比如你要對一些 HTML 文檔,PDF 文檔進行索引的話你就首先需要把HTML 文檔和 PDF 文檔轉化成文本格式的,然后將轉化后的內容交給 Lucene 進行索引,然后把創建好的索引文件保存到磁盤或者內存中,最后根據用戶輸入的查詢條件在索引文件上進行查詢。不指定要索引的文檔的格式也使 Lucene 能夠幾乎適用于所有的搜索應用程序。
-
Lucene是一套用于全文檢索和搜尋的開源程式庫,由Apache軟件基金會支持和提供
-
Lucene提供了一個簡單卻強大的應用程式接口,能夠做全文索引和搜尋, 在Java開發環境里Lucene是一個成熟的免費開放源代碼工具
-
Lucene并不是現成的搜索引擎產品,但可以用來制作搜索引擎產品
2.3. Lucene官網
官網: http://lucene.apache.org/
3. Lucene全文檢索的流程
3.1.索引和搜索流程圖
1、綠色表示索引過程,對要搜索的原始內容進行索引構建一個索引庫,索引過程包括:
確定原始內容即要搜索的內容
-
獲得文檔
-
創建文檔
-
分析文檔
-
索引文檔
用戶通過搜索界面
-
創建查詢
-
執行搜索,從索引庫搜索
-
渲染搜索結果
3.2. 索引流程
對文檔索引的過程,將用戶要搜索的文檔內容進行索引,索引存儲在索引庫(index)中。
3.2.1. 原始內容
原始內容是指要索引和搜索的內容。
原始內容包括互聯網上的網頁、數據庫中的數據、磁盤上的文件等。
3.2.2. 獲得文檔(采集數據)
從互聯網上、數據庫、文件系統中等獲取需要搜索的原始信息,這個過程就是信息采集,采集數據的目的是為了對原始內容進行索引。
采集數據分類:
1、對于互聯網上網頁,可以使用工具將網頁抓取到本地生成html文件。
2、數據庫中的數據,可以直接連接數據庫讀取表中的數據。
3、文件系統中的某個文件,可以通過I/O操作讀取文件的內容。
在Internet上采集信息的軟件通常稱為爬蟲或蜘蛛,也稱為網絡機器人,爬蟲訪問互聯網上的每一個網頁,將獲取到的網頁內容存儲起來。
3.2.3. 創建文檔
獲取原始內容的目的是為了索引,在索引前需要將原始內容創建成文檔(Document),文檔中包括一個一個的域(Field),域中存儲內容。
這里我們可以將磁盤上的一個文件當成一個document,Document中包括一些Field,如下圖:
注意:每個Document可以有多個Field,不同的Document可以有不同的Field,同一個Document可以
有相同的Field(域名和域值都相同)
3.2.4. 分析文檔
將原始內容創建為包含域(Field)的文檔(document),需要再對域中的內容進行分析,分析成為一個一個的單詞。
比如下邊的文檔經過分析如下:
原文檔內容:
vivo X23 8GB+128GB 幻夜藍 全網通4G手機
華為 HUAWEI 麥芒7 6G+64G 亮黑色 全網通4G手機
分析后得到的詞:
vivo, x23, 8GB, 128GB, 幻夜, 幻夜藍, 全網, 全網通, 網通, 4G, 手機, 華為, HUAWEI, 麥芒7。。。。
3.2.5. 索引文檔
對所有文檔分析得出的語匯單元進行索引,索引的目的是為了搜索,最終要實現只搜索被索引的語匯單元從而找到Document(文檔)。
創建索引是對語匯單元索引,通過詞語找文檔,這種索引的結構叫倒排索引結構。
倒排索引結構是根據內容(詞匯)找文檔,如下圖:
倒排索引結構也叫反向索引結構,包括索引和文檔兩部分,索引即詞匯表,它的規模較小,而文檔集合較大。
3.2.6 Lucene底層存儲結構
3.3. 搜索流程
搜索就是用戶輸入關鍵字,從索引中進行搜索的過程。根據關鍵字搜索索引,根據索引找到對應的文檔,從而找到要搜索的內容。
3.3.1. 用戶
就是使用搜索的角色,用戶可以是自然人,也可以是遠程調用的程序。
3.3.2. 用戶搜索界面
全文檢索系統提供用戶搜索的界面供用戶提交搜索的關鍵字,搜索完成展示搜索結果。如下圖:
Lucene不提供制作用戶搜索界面的功能,需要根據自己的需求開發搜索界面。
3.3.3. 創建查詢
用戶輸入查詢關鍵字執行搜索之前需要先構建一個查詢對象,查詢對象中可以指定查詢要查詢關鍵字、要搜索的Field文檔域等,查詢對象會生成具體的查詢語法,比如:
name:手機 : 表示要搜索name這個Field域中,內容為“手機”的文檔。
name:華為AND 手機 : 表示要搜索即包括關鍵字“華為” 并且也包括“手機”的文檔。
3.3.4. 執行搜索
搜索索引過程:
1.根據查詢語法在倒排索引詞典表中分別找出對應搜索詞的索引,從而找到索引所鏈接的文檔鏈表。
例如搜索語法為 “name:華為 AND 手機 ” 表示搜索出的文檔中既要包括"華為"也要包括"手機"。
2、由于是AND,所以要對包含 華為 和 手機 詞語的鏈表進行交集,得到文檔鏈表應該包括每一個搜索
詞語
3、獲取文檔中的Field域數據。
以一個友好的界面將查詢結果展示給用戶,用戶根據搜索結果找自己想要的信息,為了幫助用戶很快找
到自己的結果,提供了很多展示的效果,比如搜索結果中將關鍵字高亮顯示,百度提供的快照等。
4. Lucene入門
4.1. Lucene準備
Lucene可以在官網上下載。課程已經準備好了Lucene的文件,我們使用的是7.7.2版本,文件位置如下圖:
解壓后的效果:
使用這三個文件的jar包,就可以實現lucene功能
4.2. 開發環境
JDK: 1.8 (Lucene7以上,必須使用JDK1.8及以上版本)
數據庫: MySQL
數據庫腳本位置如下圖:
導入到MySQL效果如下圖:
4.3. 創建Java工程
創建maven工程不依賴骨架, 測試即可,效果如下:
pom.xml
<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0"xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"><modelVersion>4.0.0</modelVersion><groupId>cn.itheima</groupId><artifactId>luceneDemo</artifactId><version>1.0-SNAPSHOT</version><properties><maven.compiler.source>1.8</maven.compiler.source><maven.compiler.target>1.8</maven.compiler.target><project.build.sourceEncoding>UTF-8</project.build.sourceEncoding><project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding><skipTests>true</skipTests></properties><!--springboot --><parent><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-parent</artifactId><version>2.1.4.RELEASE</version></parent><dependencies><!--####################lucene包####################--><!--lucene核心包--><dependency><groupId>org.apache.lucene</groupId><artifactId>lucene-core</artifactId><version>7.7.2</version></dependency><!--分詞器包--><dependency><groupId>org.apache.lucene</groupId><artifactId>lucene-analyzers-common</artifactId><version>7.7.2</version></dependency><!--做查詢用的包--><dependency><groupId>org.apache.lucene</groupId><artifactId>lucene-queryparser</artifactId><version>7.7.2</version></dependency><!-- IK中文分詞器 --><dependency><groupId>org.wltea.ik-analyzer</groupId><artifactId>ik-analyzer</artifactId><version>8.1.0</version></dependency><!--####################工具包####################--><dependency><groupId>commons-io</groupId><artifactId>commons-io</artifactId><version>2.6</version></dependency><!-- Json轉換工具 --><dependency><groupId>com.alibaba</groupId><artifactId>fastjson</artifactId><version>1.2.51</version></dependency><!--####################環境包####################--><!-- 測試 --><dependency><groupId>junit</groupId><artifactId>junit</artifactId><version>4.12</version><scope>test</scope></dependency><!-- mysql數據庫驅動 --><dependency><groupId>mysql</groupId><artifactId>mysql-connector-java</artifactId><version>5.1.48</version></dependency><!--web起步依賴--><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId></dependency><!-- 引入thymeleaf --><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-thymeleaf</artifactId></dependency></dependencies></project>4.4. 索引流程
4.4.1. 數據采集
在電商網站中,全文檢索的數據源在數據庫中,需要通過jdbc訪問數據庫中 sku 表的內容。
4.4.1.1. 創建pojo
package cn.itheima.pojo;/****/ public class Sku {//商品主鍵idprivate String id;//商品名稱private String name;//價格private Integer price;//庫存數量private Integer num;//圖片private String image;//分類名稱private String categoryName;//品牌名稱private String brandName;//規格private String spec;//銷量private Integer saleNum;public String getId() {return id;}public void setId(String id) {this.id = id;}public String getName() {return name;}public void setName(String name) {this.name = name;}public Integer getPrice() {return price;}public void setPrice(Integer price) {this.price = price;}public Integer getNum() {return num;}public void setNum(Integer num) {this.num = num;}public String getImage() {return image;}public void setImage(String image) {this.image = image;}public String getCategoryName() {return categoryName;}public void setCategoryName(String categoryName) {this.categoryName = categoryName;}public String getBrandName() {return brandName;}public void setBrandName(String brandName) {this.brandName = brandName;}public String getSpec() {return spec;}public void setSpec(String spec) {this.spec = spec;}public Integer getSaleNum() {return saleNum;}public void setSaleNum(Integer saleNum) {this.saleNum = saleNum;} }4.4.1.2. 創建dao
package cn.itheima.dao;import cn.itheima.pojo.Sku;import java.util.List;/****/ public interface SkuDao {/*** 查詢所有的Sku數據* @return**/public List<Sku> querySkuList(); } package cn.itheima.dao;import cn.itheima.pojo.Sku;import java.sql.Connection; import java.sql.DriverManager; import java.sql.PreparedStatement; import java.sql.ResultSet; import java.util.ArrayList; import java.util.List;/****/ public class SkuDaoImpl implements SkuDao {public List<Sku> querySkuList() {// 數據庫鏈接Connection connection = null;// 預編譯statementPreparedStatement preparedStatement = null;// 結果集ResultSet resultSet = null;// 商品列表List<Sku> list = new ArrayList<Sku>();try {// 加載數據庫驅動Class.forName("com.mysql.jdbc.Driver");// 連接數據庫connection = DriverManager.getConnection("jdbc:mysql://rm-bp1pulu04813wr8ryzo.mysql.rds.aliyuncs.com:3306/wlplat" +"?useUnicode=true&characterEncoding=utf-8&useSSL=false&autoReconnect=true&failOverReadOnly=false", "iplat62", "iplat62");// SQL語句String sql = "SELECT * FROM tb_sku";// 創建preparedStatementpreparedStatement = connection.prepareStatement(sql);// 獲取結果集resultSet = preparedStatement.executeQuery();// 結果集解析while (resultSet.next()) {Sku sku = new Sku();sku.setId(resultSet.getString("id"));sku.setName(resultSet.getString("name"));sku.setSpec(resultSet.getString("spec"));sku.setBrandName(resultSet.getString("brand_name"));sku.setCategoryName(resultSet.getString("category_name"));sku.setImage(resultSet.getString("image"));sku.setNum(resultSet.getInt("num"));sku.setPrice(resultSet.getInt("price"));sku.setSaleNum(resultSet.getInt("sale_num"));list.add(sku);}} catch (Exception e) {e.printStackTrace();}return list;} }4.4.1.3. 測試類創建索引文件
package cn.itheima.test;import cn.itheima.dao.SkuDao; import cn.itheima.dao.SkuDaoImpl; import cn.itheima.pojo.Sku; import org.apache.lucene.analysis.Analyzer; import org.apache.lucene.analysis.standard.StandardAnalyzer; import org.apache.lucene.document.*; import org.apache.lucene.index.IndexWriter; import org.apache.lucene.index.IndexWriterConfig; import org.apache.lucene.index.Term; import org.apache.lucene.store.Directory; import org.apache.lucene.store.FSDirectory; import org.apache.lucene.store.MMapDirectory; import org.junit.Test; import org.wltea.analyzer.lucene.IKAnalyzer;import java.nio.file.Paths; import java.util.ArrayList; import java.util.List;/*** 索引庫維護*/ public class TestIndexManager {/*** 創建索引庫* 1. 采集數據* 2. 創建文檔對象* 3. 創建分詞器* 4. 創建Directory目錄對象, 目錄對象表示索引庫的位置* 5. 創建IndexWriterConfig對象, 這個對象中指定切分詞使用的分詞器* 6. 創建IndexWriter輸出流對象, 指定輸出的位置和使用的config初始化對象* 7. 寫入文檔到索引庫* 8. 釋放資源*/@Testpublic void createIndexTest() throws Exception {//1. 采集數據SkuDao skuDao = new SkuDaoImpl();List<Sku> skuList = skuDao.querySkuList();//文檔集合List<Document> docList = new ArrayList<>();for (Sku sku : skuList) {//2. 創建文檔對象Document document = new Document();//創建域對象并且放入文檔對象中/*** 是否分詞: 否, 因為主鍵分詞后無意義* 是否索引: 是, 如果根據id主鍵查詢, 就必須索引* 是否存儲: 是, 因為主鍵id比較特殊, 可以確定唯一的一條數據, 在業務上一般有重要所用, 所以存儲* 存儲后, 才可以獲取到id具體的內容*/document.add(new StringField("id", sku.getId(), Field.Store.YES));/*** 是否分詞: 是, 因為名稱字段需要查詢, 并且分詞后有意義所以需要分詞* 是否索引: 是, 因為需要根據名稱字段查詢* 是否存儲: 是, 因為頁面需要展示商品名稱, 所以需要存儲*/document.add(new TextField("name", sku.getName(), Field.Store.YES));/*** 是否分詞: 是(因為lucene底層算法規定, 如果根據價格范圍查詢, 必須分詞)* 是否索引: 是, 需要根據價格進行范圍查詢, 所以必須索引* 是否存儲: 是, 因為頁面需要展示價格*/document.add(new IntPoint("price", sku.getPrice()));document.add(new StoredField("price", sku.getPrice()));/*** 是否分詞: 否, 因為不查詢, 所以不索引, 因為不索引所以不分詞* 是否索引: 否, 因為不需要根據圖片地址路徑查詢* 是否存儲: 是, 因為頁面需要展示商品圖片*/document.add(new StoredField("image", sku.getImage()));/*** 是否分詞: 否, 因為分類是專有名詞, 是一個整體, 所以不分詞* 是否索引: 是, 因為需要根據分類查詢* 是否存儲: 是, 因為頁面需要展示分類*/document.add(new StringField("categoryName", sku.getCategoryName(), Field.Store.YES));/*** 是否分詞: 否, 因為品牌是專有名詞, 是一個整體, 所以不分詞* 是否索引: 是, 因為需要根據品牌進行查詢* 是否存儲: 是, 因為頁面需要展示品牌*/document.add(new StringField("brandName", sku.getBrandName(), Field.Store.YES));//將文檔對象放入到文檔集合中docList.add(document);}//3. 創建分詞器, StandardAnalyzer標準分詞器, 對英文分詞效果好, 對中文是單字分詞, 也就是一個字就認為是一個詞.Analyzer analyzer = new IKAnalyzer();//4. 創建Directory目錄對象, 目錄對象表示索引庫的位置 // Directory dir = FSDirectory.open(Paths.get("E:\\dir"));Directory dir = FSDirectory.open(Paths.get("/Users/miyufeng/Downloads/lucene_dev"));//5. 創建IndexWriterConfig對象, 這個對象中指定切分詞使用的分詞器IndexWriterConfig config = new IndexWriterConfig(analyzer);//6. 創建IndexWriter輸出流對象, 指定輸出的位置和使用的config初始化對象IndexWriter indexWriter = new IndexWriter(dir, config);//7. 寫入文檔到索引庫for (Document doc : docList) {indexWriter.addDocument(doc);}//8. 釋放資源indexWriter.close();} }4.4.1.4. 使用luke 查看索引文件
下圖是索引域的展示效果:
下圖是文檔域展示效果
Lucene可以通過query對象輸入查詢語句。同數據庫的sql一樣,lucene也有固定的查詢語法:
最基本的有比如:AND, OR, NOT 等(必須大寫)
舉個栗子:
用戶想找一個 name 域中包括 手 或 機 關鍵字的文檔。
它對應的查詢語句:**name:**手 **OR name:**機
如下圖是使用luke搜索的例子:
和索引過程的分詞一樣,這里要對用戶輸入的關鍵字進行分詞,一般情況索引和搜索使用的分詞器一
致。
比如:輸入搜索關鍵字“java學習”,分詞后為java和學習兩個詞,與java和學習有關的內容都搜索出來
了,如下:
4.5. 代碼實現查找索引文件
創建Query搜索對象
創建Directory流對象,聲明索引庫位置
創建索引讀取對象IndexReader
創建索引搜索對象IndexSearcher
使用索引搜索對象,執行搜索,返回結果集TopDocs
解析結果集
釋放資源
IndexSearcher搜索方法如下:
| indexSearcher.search(query, n) | 根據Query搜索,返回評分最高的n條記錄 |
| indexSearcher.search(query, fifilter, n) | 根據Query搜索,添加過濾策略,返回評分最高的n條記錄 |
| indexSearcher.search(query, n, sort) | 根據Query搜索,添加排序策略,返回評分最高的n條記錄 |
| indexSearcher.search(booleanQuery,fifilter, n, sort) | 根據Query搜索,添加過濾策略,添加排序策略,返回評分最高的n條記錄 |
5. Field域類型
5.1. Field屬性
Field是文檔中的域,包括Field名和Field值兩部分,一個文檔可以包括多個Field,Document只是Field的一個承載體,Field值即為要索引的內容,也是要搜索的內容。
- 是否分詞(tokenized)
是:作分詞處理,即將Field值進行分詞,分詞的目的是為了索引。
比如:商品名稱、商品描述等,這些內容用戶要輸入關鍵字搜索,由于搜索的內容格式大、內容多需要
分詞后將語匯單元建立索引
否:不作分詞處理
比如:商品id、訂單號、身份證號等
- 是否索引(indexed)
是:進行索引。將Field分詞后的詞或整個Field值進行索引,存儲到索引域,索引的目的是為了搜索。
比如:商品名稱、商品描述分析后進行索引,訂單號、身份證號不用分詞但也要索引,這些將來都要作
為查詢條件。
否:不索引。
比如:圖片路徑、文件路徑等,不用作為查詢條件的不用索引。
- 是否存儲(stored)
是:將Field值存儲在文檔域中,存儲在文檔域中的Field才可以從Document中獲取。
比如:商品名稱、訂單號,凡是將來要從Document中獲取的Field都要存儲。
否:不存儲Field值
比如:商品描述,內容較大不用存儲。如果要向用戶展示商品描述可以從系統的關系數據庫中獲取。
- 5.2. Field常用類型
下邊列出了開發中常用 的Filed類型,注意Field的屬性,根據需求選擇:
| StringField(FieldName, FieldValue,Store.YES)) | 字符串 | N | Y | Y或N | 這個Field用來構建一個字符串Field,但是不會進行分詞,會將整個串存儲在索引中,比如(訂單號,身份證號等)是否存儲在文檔中用Store.YES或Store.NO決定 |
| FloatPoint(FieldName, FieldValue) | Float型 | Y | Y | N | 這個Field用來構建一個Float數字型Field,進行分詞和索引,不存儲, 比如(價格) 存儲在文檔中 |
| DoublePoint(FieldName,FieldValue) | Double型 | Y | Y | N | 這個Field用來構建一個Double數字型Field,進行分詞和索引,不存儲 |
| LongPoint(FieldName, FieldValue) | Long型 | Y | Y | N | 這個Field用來構建一個Long數字型Field,進行分詞和索引,不存儲 |
| IntPoint(FieldName, FieldValue) | Integer 型 | Y | Y | N | 這個Field用來構建一個Integer數字型Field,進行分詞和索引,不存儲 |
| StoredField(FieldName, FieldValue) | 重載方法,支持多種類型 | N | N | Y | 這個Field用來構建不同類型Field不分析,不索引,但要Field存儲在文檔中 |
| TextField(FieldName, FieldValue,Store.NO) 或 TextField(FieldName,reader) | 字符串或 流 | Y | Y | Y或N | 如果是一個Reader, lucene猜測內容比較多,會采用Unstored的策略. |
| NumericDocValuesField(FieldName,FieldValue) | 數值 | _ | _ | _ | 配合其他域排序使用 |
6. 索引維護
6.1. 需求
管理人員通過電商系統更改圖書信息,這時更新的是關系數據庫,如果使用lucene搜索圖書信息,需要
在數據庫表book信息變化時及時更新lucene索引庫。
6.2. 添加索引
調用 indexWriter.addDocument(doc)添加索引。
參考入門程序的創建索引。
6.3. 修改索引
更新索引是先刪除再添加,建議對更新需求采用此方法并且要保證對已存在的索引執行更新,可以先查詢出來,確定更新記錄存在執行更新操作。
如果更新索引的目標文檔對象不存在,則執行添加。
代碼
/*** 索引庫修改操作* @throws Exception*/ @Test public void updateIndexTest() throws Exception {//需要變更成的內容Document document = new Document();document.add(new StringField("id", "100000003145", Field.Store.YES));document.add(new TextField("name", "xxxx", Field.Store.YES));document.add(new IntPoint("price", 123));document.add(new StoredField("price", 123));document.add(new StoredField("image", "xxxx.jpg"));document.add(new StringField("categoryName", "手機", Field.Store.YES));document.add(new StringField("brandName", "華為", Field.Store.YES));//3. 創建分詞器, StandardAnalyzer標準分詞器, 對英文分詞效果好, 對中文是單字分詞, 也就是一個字就認為是一個詞.Analyzer analyzer = new StandardAnalyzer();//4. 創建Directory目錄對象, 目錄對象表示索引庫的位置Directory dir = FSDirectory.open(Paths.get("E:\\dir"));//5. 創建IndexWriterConfig對象, 這個對象中指定切分詞使用的分詞器IndexWriterConfig config = new IndexWriterConfig(analyzer);//6. 創建IndexWriter輸出流對象, 指定輸出的位置和使用的config初始化對象IndexWriter indexWriter = new IndexWriter(dir, config);//修改, 第一個參數: 修改條件, 第二個參數: 修改成的內容indexWriter.updateDocument(new Term("id", "100000003145"), document);//8. 釋放資源indexWriter.close(); }6.4. 刪除索引
6.4.1. 刪除指定索引
根據Term項刪除索引,滿足條件的將全部刪除。
/*** 測試根據條件刪除* @throws Exception*/@Testpublic void deleteIndexTest() throws Exception {//3. 創建分詞器, StandardAnalyzer標準分詞器, 對英文分詞效果好, 對中文是單字分詞, 也就是一個字就認為是一個詞.Analyzer analyzer = new IKAnalyzer();//4. 創建Directory目錄對象, 目錄對象表示索引庫的位置Directory dir = FSDirectory.open(Paths.get("/Users/miyufeng/Downloads/lucene_dev"));//5. 創建IndexWriterConfig對象, 這個對象中指定切分詞使用的分詞器IndexWriterConfig config = new IndexWriterConfig(analyzer);//6. 創建IndexWriter輸出流對象, 指定輸出的位置和使用的config初始化對象IndexWriter indexWriter = new IndexWriter(dir, config);//測試根據條件刪除indexWriter.deleteDocuments(new Term("id", "100000003145"));//測試刪除所有內容 // indexWriter.deleteAll();//8. 釋放資源indexWriter.close();}6.4.2. 刪除全部索引(慎用)
將索引目錄的索引信息全部刪除,直接徹底刪除,無法恢復。
建議參照關系數據庫基于主鍵刪除方式,所以在創建索引時需要創建一個主鍵Field,刪除時根據此主鍵Field刪除。
索引刪除后將放在Lucene的回收站中,Lucene3.X版本可以恢復刪除的文檔,3.X之后無法恢復。
代碼:
/*** 測試根據條件刪除* @throws Exception*/@Testpublic void deleteIndexTest() throws Exception {//3. 創建分詞器, StandardAnalyzer標準分詞器, 對英文分詞效果好, 對中文是單字分詞, 也就是一個字就認為是一個詞.Analyzer analyzer = new IKAnalyzer();//4. 創建Directory目錄對象, 目錄對象表示索引庫的位置Directory dir = FSDirectory.open(Paths.get("/Users/miyufeng/Downloads/lucene_dev"));//5. 創建IndexWriterConfig對象, 這個對象中指定切分詞使用的分詞器IndexWriterConfig config = new IndexWriterConfig(analyzer);//6. 創建IndexWriter輸出流對象, 指定輸出的位置和使用的config初始化對象IndexWriter indexWriter = new IndexWriter(dir, config);//測試根據條件刪除 // indexWriter.deleteDocuments(new Term("id", "100000003145"));//測試刪除所有內容indexWriter.deleteAll();//8. 釋放資源indexWriter.close();}索引域數據清空
7. 分詞器
7.1. 分詞理解
在對Document中的內容進行索引之前,需要使用分詞器進行分詞 ,分詞的目的是為了搜索。分詞的主要過程就是先分詞后過濾。
- 過濾:包括去除標點符號過濾、去除停用詞過濾(的、是、a、an、the等)、大寫轉小寫、詞的形還原(復數形式轉成單數形參、過去式轉成現在式。。。)等。
什么是停用詞?停用詞是為節省存儲空間和提高搜索效率,搜索引擎在索引頁面或處理搜索請求時會自動忽略某些字或詞,這些字或詞即被稱為Stop Words(停用詞)。比如語氣助詞、副詞、介詞、連接詞等,通常自身并無明確的意義,只有將其放入一個完整的句子中才有一定作用,如常見的“的”、“在”、“是”、“啊”等。
對于分詞來說,不同的語言,分詞規則不同。Lucene作為一個工具包提供不同國家的分詞器
7.2. Analyzer使用時機
輸入關鍵字進行搜索,當需要讓該關鍵字與文檔域內容所包含的詞進行匹配時需要對文檔域內容進行分析,需要經過Analyzer分析器處理生成語匯單元(Token)。分析器分析的對象是文檔中的Field域。當Field的屬性tokenized(是否分詞)為true時會對Field值進行分析,如下圖:
對于一些Field可以不用分析:
1、不作為查詢條件的內容,比如文件路徑
2、不是匹配內容中的詞而匹配Field的整體內容,比如訂單號、身份證號等。
7.2.2. 搜索時使用Analyzer
對搜索關鍵字進行分析和索引分析一樣,使用Analyzer對搜索關鍵字進行分析、分詞處理,使用分析后每個詞語進行搜索。比如:搜索關鍵字:spring web ,經過分析器進行分詞,得出:spring web拿詞去索引詞典表查找 ,找到索引鏈接到Document,解析Document內容。
對于匹配整體Field域的查詢可以在搜索時不分析,比如根據訂單號、身份證號查詢等。
注意:搜索使用的分析器要和索引使用的分析器一致。
以下是Lucene中自帶的分詞器
7.3 原生分詞器
7.3.1. StandardAnalyzer
特點 :
Lucene提供的標準分詞器, 可以對用英文進行分詞,對中文是單字分詞, 也就是一個字就認為是一個詞.
如下是org.apache.lucene.analysis.standard.standardAnalyzer的部分源碼:
/repository_lucene/org/apache/lucene/lucene-core/7.7.2/lucene-core-7.7.2.jar!/org/apache/lucene/analysis/standard/StandardAnalyzer.class:50
protected TokenStreamComponents createComponents(String fieldName) {final StandardTokenizer src = new StandardTokenizer();src.setMaxTokenLength(this.maxTokenLength);TokenStream tok = new LowerCaseFilter(src);TokenStream tok = new StopFilter(tok, this.stopwords);return new TokenStreamComponents(src, tok) {protected void setReader(Reader reader) {src.setMaxTokenLength(StandardAnalyzer.this.maxTokenLength);super.setReader(reader);}}; }Tokenizer就是分詞器,負責將reader轉換為語匯單元即進行分詞處理,Lucene提供了很多的分詞器,也可以使用第三方的分詞,比如IKAnalyzer一個中文分詞器。
TokenFilter是分詞過濾器,負責對語匯單元進行過濾,TokenFilter可以是一個過濾器鏈兒,Lucene提供了很多的分詞器過濾器,比如大小寫轉換、去除停用詞等。
如下圖是語匯單元的生成過程:
從一個Reader字符流開始,創建一個基于Reader的Tokenizer分詞器,經過三個TokenFilter生成語匯單元Token。
比如下邊的文檔經過分析器分析如下:
原文檔內容:
分析后得到的多個語匯單元:
7.3.2. WhitespaceAnalyzer
特點 :
僅僅是去掉了空格,沒有其他任何操作,不支持中文。
/*** 去掉空格分詞器, 不支持中文* @throws Exception*/ @Test public void TestWhitespaceAnalyzer() throws Exception{// 1. 創建分詞器,分析文檔,對文檔進行分詞Analyzer analyzer = new WhitespaceAnalyzer();// 2. 創建Directory對象,聲明索引庫的位置Directory directory = FSDirectory.open(Paths.get("E:\\dir"));// 3. 創建IndexWriteConfig對象,寫入索引需要的配置IndexWriterConfig config = new IndexWriterConfig(analyzer);// 4.創建IndexWriter寫入對象IndexWriter indexWriter = new IndexWriter(directory, config);// 5.寫入到索引庫,通過IndexWriter添加文檔對象documentDocument doc = new Document();doc.add(new TextField("name", "vivo X23 8GB+128GB 幻夜藍", Field.Store.YES));indexWriter.addDocument(doc);// 6.釋放資源indexWriter.close(); }7.3.3. SimpleAnalyzer
特點 :
將除了字母以外的符號全部去除,并且將所有字母變為小寫,需要注意的是這個分詞器同樣把數字也去除了,同樣不支持中文。
/*** 簡單分詞器: 不支持中文, 將除了字母之外的所有符號全部取出, 所有大寫字母轉換成小寫字母, 對于數字也會去除* @throws Exception*/ @Test public void TestSimpleAnalyzer() throws Exception{// 1. 創建分詞器,分析文檔,對文檔進行分詞Analyzer analyzer = new SimpleAnalyzer();// 2. 創建Directory對象,聲明索引庫的位置Directory directory = FSDirectory.open(Paths.get("E:\\dir"));// 3. 創建IndexWriteConfig對象,寫入索引需要的配置IndexWriterConfig config = new IndexWriterConfig(analyzer);// 4.創建IndexWriter寫入對象IndexWriter indexWriter = new IndexWriter(directory, config);// 5.寫入到索引庫,通過IndexWriter添加文檔對象documentDocument doc = new Document();doc.add(new TextField("name", "vivo,X23。 8GB+128GB; 幻夜藍", Field.Store.YES));indexWriter.addDocument(doc);// 6.釋放資源indexWriter.close(); }7.3.4. CJKAnalyzer
特點 :
這個支持中日韓文字,前三個字母也就是這三個國家的縮寫。對中文是二分法分詞, 去掉空格,去掉標點符號。個人感覺對中文支持依舊很爛。
/*** 中日韓分詞器: 使用二分法分詞, 去掉空格, 去掉標點符號, 所有大寫字母轉換成小寫字母* @throws Exception*/ @Test public void TestCJKAnalyzer() throws Exception{// 1. 創建分詞器,分析文檔,對文檔進行分詞Analyzer analyzer = new CJKAnalyzer();// 2. 創建Directory對象,聲明索引庫的位置Directory directory = FSDirectory.open(Paths.get("E:\\dir"));// 3. 創建IndexWriteConfig對象,寫入索引需要的配置IndexWriterConfig config = new IndexWriterConfig(analyzer);// 4.創建IndexWriter寫入對象IndexWriter indexWriter = new IndexWriter(directory, config);// 5.寫入到索引庫,通過IndexWriter添加文檔對象documentDocument doc = new Document();doc.add(new TextField("name", "vivo,X23。 8GB+128GB; 幻夜藍", Field.Store.YES));indexWriter.addDocument(doc);// 6.釋放資源indexWriter.close(); }7.3.5. SmartChineseAnalyzer
特點 :
對中文支持也不是很好,擴展性差,擴展詞庫,禁用詞庫和同義詞庫等不好處理。
7.4. 第三方中文分詞器
7.4.1. 什么是中文分詞器
學過英文的都知道,英文是以單詞為單位的,單詞與單詞之間以空格或者逗號句號隔開。所以對于英文,我們可以簡單以空格判斷某個字符串是否為一個單詞,比如I love China,love 和 China很容易被程序區分開來。
而中文則以字為單位,字又組成詞,字和詞再組成句子。中文“我愛中國”就不一樣了,電腦不知道“中 國”是一個詞語還是“愛中”是一個詞語。
把中文的句子切分成有意義的詞,就是中文分詞,也稱切詞。我愛中國,分詞的結果是:我、愛、中國。
7.4.2. 第三方中文分詞器簡介
-
paoding: 庖丁解牛最新版在 https://code.google.com/p/paoding/ 中最多支持Lucene 3.0,且最新提交的代碼在 2008-06-03,在svn中最新也是2010年提交,已經過時,不予考慮。
-
mmseg4j:最新版已從 https://code.google.com/p/mmseg4j/ 移至 https://github.com/chenlb/mmseg4j-solr,支持Lucene 4.10,且在github中最新提交代碼是2014年6月,從09年~14年一共有:18個版本,也就是一年幾乎有3個大小版本,有較大的活躍度,用了mmseg算法。
-
IK-analyzer: 最新版在https://code.google.com/p/ik-analyzer/上,支持Lucene 4.10從2006年12月推出1.0版開始, IKAnalyzer已經推出了4個大版本。最初,它是以開源項目Luence為應用主體的,結合詞典分詞和文法分析算法的中文分詞組件。從3.0版本開 始,IK發展為面向Java的公用分詞組件,獨立于Lucene項目,同時提供了對Lucene的默認優化實現。在2012版本中,IK實現了簡單的分詞 歧義排除算法,標志著IK分詞器從單純的詞典分詞向模擬語義分詞衍化。 但是也就是2012年12月后沒有在更新。
-
ansj_seg:最新版本在 https://github.com/NLPchina/ansj_seg tags僅有1.1版本,從2012年到2014年更新了大小6次,但是作者本人在2014年10月10日說明:“可能我以后沒有精力來維護ansj_seg了”,現在由”nlp_china”管理。2014年11月有更新。并未說明是否支持Lucene,是一個由CRF(條件隨機場)算法所做的分詞算法。
-
imdict-chinese-analyzer:最新版在 https://code.google.com/p/imdict-chinese-analyzer/ , 最新更新也在2009年5月,下載源碼,不支持Lucene 4.10 。是利用HMM(隱馬爾科夫鏈)算法。
-
Jcseg:最新版本在git.oschina.net/lionsoul/jcseg,支持Lucene 4.10,作者有較高的活躍度。利用mmseg算法。
7.4.3. 使用中文分詞器IKAnalyzer
IKAnalyzer繼承Lucene的Analyzer抽象類,使用IKAnalyzer和Lucene自帶的分析器方法一樣,將Analyzer測試代碼改為IKAnalyzer測試中文分詞效果。
如果使用中文分詞器ik-analyzer,就需要在索引和搜索程序中使用一致的分詞器:IK-analyzer。
7.4.4. 擴展中文詞庫
如果想配置擴展詞和停用詞,就創建擴展詞的文件和停用詞的文件。
從ikanalyzer包中拷貝配置文件
拷貝到資源文件夾中
停用詞典stopword.dic作用:
停用詞典中的詞例如: a, an, the, 的, 地, 得等詞匯, 凡是出現在停用詞典中的字或者詞, 在切分詞的時候會被過濾掉.
擴展詞典ext.dic作用 :
擴展詞典中的詞例如: 傳智播客, 黑馬程序員, 貴州茅臺等專有名詞, 在漢語中一些公司名稱, 行業名稱, 分 類, 品牌等不是漢語中的詞匯, 是專有名詞. 這些分詞器默認不識別, 所以需要放入擴展詞典中, 效果是被強制分成一個詞.
8. Lucene高級搜索
8.1.文本搜索
QueryParser支持默認搜索域, 第一個參數為默認搜索域.
如果在執行parse方法的時候, 查詢語法中包含域名則從指定的這個域名中搜索, 如果只有查詢的關鍵字,則從默認搜索域中搜索結果.
需求描述 : 查詢名稱中包含華為手機關鍵字的結果.
/*1. 創建分詞器(對搜索的關鍵詞進行分詞使用)2. 創建查詢對象,3. 設置搜索關鍵詞4. 創建Directory目錄對象, 指定索引庫的位置5. 創建輸入流對象6. 創建搜索對象7. 搜索, 并返回結果*/@Testpublic void testIndexSearch() throws Exception {//1. 創建分詞器(對搜索的關鍵詞進行分詞使用)//注意: 分詞器要和創建索引的時候使用的分詞器一模一樣 // Analyzer analyzer = new StandardAnalyzer();Analyzer analyzer = new IKAnalyzer();//2. 創建查詢對象,//第一個參數: 默認查詢域, 如果查詢的關鍵字中帶搜索的域名, 則從指定域中查詢, 如果不帶域名則從, 默認搜索域中查詢//第二個參數: 使用的分詞器QueryParser queryParser = new QueryParser( "name",analyzer);//3. 設置搜索關鍵詞//華為 AND 手機 求交集 華為 OR 手機 求并集Query query = queryParser.parse("華為 AND 手機"); // Query query = queryParser.parse("price:[0 TO 9999]"); // Query query = queryParser.parse("price:[0 TO 9999]");//4. 創建Directory目錄對象, 指定索引庫的位置 // Directory dir = FSDirectory.open(Paths.get("E:\\dir"));Directory dir = FSDirectory.open(Paths.get("/Users/miyufeng/Downloads/lucene_dev"));//5. 創建輸入流對象IndexReader indexReader = DirectoryReader.open(dir);//6. 創建搜索對象IndexSearcher indexSearcher = new IndexSearcher(indexReader);//7. 搜索, 并返回結果//第二個參數: 是返回多少條數據用于展示, 分頁使用TopDocs topDocs = indexSearcher.search(query, 10);//獲取查詢到的結果集的總數, 打印System.out.println("=======count=======" + topDocs.totalHits);//8. 獲取結果集ScoreDoc[] scoreDocs = topDocs.scoreDocs;//9. 遍歷結果集if (scoreDocs != null) {for (ScoreDoc scoreDoc : scoreDocs) {//獲取查詢到的文檔唯一標識, 文檔id, 這個id是lucene在創建文檔的時候自動分配的int docID = scoreDoc.doc;//通過文檔id, 讀取文檔Document doc = indexSearcher.doc(docID);System.out.println("==================================================");//通過域名, 從文檔中獲取域值System.out.println("===id==" + doc.get("id"));System.out.println("===name==" + doc.get("name"));System.out.println("===price==" + doc.get("price"));System.out.println("===image==" + doc.get("image"));System.out.println("===brandName==" + doc.get("brandName"));System.out.println("===categoryName==" + doc.get("categoryName"));}}//10. 關閉流}8.2.數值范圍搜索
需求描述 : 查詢價格大于等于100, 小于等于1000的商品
測試代碼:
/*** 數值范圍查詢* @throws Exception*/ @Test public void testRangeQuery() throws Exception {//1. 創建分詞器(對搜索的關鍵詞進行分詞使用)//注意: 分詞器要和創建索引的時候使用的分詞器一模一樣Analyzer analyzer = new IKAnalyzer();//2. 創建查詢對象,Query query = IntPoint.newRangeQuery("price", 100, 1000);//4. 創建Directory目錄對象, 指定索引庫的位置Directory dir = FSDirectory.open(Paths.get("/Users/miyufeng/Downloads/lucene_dev"));//5. 創建輸入流對象IndexReader indexReader = DirectoryReader.open(dir);//6. 創建搜索對象IndexSearcher indexSearcher = new IndexSearcher(indexReader);//7. 搜索, 并返回結果//第二個參數: 是返回多少條數據用于展示, 分頁使用TopDocs topDocs = indexSearcher.search(query, 10);//獲取查詢到的結果集的總數, 打印System.out.println("=======count=======" + topDocs.totalHits);//8. 獲取結果集ScoreDoc[] scoreDocs = topDocs.scoreDocs;//9. 遍歷結果集if (scoreDocs != null) {for (ScoreDoc scoreDoc : scoreDocs) {//獲取查詢到的文檔唯一標識, 文檔id, 這個id是lucene在創建文檔的時候自動分配的int docID = scoreDoc.doc;//通過文檔id, 讀取文檔Document doc = indexSearcher.doc(docID);System.out.println("==================================================");//通過域名, 從文檔中獲取域值System.out.println("===id==" + doc.get("id"));System.out.println("===name==" + doc.get("name"));System.out.println("===price==" + doc.get("price"));System.out.println("===image==" + doc.get("image"));System.out.println("===brandName==" + doc.get("brandName"));System.out.println("===categoryName==" + doc.get("categoryName"));}}//10. 關閉流 }8.3.組合搜索
需求描述 : 查詢價格大于等于100, 小于等于1000, 并且名稱中不包含華為手機關鍵字的商品
BooleanClause.Occur.MUST 必須 相當于and, 并且
BooleanClause.Occur.MUST_NOT 不必須 相當于not, 非
BooleanClause.Occur.SHOULD 應該 相當于or, 或者
注意 : 如果邏輯條件中, 只有MUST_NOT, 或者多個邏輯條件都是MUST_NOT, 無效, 查詢不出任何數據.
/*** 組合查詢* @throws Exception*/ @Test public void testBooleanQuery() throws Exception {Analyzer analyzer = new IKAnalyzer();Query query1 = IntPoint.newRangeQuery("price", 100, 1000);QueryParser queryParser = new QueryParser("name", analyzer);Query query2 = queryParser.parse("name:華為手機");//創建布爾查詢對象(組合查詢對象)/*** BooleanClause.Occur.MUST 必須相當于and, 也就是并且的關系* BooleanClause.Occur.SHOULD 應該相當于or, 也就是或者的關系* BooleanClause.Occur.MUST_NOT 必須不, 相當于not, 非* 注意: 如果查詢條件都是MUST_NOT, 或者只有一個查詢條件, 然后這一個查詢條件是MUST_NOT則* 查詢不出任何數據.*/BooleanQuery.Builder query = new BooleanQuery.Builder();query.add(query1, BooleanClause.Occur.MUST);query.add(query2, BooleanClause.Occur.MUST);Directory dir = FSDirectory.open(Paths.get("/Users/miyufeng/Downloads/lucene_dev"));IndexReader indexReader = DirectoryReader.open(dir);IndexSearcher indexSearcher = new IndexSearcher(indexReader);TopDocs topDocs = indexSearcher.search(query.build(), 10);System.out.println("=======count=======" + topDocs.totalHits);ScoreDoc[] scoreDocs = topDocs.scoreDocs;if (scoreDocs != null) {for (ScoreDoc scoreDoc : scoreDocs) {//獲取查詢到的文檔唯一標識, 文檔id, 這個id是lucene在創建文檔的時候自動分配的int docID = scoreDoc.doc;//通過文檔id, 讀取文檔Document doc = indexSearcher.doc(docID);System.out.println("==================================================");//通過域名, 從文檔中獲取域值System.out.println("===id==" + doc.get("id"));System.out.println("===name==" + doc.get("name"));System.out.println("===price==" + doc.get("price"));System.out.println("===image==" + doc.get("image"));System.out.println("===brandName==" + doc.get("brandName"));System.out.println("===categoryName==" + doc.get("categoryName"));}}//10. 關閉流 }9. 搜索案例
成品效果:
9.1. 引入依賴
在項目的pom.xml中引入依賴:
9.2. 項目加入頁面和資源
將Lucene課程資料\資源\頁面和靜態資源, 下的頁面和靜態資源拷貝到項目的resources目錄下
9.3. 創建包和啟動類
創建目錄, 并加入啟動類:
啟動類代碼:
/****/ @SpringBootApplication public class Application {public static void main(String[] args) {SpringApplication.run(Application.class, args);} }9.4. 配置文件
項目的resources目錄下創建application.yml內容如下:
spring:thymeleaf:cache: false9.5. 業務代碼:
9.5.1. 封裝pojo
pojo包下加入ResultModel實體類
/*** 自定義分頁實體類*/ public class ResultModel {// 商品列表private List<Sku> skuList;// 商品總數private Long recordCount;// 總頁數private Long pageCount;// 當前頁private long curPage;9.5.2. controller代碼
/****/ @Controller @RequestMapping("/list") public class SearchController {@Autowiredprivate SearchService searchService;/*** 搜索* @param queryString 查詢的關鍵字* @param price 查詢價格范圍* @param page 當前頁* @return* @throws Exception*/@RequestMappingpublic String query(String queryString, String price, Integer page, Model model) throws Exception{//處理當前頁if (StringUtils.isEmpty(page)) {page = 1;}if (page <= 0) {page = 1;}//調用service查詢ResultModel resultModel = searchService.query(queryString, price, page);model.addAttribute("result", resultModel);//查詢條件回顯到頁面model.addAttribute("queryString", queryString);model.addAttribute("price", price);model.addAttribute("page", page);return "search";} }9.5.3. service代碼
service接口:
/****/ public interface SearchService {public ResultModel query(String queryString, String price, Integer page) throws Exception; } /****/ @Service public class SearchServiceImpl implements SearchService {//每頁查詢20條數據public final static Integer PAGE_SIZE = 20;@Overridepublic ResultModel query(String queryString, String price, Integer page) throws Exception {long startTime = System.currentTimeMillis();//1. 需要使用的對象封裝ResultModel resultModel = new ResultModel();//從第幾條開始查詢int start = (page - 1) * PAGE_SIZE;//查詢到多少條為止Integer end = page * PAGE_SIZE;//創建分詞器Analyzer analyzer = new IKAnalyzer();//創建組合查詢對象BooleanQuery.Builder builder = new BooleanQuery.Builder();//2. 根據查詢關鍵字封裝查詢對象QueryParser queryParser = new QueryParser("name", analyzer);Query query1 = null;//判斷傳入的查詢關鍵字是否為空, 如果為空查詢所有, 如果不為空, 則根據關鍵字查詢if (StringUtils.isEmpty(queryString)) {query1 = queryParser.parse("*:*");} else {query1 = queryParser.parse(queryString);}//將關鍵字查詢對象, 封裝到組合查詢對象中builder.add(query1, BooleanClause.Occur.MUST);//3. 根據價格范圍封裝查詢對象if (!StringUtils.isEmpty(price)) {String[] split = price.split("-");Query query2 = IntPoint.newRangeQuery("price", Integer.parseInt(split[0]), Integer.parseInt(split[1]));//將價格查詢對象, 封裝到組合查詢對象中builder.add(query2, BooleanClause.Occur.MUST);}//4. 創建Directory目錄對象, 指定索引庫的位置/*** 使用MMapDirectory消耗的查詢時間* ====消耗時間為=========324ms* ====消耗時間為=========18ms*/Directory directory = FSDirectory.open(Paths.get("E:\\dir"));//5. 創建輸入流對象IndexReader reader = DirectoryReader.open(directory);//6. 創建搜索對象IndexSearcher indexSearcher = new IndexSearcher(reader);//7. 搜索并獲取搜索結果TopDocs topDocs = indexSearcher.search(builder.build(), end);//8. 獲取查詢到的總條數resultModel.setRecordCount(topDocs.totalHits);//9. 獲取查詢到的結果集ScoreDoc[] scoreDocs = topDocs.scoreDocs;long endTime = System.currentTimeMillis();System.out.println("====消耗時間為=========" + (endTime - startTime) + "ms");//10. 遍歷結果集封裝返回的數據List<Sku> skuList = new ArrayList<>();if (scoreDocs != null) {for (int i = start; i < end; i ++) {//通過查詢到的文檔編號, 找到對應的文檔對象Document document = reader.document(scoreDocs[i].doc);//封裝Sku對象Sku sku = new Sku();sku.setId(document.get("id"));sku.setPrice(Integer.parseInt(document.get("price")));sku.setImage(document.get("image"));sku.setName(document.get("name"));sku.setBrandName(document.get("brandName"));sku.setCategoryName(document.get("categoryName"));skuList.add(sku);}}//封裝查詢到的結果集resultModel.setSkuList(skuList);//封裝當前頁resultModel.setCurPage(page);//總頁數Long pageCount = topDocs.totalHits % PAGE_SIZE > 0 ? (topDocs.totalHits/PAGE_SIZE) + 1 : topDocs.totalHits/PAGE_SIZE;resultModel.setPageCount(pageCount);return resultModel;} }10. Lucene底層儲存結構(高級)
10.1. 詳細理解lucene存儲結構
存儲結構 :
索引(Index) :
- 一個目錄一個索引,在Lucene中一個索引是放在一個文件夾中的。
段(Segment) :
-
一個索引(邏輯索引)由多個段組成, 多個段可以合并, 以減少讀取內容時候的磁盤IO.
-
Lucene中的數據寫入會先寫內存的一個Buffffer,當Buffffer內數據到一定量后會被flflush成一個Segment,每個Segment有自己獨立的索引,可獨立被查詢,但數據永遠不能被更改。這種模式避免了隨機寫,數據寫入都是批量追加,能達到很高的吞吐量。Segment中寫入的文檔不可被修改,但可被刪除,刪除的方式也不是在文件內部原地更改,而是會由另外一個文件保存需要被刪除的文檔的DocID,保證數據文件不可被修改。Index的查詢需要對多個Segment進行查詢并對結果進行合并,還需要處理被刪除的文檔,為了對查詢進行優化,Lucene會有策略對多個Segment進行合并。
文檔(Document) :
-
文檔是我們建索引的基本單位,不同的文檔是保存在不同的段中的,一個段可以包含多篇文檔。
-
新添加的文檔是單獨保存在一個新生成的段中,隨著段的合并,不同的文檔合并到同一個段中。
域(Field) :
- 一篇文檔包含不同類型的信息,可以分開索引,比如標題,時間,正文,描述等,都可以保存在不同的域里。不同域的索引方式可以不同。
詞(Term)
- 詞是索引的最小單位,是經過詞法分析和語言處理后的字符串。
10.2. 索引庫物理文件
10.3. 索引庫文件擴展名對照表
| Segments File | segments_N | 保存一個提交點(a commit point )的信息 |
| Lock File | Write.lock | 防止多個IndexWriter同時寫到一份索引文件中 |
| Segment Info | .si | 保存了索引段的元數據信息 |
| Compound File | .cfs, .cfe | 一個可選的虛擬文件,把所有索引信息都存儲到符合索引文件中 |
| Fields | .fnm | 保存fields的相關信息 |
| Field Data | .fdt | 保存指定field data的指針 |
| Term Dictonary | .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 | 保留索引點,如果有的話 |
10.4. 詞典的構建
為何Lucene大數據量搜索快,要分兩部分來看 :
-
一點是因為底層的倒排索引存儲結構.
-
另一點就是查詢關鍵字的時候速度快, 因為詞典的索引結構.
10.4.1. 詞典數據結構對比
倒排索引中的詞典位于內存,其結構尤為重要,有很多種詞典結構,各有各的優缺點,最簡單如排序數組,通過二分查找來檢索數據,更快的有哈希表,磁盤查找有B樹、B+樹,但一個能支持TB級數據的倒排索引結構需要在時間和空間上有個平衡,下圖列了一些常見詞典的優缺點:
| 跳躍表 | 占用內存小,且可調,但是對模糊查詢支持不好 |
| 排序列表Array/List | 使用二分法查找,不平衡 |
| 字典樹 | 查詢效率跟字符串長度有關,但只適合英文詞典 |
| 哈希表 | 性能高,內存消耗大,幾乎是原始數據的三倍 |
| 雙數組字典樹 | 適合做中文詞典,內存占用小,很多分詞工具均采用此種算法 |
| FST(Finite State Transducers) | 一種有限狀態轉移機,Lucene4有開源實現,并大量使用 |
| B樹 | 磁盤索引,更新方便,但檢索速度慢,多用于數據庫 |
Lucene3.0之前使用的也是跳躍表結構,后換成了FST,但跳躍表在Lucene其他地方還有應用如倒排表合并和文檔號索引。
10.4.2. 跳躍表原理
Lucene3.0版本之前使用的跳躍表結構后換成了FST結構
? **優點 :**結構簡單、跳躍間隔、級數可控,Lucene3.0之前使用的也是跳躍表結構,,但跳躍表在Lucene其他地方還有應用如倒排表合并和文檔號索引。
? **缺點 :**模糊查詢支持不好.
單鏈表 :
單鏈表中查詢一個元素即使是有序的,我們也不能通過二分查找法的方式縮減查詢時間。
通俗的講也就是按照鏈表順序一個一個找.
舉例: 查找85這個節點, 需要查找7次.
?
跳躍表:
舉例: 查詢85這個節點, 一共需要查詢6次.
在level3層, 查詢3次, 查詢到1結尾, 退回到37節點
在level2層, 從37節點開始查詢, 查詢2次, 查詢到1結尾, 退回到71節點
在level1層, 從71節點開始查詢, 查詢1次, 查詢到85節點.
10.4.3. FST原理簡析
Lucene現在采用的數據結構為FST,它的特點就是: **優點:**內存占用率低,壓縮率一般在3倍~20倍之間、模糊查詢支持好、查詢快 **缺點:**結構復雜、輸入要求有序、更新不易
已知FST要求輸入有序,所以Lucene會將解析出來的文檔單詞預先排序,然后構建FST,我們假設輸入為abd,abe,acf,acg,那么整個構建過程如下:
輸入數據:
String inputValues[] = {"hei","ma","cheng","xu","yuan","good"}; long outputValues[] = {0,1,2,3,4,5};輸入的數據如下:
hei/0 ma/1 cheng/2 xu/3 yuan/4 good/5
存儲結果如下:
11. Lucene優化(高級)
11.1. 解決大量磁盤IO
-
confifig.setMaxBufffferedDocs(100000); 控制寫入一個新的segment前內存中保存的document的數目,設置較大的數目可以加快建索引速度。
數值越大索引速度越快, 但是會消耗更多的內存
-
indexWriter.forceMerge(文檔數量); 設置N個文檔合并為一個段
數值越大索引速度越快, 搜索速度越慢; 值越小索引速度越慢, 搜索速度越快
更高的值意味著索引期間更低的段合并開銷,但同時也意味著更慢的搜索速度,因為此時的索引通常會包含更多的段。如果該值設置的過高,能獲得更高的索引性能。但若在最后進行索引優化,那么較低的值會帶來更快的搜索速度,因為在索引操作期間程序會利用并發機制完成段合并操作。故建議對程序分別進行高低多種值的測試,利用計算機的實際性能來告訴你最優值。
創建索引代碼優化測試:
/*** 測試創建索引速度優化* @throws Exception*/ @Test public void createIndexTest2() throws Exception {//1. 采集數據SkuDao skuDao = new SkuDaoImpl();List<Sku> skuList = skuDao.querySkuList();//文檔集合List<Document> docList = new ArrayList<>();for (Sku sku : skuList) {//2. 創建文檔對象Document document = new Document();document.add(new StringField("id", sku.getId(), Field.Store.YES));document.add(new TextField("name", sku.getName(), Field.Store.YES));document.add(new IntPoint("price", sku.getPrice()));document.add(new StoredField("price", sku.getPrice()));document.add(new StoredField("image", sku.getImage()));document.add(new StringField("categoryName", sku.getCategoryName(), Field.Store.YES));document.add(new StringField("brandName", sku.getBrandName(), Field.Store.YES));//將文檔對象放入到文檔集合中docList.add(document);}long start = System.currentTimeMillis();//3. 創建分詞器, StandardAnalyzer標準分詞器, 對英文分詞效果好, 對中文是單字分詞, 也就是一個字就認為是一個詞.Analyzer analyzer = new StandardAnalyzer();//4. 創建Directory目錄對象, 目錄對象表示索引庫的位置Directory dir = FSDirectory.open(Paths.get("/Users/miyufeng/Downloads/lucene_dev"));//5. 創建IndexWriterConfig對象, 這個對象中指定切分詞使用的分詞器/*** 沒有優化 小100萬條數據, 創建索引需要7725ms**/IndexWriterConfig config = new IndexWriterConfig(analyzer);//設置在內存中多少個文檔向磁盤中批量寫入一次數據//如果設置的數字過大, 會過多消耗內存, 但是會提升寫入磁盤的速度config.setMaxBufferedDocs(500000);//6. 創建IndexWriter輸出流對象, 指定輸出的位置和使用的config初始化對象IndexWriter indexWriter = new IndexWriter(dir, config);//設置多少給文檔合并成一個段文件,數值越大索引速度越快, 搜索速度越慢; 值越小索引速度越慢, 搜索速度越快indexWriter.forceMerge(1000000);//7. 寫入文檔到索引庫for (Document doc : docList) {indexWriter.addDocument(doc);}//8. 釋放資源indexWriter.close();long end = System.currentTimeMillis();System.out.println("=====消耗的時間為:==========" + (end - start) + "ms"); }11.2. 選擇合適的分詞器
不同的分詞器分詞效果不同, 所用時間也不同
雖然StandardAnalyzer切分詞速度快過IKAnalyzer, 但是由于StandardAnalyzer對中文支持不好, 所以為了追求好的分詞效果,為了追求查詢時的準確率,也只能用IKAnalyzer分詞器, IKAnalyzer支持停用詞典和擴展詞典,可以通過調整兩個詞典中的內容,來提升查詢匹配的精度
11.3. 選擇合適的位置存放索引庫
| SimpleFSDirectory | Java.io.RandomAccessFile | Java.io.RandomAccessFile | 簡單實現,并發能力差 |
| NIOFSDirectory | Java.nio.FileChannel | FSDirectory.FSIndexOutput | 并發能力強,windows平臺下有重大bug |
| MMapDirectory | 內存映射 | FSDirectory.FSIndexOutput | 讀取操作基于內存 |
測試代碼修改:
Directory directory = MMapDirectory.open(Paths.get("E:\\dir"));11.4. 搜索api的選擇
盡量使用TermQuery代替QueryParser
盡量避免大范圍的日期查詢
12. Lucene相關度排序(高級)
12.1. 什么是相關度排序
Lucene對查詢關鍵字和索引文檔的相關度進行打分,得分高的就排在前邊。
12.1.1. 如何打分
Lucene是在用戶進行檢索時實時根據搜索的關鍵字計算出來的,分兩步:
計算出詞(Term)的權重
根據詞的權重值,計算文檔相關度得分。
12.1.2. 什么是詞的權重
明確索引的最小單位是一個Term(索引詞典中的一個詞),搜索也是要從Term中搜索,再根據Term找到文檔,Term對文檔的重要性稱為權重,影響Term權重有兩個因素:
-
Term Frequency (tf): 指此Term在此文檔中出現了多少次。tf 越大說明越重要。 詞(Term)在文檔中出現的次數越多,說明此詞(Term)對該文檔越重要,如“Lucene”這個詞,在文檔中出現的次數很多,說明該文檔主要就是講Lucene技術的。
-
Document Frequency (df): 指有多少文檔包含次Term。df 越大說明越不重要。 比如,在一篇英語文檔中,this出現的次數更多,就說明越重要嗎?不是的,有越多的文檔包含此詞(Term), 說明此詞(Term)太普通,不足以區分這些文檔,因而重要性越低。
12.1.3. 怎樣影響相關度排序
boost是一個加權值(默認加權值為1.0f),它可以影響權重的計算。
-
在索引時對某個文檔中的fifield設置加權值高,在搜索時匹配到這個文檔就可能排在前邊。
-
在搜索時對某個域進行加權,在進行組合域查詢時,匹配到加權值高的域最后計算的相關度得分就高。
設置boost是給域(fifield)或者Document設置的。
12.2.人為影響相關度排序
查詢的時候, 通過設置查詢域的權重, 可以人為影響查詢結果.
/*** 測試相關度排序* @throws Exception*/ @Test public void testIndexSearch2() throws Exception {//1. 創建分詞器(對搜索的關鍵詞進行分詞使用)//注意: 分詞器要和創建索引的時候使用的分詞器一模一樣Analyzer analyzer = new IKAnalyzer();//需求: 不管是名稱域還是品牌域或者是分類域有關于手機關鍵字的查詢出來//查詢的多個域名String[] fields = {"name", "categoryName", "brandName"};//設置影響排序的權重, 這里設置域的權重Map<String, Float> boots = new HashMap<>();boots.put("categoryName", 10000000000f);//從多個域查詢對象MultiFieldQueryParser multiFieldQueryParser = new MultiFieldQueryParser(fields, analyzer, boots);//設置查詢的關鍵詞Query query = multiFieldQueryParser.parse("手機");//4. 創建Directory目錄對象, 指定索引庫的位置Directory dir = FSDirectory.open(Paths.get("E:\\dir"));//5. 創建輸入流對象IndexReader indexReader = DirectoryReader.open(dir);//6. 創建搜索對象IndexSearcher indexSearcher = new IndexSearcher(indexReader);//7. 搜索, 并返回結果//第二個參數: 是返回多少條數據用于展示, 分頁使用TopDocs topDocs = indexSearcher.search(query, 10);//獲取查詢到的結果集的總數, 打印System.out.println("=======count=======" + topDocs.totalHits);//8. 獲取結果集ScoreDoc[] scoreDocs = topDocs.scoreDocs;//9. 遍歷結果集if (scoreDocs != null) {for (ScoreDoc scoreDoc : scoreDocs) {//獲取查詢到的文檔唯一標識, 文檔id, 這個id是lucene在創建文檔的時候自動分配的int docID = scoreDoc.doc;//通過文檔id, 讀取文檔Document doc = indexSearcher.doc(docID);System.out.println("==================================================");//通過域名, 從文檔中獲取域值System.out.println("===id==" + doc.get("id"));System.out.println("===name==" + doc.get("name"));System.out.println("===price==" + doc.get("price"));System.out.println("===image==" + doc.get("image"));System.out.println("===brandName==" + doc.get("brandName"));System.out.println("===categoryName==" + doc.get("categoryName"));}}//10. 關閉流 }13. Lucene使用注意事項(高級)
-
關鍵詞區分大小寫 OR AND TO等關鍵詞是區分大小寫的,lucene只認大寫的,小寫的當做普通單詞。
-
讀寫互斥性 同一時刻只能有一個對索引的寫操作,在寫的同時可以進行搜索
-
文件鎖 在寫索引的過程中強行退出將在tmp目錄留下一個lock文件,使以后的寫操作無法進行,可以將其手工刪除
-
時間格式 lucene只支持一種時間格式yyMMddHHmmss,所以你傳一個yy-MM-dd HH:mm:ss的時間給lucene它是不會當作時間來處理的
-
設置boost 有些時候在搜索時某個字段的權重需要大一些,例如你可能認為標題中出現關鍵詞的文章比正文中出現關鍵詞的文章更有價值,你可以把標題的boost設置的更大,那么搜索結果會優先顯示標題中出現關鍵詞的文章.
總結
以上是生活随笔為你收集整理的lucene ---- 黑马教程的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: R语言之逻辑回归
- 下一篇: 硬件基础知识(电容)