1. 什么是Lucence
Lucene提供了一個簡單卻強大的應用程式接口,能夠做全文索引[把非結構化的文件信息形成結構化的數據(就像數據庫信息)]和搜尋。在 Java 開發環境里 Lucene 是一個成熟的免費開源工具。就其本身而言,Lucene 是當前以及最近幾年最受歡迎的免費 Java 信息檢索程序庫。
1.1 全文檢索
何為全文檢索?舉個例子,比如要在一個文件中查找某個字符串,最直接的想法就是從頭開始檢索,查到了就OK,這種對小數據量的文件來說,很簡單實用,但是對于大數據量的文件來說,就比較吃力了?;蛘哒f反過來查找包含某個字符串的文件(比如哪個文件中包含springboot),也是這樣,如果在一個擁有幾十個 G 的硬盤中找那效率可想而知,是非常低的。
文件中的數據是屬于非結構化數據,也就是說它沒有什么結構可言(不像我們數據庫中的信息,可以一行一行的去匹配查詢),要解決上面提到的效率問題,首先我們得將非結構化數據中的一部分信息提取出來,重新組織,使其變得有一定結構(說白了,就是變成關系數據庫型一行一行的數據),然后對這些有一定結構的數據進行搜索,從而達到搜索相對較快的目的。這就叫全文搜索。即先建立索引(表結構,把文件中的關鍵詞提取出來),再對索引進行搜索的過程。
1.2 Lucene 建立索引的方式
那么 Lucene 中是如何建立索引的呢?假設現在有兩篇文章,內容如下:
文章1的內容為:Tom lives in Guangzhou, I live in Guangzhou too.
文章2的內容為:He once lived in Shanghai.
首先第一步是將文檔傳給分詞組件(Tokenizer),分詞組件會將文檔分成一個個單詞,并去除標點符
號和停詞。所謂的停詞指的是沒有特別意義的詞,比如英文中的 a,the,too 等。經過分詞后,得到詞
元(Token) 。如下:
文章1經過分詞后的結果: [Tom] [lives] [Guangzhou] [I] [live] [Guangzhou]
文章2經過分詞后的結果: [He] [lives] [Shanghai]
然后將詞元傳給語言處理組件(Linguistic Processor),對于英語,語言處理組件一般會將字母變為小寫,將單詞縮減為詞根形式,如 ”lives” 到 ”live” 等,將單詞轉變為詞根形式,如 ”drove” 到 ”drive”等。然后得到詞(Term)。如下:
文章1經過處理后的結果: [tom] [live] [guangzhou] [i] [live] [guangzhou]
文章2經過處理后的結果: [he] [live] [shanghai]
最后將得到的詞傳給索引組件(Indexer),索引組件經過處理,得到下面的索引結構:
關鍵詞 | 文章號[出現頻率] | 出現位置 |
guangzhou | 1[2] | 3,6 |
he | 2[1] | 1 |
i | 1[1] | 4 |
live | 1[2],2[1] | 2,5,2 |
shanghai | 2[1] | 3 |
tom | 1[1] | 1 |
以上就是Lucene 索引結構中最核心的部分。它的關鍵字是按字符順序排列的,因此 Lucene 可以用二元搜索算法快速定位關鍵詞。實現時 Lucene 將上面三列分別作為詞典文件(Term Dictionary)、頻率文件(frequencies)和位置文件(positions)保存。其中詞典文件不僅保存有每個關鍵詞,還保留了指向頻率文件和位置文件的指針,通過指針可以找到該關鍵字的頻率信息和位置信息。
搜索的過程是先對詞典二元查找、找到該詞,通過指向頻率文件的指針讀出所有文章號,然后返回結果,然后就可以在具體的文章中根據出現位置找到該詞了。所以 Lucene 在第一次建立索引的時候可能會比較慢,但是以后就不需要每次都建立索引了,就快了
知道了Lucene的分詞及創建索引的原理,接下來通過Spring Boot中集成Lucene并實現 創建索引(可以理解為把各個文件中的信息通過分詞然后有序的存儲的數據庫表中)和搜索功能
2. Spring Boot 中集成 Lucence
首先需要導入 Lucene 的依賴,它的依賴有好幾個,如下:
<!-- Lucence核心包 --> <dependency> <groupId>org.apache.lucene</groupId> <artifactId>lucene-core</artifactId> <version>5.3.1</version> </dependency> <!-- Lucene查詢解析包 --> <dependency> <groupId>org.apache.lucene</groupId> <artifactId>lucene-queryparser</artifactId> <version>5.3.1</version> </dependency> <!-- 常規的分詞(英文) --> <dependency> <groupId>org.apache.lucene</groupId> <artifactId>lucene-analyzers-common</artifactId> <version>5.3.1</version> </dependency> <!--支持分詞高亮 --> <dependency> <groupId>org.apache.lucene</groupId> <artifactId>lucene-highlighter</artifactId> <version>5.3.1</version> </dependency> <!--支持中文分詞 --> <dependency> <groupId>org.apache.lucene</groupId> <artifactId>lucene-analyzers-smartcn</artifactId> <version>5.3.1</version> </dependency>
最后一個依賴是用來支持中文分詞的,因為默認是支持英文的。
2.2 快速入門
根據上文的分析,全文檢索有兩個步驟,先建立索引,再檢索。所以為了測試這個過程,我們這里創建兩個java 類,一個用來建立索引,另一個用來檢索。
2.2.1 建立索引
我們自己弄幾個文件,放到 F:\lucene\datas 目錄下,新建一個 Indexer 類來實現建立索引功能。首
先在構造方法中初始化標準分詞器并生成索引實例。
import org.apache.lucene.analysis.standard.StandardAnalyzer; import org.apache.lucene.document.Document; import org.apache.lucene.document.Field; import org.apache.lucene.document.TextField; import org.apache.lucene.index.IndexWriter; import org.apache.lucene.index.IndexWriterConfig; import org.apache.lucene.store.Directory; import org.apache.lucene.store.FSDirectory; import java.io.File; import java.io.FileReader; import java.io.IOException; import java.nio.file.Paths; public class Indexer { /*writer : 索引對象, 能夠建立索引(即能夠把文件中的詞提取出來,并標注出現的次數及出現的位置及哪個文件)*/ private IndexWriter writer; /* * 構造方法,實例化IndexWriter * @param indexDir //索引目錄(要搜索信息的目錄) * @throws Exception */ public Indexer(String indexDir) throws IOException { // 構造方法傳遞一個存儲建立索引的目錄(文件夾的路徑), 即要放建立的索引存儲在哪里 Directory dir = FSDirectory.open(Paths.get(indexDir)); //打開索引文件夾 StandardAnalyzer analyzer = new StandardAnalyzer(); //標準分詞器,會自動去掉空格, is a the等單詞 IndexWriterConfig config = new IndexWriterConfig(analyzer); //將標準分詞器配置到寫索引的配置中, 索引時將會 去掉空格, is, a, the等 writer = new IndexWriter(dir, config); //創建實例化索引對象 } /** * 獲取文檔,文檔里再設置每個字段,就類似于數據庫中的一行記錄 * @param file * @return * @throws Exception */ private Document getDocument(File file) throws Exception{ Document doc = new Document(); //開始添加字段 // 把doc當成數據庫中的表的一行記錄信息, 三個字段及對應的值 //字段一: contents:值(表中的內容) //字段二: fileName:值(文件名) //字段三: fullPath:值(文件的路徑) //添加內容 doc.add(new TextField("contents", new FileReader(file))); //添加文件名,并把這個字段存到索引文件里 doc.add(new TextField("fileName", file.getName(), Field.Store.YES)); //添加文件路徑 doc.add(new TextField("fullPath", file.getCanonicalPath(), Field.Store.YES)); return doc; //doc: 文檔對象,有三個屬性contents,fileName,fullPath } /*索引指定的文件 @param file @throws Exception */ private void indexFile(File file) throws Exception{ System.out.println("索引文件的路徑:" + file.getCanonicalPath()); Document doc = getDocument(file); //調用上面的getDocument方法, 獲取該文件的document對象 writer.addDocument(doc); //將doc添加到索引實例對象中 } /* 索引指定目錄下的所有文件 @param dataDir @return @throws Exception*/ public int indexAll(String dataDir) throws Exception{ File[] files = new File(dataDir).listFiles(); //獲取dataDir目錄下的所有文件 int numDocs = 0; if(null != files){ for(File file:files){ //調用上面的indexFile方法,對每個文件進行索引 indexFile(file); //理解為: 有多少個文件,在writer中就有多少行信息 //每行信息含有文件名,文件路徑,及文件內容 } numDocs = writer.numDocs(); writer.close(); } return numDocs; //返回索引的文件數 } }
生成索引:
public class MakeIndexer { public static void main(String[] args) { String indexDir = "F:\\java\\lucence"; //索引保存到的路徑 String dataDir = "F:\\java\\lucence\\data"; Indexer indexer = null; int indexedNum = 0; //記錄索引開始時間 long startTime = System.currentTimeMillis(); try{ //開始構建索引 indexer = new Indexer(indexDir); indexedNum = indexer.indexAll(dataDir); } catch (Exception e){ e.printStackTrace(); } long endTime = System.currentTimeMillis(); System.out.println("索引耗時" + (endTime - startTime) + "毫秒"); System.out.println("共索引了" + indexedNum + "個文件"); } }
建立搜索索引類:
import org.apache.lucene.analysis.Analyzer; import org.apache.lucene.analysis.standard.StandardAnalyzer; import org.apache.lucene.document.Document; import org.apache.lucene.index.DirectoryReader; import org.apache.lucene.index.IndexReader; import org.apache.lucene.queryparser.classic.QueryParser; import org.apache.lucene.search.IndexSearcher; import org.apache.lucene.search.Query; import org.apache.lucene.search.ScoreDoc; import org.apache.lucene.search.TopDocs; import org.apache.lucene.store.Directory; import org.apache.lucene.store.FSDirectory; import java.nio.file.Paths; public class Searcher { public static void search(String indexDir,String q) throws Exception{ Directory dir = FSDirectory.open(Paths.get(indexDir)); //獲取要查詢的路徑, 也就是索引所在的位置 IndexReader reader = DirectoryReader.open(dir); //構建IndexSearcher IndexSearcher searcher = new IndexSearcher(reader); //標準分詞器, 會自動去掉空格, is a the等單詞 Analyzer analyzer = new StandardAnalyzer(); //查詢解析器 查詢的字段為contents(建立索引時生成的表字段) QueryParser parser = new QueryParser("contents",analyzer); //通過解析要查詢的String, 獲取查詢對象, q為傳赤來的待查的字符串 Query query = parser.parse(q); //記錄索引開始時間 long startTime = System.currentTimeMillis(); //開始查詢,查詢前10條數據, 將記錄保存在docs中 TopDocs docs = searcher.search(query,10); //記錄索引結束時間 long endTime = System.currentTimeMillis(); System.out.println("匹配" + q + "共耗時" + (endTime - startTime) + "毫秒"); System.out.println("查詢到" + docs.totalHits + "條記錄"); //取出每條查詢結果 for(ScoreDoc scoreDoc : docs.scoreDocs){ //scoreDoc.doc相當于docId, 根據這個docID來獲取文檔 Document doc = searcher.doc(scoreDoc.doc); //fullPath是剛剛建立索引時候我們定義的一個字段,表示路徑。也可以取其它的內容,只要我們在建立索引時有定義即可. System.out.println(doc.get("fullPath")); } reader.close(); } }
搜索測試操作:
public class SerchIndexer { public static void main(String[] args) { String indexDir = "F:\\java\\lucence"; //查詢這個字符串 String q = "thank"; try{ Searcher.search(indexDir,q); } catch (Exception e){ e.printStackTrace(); } } }
執行搜索結果如下: 匹配thank共耗時35毫秒 查詢到1條記錄 F:\java\lucence\data\2.txt