博客地址:http://www.moonxy.comhtml
1、前言java
Lucene 是 apache 軟件基金會的一個子項目,由 Doug Cutting 開發,是一個開放源代碼的全文檢索引擎工具包,但它不是一個完整的全文檢索引擎,而是一個全文檢索引擎的庫,提供了完整的查詢引擎和索引引擎,部分文本分析引擎(英文與德文兩種西方語言)。Lucene 的目的是爲軟件開發人員提供一個簡單易用的工具包,以方便的在目標系統中實現全文檢索的功能,或者是以此爲基礎創建起完整的全文檢索引擎。Lucene 是一套用於全文檢索和搜尋的開源程式庫,由 Apache 軟件基金會支持和提供。python
Lucene 提供了一個簡單卻強大的應用程式接口,可以作全文索引和搜尋。在 Java 開發環境裏 Lucene 是一個成熟的免費開源工具。就其自己而言,Lucene 是當前以及最近幾年最受歡迎的免費 Java 信息檢索程序庫。人們常常提到信息檢索程序庫,雖然與搜索引擎有關,但不該該將信息檢索程序庫與搜索引擎相混淆。git
最開始 Lucene 只由 java 開發,供 java 程序調用,隨着 python 愈來愈火,Lucene 官網也提供了 python 版本的 lucene 庫,供 python 程序調用,即 PyLucene。github
2、下載 Lucene算法
2.1 下載數據庫
訪問 Lucene 官網 http://lucene.apache.org/,能夠看到 綠色和紅色兩個下載按鈕,分別提供 Lucene 和 Solr 的下載。apache
這裏簡要說明一下 Lucene 和 Solr,Lucene 是一個作全文檢索的庫,開發者能夠按照本身的實際業務需求來使用,而 Solr 是一個基於 Lucene 的全文檢索服務器。Solr 是在 Lucene 的基礎上進行擴展,而且提供了更加豐富的查詢語句,可擴展性和可配置性比 Lucene 更高。除此以外 Solr 還提供了一個完善的管理頁面,是一個產品級的全文搜索引擎。數組
官網首頁提供了最新版本的下載連接,若是須要下載使用歷史版本,能夠訪問 http://archive.apache.org/dist/lucene/java/,能夠下載 Lucene 全部的發行版本。此處下載 6.6.0 版本。服務器
2.2 添加依賴
將下載的 Lucene 包解壓以後,找到以下的 jar 包,新建本身的工程,此處不使用 Maven,因此手動添加 jar 包到工程的 lib 目錄下,以下:
IKAnalyzer2012_u6.jar,此 jar 包在 IK 分詞器項目中,是單獨的一個工具包,須要額外在網上下載:IKAnalyzer2012_u6。IK 分詞器採用了特有的 "正向迭代最細粒度切分算法",即從左到右的 正向最大(最長)和最小(最短)匹配,支持細粒度和智能分詞兩種切分模式,可將分詞器擴展配置文件 IKAnalyzer.cfg.xml 放在項目的 class 根目錄,並在其中配置擴展詞典路徑。當 IKAnalyzer6x() 構造方法參數爲空或者 false 時,是最細粒度分詞,爲 true 時是智能分詞。
lucene-analyzers-common-6.6.0.jar:lucene-6.6.0/common/
lucene-analyzers-smartcn-6.6.0.jar:lucene-6.6.0/smartcn/
lucene-core-6.6.0.jar:lucene-6.6.0/core/
lucene-highlighter-6.6.0.jar:lucene-6.6.0/highlighter/
lucene-memory-6.6.0.jar:lucene-6.6.0/memory/
lucene-queries-6.6.0.jar:lucene-6.6.0/queries/
lucene-queryparser-6.6.0.jar:lucene-6.6.0/queryparser/
2.3 Lucene 架構
首先是信息採集的過程,文件系統、數據庫、萬維網以及手工輸入的文件均可以做爲信息採集的對象,也是要搜索的文檔的來源,採集萬維網上的信息通常使用網絡爬蟲。完成信息採集以後到 Lucene 層面主要有兩個任務:索引文檔和搜索文檔。
索引文檔的過程完成由原始文檔到倒排索引的構建過程;
搜索文檔用以處理用戶查詢。而後當用戶輸入查詢關鍵詞,Lucene 完成文檔搜索任務,通過分詞、匹配、評分、排序等一系列過程以後返回用戶想要的文檔。
倒排索引(Inverted index),也常被稱爲反向索引,是一種索引方法,被用來存儲在全文搜索下某個單詞在一個文檔或者一組文檔中的存儲位置的映射,它是文檔檢索系統中最經常使用的數據結構,包括詞項所在的文章號、詞項頻率、詞項位置等。
3、Lucene 索引詳解
3.1 建立實體模型
建立新聞實體類模型
package tup.lucene.index; /** * 新聞實體類 * @author moonxy * */ public class News { private int id;//新聞id private String title;//新聞標題 private String content;//新聞內容 private int reply;//評論數 public News() { } public News(int id, String title, String content, int reply) { super(); this.id = id; this.title = title; this.content = content; this.reply = reply; } // 省略 setter 和 getter 方法 }
3.2 建立索引
Lucene 索引文檔須要依靠 IndexWriter 對象,建立 IndexWriter 須要兩個參數:一個是 IndexWriterConfig 對象,該對象能夠設置建立索引使用哪一種分詞器,另外一個是索引的保存路徑。IndexWriter 對象的 addDocument() 方法用於添加文檔,該方法的參數爲 Document 對象,IndexWriter 對象一次能夠添加多個文檔,最後調用 commit() 方法生成索引。
package tup.lucene.index; import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; import java.util.Date; import org.apache.lucene.analysis.Analyzer; import org.apache.lucene.document.Document; import org.apache.lucene.document.Field; import org.apache.lucene.document.FieldType; import org.apache.lucene.document.IntPoint; import org.apache.lucene.document.StoredField; import org.apache.lucene.index.IndexOptions; import org.apache.lucene.index.IndexWriter; import org.apache.lucene.index.IndexWriterConfig; import org.apache.lucene.index.IndexWriterConfig.OpenMode; import org.apache.lucene.store.Directory; import org.apache.lucene.store.FSDirectory; import tup.lucene.ik.IKAnalyzer6x; /** * Lucene 建立索引 * @author moonxy * */ public class CreateIndex { public static void main(String[] args) { // 建立3個News對象 News news1 = new News(); news1.setId(1); news1.setTitle("安倍晉三本週會晤特朗普 將強調日本對美國益處"); news1.setContent("日本首相安倍晉三計劃2月10日在華盛頓與美國總統特朗普舉行會晤時提出加大日本在美國投資的設想"); news1.setReply(672); News news2 = new News(); news2.setId(2); news2.setTitle("北大迎4380名新生 農村學生700多人近年最多"); news2.setContent("昨天,北京大學迎來4380名來自全國各地及數十個國家的本科新生。其中,農村學生共700餘名,爲近年最多..."); news2.setReply(995); News news3 = new News(); news3.setId(3); news3.setTitle("特朗普宣誓(Donald Trump)就職美國第45任總統"); news3.setContent("當地時間1月20日,唐納德·特朗普在美國國會宣誓就任,正式成爲美國第45任總統。"); news3.setReply(1872); // 開始時間 Date start = new Date(); System.out.println("**********開始建立索引**********"); // 建立IK分詞器 Analyzer analyzer = new IKAnalyzer6x();//使用IK最細粒度分詞 IndexWriterConfig icw = new IndexWriterConfig(analyzer);
// CREATE 表示先清空索引再從新建立 icw.setOpenMode(OpenMode.CREATE); Directory dir = null; IndexWriter inWriter = null; // 存儲索引的目錄 Path indexPath = Paths.get("indexdir"); try { if (!Files.isReadable(indexPath)) { System.out.println("索引目錄 '" + indexPath.toAbsolutePath() + "' 不存在或者不可讀,請檢查"); System.exit(1); } dir = FSDirectory.open(indexPath); inWriter = new IndexWriter(dir, icw); // 設置新聞ID索引並存儲 FieldType idType = new FieldType(); idType.setIndexOptions(IndexOptions.DOCS); idType.setStored(true); // 設置新聞標題索引文檔、詞項頻率、位移信息和偏移量,存儲並詞條化 FieldType titleType = new FieldType(); titleType.setIndexOptions(IndexOptions.DOCS_AND_FREQS_AND_POSITIONS_AND_OFFSETS); titleType.setStored(true); titleType.setTokenized(true); FieldType contentType = new FieldType(); contentType.setIndexOptions(IndexOptions.DOCS_AND_FREQS_AND_POSITIONS_AND_OFFSETS); contentType.setStored(true); contentType.setTokenized(true); contentType.setStoreTermVectors(true); contentType.setStoreTermVectorPositions(true); contentType.setStoreTermVectorOffsets(true); Document doc1 = new Document(); doc1.add(new Field("id", String.valueOf(news1.getId()), idType)); doc1.add(new Field("title", news1.getTitle(), titleType)); doc1.add(new Field("content", news1.getContent(), contentType)); doc1.add(new IntPoint("reply", news1.getReply())); doc1.add(new StoredField("reply_display", news1.getReply())); Document doc2 = new Document(); doc2.add(new Field("id", String.valueOf(news2.getId()), idType)); doc2.add(new Field("title", news2.getTitle(), titleType)); doc2.add(new Field("content", news2.getContent(), contentType)); doc2.add(new IntPoint("reply", news2.getReply())); doc2.add(new StoredField("reply_display", news2.getReply())); Document doc3 = new Document(); doc3.add(new Field("id", String.valueOf(news3.getId()), idType)); doc3.add(new Field("title", news3.getTitle(), titleType)); doc3.add(new Field("content", news3.getContent(), contentType)); doc3.add(new IntPoint("reply", news3.getReply())); doc3.add(new StoredField("reply_display", news3.getReply())); inWriter.addDocument(doc1); inWriter.addDocument(doc2); inWriter.addDocument(doc3); inWriter.commit(); inWriter.close(); dir.close(); } catch (IOException e) { e.printStackTrace(); } Date end = new Date(); System.out.println("索引文檔用時:" + (end.getTime() - start.getTime()) + " milliseconds"); System.out.println("**********索引建立完成**********"); } }
執行以後,在控制檯輸出以下:
**********開始建立索引********** 加載擴展詞典:dict/ext.dic 加載擴展中止詞典:dict/stopword.dic 加載擴展中止詞典:dict/ext_stopword.dic 索引文檔用時:1064 milliseconds **********索引建立完成**********
而且在項目中生成以下索引文件:
3.3 Luke 查看索引
索引建立完成之後生成了如上的一批特殊格式的文件,若是直接用工具打開,會顯示的都是亂碼。可使用索引查看工具 Luke 來查看。
Luke 是開源工具,代碼託管在 GitHub 上,項目地址:https://github.com/DmitryKey/luke/releases,此處下載 luke 6.6.0,地址爲 https://github.com/DmitryKey/luke/releases/download/luke-6.6.0/luke-6.6.0-luke-release.zip,若是在 Windows 中沒法下載,能夠在 Linux 中使用 wget 下載,命令爲:wget https://github.com/DmitryKey/luke/releases/download/luke-6.6.0/luke-6.6.0-luke-release.zip。
下載後解壓,進入 luke 目錄,若是是在 Linux 平臺,運行 luke.bat 便可啓動軟件,並在 Path 中輸入 index 存儲的目錄,便可打開索引文件,顯示出索引的具體內容。
注意:對於不一樣版本的 Lucene,須要選擇對應版本的 Luke,不然可能會出現不能正常解析的錯誤。
3.4 Lucene 查詢詳解
在 Lucene 中,處理用戶輸入的查詢關鍵詞其實就是構建 Query 對象的過程。Lucene 搜索文檔須要先讀入索引文件,實例化一個 IndexReader 對象,而後實例化出 IndexSearch 對象,IndexSearch 對象的 search() 方法完成搜索過程,Query 對象做爲 search() 方法的對象。搜索結果保存在一個 TopDocs 類型的文檔集合中,遍歷 TopDocs 集合輸出文檔信息。
QueryParser 能夠搜索單個字段,而 MultiFieldQueryParser 則能夠查詢多個字段,而且多個字段之間是或的關係,因此在開發中,MultiFieldQueryParser 使用的較多。
package tup.lucene.queries; import java.io.IOException; import java.nio.file.Path; import java.nio.file.Paths; import org.apache.lucene.analysis.Analyzer; import org.apache.lucene.document.Document; import org.apache.lucene.index.DirectoryReader; import org.apache.lucene.index.IndexReader; import org.apache.lucene.queryparser.classic.MultiFieldQueryParser; import org.apache.lucene.queryparser.classic.ParseException; import org.apache.lucene.queryparser.classic.QueryParser; import org.apache.lucene.queryparser.classic.QueryParser.Operator; 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 tup.lucene.ik.IKAnalyzer6x; /** * 單域搜索 * @author moonxy * */ public class QueryParseTest { public static void main(String[] args) throws ParseException, IOException { // 搜索單個字段 String field = "title"; // 搜索多個字段時使用數組 //String[] fields = { "title", "content" }; Path indexPath = Paths.get("indexdir"); Directory dir = FSDirectory.open(indexPath); IndexReader reader = DirectoryReader.open(dir); IndexSearcher searcher = new IndexSearcher(reader); Analyzer analyzer = new IKAnalyzer6x(false);//最細粒度分詞 QueryParser parser = new QueryParser(field, analyzer); // 多域搜索 //MultiFieldQueryParser multiParser = new MultiFieldQueryParser(fields, analyzer); // 關鍵字同時成立使用 AND, 默認是 OR parser.setDefaultOperator(Operator.AND); // 查詢語句 Query query = parser.parse("農村學生");//查詢關鍵詞 System.out.println("Query:" + query.toString()); // 返回前10條 TopDocs tds = searcher.search(query, 10); for (ScoreDoc sd : tds.scoreDocs) { // Explanation explanation = searcher.explain(query, sd.doc); // System.out.println("explain:" + explanation + "\n"); Document doc = searcher.doc(sd.doc); System.out.println("DocID:" + sd.doc); System.out.println("id:" + doc.get("id")); System.out.println("title:" + doc.get("title")); System.out.println("content:" + doc.get("content")); System.out.println("文檔評分:" + sd.score); } dir.close(); reader.close(); } }
控制檯輸出以下:
加載擴展詞典:dict/ext.dic 加載擴展中止詞典:dict/stopword.dic 加載擴展中止詞典:dict/ext_stopword.dic Query:+title:農村 +title:村學 +title:學生 DocID:1 id:2 title:北大迎4380名新生 農村學生700多人近年最多 content:昨天,北京大學迎來4380名來自全國各地及數十個國家的本科新生。其中,農村學生共700餘名,爲近年最多... 文檔評分:2.320528
注意,在結果中打印了 DocID 和 id,前者是文檔的 ID,是 Lucene 爲索引的每一個文檔標記,後者是文檔自定義的 id 字段。
3.5 Lucene 查詢高亮
高亮功能一直都是全文檢索的一項很是優秀的模塊,在一個標準的搜索引擎中,高亮的返回命中結果,幾乎是必不可少的一項需求,由於經過高亮,能夠在搜索界面上快速標記出用戶的搜索關鍵字,從而減小了用戶本身尋找想要的結果的時間,在必定程度上大大提升了用戶的體驗性和友好度。
Highlight 包含3個主要部分:
1)段劃分器:Fragmenter
2)計分器:Score
3)格式化器:Formatter
package tup.lucene.highlfighter; import java.io.IOException; import java.nio.file.Path; import java.nio.file.Paths; import org.apache.lucene.analysis.Analyzer; import org.apache.lucene.analysis.TokenStream; import org.apache.lucene.document.Document; import org.apache.lucene.index.DirectoryReader; import org.apache.lucene.index.IndexReader; import org.apache.lucene.queryparser.classic.ParseException; 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.search.highlight.Fragmenter; import org.apache.lucene.search.highlight.Highlighter; import org.apache.lucene.search.highlight.InvalidTokenOffsetsException; import org.apache.lucene.search.highlight.QueryScorer; import org.apache.lucene.search.highlight.SimpleHTMLFormatter; import org.apache.lucene.search.highlight.SimpleSpanFragmenter; import org.apache.lucene.search.highlight.TokenSources; import org.apache.lucene.store.Directory; import org.apache.lucene.store.FSDirectory; import tup.lucene.ik.IKAnalyzer6x; /** * Lucene查詢高亮 * @author moonxy * */ public class HighlighterTest { public static void main(String[] args) throws IOException, InvalidTokenOffsetsException, ParseException { String field = "title"; Path indexPath = Paths.get("indexdir"); Directory dir = FSDirectory.open(indexPath); IndexReader reader = DirectoryReader.open(dir); IndexSearcher searcher = new IndexSearcher(reader); Analyzer analyzer = new IKAnalyzer6x(); QueryParser parser = new QueryParser(field, analyzer); Query query = parser.parse("北大學生"); System.out.println("Query:" + query); // 查詢高亮 QueryScorer score = new QueryScorer(query, field); SimpleHTMLFormatter fors = new SimpleHTMLFormatter("<span style=\"color:red;\">", "</span>");// 定製高亮標籤 Highlighter highlighter = new Highlighter(fors, score);// 高亮分析器 // 返回前10條 TopDocs tds = searcher.search(query, 10); for (ScoreDoc sd : tds.scoreDocs) { // Explanation explanation = searcher.explain(query, sd.doc); // System.out.println("explain:" + explanation + "\n"); Document doc = searcher.doc(sd.doc); System.out.println("id:" + doc.get("id")); System.out.println("title:" + doc.get("title")); Fragmenter fragment = new SimpleSpanFragmenter(score); highlighter.setTextFragmenter(fragment); // TokenStream tokenStream = TokenSources.getAnyTokenStream(searcher.getIndexReader(), sd.doc, field, analyzer);// 獲取tokenstream // String str = highlighter.getBestFragment(tokenStream, doc.get(field));// 獲取高亮的片斷 String str = highlighter.getBestFragment(analyzer, field, doc.get(field));// 獲取高亮的片斷 System.out.println("高亮的片斷:" + str); } dir.close(); reader.close(); } }
控制檯輸出以下:
加載擴展詞典:dict/ext.dic 加載擴展中止詞典:dict/stopword.dic 加載擴展中止詞典:dict/ext_stopword.dic Query:title:北大學生 title:北大 title:大學生 title:大學 title:學生 id:2 title:北大迎4380名新生 農村學生700多人近年最多 高亮的片斷:<span style="color:red;">北大</span>迎4380名新生 農村<span style="color:red;">學生</span>700多人近年最多
4、Tika 文件內容提取
Apache Tika 是一個用於文件類型檢測和文件內容提取的庫,是 Apache 軟件基金會的項目。Tika 能夠檢測操做 1000 種不一樣類型的文檔,好比 PPT、PDF、DOC、XLS 等,全部的文本類型均可以經過一個簡單的接口被解析,Tika 普遍應用於搜索引擎、內容分析、文本翻譯等領域。
Tika 下載的官網地址:http://tika.apache.org/download.html,其歷史版本下載地址爲:http://archive.apache.org/dist/tika/,此處如今當前最新版 tika-app-1.18.jar
下載下來以後爲一個 jar 包,但 Tika 可做爲 GUI 工具使用,在 CMD 中先進入下載目錄,而後使用以下命令啓動 Tika GUI:
java -jar tika-app-1.18.jar -g
java -jar 表示啓動 jar 包,後面跟 jar 包的名字,-g(--gui) 參數表示以 GUI 的方式啓動 Tika(Start the Apache Tika GUI),固然前提是已經配置好了 java 環境變量。
界面以下:
能夠點擊 File 菜單中的 Open...來打開一個本地文件或者輸入 URL 來打開遠程文件,也可直接將本地文件拖入到 Tika,Tika 將會自動識別文件類型並顯示文件信息。
默認顯示文件的元數據信息,以下:
點擊 View 菜單,裏面能夠選擇查看文件的具體內容,如選擇其中的 Plain Text,顯示以下:
將該 jar 包放入項目 lib 中,調用相應的接口就能夠提取不一樣文件的內容,主要分爲以下提取方法。
方法一:使用 Tika 對象提取文檔內容
package tup.tika.demo; import java.io.File; import java.io.IOException; import org.apache.tika.Tika; import org.apache.tika.exception.TikaException; /** * 使用Tika對象自動解析文檔,提取文檔內容 * @author moonxy * */ public class TikaExtraction { public static void main(String[] args) throws IOException, TikaException { Tika tika = new Tika(); // 新建存放各類文件的files文件夾 File fileDir = new File("files"); // 若是文件夾路徑錯誤,退出程序 if (!fileDir.exists()) { System.out.println("文件夾不存在, 請檢查!"); System.exit(0); } // 獲取文件夾下的全部文件,存放在File數組中 File[] fileArr = fileDir.listFiles(); String filecontent; for (File f : fileArr) { // 獲取文件名 System.out.println("File Name: " + f.getName()); filecontent = tika.parseToString(f);// 自動解析 // 獲取文件內容 System.out.println("Extracted Content: " + filecontent); } } }
在工程中新建 Files 目錄,放入須要解析的文件:
上述代碼中首先新建一個 File 對象指向存放各類文檔的文件夾 Files,經過 File 對象的 exists() 方法判斷目錄路徑是否存在,若是路徑錯誤則退出程序,打印提示信息。接下來,經過 listFiles() 方法獲取 files 目錄下全部的文件,存放在文件數組中。最後新建一個 Tika 對象,調用 parseToString() 方法獲取文檔內容,該方法的傳入參數爲 File 對象。
方法二:使用 Parser 接口提取文檔內容
package tup.tika.demo; import java.io.File; import java.io.FileInputStream; import java.io.IOException; import org.apache.tika.exception.TikaException; import org.apache.tika.metadata.Metadata; import org.apache.tika.parser.AutoDetectParser; import org.apache.tika.parser.ParseContext; import org.apache.tika.parser.Parser; import org.apache.tika.sax.BodyContentHandler; import org.xml.sax.SAXException; /** * 使用Parser接口自動解析文檔,提取文檔內容 * @author moonxy * */ public class ParserExtraction { public static void main(String[] args) throws IOException, SAXException, TikaException { // 新建存放各類文件的files文件夾 File fileDir = new File("files"); // 若是文件夾路徑錯誤,退出程序 if (!fileDir.exists()) { System.out.println("文件夾不存在, 請檢查!"); System.exit(0); } // 獲取文件夾下的全部文件,存放在File數組中 File[] fileArr = fileDir.listFiles(); // 建立內容處理器對象 BodyContentHandler handler = new BodyContentHandler(); // 建立元數據對象 Metadata metadata = new Metadata(); FileInputStream inputStream = null; Parser parser = new AutoDetectParser(); // 自動檢測分析器 ParseContext context = new ParseContext(); for (File f : fileArr) { // 獲取文件名 System.out.println("File Name: " + f.getName()); inputStream = new FileInputStream(f); parser.parse(inputStream, handler, metadata, context); // 獲取文件內容 System.out.println(f.getName() + ":\n" + handler.toString()); } } }
使用 Parse 接口自動提取內容和單一的提取一種文檔的區別在於實例化對象不同,AutoDetectParser 是 CompositeParser 的子類,它可以自動檢測文件類型,並使用相應的方法把接收到的文檔自動發送給最接近的解析器類。
能夠參考官方文檔,將文檔解析爲不一樣的格式,如上面都是解析爲純文本格式(Plain text),也可解析爲 html 格式(Structured text)等,如:
http://tika.apache.org/1.18/examples.html#Parsing_to_XHTML