在了解Lucene之前,我們先了解下全文數據查詢。
我們的數據一般分為兩種:結構化數據和非結構化數據
數據庫適合結構化數據的精確查詢,而不適合半結構化、非結構化數據的模糊查詢及靈活搜索(特別是數據量大時),無法提供想要的實時性。
所謂順序掃描,就是要找內容包含一個字符串的文件,就是一個文檔一個文檔的看。對于每一個文檔,從頭看到尾,如果此文檔包含此字符串,則此文檔為我們要找的文件,接著看下一個文件,直到掃描完所有的文件。
全文檢索是指計算機索引程序通過掃描文章中的每一個詞,對每一個詞建立一個索引,指明該詞在文章中出現的次數和位置,當用戶查詢時,檢索程序就根據事先建立的索引進行查找,并將查找的結果反饋給用戶的檢索方式。這個過程類似于通過字典中的檢索字表查字的過程。
全文檢索的基本思路,就是將非結構化數據中的一部分信息提取出來,重新組織,使其變得有一定結構,然后對這個有一定結構的數據進行搜索,從而達到搜索相對較快的目的。
這部分從非結構化數據中提取出的然后重新組織的信息,我們稱之索引,這種先建立索引,再對索引進行搜索的過程就叫全文檢索(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的作者Doug Cutting是資深的全文索引/檢索專家,最開始發布在他本人的主頁上,2000年開源,2001年10月貢獻給Apache,成為Apache基金的一個子項目。官網https://lucene.apache.org/core。現在是開源全文檢索方案的重要選擇。
Lucene是非常優秀的成熟的開源的免費的純java語言的全文索引檢索工具包。
Lucene是一個高性能、可伸縮的信息搜索(IR)庫。 Information Retrieval (IR) library.它可以為你的應用程序添加索引和搜索能力。
Lucene是為軟件開發人員提供一個簡單易用的工具包,以方便的在目標系統中實現全文檢索的功能,或者是以此為基礎建立起完整的全文檢索引擎。由Apache軟件基金會支持和提供,Lucene提供了一個簡單卻強大的應用程序接口,能夠做全文索引和搜索。Lucene是當前以及最近幾年非常受歡迎的免費Java信息檢索程序庫。
作為一個開放源代碼項目,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是一個用Java寫的高性能、可伸縮的全文檢索引擎工具包,它可以方便的嵌入到各種應用中實現針對應用的全文索引、檢索功能。Lucene的目標是為各種中小型應用程序加入全文檢索功能
第一步:采集一些要索引的原文檔數據
采集數據分類:
1、對于互聯網上網頁,可以使用工具將網頁抓取到本地生成html文件。
2、數據庫中的數據,可以直接連接數據庫讀取表中的數據。
3、文件系統中的某個文件,可以通過I/O操作讀取文件的內容。
第二步:創建文檔對象,進行語法分析,將文檔傳給分詞器(Tokenizer)形成一系列詞(Term)
獲取原始內容的目的是為了索引,在索引前需要將原始內容創建成文檔(Document),文檔中包括一個一個的域(Field),域中存儲內容,再對域中的內容進行分析,分析成為一個一個的單詞(Term)。每個Document可以有多個Field。
第三步:索引創建,將得到的詞傳給索引組件(Indexer)形成倒排索引結構
對所有文檔分析得出的詞匯單元進行索引,索引的目的是為了搜索,最終要實現只搜索被索引的語匯單元從而找到Document(文檔)。
創建索引是對語匯單元索引,通過詞語找文檔,這種索引的結構叫倒排索引結構。
第四步:通過索引存儲器,將索引寫入到磁盤
引入依賴:
<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();
}
}
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();
}
}
Lucene存儲對象是以Document為存儲單元,對象中相關的屬性值則存放到Field中。Field是文檔中的域,包括Field名和Field值兩部分,一個文檔包括多個Field,Field值即為要索引的內容,也是要搜索的內容。
Field的三大屬性:
是否做分詞處理。是:即將Field值進行分詞,分詞的目的是為了索引。
是否進行索引,將Field分詞后的詞或整個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,不分詞,不索引,要存儲 |
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 "http://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解析查詢表達式
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,實現組合條件查詢。
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());
}
組合關系代表的意思如下:
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);
}
查詢語法:
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
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);
}
多個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);
}
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);
}
其他查詢:
者 | 倪升武
責編 | 胡巍巍
現在基本上所有網站都支持搜索功能,現在搜索的工具有很多,比如Solr、Elasticsearch,它們都是基于 Lucene 實現的,各有各的使用場景。Lucene 比較靈活,中小型項目中使用的比較多,我個人也比較喜歡用。
我前段時間做了一個網站,搜索功能用的就是 Lucene 技術,效果還可以,支持中文高亮顯示,支持標題和摘要同時檢索,若能檢索出,均高亮展示等功能,可以看下效果。
點擊查看更清晰
可以看出,搜索 “微服務” 之后,可以將相關的資源全部檢索出來,不管是標題包含還是摘要包含都可以檢索出來。
這是比較精確的匹配,還有非精確的匹配也支持,比如我搜索 “Java項目實戰”,看看結果如何。
點擊查看更清晰
可以看出,如果不能完全精確匹配,Lucene 也可以做模糊匹配,將最接近搜索的內容給檢索出來,展示在頁面上。
我個人還是比較喜歡使用 Lucene 的,關于 Lucene 全文檢索的原理我就不浪費篇幅介紹了,谷歌百度有一大堆原理。這篇文章主要來分享下如何使用 Lucene 做到這個功能。
使用 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-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>
使用 Lucene 首先要建立索引,然后再查詢。如何建立索引呢?為了更好的說明問題,我在這寫一個 demo:直接對字符串內容建立索引。
因為在實際項目中,絕大部分情況是獲取到一些文本字符串(比如從表中查詢出來的結果),然后對該文本字符串建立索引。
索引建立的過程,先要獲取 IndexWriter 對象,然后將相關的內容生成索引,索引的 Key 可以自己根據項目中的情況來自定義,value 是自己處理過的文本,或者從數據庫中查詢出來的文本。生成的時候,我們需要使用中文分詞器。代碼如下:
public class ChineseIndexer { /** * 存放索引的位置 */ private Directory dir; //準備一下用來測試的數據 //用來標識文檔 private Integer ids[]={1, 2, 3}; private String citys[]={"上海", "南京", "青島"}; private String descs[]={ "上海是個繁華的城市。", "南京是一個文化的城市南京,簡稱寧,是江蘇省會,地處中國東部地區, 長江下游,瀕江近海。全市下轄11個區,總面積6597平方公里,2013年建 成區面積752.83平方公里,常住人口818.78萬,其中城鎮人口659.1萬人。 [1-4] “江南佳麗地,金陵帝王州”,南京擁有著6000多年文明史、近2600 年建城史和近500年的建都史,是中國四大古都之一,有“六朝古都”、 “十朝都會”之稱,是中華文明的重要發祥地,歷史上曾數次庇佑華夏之正 朔,長期是中國南方的政治、經濟、文化中心,擁有厚重的文化底蘊和豐富 的歷史遺存。[5-7] 南京是國家重要的科教中心,自古以來就是一座崇文重 教的城市,有“天下文樞”、“東南第一學”的美譽。截至2013年,南京有 高等院校75所,其中211高校8所,僅次于北京上海;國家重點實驗室25所、 國家重點學科169個、兩院院士83人,均居中國第三。[8-10] 。", "青島是一個美麗的城市。" }; /** * 生成索引 * @param indexDir * @throws Exception */ public void index(String indexDir) throws Exception { dir=FSDirectory.open(Paths.get(indexDir)); // 先調用 getWriter 獲取IndexWriter對象 IndexWriter writer=getWriter(); for(int i=0; i < ids.length; i++) { Document doc=new Document(); // 把上面的數據都生成索引,分別用id、city和desc來標識 doc.add(new IntField("id", ids[i], Field.Store.YES)); doc.add(new StringField("city", citys[i], Field.Store.YES)); doc.add(new TextField("desc", descs[i], Field.Store.YES)); //添加文檔 writer.addDocument(doc); } //close了才真正寫到文檔中 writer.close(); } /** * 獲取IndexWriter實例 * @return * @throws Exception */ private IndexWriter getWriter() throws Exception { //使用中文分詞器 SmartChineseAnalyzer analyzer=new SmartChineseAnalyzer(); //將中文分詞器配到寫索引的配置中 IndexWriterConfig config=new IndexWriterConfig(analyzer); //實例化寫索引對象 IndexWriter writer=new IndexWriter(dir, config); return writer; } public static void main(String[] args) throws Exception { new ChineseIndexer().index("D:\\lucene2"); } }
這里我們用 ID、city、desc 分別代表 ID、城市名稱和城市描述,用他們作為關鍵字來建立索引,后面我們獲取內容的時候,主要來獲取城市描述。
南京的描述我故意寫的長一點,因為下文檢索的時候,根據不同的關鍵字會檢索到不同部分的信息,有個權重的概念在里面。
然后執行一下 main 方法,將索引保存到 D:\lucene2\ 中。
中文分詞查詢效果是:將查詢出來的關鍵字標紅加粗。它的原理很簡單:需要計算出一個得分片段,這是什么意思呢?
比如上面那個文本中我搜索 “南京文化” 跟搜索 “南京文明”,應該會返回不同的結果,這個結果是根據計算出的得分片段來確定的。
這么說,大家可能不太明白,我舉個更加通俗的例子,比如有一段文本:“你好,我的名字叫倪升武,科大訊飛軟件開發工程師……江湖人都叫我武哥,我一直覺得,人與人之間講的是真誠,而不是套路。……”。
如果我搜 “倪升武”,可能會給我返回結果:“我的名字叫倪升武,科大訊飛軟件開發工程師”;
如果我搜 “武哥”,可能會給我返回結果:“江湖人都叫我武哥,我一直覺得”。這就是根據搜索關鍵字來計算一段文本不同地方的得分,將最符合的部分搜出來。
明白了原理,我們看一下代碼,我把詳細的步驟寫在注釋中了,避免大篇幅闡述。
public class ChineseSearch { private static final Logger logger=LoggerFactory.getLogger(ChineseSearch.class); public static List<String> search(String indexDir, String q) throws Exception { //獲取要查詢的路徑,也就是索引所在的位置 Directory dir=FSDirectory.open(Paths.get(indexDir)); IndexReader reader=DirectoryReader.open(dir); IndexSearcher searcher=new IndexSearcher(reader); //使用中文分詞器 SmartChineseAnalyzer analyzer=new SmartChineseAnalyzer(); //由中文分詞器初始化查詢解析器 QueryParser parser=new QueryParser("desc", analyzer); //通過解析要查詢的String,獲取查詢對象 Query query=parser.parse(q); //記錄索引開始時間 long startTime=System.currentTimeMillis(); //開始查詢,查詢前10條數據,將記錄保存在docs中 TopDocs docs=searcher.search(query, 10); //記錄索引結束時間 long endTime=System.currentTimeMillis(); logger.info("匹配{}共耗時{}毫秒", q, (endTime - startTime)); logger.info("查詢到{}條記錄", docs.totalHits); //如果不指定參數的話,默認是加粗,即<b><b/> SimpleHTMLFormatter simpleHTMLFormatter=new SimpleHTMLFormatter("<b><font color=red>","</font></b>"); //根據查詢對象計算得分,會初始化一個查詢結果最高的得分 QueryScorer scorer=new QueryScorer(query); //根據這個得分計算出一個片段 Fragmenter fragmenter=new SimpleSpanFragmenter(scorer); //將這個片段中的關鍵字用上面初始化好的高亮格式高亮 Highlighter highlighter=new Highlighter(simpleHTMLFormatter, scorer); //設置一下要顯示的片段 highlighter.setTextFragmenter(fragmenter); //取出每條查詢結果 List<String> list=new ArrayList<>(); for(ScoreDoc scoreDoc : docs.scoreDocs) { //scoreDoc.doc相當于docID,根據這個docID來獲取文檔 Document doc=searcher.doc(scoreDoc.doc); logger.info("city:{}", doc.get("city")); logger.info("desc:{}", doc.get("desc")); String desc=doc.get("desc"); //顯示高亮 if(desc !=null) { TokenStream tokenStream=analyzer.tokenStream("desc", new StringReader(desc)); String summary=highlighter.getBestFragment(tokenStream, desc); logger.info("高亮后的desc:{}", summary); list.add(summary); } } reader.close(); return list; } }
到這里,最核心的功能都實現好了,我們可以自己寫個小接口來調用下,看看效果。
@Controller @RequestMapping("/lucene") public class IndexController { @GetMapping("/test") public String test(Model model) { // 索引所在的目錄 String indexDir="D:\\lucene2"; // 要查詢的字符 String q="南京文化"; try { List<String> list=ChineseSearch.search(indexDir, q); model.addAttribute("list", list); } catch (Exception e) { e.printStackTrace(); } return "result"; } }
在 result.html 頁面做一個簡單的展示操作:
<!DOCTYPE html> <html lang="en" xmlns:th="http://www.thymeleaf.org"> <head> <meta charset="UTF-8"> <title>Title</title> </head> <body> <div th:each="desc : ${list}"> <div th:utext="${desc}"></div> </div> </body> </html>
上面我們搜索的是 “南京文化”,來看下檢索出來的結果是什么。
再將搜索關鍵字改成 “南京文明”,看下命中的效果如何?
可以看出,不同的關鍵詞,它會計算一個得分片段,也就是說不同的關鍵字會命中同一段文本中不同位置的內容,然后將關鍵字根據我們自己設定的形式高亮顯示。
從結果中可以看出,Lucene 也可以很智能地將關鍵字拆分命中,這在實際項目中會很好用。
作者簡介:倪升武,CSDN 博客專家,CSDN達人課作者。碩士畢業于同濟大學,曾先后就職于 eBay、愛奇藝、華為。目前在科大訊飛從事Java領域的軟件開發,他的世界不僅只有Coding。
聲明:本文為作者投稿,版權歸其個人所有。
.lucene工具包下載(下載下來就是jar包)
官方網站:http://lucene.apache.org/
下載地址:http://archive.apache.org/dist/lucene/java/
API:https://lucene.apache.org/core/6_6_0/core/index.html
2.什么是lucene
lucene是java編寫的高性能、功能齊全的、全文檢索引擎包。
3.lucene創建索引和搜索流程圖
4.代碼實現
4.1pom依賴
<!--lucene相關-->
<!--核心包-->
<dependency>
<groupId>org.apache.lucene</groupId>
<artifactId>lucene-core</artifactId>
<version>6.6.0</version>
</dependency>
<!--一般分詞器,適用于英文分詞-->
<dependency>
<groupId>org.apache.lucene</groupId>
<artifactId>lucene-analyzers-common</artifactId>
<version>6.6.0</version>
</dependency>
<!--中文分詞器-->
<dependency>
<groupId>org.apache.lucene</groupId>
<artifactId>lucene-analyzers-smartcn</artifactId>
<version>6.6.0</version>
</dependency>
<!--對分詞索引查詢解析-->
<dependency>
<groupId>org.apache.lucene</groupId>
<artifactId>lucene-queryparser</artifactId>
<version>6.6.0</version>
</dependency>
<!--檢索關鍵字高亮顯示-->
<dependency>
<groupId>org.apache.lucene</groupId>
<artifactId>lucene-highlighter</artifactId>
<version>6.6.0</version>
</dependency>
4.2創建索引
package com.example.springboottest.lucene;
import org.apache.commons.io.FileUtils;
import org.apache.lucene.analysis.Analyzer;
import org.apache.lucene.analysis.standard.StandardAnalyzer;
import org.apache.lucene.document.*;
import org.apache.lucene.index.*;
import org.apache.lucene.search.*;
import org.apache.lucene.store.Directory;
import org.apache.lucene.store.FSDirectory;
import java.io.File;
import java.nio.file.Paths;
public class CreateLuceneIndex {
public static void main(String[] args) throws Exception{
/*
* 第一步:創建一個indexwriter對象
* 1指定索引庫的存放位置Directory對象
* 2指定一個分析器,對文檔內容進行分析。
*/
Directory directory=FSDirectory.open(Paths.get("F:\temp\index"));
//官方推薦分詞器,對中文不友好
Analyzer analyzer=new StandardAnalyzer();
IndexWriterConfig indexWriterConfig=new IndexWriterConfig(analyzer);
IndexWriter indexWriter=new IndexWriter(directory,indexWriterConfig);
// 第二步:通過IO讀取磁盤上的文件信息
File file=new File("F:\temp\lucene");
File[] files=file.listFiles();
if(files !=null){
for(File file1:files){
if(file1.isFile()){
// 第三步:創建document對象, 并把文件信息添加到document對象中
Document document=new Document();
//獲取文件名稱
String fileName=file1.getName();
Field fileNameField=new TextField("fileName",fileName,Field.Store.YES);
//獲取文件路徑
String filePath=file1.getPath();
Field filePathField=new StoredField("filePath",filePath);
//文件大小
long fileSize=FileUtils.sizeOfDirectory(file1);
//索引
Field fileSizeField1=new LongPoint("fileSize",fileSize);
//存儲
Field fileSizeField2=new StoredField("fileSize", fileSize);
//文件內容
String fileContent=FileUtils.readFileToString(file1,"utf-8");
Field fileContentField=new TextField("fileContent",fileContent,Field.Store.NO);
document.add(fileNameField);
document.add(filePathField);
document.add(fileSizeField1);
document.add(fileSizeField2);
document.add(fileContentField);
// 第四步:使用indexwriter對象將document對象寫入索引庫,此過程進行索引創建。并將索引和document對象寫入索引庫。
indexWriter.addDocument(document);
}
}
}
indexWriter.close();
}
}
*請認真填寫需求信息,我們會在24小時內與您取得聯系。