Lucene是apache軟件基金會發布的一個開放源代碼的全文檢索引擎工具包,由資深全文檢索專家Doug Cutting所撰寫,它是一個全文檢索引擎的架構,提供了完整的建立索引和查詢索引,以及部分文本分析的引擎,Lucene的目的是爲軟件開發人員提供一個簡單易用的工具包,以方便在目標系統中實現全文檢索的功能,或者是以此爲基礎創建起完整的全文檢索引擎,Lucene在全文檢索領域是一個經典的祖先,如今不少檢索引擎都是在其基礎上建立的,思想是相通的。css
Lucene是根據關健字來搜索的文本搜索工具,只能在某個網站內部搜索文本內容,不能跨網站搜索java
既然談到了網站內部的搜索,那麼咱們就談談咱們熟悉的百度、google那些搜索引擎又是基於什麼搜索的呢....程序員
從圖上已經看得很清楚,baidu、google等搜索引擎實際上是經過網絡爬蟲的程序來進行搜索的...算法
在介紹Lucene的時候,咱們已經說了:Lucene又不是搜索引擎,僅僅是在網站內部進行文本的搜索。那咱們爲何要學他呢???數據庫
咱們以前編寫納稅服務系統的時候,其實就已經使用過SQL來進行站內的搜索..apache
既然SQL能作的功能,咱們還要學Lucene,爲何呢???微信
咱們來看看咱們用SQL來搜索的話,有什麼缺點:網絡
咱們來看看在baidu中搜索Lucene爲關鍵字搜索出的內容是怎麼樣的:架構
以上所說的,咱們若是使用SQL的話,是作不到的。所以咱們就學習Lucene來幫咱們在站內根據文本關鍵字來進行搜索數據!工具
咱們若是網站須要根據關鍵字來進行搜索,可使用SQL,也可使用Lucene...那麼咱們Lucene和SQL是同樣的,都是在持久層中編寫代碼的。。
接下來,咱們就講解怎麼使用Lucene了.....在講解Lucene的API以前,咱們首先來說講Lucene存放的到底是什麼內容...咱們的SQL使用的是數據庫中的內存,在硬盤中爲DBF文件...那麼咱們Lucene內部又是什麼東西呢??
Lucene中存的就是一系列的二進制壓縮文件和一些控制文件,它們位於計算機的硬盤上, 這些內容統稱爲索引庫,索引庫有二部份組成:
也就是說:Lucene存放數據的地方咱們一般稱之爲索引庫,索引庫又分爲兩部分組成:原始記錄和詞彙表....
當咱們想要把數據存到索引庫的時候,咱們首先存入的是將數據存到原始記錄上面去....
又因爲咱們給用戶使用的時候,用戶使用的是關鍵字來進行查詢咱們的具體記錄。所以,咱們須要把咱們原始存進的數據進行拆分!將拆分出來的數據存進詞彙表中。
詞彙表就是相似於咱們在學Oracle中的索引表,拆分的時候會給出對應的索引值。
一旦用戶根據關鍵字來進行搜索,那麼程序就先去查詢詞彙表中有沒有該關鍵字,若是有該關鍵字就定位到原始記錄表中,將符合條件的原始記錄返回給用戶查看。
咱們查看如下的圖方便理解:
到了這裏,有人可能就會疑問:難道原始記錄拆分的數據都是一個一個漢字進行拆分的嗎??而後在詞彙表中不就有不少的關鍵字了???
其實,咱們在存到原始記錄表中的時候,能夠指定咱們使用哪一種算法來將數據拆分,存到詞彙表中.....咱們的圖是Lucene的標準分詞算法,一個一個漢字進行拆分。咱們可使用別的分詞算法,兩個兩個拆分或者其餘的算法。
首先,咱們來導入Lucene的必要開發包:
建立User對象,User對象封裝了數據....
/** * Created by ozc on 2017/7/12. */
public class User {
private String id ;
private String userName;
private String sal;
public User() {
}
public User(String id, String userName, String sal) {
this.id = id;
this.userName = userName;
this.sal = sal;
}
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
public String getUserName() {
return userName;
}
public void setUserName(String userName) {
this.userName = userName;
}
public String getSal() {
return sal;
}
public void setSal(String sal) {
this.sal = sal;
}
}
複製代碼
咱們想要使用Lucene來查詢出站內的數據,首先咱們得要有個索引庫吧!因而咱們先建立索引庫,將咱們的數據存到索引庫中。
建立索引庫的步驟:
@Test
public void createIndexDB() throws Exception {
//把數據填充到JavaBean對象中
User user = new User("1", "鍾福成", "將來的程序員");
//建立Document對象【導入的是Lucene包下的Document對象】
Document document = new Document();
//將JavaBean對象全部的屬性值,均放到Document對象中去,屬性名能夠和JavaBean相同或不一樣
/** * 向Document對象加入一個字段 * 參數一:字段的關鍵字 * 參數二:字符的值 * 參數三:是否要存儲到原始記錄表中 * YES表示是 * NO表示否 * 參數四:是否須要將存儲的數據拆分到詞彙表中 * ANALYZED表示拆分 * NOT_ANALYZED表示不拆分 * * */
document.add(new Field("id", user.getId(), Field.Store.YES, Field.Index.ANALYZED));
document.add(new Field("userName", user.getUserName(), Field.Store.YES, Field.Index.ANALYZED));
document.add(new Field("sal", user.getSal(), Field.Store.YES, Field.Index.ANALYZED));
//建立IndexWriter對象
//目錄指定爲E:/createIndexDB
Directory directory = FSDirectory.open(new File("E:/createIndexDB"));
//使用標準的分詞算法對原始記錄表進行拆分
Analyzer analyzer = new StandardAnalyzer(Version.LUCENE_30);
//LIMITED默認是1W個
IndexWriter.MaxFieldLength maxFieldLength = IndexWriter.MaxFieldLength.LIMITED;
/** * IndexWriter將咱們的document對象寫到硬盤中 * * 參數一:Directory d,寫到硬盤中的目錄路徑是什麼 * 參數二:Analyzer a, 以何種算法來對document中的原始記錄表數據進行拆分紅詞彙表 * 參數三:MaxFieldLength mfl 最多將文本拆分出多少個詞彙 * * */
IndexWriter indexWriter = new IndexWriter(directory, analyzer, maxFieldLength);
//將Document對象經過IndexWriter對象寫入索引庫中
indexWriter.addDocument(document);
//關閉IndexWriter對象
indexWriter.close();
}
複製代碼
程序執行完,咱們就會在硬盤中見到咱們的索引庫。
那咱們如今是不知道記錄是否真真正正存儲到索引庫中的,由於咱們看不見。索引庫存放的數據放在cfs文件下,咱們也是不能打開cfs文件的。
因而,咱們如今用一個關鍵字,把索引庫的數據讀取。看看讀取數據是否成功。
根據關鍵字查詢索引庫中的內容:
@Test
public void findIndexDB() throws Exception {
/** * 參數一: IndexSearcher(Directory path)查詢以xxx目錄的索引庫 * * */
Directory directory = FSDirectory.open(new File("E:/createIndexDB"));
//建立IndexSearcher對象
IndexSearcher indexSearcher = new IndexSearcher(directory);
//建立QueryParser對象
/** * 參數一: Version matchVersion 版本號【和上面是同樣的】 * 參數二:String f,【要查詢的字段】 * 參數三:Analyzer a【使用的拆詞算法】 * */
Analyzer analyzer = new StandardAnalyzer(Version.LUCENE_30);
QueryParser queryParser = new QueryParser(Version.LUCENE_30, "userName", analyzer);
//給出要查詢的關鍵字
String keyWords = "鍾";
//建立Query對象來封裝關鍵字
Query query = queryParser.parse(keyWords);
//用IndexSearcher對象去索引庫中查詢符合條件的前100條記錄,不足100條記錄的以實際爲準
TopDocs topDocs = indexSearcher.search(query, 100);
//獲取符合條件的編號
for (int i = 0; i < topDocs.scoreDocs.length; i++) {
ScoreDoc scoreDoc = topDocs.scoreDocs[i];
int no = scoreDoc.doc;
//用indexSearcher對象去索引庫中查詢編號對應的Document對象
Document document = indexSearcher.doc(no);
//將Document對象中的全部屬性取出,再封裝回JavaBean對象中去
String id = document.get("id");
String userName = document.get("userName");
String sal = document.get("sal");
User user = new User(id, userName, sal);
System.out.println(user);
}
複製代碼
效果:
咱們的Lucene程序就是大概這麼一個思路:將JavaBean對象封裝到Document對象中,而後經過IndexWriter把document寫入到索引庫中。當用戶須要查詢的時候,就使用IndexSearcher從索引庫中讀取數據,找到對應的Document對象,從而解析裏邊的內容,再封裝到JavaBean對象中讓咱們使用。
咱們再次看回咱們上一篇快速入門寫過的代碼,我來截取一些有表明性的:
如下代碼在把數據填充到索引庫,和從索引庫查詢數據的時候,都出現了。是重複代碼!
Directory directory = FSDirectory.open(new File("E:/createIndexDB"));
//使用標準的分詞算法對原始記錄表進行拆分
Analyzer analyzer = new StandardAnalyzer(Version.LUCENE_30);
複製代碼
如下的代碼其實就是將JavaBean的數據封裝到Document對象中,咱們是能夠經過反射來對其進行封裝....若是不封裝的話,咱們若是有不少JavaBean都要添加到Document對象中,就會出現不少相似的代碼。
document.add(new Field("id", user.getId(), Field.Store.YES, Field.Index.ANALYZED));
document.add(new Field("userName", user.getUserName(), Field.Store.YES, Field.Index.ANALYZED));
document.add(new Field("sal", user.getSal(), Field.Store.YES, Field.Index.ANALYZED));
複製代碼
如下代碼就是從Document對象中把數據取出來,封裝到JavaBean去。若是JavaBean中有不少屬性,也是須要咱們寫不少次相似代碼....
//將Document對象中的全部屬性取出,再封裝回JavaBean對象中去
String id = document.get("id");
String userName = document.get("userName");
String sal = document.get("sal");
User user = new User(id, userName, sal);
複製代碼
在編寫工具類的時候,值得注意的地方:
import org.apache.commons.beanutils.BeanUtils;
import org.apache.lucene.analysis.Analyzer;
import org.apache.lucene.analysis.standard.StandardAnalyzer;
import org.apache.lucene.document.Document;
import org.apache.lucene.index.IndexWriter;
import org.apache.lucene.store.Directory;
import org.apache.lucene.store.FSDirectory;
import org.apache.lucene.util.Version;
import org.junit.Test;
import java.io.File;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
/** * Created by ozc on 2017/7/12. */
/** * 使用單例事例模式 * */
public class LuceneUtils {
private static Directory directory;
private static Analyzer analyzer;
private static IndexWriter.MaxFieldLength maxFieldLength;
private LuceneUtils() {}
static{
try {
directory = FSDirectory.open(new File("E:/createIndexDB"));
analyzer = new StandardAnalyzer(Version.LUCENE_30);
maxFieldLength = IndexWriter.MaxFieldLength.LIMITED;
} catch (Exception e) {
e.printStackTrace();
}
}
public static Directory getDirectory() {
return directory;
}
public static Analyzer getAnalyzer() {
return analyzer;
}
public static IndexWriter.MaxFieldLength getMaxFieldLength() {
return maxFieldLength;
}
/** * @param object 傳入的JavaBean類型 * @return 返回Document對象 */
public static Document javaBean2Document(Object object) {
try {
Document document = new Document();
//獲得JavaBean的字節碼文件對象
Class<?> aClass = object.getClass();
//經過字節碼文件對象獲得對應的屬性【所有的屬性,不能僅僅調用getFields()】
Field[] fields = aClass.getDeclaredFields();
//獲得每一個屬性的名字
for (Field field : fields) {
String name = field.getName();
//獲得屬性的值【也就是調用getter方法獲取對應的值】
String method = "get" + name.substring(0, 1).toUpperCase() + name.substring(1);
//獲得對應的值【就是獲得具體的方法,而後調用就好了。由於是get方法,沒有參數】
Method aClassMethod = aClass.getDeclaredMethod(method, null);
String value = aClassMethod.invoke(object).toString();
System.out.println(value);
//把數據封裝到Document對象中。
document.add(new org.apache.lucene.document.Field(name, value, org.apache.lucene.document.Field.Store.YES, org.apache.lucene.document.Field.Index.ANALYZED));
}
return document;
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
/** * @param aClass 要解析的對象類型,要用戶傳入進來 * @param document 將Document對象傳入進來 * @return 返回一個JavaBean */
public static Object Document2JavaBean(Document document, Class aClass) {
try {
//建立該JavaBean對象
Object obj = aClass.newInstance();
//獲得該JavaBean全部的成員變量
Field[] fields = aClass.getDeclaredFields();
for (Field field : fields) {
//設置容許暴力訪問
field.setAccessible(true);
String name = field.getName();
String value = document.get(name);
//使用BeanUtils把數據封裝到Bean中
BeanUtils.setProperty(obj, name, value);
}
return obj;
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
@Test
public void test() {
User user = new User();
LuceneUtils.javaBean2Document(user);
}
}
複製代碼
@Test
public void createIndexDB() throws Exception {
//把數據填充到JavaBean對象中
User user = new User("2", "鍾福成2", "將來的程序員2");
Document document = LuceneUtils.javaBean2Document(user);
/** * IndexWriter將咱們的document對象寫到硬盤中 * * 參數一:Directory d,寫到硬盤中的目錄路徑是什麼 * 參數二:Analyzer a, 以何種算法來對document中的原始記錄表數據進行拆分紅詞彙表 * 參數三:MaxFieldLength mfl 最多將文本拆分出多少個詞彙 * * */
IndexWriter indexWriter = new IndexWriter(LuceneUtils.getDirectory(), LuceneUtils.getAnalyzer(), LuceneUtils.getMaxFieldLength());
//將Document對象經過IndexWriter對象寫入索引庫中
indexWriter.addDocument(document);
//關閉IndexWriter對象
indexWriter.close();
}
@Test
public void findIndexDB() throws Exception {
//建立IndexSearcher對象
IndexSearcher indexSearcher = new IndexSearcher(LuceneUtils.getDirectory());
//建立QueryParser對象
QueryParser queryParser = new QueryParser(Version.LUCENE_30, "userName", LuceneUtils.getAnalyzer());
//給出要查詢的關鍵字
String keyWords = "鍾";
//建立Query對象來封裝關鍵字
Query query = queryParser.parse(keyWords);
//用IndexSearcher對象去索引庫中查詢符合條件的前100條記錄,不足100條記錄的以實際爲準
TopDocs topDocs = indexSearcher.search(query, 100);
//獲取符合條件的編號
for (int i = 0; i < topDocs.scoreDocs.length; i++) {
ScoreDoc scoreDoc = topDocs.scoreDocs[i];
int no = scoreDoc.doc;
//用indexSearcher對象去索引庫中查詢編號對應的Document對象
Document document = indexSearcher.doc(no);
//將Document對象中的全部屬性取出,再封裝回JavaBean對象中去
User user = (User) LuceneUtils.Document2JavaBean(document, User.class);
System.out.println(user);
}
}
複製代碼
咱們已經能夠建立索引庫而且從索引庫讀取對象的數據了。其實索引庫還有地方能夠優化的....
咱們把數據添加到索引庫中的時候,每添加一次,都會幫咱們自動建立一個cfs文件...
這樣其實很差,由於若是數據量一大,咱們的硬盤就有很是很是多的cfs文件了.....其實索引庫會幫咱們自動合併文件的,默認是10個。
若是,咱們想要修改默認的值,咱們能夠經過如下的代碼修改:
//索引庫優化
indexWriter.optimize();
//設置合併因子爲3,每當有3個cfs文件,就合併
indexWriter.setMergeFactor(3);
複製代碼
咱們的目前的程序是直接與文件進行操做,這樣對IO的開銷實際上是比較大的。並且速度相對較慢....咱們可使用內存索引庫來提升咱們的讀寫效率...
對於內存索引庫而言,它的速度是很快的,由於咱們直接操做內存...可是呢,咱們要將內存索引庫是要到硬盤索引庫中保存起來的。當咱們讀取數據的時候,先要把硬盤索引庫的數據同步到內存索引庫中去的。
Article article = new Article(1,"培訓","傳智是一家Java培訓機構");
Document document = LuceneUtil.javabean2document(article);
Directory fsDirectory = FSDirectory.open(new File("E:/indexDBDBDBDBDBDBDBDB"));
Directory ramDirectory = new RAMDirectory(fsDirectory);
IndexWriter fsIndexWriter = new IndexWriter(fsDirectory,LuceneUtil.getAnalyzer(),true,LuceneUtil.getMaxFieldLength());
IndexWriter ramIndexWriter = new IndexWriter(ramDirectory,LuceneUtil.getAnalyzer(),LuceneUtil.getMaxFieldLength());
ramIndexWriter.addDocument(document);
ramIndexWriter.close();
fsIndexWriter.addIndexesNoOptimize(ramDirectory);
fsIndexWriter.close();
複製代碼
咱們在前面中就已經說過了,在把數據存到索引庫的時候,咱們會使用某些算法,將原始記錄表的數據存到詞彙表中.....那麼這些算法總和咱們能夠稱之爲分詞器
分詞器: ** 採用一種算法,將中英文本中的字符拆分開來,造成詞彙,以待用戶輸入關健字後搜索**
對於爲何要使用分詞器,咱們也明確地說過:因爲用戶不可能把咱們的原始記錄數據完完整整地記錄下來,因而他們在搜索的時候,是經過關鍵字進行對原始記錄表的查詢....此時,咱們就採用分詞器來最大限度地匹配相關的數據
步一:按分詞器拆分出詞彙
複製代碼
步二:去除停用詞和禁用詞
複製代碼
步三:若是有英文,把英文字母轉爲小寫,即搜索不分大小寫
複製代碼
咱們在選擇分詞算法的時候,咱們會發現有很是很是多地分詞器API,咱們能夠用如下代碼來看看該分詞器是怎麼將數據分割的:
private static void testAnalyzer(Analyzer analyzer, String text) throws Exception {
System.out.println("當前使用的分詞器:" + analyzer.getClass());
TokenStream tokenStream = analyzer.tokenStream("content",new StringReader(text));
tokenStream.addAttribute(TermAttribute.class);
while (tokenStream.incrementToken()) {
TermAttribute termAttribute = tokenStream.getAttribute(TermAttribute.class);
System.out.println(termAttribute.term());
}
}
複製代碼
在實驗完以後,咱們就能夠選擇恰當的分詞算法了....
這是一個第三方的分詞器,咱們若是要使用的話須要導入對應的jar包
這個第三方的分詞器有什麼好呢????他是中文首選的分詞器...也就是說:他是按照中文的詞語來進行拆分的!
咱們在使用SQL時,搜索出來的數據是沒有高亮的...而咱們使用Lucene,搜索出來的內容咱們能夠設置關鍵字爲高亮...這樣一來就更加註重用戶體驗了!
String keywords = "鍾福成";
List<Article> articleList = new ArrayList<Article>();
QueryParser queryParser = new QueryParser(LuceneUtil.getVersion(),"content",LuceneUtil.getAnalyzer());
Query query = queryParser.parse(keywords);
IndexSearcher indexSearcher = new IndexSearcher(LuceneUtil.getDirectory());
TopDocs topDocs = indexSearcher.search(query,1000000);
//設置關鍵字高亮
Formatter formatter = new SimpleHTMLFormatter("<font color='red'>","</font>");
Scorer scorer = new QueryScorer(query);
Highlighter highlighter = new Highlighter(formatter,scorer);
for(int i=0;i<topDocs.scoreDocs.length;i++){
ScoreDoc scoreDoc = topDocs.scoreDocs[i];
int no = scoreDoc.doc;
Document document = indexSearcher.doc(no);
//設置內容高亮
String highlighterContent = highlighter.getBestFragment(LuceneUtil.getAnalyzer(),"content",document.get("content"));
document.getField("content").setValue(highlighterContent);
Article article = (Article) LuceneUtil.document2javabean(document,Article.class);
articleList.add(article);
}
for(Article article : articleList){
System.out.println(article);
}
}
複製代碼
若是咱們搜索出來的文章內容太大了,而咱們只想顯示部分的內容,那麼咱們能夠對其進行摘要...
值得注意的是:搜索結果摘要須要與設置高亮一塊兒使用
String keywords = "鍾福成";
List<Article> articleList = new ArrayList<Article>();
QueryParser queryParser = new QueryParser(LuceneUtil.getVersion(),"content",LuceneUtil.getAnalyzer());
Query query = queryParser.parse(keywords);
IndexSearcher indexSearcher = new IndexSearcher(LuceneUtil.getDirectory());
TopDocs topDocs = indexSearcher.search(query,1000000);
Formatter formatter = new SimpleHTMLFormatter("<font color='red'>","</font>");
Scorer scorer = new QueryScorer(query);
Highlighter highlighter = new Highlighter(formatter,scorer);
//設置摘要
Fragmenter fragmenter = new SimpleFragmenter(4);
highlighter.setTextFragmenter(fragmenter);
for(int i=0;i<topDocs.scoreDocs.length;i++){
ScoreDoc scoreDoc = topDocs.scoreDocs[i];
int no = scoreDoc.doc;
Document document = indexSearcher.doc(no);
String highlighterContent = highlighter.getBestFragment(LuceneUtil.getAnalyzer(),"content",document.get("content"));
document.getField("content").setValue(highlighterContent);
Article article = (Article) LuceneUtil.document2javabean(document,Article.class);
articleList.add(article);
}
for(Article article : articleList){
System.out.println(article);
}
}
複製代碼
咱們搜索引擎確定用得也很多,使用不一樣的搜索引擎來搜索相同的內容。他們首頁的排行順序也會不一樣...這就是它們內部用了搜索結果排序....
影響網頁的排序有很是多種:
head/meta/【keywords關鍵字】
複製代碼
網頁的標籤整潔
複製代碼
網頁執行速度
複製代碼
採用div+css
複製代碼
等等等等
複製代碼
而在Lucene中咱們就能夠設置相關度得分來使不一樣的結果對其進行排序:
IndexWriter indexWriter = new IndexWriter(LuceneUtil.getDirectory(),LuceneUtil.getAnalyzer(),LuceneUtil.getMaxFieldLength());
//爲結果設置得分
document.setBoost(20F);
indexWriter.addDocument(document);
indexWriter.close();
複製代碼
固然了,咱們也能夠按單個字段排序:
//true表示降序
Sort sort = new Sort(new SortField("id",SortField.INT,true));
TopDocs topDocs = indexSearcher.search(query,null,1000000,sort);
複製代碼
也能夠按多個字段排序:在多字段排序中,只有第一個字段排序結果相同時,第二個字段排序纔有做用 提倡用數值型排序
Sort sort = new Sort(new SortField("count",SortField.INT,true),new SortField("id",SortField.INT,true));
TopDocs topDocs = indexSearcher.search(query,null,1000000,sort);
複製代碼
在咱們的例子中,咱們使用的是根據一個關鍵字來對某個字段的內容進行搜索。語法相似於下面:
QueryParser queryParser = new QueryParser(LuceneUtil.getVersion(),"content",LuceneUtil.getAnalyzer());
複製代碼
其實,咱們也可使用關鍵字來對多個字段進行搜索,也就是多條件搜索。咱們實際中經常用到的是多條件搜索,多條件搜索可使用咱們最大限度匹配對應的數據!
QueryParser queryParser = new MultiFieldQueryParser(LuceneUtil.getVersion(),new String[]{"content","title"},LuceneUtil.getAnalyzer());
複製代碼
這篇這是Lucene的冰山一角,通常如今用的可能都是Solr、Elasticsearch的了,但想要更加深刻了解Lucene可翻閱其餘資料哦~
若是文章有錯的地方歡迎指正,你們互相交流。習慣在微信看技術文章,想要獲取更多的Java資源的同窗,能夠關注微信公衆號:Java3y