由於工做中忽然要用到lucene,就到官網上下了lucene4.7的jar包和文檔,回頭開始學習的時候才發現,網上lucene相關的資料大部分都停留在3.*階段,因而結合前輩的代碼,本身寫了下面一個例子,該例子能夠實現對mysql數據庫中單個表的索引和多字段同時搜索以及按各個字段設定的權重對結果排序,我在最初排序的結果出來時很不理解,通過一天的研究,終於略知其因此然,下面貼例子程序和分析。java
數據庫鏈接的工具類mysql
package com.ztlc.lucene; import java.sql.Connection; import java.sql.DriverManager; import java.sql.SQLException; /** * JdbcUtil.java * @version 1.0 * @createTime JDBC獲取Connection工具類 */ public class JdbcUtil { private static Connection conn = null; //設置數據庫鏈接地址 private static final String URL = "jdbc:mysql://127.0.0.1/shopping?autoReconnect=true&characterEncoding=utf8"; private static final String JDBC_DRIVER = "com.mysql.jdbc.Driver"; //設置你的數據庫用戶名 private static final String USER_NAME = "root"; //設置你的數據庫密碼 private static final String PASSWORD = "123456"; public static Connection getConnection() { try { Class.forName(JDBC_DRIVER); conn = DriverManager.getConnection(URL, USER_NAME, PASSWORD); } catch (ClassNotFoundException e) { e.printStackTrace(); } catch (SQLException e) { e.printStackTrace(); } return conn; } }
建立索引和搜索的方法web
package com.ztlc.lucene; import java.io.File; import java.sql.Connection; import java.sql.ResultSet; import java.sql.Statement; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; import org.apache.lucene.analysis.Analyzer; import org.apache.lucene.analysis.standard.StandardAnalyzer; import org.apache.lucene.document.Document; import org.apache.lucene.document.Field; import org.apache.lucene.document.IntField; import org.apache.lucene.document.TextField; import org.apache.lucene.index.DirectoryReader; import org.apache.lucene.index.IndexReader; import org.apache.lucene.index.IndexWriter; import org.apache.lucene.index.IndexWriterConfig; import org.apache.lucene.queryparser.classic.MultiFieldQueryParser; 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 org.apache.lucene.util.Version; /** * SearchLogic.java * * @version 1.0 * @createTime Lucene數據庫檢索 */ public class SearchLogic { private static Connection conn = null; private static Statement stmt = null; private static ResultSet rs = null; //設定索引的存放路徑 private String searchDir = "E:\\javafiles\\workspace\\LuceneAndMysql\\index"; private static String[] field = null; private static File indexFile = null; private static IndexSearcher searcher = null; private static Analyzer analyzer = null; public Query query = null; /** 索引頁面緩衝 */ /** * 獲取數據庫數據 * * @return ResultSet * @throws Exception */ public List<Product> getResult(String queryStr) throws Exception { List<Product> result = null; TopDocs topDocs = this.search(queryStr); ScoreDoc[] scoreDocs = topDocs.scoreDocs; result = this.addHits2List(scoreDocs); return result; } /** * 爲數據庫檢索數據建立索引 * * @param rs * @throws Exception */ private void createIndex() throws Exception { conn = JdbcUtil.getConnection(); Directory directory = null; IndexWriter indexWriter = null; if (conn == null) { throw new Exception("數據庫鏈接失敗!"); } String sql = "select id, name, descr from product"; try { stmt = conn.createStatement(); rs = stmt.executeQuery(sql); indexFile = new File(searchDir); if (!indexFile.exists()) { indexFile.mkdir(); } directory = FSDirectory.open(indexFile); analyzer = new StandardAnalyzer(Version.LUCENE_47); IndexWriterConfig iwc = new IndexWriterConfig(Version.LUCENE_47, analyzer); indexWriter = new IndexWriter(directory, iwc); Document doc = null; while (rs.next()) { int id = rs.getInt("id"); String name = rs.getString("name"); String descr = rs.getString("descr"); doc = new Document(); /**此處是相對lucene4.*以前的版本改動比較大的地方,不能再直接new Field,而是new IntField,StringField,TextField等,其中 * TextField默認分詞,StringField默認不分詞,由於我這裏name和descr都須要分詞,因此都用的TextField * */ doc.add(new IntField("id", id, Field.Store.YES)); doc.add(new TextField("name", name, Field.Store.YES)); doc.add(new TextField("descr", descr, Field.Store.YES)); indexWriter.addDocument(doc); } indexWriter.close(); } catch (Exception e) { e.printStackTrace(); } finally { if (rs != null) rs.close(); if (stmt != null) stmt.close(); if (conn != null) conn.close(); } } /** * 搜索索引 * * @param queryStr * @return * @throws Exception */ private TopDocs search(String queryStr) throws Exception { if (searcher == null) { indexFile = new File(searchDir); IndexReader reader = DirectoryReader.open(FSDirectory .open(indexFile)); searcher = new IndexSearcher(reader); } /**同時搜索name和descr兩個field,並設定它們在搜索結果排序過程當中的權重,權重越高,排名越靠前 *爲了後面解釋score問題的方便,這裏設置相同的權重 * */ Map<String , Float> boosts = new HashMap<String, Float>(); boosts.put("name", 1.0f); boosts.put("descr", 1.0f); /**用MultiFieldQueryParser類實現對同一關鍵詞的跨域搜索 * */ MultiFieldQueryParser parser = new MultiFieldQueryParser(Version.LUCENE_47, field, new StandardAnalyzer(Version.LUCENE_47),boosts); query = parser.parse(queryStr); System.out.println("QueryParser :" + query.toString()); TopDocs topDocs = searcher.search(query, 10000); return topDocs; } /** * 返回結果並添加到List中 * * @param scoreDocs * @return * @throws Exception */ private List<Product> addHits2List(ScoreDoc[] scoreDocs) throws Exception { List<Product> listBean = new ArrayList<Product>(); Product proudct = null; for (int i = 0; i < scoreDocs.length; i++) { int docId = scoreDocs[i].doc; Document doc = searcher.doc(docId); proudct = new Product(); proudct.setId(Integer.parseInt(doc.get("id"))); proudct.setName(doc.get("name")); proudct.setDescr(doc.get("descr")); /** * 打印對結果score的解析,用於分析排序的依據,經過觀察結果和搜索相關信息,獲得如下結論 * 1.結果排序的score依據是fieldWeight的大小,fieldWeight的計算公式爲fieldWeight = tf * idf * fieldNorm; * * tf表示的是查詢條件中,每一個查詢詞(t:term)在本文檔(d)中的出現頻率。查詢關鍵詞出現的頻率越高,文檔的得分就越高。這個部分的默認計算公式是: * tf(t in d) = frequency½ * * idf表示的是反轉文檔頻率( Inverse Document Frequency).這個函數表示的是(t:term)在全部文檔中一共在多少個文檔中出現過。 * 由於文檔出現的次數越少就越容易定位,因此文檔數越少,得分就越高。這個函數的默認計算公式以下: * idf = log(numDocs/(docFreq+1)) + 1 * 在如下的例子裏,能夠簡單的理解爲,在所有的文檔裏,name這個field裏出現查詢詞「飛劍俠」的文檔越多該文檔的idf值就越低 * bean.id 24 : bean.name 飛劍俠 : bean.descr 飛劍俠! * bean.id 27 : bean.name 飛劍俠 : bean.descr 飛劍俠武器! * bean.id 25 : bean.name 飛劍俠飛劍俠 : bean.descr 測試修改數據新! * bean.id 8 : bean.name 飛劍俠 : bean.descr 鋼筆 * bean.id 22 : bean.name 3鋼筆 : bean.descr 飛劍俠飛劍俠!飛劍俠 * bean.id 23 : bean.name 4鋼筆 : bean.descr 飛劍俠飛劍俠! * bean.id 26 : bean.name 鋼筆 : bean.descr 飛劍俠 * searchBean.result.size : 7 * * 以上例子中name和descr的權重相同,ID爲8的文檔name這個field裏命中1個查詢詞「飛劍俠」,爲何會比ID爲26的文檔中descr這個field裏 * 命中一個查詢詞「飛劍俠」排名靠前哪麼多? * 這就是由於在最終獲得的結果裏,有4個文檔是在name這個field裏查到了「飛劍俠」,而有5個文檔在descr這個 * field裏查到了「飛劍俠」,因此在name裏查到「飛劍俠」的文檔8比在descr裏查到「飛劍俠」的文檔26的idf要高,因此文檔8排在了26前面 * * fieldNorm是事先計算好了的,它等於1/sqrt(wordsNum - 1)。咱們能夠簡單的理解爲在tf和idf不變的狀況下,文檔的包含的內容越少 * fieldNorm的值就越高,這也是爲何上面例子的結果裏id24比27排名靠前。 * */ System.out.println("**************************************************************************"); System.out.println(searcher.explain(query, docId)); System.out.println("**************************************************************************"); listBean.add(proudct); } return listBean; } public static void main(String[] args) throws Exception { SearchLogic logic = new SearchLogic(); //設定查詢詞爲「飛劍俠,而且在全部文檔的name 和 descr兩個field裏找包含這個查詢詞的文檔」 String queryStr = "飛劍俠"; field = new String[]{"name","descr"}; try { Long startTime = System.currentTimeMillis(); logic.createIndex(); List<Product> result = logic.getResult(queryStr); int i = 0; for (Product bean : result) { if (i == 100) break; /** * 打印完整的結果 * */ System.out.println("bean.id " + bean.getId() + " : bean.name " + bean.getName() + " : bean.descr " + bean.getDescr()); i++; } System.out.println("searchBean.result.size : " + result.size()); Long endTime = System.currentTimeMillis(); System.out.println("查詢所花費的時間爲:" + (endTime - startTime) / 1000); } catch (Exception e) { e.printStackTrace(); System.out.println(e.getMessage()); } } }
用到的beansql
package com.ztlc.lucene; public class Product { private int id; private String name; private String descr; public String getDescr() { return descr; } public void setDescr(String descr) { this.descr = descr; } public int getId() { return id; } public void setId(int id) { this.id = id; } public String getName() { return name; } public void setName(String name) { this.name = name; } }
該例子用lucene4.7實現了對mysql數據庫單表的索引和檢索,用到了lucene4.7裏如下幾個包:lucene-analyzers-common-4.7.0.jar;lucene-core-4.7.0.jar;lucene-demo-4.7.0.jar;lucene-queryparser-4.7.0.jar以及用於數據庫鏈接的jar包mysql-connector-java-5.1.18-bin.jar數據庫