一、基礎(chǔ)知識(shí)
1、Lucene 是什么
Lucene 是一個(gè)本地全文搜索引擎,Solr 和 ElasticSearch 都是基于 Lucene 的封裝
Lucene 適合那種輕量級(jí)的全文搜索,我就是服務(wù)器資源不夠,如果上 ES 的話會(huì)很占用服務(wù)器資源,所有就選擇了 Lucene 搜索引擎
2、倒排索引原理
全文搜索的原理是使用了倒排索引,那么什么是倒排索引呢?
- 先通過(guò)中文分詞器,將文檔中包含的關(guān)鍵字全部提取出來(lái),比如我愛(ài)中國(guó),會(huì)通過(guò)分詞器分成我,愛(ài),中國(guó),然后分別對(duì)應(yīng)‘我愛(ài)中國(guó)’
- 然后再將關(guān)鍵字與文檔的對(duì)應(yīng)關(guān)系保存起來(lái)
- 最后對(duì)關(guān)鍵字本身做索引排序
3、與傳統(tǒng)數(shù)據(jù)庫(kù)對(duì)比
DB | Lucene |
---|---|
數(shù)據(jù)庫(kù)表(table) | 索引(index) |
行(row) | 文檔(document) |
列(column) | 字段(field) |
4、數(shù)據(jù)類型
常見(jiàn)的字段類型
- StringField:這是一個(gè)不可分詞的字符串字段類型,適用于精確匹配和排序。
- TextField:這是一個(gè)可分詞的字符串字段類型,適用于全文搜索和模糊匹配。
- IntField、LongField、FloatField、DoubleField:這些是數(shù)值字段類型,用于存儲(chǔ)整數(shù)和浮點(diǎn)數(shù)。
- DateField:這是一個(gè)日期字段類型,用于存儲(chǔ)日期和時(shí)間。
- BinaryField:這是一個(gè)二進(jìn)制字段類型,用于存儲(chǔ)二進(jìn)制數(shù)據(jù),如圖片、文件等。
- StoredField:這是一個(gè)存儲(chǔ)字段類型,用于存儲(chǔ)不需要被索引的原始數(shù)據(jù),如文檔的內(nèi)容或其他附加信息。
Lucene 分詞器是將文本內(nèi)容分解成單獨(dú)的詞匯(term)的工具。Lucene 提供了多種分詞器,其中一些常見(jiàn)的包括
- StandardAnalyzer:這是 Lucene 默認(rèn)的分詞器,它使用 UnicodeText 解析器將文本轉(zhuǎn)換為小寫(xiě)字母,并且根據(jù)空格、標(biāo)點(diǎn)符號(hào)和其他字符來(lái)進(jìn)行分詞。
- CJKAnalyzer:這個(gè)分詞器專門(mén)為中日韓語(yǔ)言設(shè)計(jì),它可以正確地處理中文、日文和韓文的分詞。
- KeywordAnalyzer:這是一個(gè)不分詞的分詞器,它將輸入的文本作為一個(gè)整體來(lái)處理,常用于處理精確匹配的情況。
- SimpleAnalyzer:這是一個(gè)非常簡(jiǎn)單的分詞器,它僅僅按照非字母字符將文本分割成小寫(xiě)詞匯。
- WhitespaceAnalyzer:這個(gè)分詞器根據(jù)空格將文本分割成小寫(xiě)詞匯,不會(huì)進(jìn)行任何其他的處理。
但是對(duì)于中文分詞器,我們一般常用第三方分詞器IKAnalyzer,需要引入它的POM文件
二、最佳實(shí)踐
1、依賴導(dǎo)入
<lucene.version>8.1.1</lucene.version>
<IKAnalyzer-lucene.version>8.0.0</IKAnalyzer-lucene.version>
<!--============lucene start================-->
<!-- Lucene核心庫(kù) -->
<dependency>
<groupId>org.apache.lucene</groupId>
<artifactId>lucene-core</artifactId>
<version>${lucene.version}</version>
</dependency>
<!-- Lucene的查詢解析器 -->
<dependency>
<groupId>org.apache.lucene</groupId>
<artifactId>lucene-queryparser</artifactId>
<version>${lucene.version}</version>
</dependency>
<!-- Lucene的默認(rèn)分詞器庫(kù) -->
<dependency>
<groupId>org.apache.lucene</groupId>
<artifactId>lucene-analyzers-common</artifactId>
<version>${lucene.version}</version>
</dependency>
<!-- Lucene的高亮顯示 -->
<dependency>
<groupId>org.apache.lucene</groupId>
<artifactId>lucene-highlighter</artifactId>
<version>${lucene.version}</version>
</dependency>
<!-- ik分詞器 -->
<dependency>
<groupId>com.jianggujin</groupId>
<artifactId>IKAnalyzer-lucene</artifactId>
<version>${IKAnalyzer-lucene.version}</version>
</dependency>
<!--============lucene end================-->
2、創(chuàng)建索引
- 先制定索引的基本數(shù)據(jù),包括索引名稱和字段
/**
* @author: sunhhw
* @date: 2023/12/25 17:39
* @description: 定義文章文檔字段和索引名稱
*/
public interface IArticleIndex {
/**
* 索引名稱
*/
String INDEX_NAME = "article";
// --------------------- 文檔字段 ---------------------
String COLUMN_ID = "id";
String COLUMN_ARTICLE_NAME = "articleName";
String COLUMN_COVER = "cover";
String COLUMN_SUMMARY = "summary";
String COLUMN_CONTENT = "content";
String COLUMN_CREATE_TIME = "createTime";
}
- 創(chuàng)建索引并新增文檔
/**
* 創(chuàng)建索引并設(shè)置數(shù)據(jù)
*
* @param indexName 索引地址
*/
public void addDocument(String indexName, List<Document> documentList) {
// 配置索引的位置 例如:indexDir = /app/blog/index/article
String indexDir = luceneProperties.getIndexDir() + File.separator + indexName;
try {
File file = new File(indexDir);
// 若不存在,則創(chuàng)建目錄
if (!file.exists()) {
FileUtils.forceMkdir(file);
}
// 讀取索引目錄
Directory directory = FSDirectory.open(Paths.get(indexDir));
// 中文分析器
Analyzer analyzer = new IKAnalyzer();
// 索引寫(xiě)出工具的配置對(duì)象
IndexWriterConfig conf = new IndexWriterConfig(analyzer);
// 創(chuàng)建索引
IndexWriter indexWriter = new IndexWriter(directory, conf);
long count = indexWriter.addDocuments(documentList);
log.info("[批量添加索引庫(kù)]總數(shù)量:{}", documentList.size());
// 提交記錄
indexWriter.commit();
// 關(guān)閉close
indexWriter.close();
} catch (Exception e) {
log.error("[創(chuàng)建索引失敗]indexDir:{}", indexDir, e);
throw new UtilsException("創(chuàng)建索引失敗", e);
}
}
- 注意這里有個(gè)坑,就是這個(gè)
indexWriter.close();
必須要關(guān)閉, 不然在執(zhí)行其他操作的時(shí)候會(huì)有一個(gè)write.lock
文件鎖控制導(dǎo)致操作失敗indexWriter.addDocuments(documentList)
這是批量添加,單個(gè)添加可以使用indexWriter.addDocument()
- 單元測(cè)試
@Test
public void create_index_test() {
ArticlePO articlePO = new ArticlePO();
articlePO.setArticleName("git的基本使用" + i);
articlePO.setContent("這里是git的基本是用的內(nèi)容" + i);
articlePO.setSummary("測(cè)試摘要" + i);
articlePO.setId(String.valueOf(i));
articlePO.setCreateTime(LocalDateTime.now());
Document document = buildDocument(articlePO);
LuceneUtils.X.addDocument(IArticleIndex.INDEX_NAME, document);
}
private Document buildDocument(ArticlePO articlePO) {
Document document = new Document();
LocalDateTime createTime = articlePO.getCreateTime();
String format = LocalDateTimeUtil.format(createTime, DateTimeFormatter.ISO_LOCAL_DATE);
// 因?yàn)镮D不需要分詞,使用StringField字段
document.add(new StringField(IArticleIndex.COLUMN_ID, articlePO.getId() == null ? "" : articlePO.getId(), Field.Store.YES));
// 文章標(biāo)題articleName需要搜索,所以要分詞保存
document.add(new TextField(IArticleIndex.COLUMN_ARTICLE_NAME, articlePO.getArticleName() == null ? "" : articlePO.getArticleName(), Field.Store.YES));
// 文章摘要summary需要搜索,所以要分詞保存
document.add(new TextField(IArticleIndex.COLUMN_SUMMARY, articlePO.getSummary() == null ? "" : articlePO.getSummary(), Field.Store.YES));
// 文章內(nèi)容content需要搜索,所以要分詞保存
document.add(new TextField(IArticleIndex.COLUMN_CONTENT, articlePO.getContent() == null ? "" : articlePO.getContent(), Field.Store.YES));
// 文章封面不需要分詞,但是需要被搜索出來(lái)展示
document.add(new StoredField(IArticleIndex.COLUMN_COVER, articlePO.getCover() == null ? "" : articlePO.getCover()));
// 創(chuàng)建時(shí)間不需要分詞,僅需要展示
document.add(new StringField(IArticleIndex.COLUMN_CREATE_TIME, format, Field.Store.YES));
return document;
}
3、更新文檔
- 更新索引方法
/**
* 更新文檔
*
* @param indexName 索引地址
* @param document 文檔
* @param condition 更新條件
*/
public void updateDocument(String indexName, Document document, Term condition) {
String indexDir = luceneProperties.getIndexDir() + File.separator + indexName;
try {
// 讀取索引目錄
Directory directory = FSDirectory.open(Paths.get(indexDir));
// 中文分析器
Analyzer analyzer = new IKAnalyzer();
// 索引寫(xiě)出工具的配置對(duì)象
IndexWriterConfig conf = new IndexWriterConfig(analyzer);
// 創(chuàng)建索引
IndexWriter indexWriter = new IndexWriter(directory, conf);
indexWriter.updateDocument(condition, document);
indexWriter.commit();
indexWriter.close();
} catch (Exception e) {
log.error("[更新文檔失敗]indexDir:{},document:{},condition:{}", indexDir, document, condition, e);
throw new ServiceException();
}
}
- 單元測(cè)試
@Test
public void update_document_test() {
ArticlePO articlePO = new ArticlePO();
articlePO.setArticleName("git的基本使用=編輯");
articlePO.setContent("這里是git的基本是用的內(nèi)容=編輯");
articlePO.setSummary("測(cè)試摘要=編輯");
articlePO.setId("2");
articlePO.setCreateTime(LocalDateTime.now());
Document document = buildDocument(articlePO);
LuceneUtils.X.updateDocument(IArticleIndex.INDEX_NAME, document, new Term("id", "2"));
}
- 更新的時(shí)候,如果存在就更新那條記錄,如果不存在就會(huì)新增一條記錄
new Term("id", "2")
搜索條件,跟數(shù)據(jù)庫(kù)里的where id = 2
差不多IArticleIndex.INDEX_NAME = article
索引名稱
4、刪除文檔
- 刪除文檔方法
/**
* 刪除文檔
*
* @param indexName 索引名稱
* @param condition 更新條件
*/
public void deleteDocument(String indexName, Term condition) {
String indexDir = luceneProperties.getIndexDir() + File.separator + indexName;
try {
// 讀取索引目錄
Directory directory = FSDirectory.open(Paths.get(indexDir));
// 索引寫(xiě)出工具的配置對(duì)象
IndexWriterConfig conf = new IndexWriterConfig();
// 創(chuàng)建索引
IndexWriter indexWriter = new IndexWriter(directory, conf);
indexWriter.deleteDocuments(condition);
indexWriter.commit();
indexWriter.close();
} catch (Exception e) {
log.error("[刪除文檔失敗]indexDir:{},condition:{}", indexDir, condition, e);
throw new ServiceException();
}
}
- 單元測(cè)試
@Test
public void delete_document_test() {
LuceneUtils.X.deleteDocument(IArticleIndex.INDEX_NAME, new Term(IArticleIndex.COLUMN_ID, "1"));
}
- 刪除文檔跟編輯文檔類似
5、刪除索引
把改索引下的數(shù)據(jù)全部清空
/**
* 刪除索引
*
* @param indexName 索引地址
*/
public void deleteIndex(String indexName) {
String indexDir = luceneProperties.getIndexDir() + File.separator + indexName;
try {
// 讀取索引目錄
Directory directory = FSDirectory.open(Paths.get(indexDir));
// 索引寫(xiě)出工具的配置對(duì)象
IndexWriterConfig conf = new IndexWriterConfig();
// 創(chuàng)建索引
IndexWriter indexWriter = new IndexWriter(directory, conf);
indexWriter.deleteAll();
indexWriter.commit();
indexWriter.close();
} catch (Exception e) {
log.error("[刪除索引失敗]indexDir:{}", indexDir, e);
throw new ServiceException();
}
}
6、普通查詢
- TermQuery查詢
Term term = new Term("title", "lucene");
Query query = new TermQuery(term);
上述代碼表示通過(guò)精確匹配字段"title"中包含"lucene"的文檔。
- PhraseQuery查詢
PhraseQuery.Builder builder = new PhraseQuery.Builder();
builder.add(new Term("content", "open"));
builder.add(new Term("content", "source"));
PhraseQuery query = builder.build();
上述代碼表示在字段"content"中查找包含"open source"短語(yǔ)的文檔
- BooleanQuery查詢
TermQuery query1 = new TermQuery(new Term("title", "lucene"));
TermQuery query2 = new TermQuery(new Term("author", "john"));
BooleanQuery.Builder builder = new BooleanQuery.Builder();
builder.add(query1, BooleanClause.Occur.MUST);
builder.add(query2, BooleanClause.Occur.MUST);
BooleanQuery query = builder.build();
上述代碼表示使用布爾查詢同時(shí)滿足"title"字段包含"lucene"和"author"字段包含"john"的文檔。
- WildcardQuery查詢
WildcardQuery示例:
java
WildcardQuery query = new WildcardQuery(new Term("title", "lu*n?e"));
上述代碼表示使用通配符查詢匹配"title"字段中以"lu"開(kāi)頭,且第三個(gè)字符為任意字母,最后一個(gè)字符為"e"的詞項(xiàng)
- MultiFieldQueryParser查詢
String[] fields = {"title", "content", "author"};
Analyzer analyzer = new StandardAnalyzer();
MultiFieldQueryParser parser = new MultiFieldQueryParser(fields, analyzer);
Query query = parser.parse("lucene search");
a. 在"title", “content”, "author"三個(gè)字段中搜索關(guān)鍵字"lucene search"的文本數(shù)據(jù)
b. MultiFieldQueryParser 默認(rèn)使用 OR 運(yùn)算符將多個(gè)字段的查詢結(jié)果合并,即只要在任意一個(gè)字段中匹配成功即
可以使用MultiFieldQueryParser查詢來(lái)封裝一個(gè)簡(jiǎn)單的搜索工具類,這個(gè)較為常用
/**
* 關(guān)鍵詞搜索
*
* @param indexName 索引目錄
* @param keyword 查詢關(guān)鍵詞
* @param columns 被搜索的字段
* @param current 當(dāng)前頁(yè)
* @param size 每頁(yè)數(shù)據(jù)量
* @return
*/
public List<Document> search(String indexName, String keyword, String[] columns, int current, int size) {
String indexDir = luceneProperties.getIndexDir() + File.separator + indexName;
try {
// 打開(kāi)索引目錄
Directory directory = FSDirectory.open(Paths.get(indexDir));
IndexReader reader = DirectoryReader.open(directory);
IndexSearcher searcher = new IndexSearcher(reader);
// 中文分析器
Analyzer analyzer = new IKAnalyzer();
// 查詢解析器
QueryParser parser = new MultiFieldQueryParser(columns, analyzer);
// 解析查詢關(guān)鍵字
Query query = parser.parse(keyword);
// 執(zhí)行搜索,獲取匹配查詢的前 limit 條結(jié)果。
int limit = current * size;
// 搜索前 limit 條結(jié)果
TopDocs topDocs = searcher.search(query, limit);
// 匹配的文檔數(shù)組
ScoreDoc[] scoreDocs = topDocs.scoreDocs;
// 計(jì)算分頁(yè)的起始 - 結(jié)束位置
int start = (current - 1) * size;
int end = Math.min(start + size, scoreDocs.length);
// 返回指定頁(yè)碼的文檔
List<Document> documents = new ArrayList<>();
for (int i = start; i < end; i++) {
Document doc = searcher.doc(scoreDocs[i].doc);
documents.add(doc);
}
// 釋放資源
reader.close();
return documents;
} catch (Exception e) {
log.error("查詢 Lucene 錯(cuò)誤: ", e);
return null;
}
}
7、關(guān)鍵字高亮
@Test
public void searchArticle() throws InvalidTokenOffsetsException, IOException, ParseException {
String keyword = "安裝";
String[] fields = {IArticleIndex.COLUMN_CONTENT, IArticleIndex.COLUMN_ARTICLE_NAME};
// 先查詢出文檔列表
List<Document> documentList = LuceneUtils.X.search(IArticleIndex.INDEX_NAME, keyword, fields, 1, 100);
// 中文分詞器
Analyzer analyzer = new IKAnalyzer();
// 搜索條件
QueryParser queryParser = new MultiFieldQueryParser(fields, analyzer);
// 搜索關(guān)鍵詞,也就是需要高亮的字段
Query query = queryParser.parse(keyword);
// 高亮html語(yǔ)句
Formatter formatter = new SimpleHTMLFormatter("<span style=\"color: #f73131\">", "</span>");
QueryScorer scorer = new QueryScorer(query);
Highlighter highlighter = new Highlighter(formatter, scorer);
// 設(shè)置片段長(zhǎng)度,一共展示的長(zhǎng)度
highlighter.setTextFragmenter(new SimpleFragmenter(50));
List<SearchArticleVO> list = new ArrayList<>();
for (Document doc : documentList) {
SearchArticleVO articleVO = new SearchArticleVO();
articleVO.setId(doc.get(IArticleIndex.COLUMN_ID));
articleVO.setCover(doc.get(IArticleIndex.COLUMN_COVER));
articleVO.setArticleName(doc.get(IArticleIndex.COLUMN_ARTICLE_NAME));
articleVO.setSummary(doc.get(IArticleIndex.COLUMN_SUMMARY));
articleVO.setCreateTime(LocalDate.parse(doc.get(IArticleIndex.COLUMN_CREATE_TIME)));
for (String field : fields) {
// 為文檔生成高亮
String text = doc.get(field);
// 使用指定的分析器對(duì)文本進(jìn)行分詞
TokenStream tokenStream = TokenSources.getTokenStream(field, text, analyzer);
// 找到其中一個(gè)關(guān)鍵字就行了
String bestFragment = highlighter.getBestFragment(tokenStream, text);
if (StringUtils.isNotBlank(bestFragment)) {
// 輸出高亮結(jié)果,取第一條即可
if (field.equals(IArticleIndex.COLUMN_ARTICLE_NAME)) {
articleVO.setArticleName(bestFragment);
}
if (field.equals(IArticleIndex.COLUMN_CONTENT)) {
articleVO.setSummary(bestFragment);
}
}
}
list.add(articleVO);
}
}
我是一零貳肆,一個(gè)關(guān)注Java技術(shù)和記錄生活的博主。
歡迎掃碼關(guān)注“一零貳肆”的公眾號(hào),一起學(xué)習(xí),共同進(jìn)步,多看路,少踩坑。文章來(lái)源:http://www.zghlxwxcb.cn/news/detail-840358.html
文章來(lái)源地址http://www.zghlxwxcb.cn/news/detail-840358.html
到了這里,關(guān)于Lucene輕量級(jí)搜索引擎,Solr 和 ElasticSearch 都是基于 Lucene 的封裝的文章就介紹完了。如果您還想了解更多內(nèi)容,請(qǐng)?jiān)谟疑辖撬阉鱐OY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!