Lucene從入門到實戰
Lucene
在了解Lucene之前,我們先了解下全文數據查詢。
全文數據查詢
我們的數據一般分為兩種:結構化數據和非結構化數據
- 結構化數據:有固定格式或有限長度的數據,如資料庫中的數據、元數據
- 非結構化數據:又叫全文數據,指不定長或無固定格式的數據,如郵件、word文檔
資料庫適合結構化數據的精確查詢,而不適合半結構化、非結構化數據的模糊查詢及靈活搜索(特別是數據量大時),無法提供想要的實時性。
全文數據查詢
- 順序掃描法
所謂順序掃描,就是要找內容包含一個字元串的文件,就是一個文檔一個文檔的看。對於每一個文檔,從頭看到尾,如果此文檔包含此字元串,則此文檔為我們要找的文件,接著看下一個文件,直到掃描完所有的文件。
- 全文檢索
全文檢索是指電腦索引程式通過掃描文章中的每一個詞,對每一個詞建立一個索引,指明該詞在文章中出現的次數和位置,當用戶查詢時,檢索程式就根據事先建立的索引進行查找,並將查找的結果回饋給用戶的檢索方式。這個過程類似於通過字典中的檢索字表查字的過程。
全文檢索的基本思路,就是將非結構化數據中的一部分資訊提取出來,重新組織,使其變得有一定結構,然後對這個有一定結構的數據進行搜索,從而達到搜索相對較快的目的。
這部分從非結構化數據中提取出的然後重新組織的資訊,我們稱之索引,這種先建立索引,再對索引進行搜索的過程就叫全文檢索(Full-text Search) 。
具體應用的有單機軟體的搜索(word中的搜索) 站內搜索 ( 京東、 taobao、拉勾職位搜索) 專業搜索引擎公司 (google、baidu)的搜索。
全文檢索通常使用倒排索引來實現。
3. 正排索引
正排索引是指文檔ID為key,表中記錄每個關鍵字出現的次數位置等,查找時掃描表中的每個文檔中字的資訊,直到找到所有包含查詢關鍵字的文檔。
格式如下:
文檔1的ID > 單詞1:出現次數,出現位置列表;單詞2:出現次數,出現位置列表…………
文檔2的ID > 單詞1:出現次數,出現位置列表;單詞2:出現次數,出現位置列表…………
當用戶在主頁上搜索關鍵詞「華為手機」時,假設只存在正向索引(forward index),那麼就需要掃描索引庫中的所有文檔,找出所有包含關鍵詞「華為手機」的文檔,再根據打分模型進行打分,排出名次後呈現給用戶。因為互聯網上收錄在搜索引擎中的文檔的數目是個天文數字,這樣的索引結構根本無法滿足實時返回排名結果的要求
4. 倒排索引
被用來存儲在全文搜索下某個單詞在一個文檔或一組文檔中的存儲位置的映射。它是文檔檢索系統中常用的數據結構。通過倒排索引,可以根據單詞快速獲取包含這個單詞的文檔列表。
格式如下:
關鍵詞1 > 文檔1的ID :出現次數,出現的位置;文檔2的ID:出現次數 ,出現的位置…………
關鍵詞2 > 文檔1的ID :出現次數,出現的位置;文檔2的ID:出現次數 ,出現的位置…………
Lucene基礎入門
Lucene簡介
Lucene的作者Doug Cutting是資深的全文索引/檢索專家,最開始發布在他本人的主頁上,2000年開源,2001年10月貢獻給Apache,成為Apache基金的一個子項目。官網//lucene.apache.org/core。現在是開源全文檢索方案的重要選擇。
Lucene是非常優秀的成熟的開源的免費的純java語言的全文索引檢索工具包。
Lucene是一個高性能、可伸縮的資訊搜索(IR)庫。 Information Retrieval (IR) library.它可以為你的應用程式添加索引和搜索能力。
Lucene是為軟體開發人員提供一個簡單易用的工具包,以方便的在目標系統中實現全文檢索的功能,或者是以此為基礎建立起完整的全文檢索引擎。由Apache軟體基金會支援和提供,Lucene提供了一個簡單卻強大的應用程式介面,能夠做全文索引和搜索。Lucene是當前以及最近幾年非常受歡迎的免費Java資訊檢索程式庫。
Lucene實現的產品
作為一個開放源程式碼項目,Lucene從問世之後,引發了開放源程式碼社群的巨大反響,程式設計師們不僅使用它構建具體的全文檢索應用,而且將之集成到各種系統軟體中去,以及構建Web應用,甚至某些商業軟體也採用了Lucene作為其內部全文檢索子系統的核心。
Nutch:Apache頂級開源項目,包含網路爬蟲和搜索引擎(基於lucene)的系統(同 百度、google)。
Hadoop因它而生。
Solr : Lucene下的子項目,基於Lucene構建的獨立的企業級開源搜索平台,一個服務。它提供了基於xml/JSON/http的api供外界訪問,還有web管理介面。
Elasticsearch:基於Lucene的企業級分散式搜索平台,它對外提供restful-web介面,讓程式設計師可以輕鬆、方便使用搜索平台。
還有大家所熟知的OSChina、Eclipse、MyEclipse、JForum等等都是使用了Lucene做搜索框架來實現自己的搜索部分內容,在我們自己的項目中很有必要加入他的搜索能力,可以大大提高我們開發系統的搜索體驗度。
Lucene的特性
- 穩定、索引性能高
- 每小時能夠索引150GB以上的數據。
- 對記憶體的要求小,只需要1MB的堆記憶體
- 增量索引和批量索引一樣快。
- 索引的大小約為索引文本大小的20%~30%。
- 高效、準確、高性能
- 範圍搜索 – 優先返回最佳結果很多強大的
- 良好的搜索排序。
- 強大的查詢方式支援:短語查詢、通配符查詢、臨近查詢、範圍查詢等。
- 支援欄位搜索(如標題、作者、內容)。
- 可根據任意欄位排序
- 支援多個索引查詢結果合併
- 支援更新操作和查詢操作同時進行
- 支援高亮、join、分組結果功能
- 速度快
- 可擴展排序模組,內置包含向量空間模型、BM25模型可選
- 可配置存儲引擎
- 跨平台
- 純java編寫
- Lucene有多種語言實現版(如C、C++、Python等)
Lucence模組構成
Lucene是一個用Java寫的高性能、可伸縮的全文檢索引擎工具包,它可以方便的嵌入到各種應用中實現針對應用的全文索引、檢索功能。Lucene的目標是為各種中小型應用程式加入全文檢索功能
Lucene應用實戰
索引創建流程
第一步:採集一些要索引的原文檔數據
採集數據分類:
1、對於互聯網上網頁,可以使用工具將網頁抓取到本地生成html文件。
2、資料庫中的數據,可以直接連接資料庫讀取表中的數據。
3、文件系統中的某個文件,可以通過I/O操作讀取文件的內容。
第二步:創建文檔對象,進行語法分析,將文檔傳給分詞器(Tokenizer)形成一系列詞(Term)
獲取原始內容的目的是為了索引,在索引前需要將原始內容創建成文檔(Document),文檔中包括一個一個的域(Field),域中存儲內容,再對域中的內容進行分析,分析成為一個一個的單詞(Term)。每個Document可以有多個Field。
第三步:索引創建,將得到的詞傳給索引組件(Indexer)形成倒排索引結構
對所有文檔分析得出的辭彙單元進行索引,索引的目的是為了搜索,最終要實現只搜索被索引的語彙單元從而找到Document(文檔)。
創建索引是對語彙單元索引,通過詞語找文檔,這種索引的結構叫倒排索引結構。
第四步:通過索引存儲器,將索引寫入到磁碟
Java程式碼實現索引創建
引入依賴:
<dependency>
<groupId>org.apache.lucene</groupId>
<artifactId>lucene-core</artifactId>
<version>${lucene-version}</version>
</dependency>
<dependency>
<groupId>org.apache.lucene</groupId>
<artifactId>lucene-queryparser</artifactId>
<version>${lucene-version}</version>
</dependency>
<dependency>
<groupId>org.apache.lucene</groupId>
<artifactId>lucene-analyzers-common</artifactId>
<version>${lucene-version}</version>
</dependency>
<dependency>
<groupId>commons-io</groupId>
<artifactId>commons-io</artifactId>
<version>2.5</version>
</dependency>
public class TestLuceneIndex {
public static void main(String[] args) throws Exception{
// 1. 採集數據
List<Book> bookList = new ArrayList<Book>();
Book book1=new Book();
book1.setId(1);
book1.setName("Lucene");
book1.setPrice(new BigDecimal("100.45"));
book1.setDesc("Lucene Core is a Java library providing powerful indexing\n" +
"and search features, as well as spellchecking, hit highlighting and advanced\n" +
"analysis/tokenization capabilities. The PyLucene sub project provides Python\n" +
"bindings for Lucene Core");
bookList.add(book1);
Book book2=new Book();
book2.setId(2);
book2.setName("Solr");
book2.setPrice(new BigDecimal("66.45"));
book2.setDesc("Solr is highly scalable, providing fully fault tolerant\n" +
"distributed indexing, search and analytics. It exposes Lucene's features through\n" +
"easy to use JSON/HTTP interfaces or native clients for Java and other languages");
bookList.add(book2);
Book book3=new Book();
book3.setId(3);
book3.setName("Hadoop");
book3.setPrice(new BigDecimal("318.33"));
book3.setDesc("The Apache Hadoop software library is a framework that\n" +
"allows for the distributed processing of large data sets across clusters of\n" +
"computers using simple programming models");
bookList.add(book3);
//2. 創建docment文檔對象
List<Document> documents = new ArrayList<>();
bookList.forEach(x->{
Document document=new Document();
document.add(new TextField("id",x.getId().toString(), Field.Store.YES));
document.add(new TextField("name",x.getName(), Field.Store.YES));
document.add(new TextField("price",x.getPrice().toString(), Field.Store.YES));
document.add(new TextField("desc",x.getDesc(), Field.Store.YES));
documents.add(document);
});
//3.創建Analyzer分詞器,對文檔分詞
Analyzer analyzer=new StandardAnalyzer();
//創建Directory對象,聲明索引庫的位置
Directory directory=FSDirectory.open(Paths.get("D://lucene/index"));
//創建IndexWriteConfig對象,寫入索引需要的配置
IndexWriterConfig config=new IndexWriterConfig(analyzer);
//4.創建IndexWriter對象,添加文檔document
IndexWriter indexWriter=new IndexWriter(directory,config);
documents.forEach(doc-> {
try {
indexWriter.addDocument(doc);
} catch (IOException e) {
e.printStackTrace();
}
});
//釋放資源
indexWriter.close();
}
}
索引搜索流程
- 用戶輸入查詢語句
- 對查詢語句經過詞法分析和語言分析得到一系列詞(Term)
- 通過語法分析得到一個查詢樹
- 通過索引存儲將索引讀到記憶體
- 利用查詢樹搜索索引,從而得到每個詞(Term)的文檔列表,對文檔列表進行交、差、並得到結果文檔
- 將搜索到的結果文檔按照對查詢語句的相關性進行排序
- 返回查詢結果給用戶
Java程式碼實現索引查詢
public class TestLuceneSearch {
public static void main(String[] args) throws IOException, ParseException {
//1. 創建Query搜索對象
Analyzer analyzer=new StandardAnalyzer();
//創建搜索解析器
QueryParser queryParser=new QueryParser("id",analyzer);
Query query=queryParser.parse("desc:data");
//2. 創建Directory流對象,聲明索引庫位置
Directory directory=FSDirectory.open(Paths.get("D:/lucene/index"));
//3. 創建索引讀取對象IndexReader
IndexReader reader=DirectoryReader.open(directory);
// 4. 創建索引搜索對象
IndexSearcher searcher= new IndexSearcher(reader);
//5. 執行搜索,指定返回最頂部的10條數據
TopDocs topDocs = searcher.search(query, 10);
ScoreDoc[] scoreDocs = topDocs.scoreDocs;
//6. 解析結果集
Stream.of(scoreDocs).forEach(doc->{
//獲取文檔
Document document = null;
try {
document = searcher.doc(doc.doc);
} catch (IOException e) {
e.printStackTrace();
}
System.out.println(document.get("name"));
System.out.println(document.get("id"));
});
reader.close();
}
}
Field域
- Field屬性
Lucene存儲對象是以Document為存儲單元,對象中相關的屬性值則存放到Field中。Field是文檔中的域,包括Field名和Field值兩部分,一個文檔包括多個Field,Field值即為要索引的內容,也是要搜索的內容。
Field的三大屬性:
- 是否分詞(tokenized)
是否做分詞處理。是:即將Field值進行分詞,分詞的目的是為了索引。
- 是否索引
是否進行索引,將Field分詞後的詞或整個Field值進行索引,索引的目的是為了搜索。
- 是否存儲
將Field的值存儲在文檔中,存儲在文檔中的Field中才可以從Document中獲取。
- Field常用類型
Field類型 | 數據類型 | 是否分詞 | 是否索引 | 是否存儲 | 說明 |
---|---|---|---|---|---|
StringField(FieldName,FieldValue, Store.YES) | 字元串 | N | Y | Y/N | 字元串類型Field, 不分詞, 作為一個整體進行索引(如: 身份證號, 訂單編號), 是否需要存儲由Store.YES或Store.NO決定 |
TextField(FieldName,FieldValue, Store.NO) | 文本類型 | Y | Y | Y/N | 文本類型Field,分詞並且索引,是否需要存儲由Store.YES或Store.NO決定 |
LongField(FieldName,FieldValue, Store.YES) 或LongPoint(String name,int… point)等 | 數值型代表 | Y | Y | Y/N | 在Lucene 6.0中,LongField替換為LongPoint,IntField替換為IntPoint,FloatField替換為FloatPoint,DoubleField替換為DoublePoint。對數值型欄位索引,索引不存儲。要存儲結合StoredField即可。 |
StoredField(FieldName,FieldValue) | 支援多種類型 | N | N | Y | 構建不同類型的Field,不分詞,不索引,要存儲 |
- Field程式碼應用
public static void main(String[] args) throws IOException {
// 1. 採集數據
List<Book> bookList = Book.buildBookData();
List<Document> documents=new ArrayList<>();
bookList.forEach(book -> {
Document document=new Document();
Field id=new IntPoint("id",book.getId());
Field id_v=new StoredField("id",book.getId());
Field name=new TextField("name",book.getName(),Field.Store.YES);
Field price=new FloatPoint("price",book.getPrice().floatValue());
Field desc=new TextField("desc",book.getDesc(),Field.Store.NO);
document.add(id);
document.add(id_v);
document.add(name);
document.add(price);
document.add(desc);
documents.add(document);
});
StandardAnalyzer analyzer = new StandardAnalyzer();
Directory directory=FSDirectory.open(Paths.get("D:/lucene/index2"));
IndexWriterConfig indexWriterConfig=new IndexWriterConfig(analyzer);
IndexWriter indexWriter=new IndexWriter(directory,indexWriterConfig);
documents.forEach(doc-> {
try {
indexWriter.addDocument(doc);
} catch (IOException e) {
e.printStackTrace();
}
});
indexWriter.close();
}
索引維護
- 索引添加
indexWriter.addDocument(document);
- 索引刪除
根據Term項刪除
indexWriter.deleteDocuments(new Term("name", "solr"));
全部刪除
indexWriter.deleteAll();
- 更新索引
public static void main(String[] args) throws IOException {
Analyzer analyzer=new StandardAnalyzer();
Directory directory=FSDirectory.open(Paths.get("d:/lucene/index2"));
IndexWriterConfig config=new IndexWriterConfig(analyzer);
IndexWriter indexWriter=new IndexWriter(directory,config);
Document document=new Document();
document.add(new TextField("id","1002", Field.Store.YES));
document.add(new TextField("name","修改後", Field.Store.YES));
indexWriter.updateDocument(new Term("name","solr"),document);
indexWriter.close();
}
分詞器
分詞器相關概念
分詞器:採集到的數據會存儲到Document對象的Field域中,分詞器就是將Document中Field的value的值切分為一個一個的詞。
停用詞:停用詞是為了節省存儲空間和提高搜索效率,搜索程式在索引頁面或處理搜索請求時回自動忽略某些字或詞,這些字或詞被稱為Stop Wordds(停用詞)。比如語氣助詞、副詞、介詞、連接詞等。如:「的」、「啊」、「a」、「the」
擴展詞:就是分詞器默認不會切出的詞,但我們希望分詞器切出這樣的詞
藉助一些工具,我們可以看到分詞後的結果:
可以看出他將我們的詞「修改後」分為了3個字:「修」、「改」、「後」。另外英文是按照一個個單詞分的。
中文分詞器
英文是以單詞為單位的,單詞與單詞之間以空格或逗號分開,所以英文程式是比較好處理的。
而中文是以字為單位,字又組成詞,字和詞又組成句子。比如「我愛吃紅薯」,程式不知道「紅薯」是一個詞語還是「吃紅」是一個詞語。
為了解決這個問題,中文分詞器IKAnalyzer應運而生
可以看出它把「我愛吃紅薯」分成了很多個符合我們語義的詞語了。但是裡面有一個「吃紅」我們是不需要的。這種就需要我們自己自定義配置
擴展中文詞庫
如果想配置擴展詞和停用詞,就創建擴展詞的文件和停用詞的文件。ik給我們提供了自定義配置的擴展,從IKAnalyzer.cfg.xml配置文件可以看出:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE properties SYSTEM "//java.sun.com/dtd/properties.dtd">
<properties>
<comment>IK Analyzer 擴展配置</comment>
<!--用戶可以在這裡配置自己的擴展字典 -->
<entry key="ext_dict">ext.dic;</entry>
<!--用戶可以在這裡配置自己的擴展停止詞字典-->
<entry key="ext_stopwords">stopword.dic;</entry>
</properties>
我們新建一個ext.dic,並配上「吃紅」。
現在看就沒有「吃紅」這個詞了。擴展詞典同理。
注意:不要用window自帶的記事本保存擴展詞文件和停用詞文件,那樣的話,格式中是含有bom的
搜索
創建查詢的兩種方式。
1)使用Lucene提供的Query子類
2)使用QueryParse解析查詢表達式
Query子類
- TermQuery
TermQuery詞項查詢,TermQuery不使用分詞器,精確搜索Field域中的詞。
public class TestSearch {
public static void main(String[] args) throws IOException {
Query query=new TermQuery(new Term("name","solr"));
doSearch(query);
}
private static void doSearch(Query query) throws IOException {
Directory directory=FSDirectory.open(Paths.get("D:/lucene/index"));
IndexReader indexReader=DirectoryReader.open(directory);
IndexSearcher searcher=new IndexSearcher(indexReader);
TopDocs topDocs = searcher.search(query, 10);
System.out.println("查詢到數據的總條數:"+topDocs.totalHits);
Stream.of(topDocs.scoreDocs).forEach(doc->{
//根據docId查詢文檔
Document document = null;
try {
document = searcher.doc(doc.doc);
} catch (IOException e) {
e.printStackTrace();
}
System.out.println(document);
});
}
}
- BooleanQuery
BooleanQuery,實現組合條件查詢。
public static void testBooleanQuery() throws IOException {
Query query1=new TermQuery(new Term("name","lucene"));
Query query2=new TermQuery(new Term("desc","java"));
BooleanQuery.Builder builder = new BooleanQuery.Builder();
builder.add(query1,BooleanClause.Occur.MUST);
builder.add(query2,BooleanClause.Occur.SHOULD);
doSearch(builder.build());
}
組合關係代表的意思如下:
- MUST和MUST表示「與」的關係,即「交集」。
- MUST和MUST_NOT前者包含後者不包含。
- MUST_NOT和MUST_NOT沒意義,查不出來數據
- SHOULD與MUST表示MUST,SHOULD失去意義,相當於僅MUST一個條件
- SHOULD與MUST_NOT相當於MUST與MUST_NOT。
- SHOULD與SHOULD表示「或」的關係,即「並集」。
- 短語查詢PhraseQuery
PhraseQuery phraseQuery = new PhraseQuery("desc","lucene");
兩個短語中間有間隔詞的查詢:
PhraseQuery phraseQuery = new PhraseQuery(3,"desc","lucene","java");
能把類似的句子查出來:
Lucene Core is a Java library providing
lucene和java之間隔了3個詞語
4. 跨度查詢
兩個詞語之間有其他詞語的情況的查詢
public static void testSpanTermQuery() throws IOException {
SpanTermQuery tq1 = new SpanTermQuery(new Term("desc", "lucene"));
SpanTermQuery tq2 = new SpanTermQuery(new Term("desc", "java"));
SpanNearQuery spanNearQuery = new SpanNearQuery(new SpanQuery[] { tq1, tq2
},3,true);
doSearch(spanNearQuery);
}
- 模糊查詢
WildcardQuery:通配符查詢,*代表0或多個字元,?代表1個字元,\是轉義符。通配符查詢會比較慢,不可以通配符開頭(那樣就是所有詞項了)
public static void testWildcardQuery() throws IOException {
WildcardQuery wildcardQuery=new WildcardQuery(new Term("name","so*"));
doSearch(wildcardQuery);
}
FuzzyQuery:允許查詢中有錯別字
FuzzyQuery fuzzyQuery = new FuzzyQuery(new Term("name", "slors"), 2);
如上面的我把solr打成了slors,也能查詢到,上面的參數2代表錯別字能錯多少個,此參數最大為2.
- 數值查詢
通過 IntPoint, LongPoint,FloatPoint,DoublePoint中的方法構建對應的查詢。
public static void testPointQuery() throws IOException {
Query query = IntPoint.newRangeQuery("id", 1, 4);
doSearch(query);
}
QueryParser搜索
- 基礎查詢
查詢語法:
Field域名 +”:”+搜索的關鍵字。 例如: name:java
- 範圍查詢
Field域名+”:”+[最小值 TO 最大值]。例如: size:[A TO C]
注意:QueryParser不支援對數字範圍的搜索,支援的是字元串範圍
- 組合條件查詢
有兩種寫法:
寫法一:
使用+、減號和不用符號
邏輯 | 實現 |
---|---|
Occur.MUST 查詢條件必須滿足,相當於AND | +(加號) |
Occur.SHOULD 查詢條件可選,相當於OR | 空(不用符號) |
Occur.MUST_NOT 查詢條件不能滿足,相當於NOT非 | -(減號) |
示例:
+filename:lucene + content:lucene
+filename:lucene content:lucene
filename:lucene content:lucene
-filename:lucene content:lucene
寫法二:
使用 AND、OR 、NOT
QueryParser
public static void testQueryParser() throws ParseException, IOException {
Analyzer analyzer=new StandardAnalyzer();
QueryParser queryParser=new QueryParser("desc",analyzer);
Query query = queryParser.parse("desc:java AND name:lucene");
doSearch(query);
}
MultiFieldQueryParser
多個Field的查詢,以下查詢等同於:name:lucene desc:lucene
public static void testSearchMultiFieldQuery() throws IOException, ParseException {
Analyzer analyzer=new IKAnalyzer();
String[] fields={"name","desc"};
MultiFieldQueryParser multiFieldQueryParser=new MultiFieldQueryParser(fields,analyzer);
Query query = multiFieldQueryParser.parse("lucene");
System.out.println(query);
doSearch(query);
}
StandardQueryParser
public static void testStandardQuery() throws QueryNodeException, IOException {
Analyzer analyzer=new StandardAnalyzer();
StandardQueryParser parser = new StandardQueryParser(analyzer);
Query query = parser.parse("desc:java AND name:lucene", "desc");
System.out.println(query);
doSearch(query);
}
其他查詢:
/通配符匹配 建議通配符在後 通配符在前效率低
query = parser.parse("name:L*","desc");
query = parser.parse("name:L???","desc");
//模糊匹配
query = parser.parse("lucene~","desc");
//區間查詢
query = parser.parse("id:[1 TO 100]","desc");
//跨度查詢 ~2表示詞語之間包含兩個詞語
query= parser.parse("\"lucene java\"~2","desc");