一、基础知识
1、Lucene 是什么
Lucene 是一个本地全文搜索引擎,Solr 和 ElasticSearch 都是基于 Lucene 的封装
Lucene 适合那种轻量级的全文搜索,我就是服务器资源不够,如果上 ES 的话会很占用服务器资源,所有就选择了 Lucene 搜索引擎
2、倒排索引原理
全文搜索的原理是使用了倒排索引,那么什么是倒排索引呢?
-
先通过中文分词器,将文档中包含的关键字全部提取出来,比如我爱中国,会通过分词器分成我,爱,中国,然后分别对应‘我爱中国’ -
然后再将关键字与文档的对应关系保存起来 -
最后对关键字本身做索引排序
3、与传统数据库对比
Lucene | DB |
---|---|
数据库表(table) | 索引(index) |
行(row) | 文档(document) |
列(column) | 字段(field) |
4、数据类型
常见的字段类型
-
StringField:这是一个不可分词的字符串字段类型,适用于精确匹配和排序。 -
TextField:这是一个可分词的字符串字段类型,适用于全文搜索和模糊匹配。 -
IntField、LongField、FloatField、DoubleField:这些是数值字段类型,用于存储整数和浮点数。 -
DateField:这是一个日期字段类型,用于存储日期和时间。 -
BinaryField:这是一个二进制字段类型,用于存储二进制数据,如图片、文件等。 -
StoredField:这是一个存储字段类型,用于存储不需要被索引的原始数据,如文档的内容或其他附加信息。
Lucene 分词器是将文本内容分解成单独的词汇(term)的工具。Lucene 提供了多种分词器,其中一些常见的包括
-
StandardAnalyzer:这是 Lucene 默认的分词器,它使用 UnicodeText 解析器将文本转换为小写字母,并且根据空格、标点符号和其他字符来进行分词。 -
CJKAnalyzer:这个分词器专门为中日韩语言设计,它可以正确地处理中文、日文和韩文的分词。 -
KeywordAnalyzer:这是一个不分词的分词器,它将输入的文本作为一个整体来处理,常用于处理精确匹配的情况。 -
SimpleAnalyzer:这是一个非常简单的分词器,它仅仅按照非字母字符将文本分割成小写词汇。 -
WhitespaceAnalyzer:这个分词器根据空格将文本分割成小写词汇,不会进行任何其他的处理。
但是对于中文分词器,我们一般常用第三方分词器IKAnalyzer,需要引入它的POM文件
二、最佳实践
1、依赖导入
8.1.1
8.0.0
org.apache.lucene
lucene-core
${lucene.version}
org.apache.lucene
lucene-queryparser
${lucene.version}
org.apache.lucene
lucene-analyzers-common
${lucene.version}
org.apache.lucene
lucene-highlighter
${lucene.version}
com.jianggujin
IKAnalyzer-lucene
${IKAnalyzer-lucene.version}
2、创建索引
-
先制定索引的基本数据,包括索引名称和字段
/**
*@author:sunhhw
*@date:2023/12/2517:39
*@description:定义文章文档字段和索引名称
*/
publicinterfaceIArticleIndex{
/**
*索引名称
*/
StringINDEX_NAME="article";
//---------------------文档字段---------------------
StringCOLUMN_ID="id";
StringCOLUMN_ARTICLE_NAME="articleName";
StringCOLUMN_COVER="cover";
StringCOLUMN_SUMMARY="summary";
StringCOLUMN_CONTENT="content";
StringCOLUMN_CREATE_TIME="createTime";
}
-
创建索引并新增文档
/**
*创建索引并设置数据
*
*@paramindexName索引地址
*/
publicvoidaddDocument(StringindexName,ListdocumentList){
//配置索引的位置例如:indexDir=/app/blog/index/article
StringindexDir=luceneProperties.getIndexDir()+File.separator+indexName;
try{
Filefile=newFile(indexDir);
//若不存在,则创建目录
if(!file.exists()){
FileUtils.forceMkdir(file);
}
//读取索引目录
Directorydirectory=FSDirectory.open(Paths.get(indexDir));
//中文分析器
Analyzeranalyzer=newIKAnalyzer();
//索引写出工具的配置对象
IndexWriterConfigconf=newIndexWriterConfig(analyzer);
//创建索引
IndexWriterindexWriter=newIndexWriter(directory,conf);
longcount=indexWriter.addDocuments(documentList);
log.info("[批量添加索引库]总数量:{}",documentList.size());
//提交记录
indexWriter.commit();
//关闭close
indexWriter.close();
}catch(Exceptione){
log.error("[创建索引失败]indexDir:{}",indexDir,e);
thrownewUtilsException("创建索引失败",e);
}
}
注意这里有个坑,就是这个 indexWriter.close();
必须要关闭, 不然在执行其他操作的时候会有一个write.lock
文件锁控制导致操作失败indexWriter.addDocuments(documentList)
这是批量添加,单个添加可以使用i服务器托管网ndexWriter.addDocument()
-
单元测试
@Test
publicvoidcreate_index_test(){
ArticlePOarticlePO=newArticlePO();
articlePO.setArticleName("git的基本使用"+i);
articlePO.setContent("这里是git的基本是用的内容"+i);
articlePO.setSummary("测试摘要"+i);
articlePO.setId(String.valueOf(i));
articlePO.setCreateTime(LocalDateTime.now());
Documentdocument=buildDocument(articlePO);
LuceneUtils.X.addDocument(IArticleIndex.INDEX_NAME,document);
}
privateDocumentbuildDocument(ArticlePOarticlePO){
Documentdocument=newDocument();
LocalDateTimecreateTime=articlePO.getCreateTime();
Stringformat=LocalDateTimeUtil.format(createTime,DateTimeFormatter.ISO_LOCAL_DATE);
//因为ID不需要分词,使用StringField字段
document.add(newStringField(IArticleIndex.COLUMN_ID,articlePO.getId()==null?"":articlePO.getId(),Field.Store.YES));
//文章标题articleName需要搜索,所以要分词保存
document.add(newTextField(IArticleIndex.COLUMN_ARTICLE_NAME,articlePO.getArticleName()==null?"":articlePO.getArticleName(),Field.Store.YES));
//文章摘要summary需要搜索,所以要分词保存
document.add(newTextField(IArticleIndex.COLUMN_SUMMARY,articlePO.getSummary()==null?"":articlePO.getSummary(),Field.Store.YES));
//文章内容content需要搜索,所以要分词保存
document.add(newTextField(IArticleIndex.COLUMN_CONTENT,articlePO.getContent()==null?"":articlePO.getContent(),Field.Store.YES));
//文章封面不需要分词,但是需要被搜索出来展示
document.add(服务器托管网newStoredField(IArticleIndex.COLUMN_COVER,articlePO.getCover()==null?"":articlePO.getCover()));
//创建时间不需要分词,仅需要展示
document.add(newStringField(IArticleIndex.COLUMN_CREATE_TIME,format,Field.Store.YES));
returndocument;
}
3、更新文档
-
更新索引方法
/**
*更新文档
*
*@paramindexName索引地址
*@paramdocument文档
*@paramcondition更新条件
*/
publicvoidupdateDocument(StringindexName,Documentdocument,Termcondition){
StringindexDir=luceneProperties.getIndexDir()+File.separator+indexName;
try{
//读取索引目录
Directorydirectory=FSDirectory.open(Paths.get(indexDir));
//中文分析器
Analyzeranalyzer=newIKAnalyzer();
//索引写出工具的配置对象
IndexWriterConfigconf=newIndexWriterConfig(analyzer);
//创建索引
IndexWriterindexWriter=newIndexWriter(directory,conf);
indexWriter.updateDocument(condition,document);
indexWriter.commit();
indexWriter.close();
}catch(Exceptione){
log.error("[更新文档失败]indexDir:{},document:{},condition:{}",indexDir,document,condition,e);
thrownewServiceException();
}
}
-
单元测试
@Test
publicvoidupdate_document_test(){
ArticlePOarticlePO=newArticlePO();
articlePO.setArticleName("git的基本使用=编辑");
articlePO.setContent("这里是git的基本是用的内容=编辑");
articlePO.setSummary("测试摘要=编辑");
articlePO.setId("2");
articlePO.setCreateTime(LocalDateTime.now());
Documentdocument=buildDocument(articlePO);
LuceneUtils.X.updateDocument(IArticleIndex.INDEX_NAME,document,newTerm("id","2"));
}
更新的时候,如果存在就更新那条记录,如果不存在就会新增一条记录 new Term("id", "2")
搜索条件,跟数据库里的where id = 2
差不多IArticleIndex.INDEX_NAME = article
索引名称
4、删除文档
-
删除文档方法
/**
*删除文档
*
*@paramindexName索引名称
*@paramcondition更新条件
*/
publicvoiddeleteDocument(StringindexName,Termcondition){
StringindexDir=luceneProperties.getIndexDir()+File.separator+indexName;
try{
//读取索引目录
Directorydirectory=FSDirectory.open(Paths.get(indexDir));
//索引写出工具的配置对象
IndexWriterConfigconf=newIndexWriterConfig();
//创建索引
IndexWriterindexWriter=newIndexWriter(directory,conf);
indexWriter.deleteDocuments(condition);
indexWriter.commit();
indexWriter.close();
}catch(Exceptione){
log.error("[删除文档失败]indexDir:{},condition:{}",indexDir,condition,e);
thrownewServiceException();
}
}
-
单元测试
@Test
publicvoiddelete_document_test(){
LuceneUtils.X.deleteDocument(IArticleIndex.INDEX_NAME,newTerm(IArticleIndex.COLUMN_ID,"1"));
}
删除文档跟编辑文档类似
5、删除索引
把改索引下的数据全部清空
/**
*删除索引
*
*@paramindexName索引地址
*/
publicvoiddeleteIndex(StringindexName){
StringindexDir=luceneProperties.getIndexDir()+File.separator+indexName;
try{
//读取索引目录
Directorydirectory=FSDirectory.open(Paths.get(indexDir));
//索引写出工具的配置对象
IndexWriterConfigconf=newIndexWriterConfig();
//创建索引
IndexWriterindexWriter=newIndexWriter(directory,conf);
indexWriter.deleteAll();
indexWriter.commit();
indexWriter.close();
}catch(Exceptione){
log.error("[删除索引失败]indexDir:{}",indexDir,e);
thrownewServiceException();
}
}
6、普通查询
-
TermQuery查询
Termterm=newTerm("title","lucene");
Queryquery=newTermQuery(term);
上述代码表示通过精确匹配字段”title”中包含”lucene”的文档。
-
PhraseQuery查询
PhraseQuery.Builderbuilder=newPhraseQuery.Builder();
builder.add(newTerm("content","open"));
builder.add(newTerm("content","source"));
PhraseQueryquery=builder.build();
上述代码表示在字段”content”中查找包含”open source”短语的文档
-
BooleanQuery查询
TermQueryquery1=newTermQuery(newTerm("title","lucene"));
TermQueryquery2=newTermQuery(newTerm("author","john"));
BooleanQuery.Builderbuilder=newBooleanQuery.Builder();
builder.add(query1,BooleanClause.Occur.MUST);
builder.add(query2,BooleanClause.Occur.MUST);
BooleanQueryquery=builder.build();
上述代码表示使用布尔查询同时满足”title”字段包含”lucene”和”author”字段包含”john”的文档。
-
WildcardQuery查询
WildcardQuery示例:
java
WildcardQueryquery=newWildcardQuery(newTerm("title","lu*n?e"));
上述代码表示使用通配符查询匹配”title”字段中以”lu”开头,且第三个字符为任意字母,最后一个字符为”e”的词项
-
MultiFieldQueryParser查询
String[]fields={"title","content","author"};
Analyzeranalyzer=newStandardAnalyzer();
MultiFieldQueryParserparser=newMultiFieldQueryParser(fields,analyzer);
Queryquery=parser.parse("lucenesearch");
a. 在”title”, “content”, “author”三个字段中搜索关键字”lucene search”的文本数据
b. MultiFieldQueryParser 默认使用 OR 运算符将多个字段的查询结果合并,即只要在任意一个字段中匹配成功即
可以使用MultiFieldQueryParser查询来封装一个简单的搜索工具类,这个较为常用
/**
*关键词搜索
*
*@paramindexName索引目录
*@paramkeyword查询关键词
*@paramcolumns被搜索的字段
*@paramcurrent当前页
*@paramsize每页数据量
*@return
*/
publicListsearch(StringindexName,Stringkeyword,String[]columns,intcurrent,intsize){
StringindexDir=luceneProperties.getIndexDir()+File.separator+indexName;
try{
//打开索引目录
Directorydirectory=FSDirectory.open(Paths.get(indexDir));
IndexReaderreader=DirectoryReader.open(directory);
IndexSearchersearcher=newIndexSearcher(reader);
//中文分析器
Analyzeranalyzer=newIKAnalyzer();
//查询解析器
QueryParserparser=newMultiFieldQueryParser(columns,analyzer);
//解析查询关键字
Queryquery=parser.parse(keyword);
//执行搜索,获取匹配查询的前limit条结果。
intlimit=current*size;
//搜索前limit条结果
TopDocstopDocs=searcher.search(query,limit);
//匹配的文档数组
ScoreDoc[]scoreDocs=topDocs.scoreDocs;
//计算分页的起始-结束位置
intstart=(current-1)*size;
intend=Math.min(start+size,scoreDocs.length);
//返回指定页码的文档
Listdocuments=newArrayList();
for(inti=start;iDocumentdoc=searcher.doc(scoreDocs[i].doc);
documents.add(doc);
}
//释放资源
reader.close();
returndocuments;
}catch(Exceptione){
log.error("查询Lucene错误:",e);
returnnull;
}
}
7、关键字高亮
@Test
publicvoidsearchArticle()throwsInvalidTokenOffsetsException,IOException,ParseException{
Stringkeyword="安装";
String[]fields={IArticleIndex.COLUMN_CONTENT,IArticleIndex.COLUMN_ARTICLE_NAME};
//先查询出文档列表
ListdocumentList=LuceneUtils.X.search(IArticleIndex.INDEX_NAME,keyword,fields,1,100);
//中文分词器
Analyzeranalyzer=newIKAnalyzer();
//搜索条件
QueryParserqueryParser=newMultiFieldQueryParser(fields,analyzer);
//搜索关键词,也就是需要高亮的字段
Queryquery=queryParser.parse(keyword);
//高亮html语句
Formatterformatter=newSimpleHTMLFormatter("","");
QueryScorerscorer=newQueryScorer(query);
Highlighterhighlighter=newHighlighter(formatter,scorer);
//设置片段长度,一共展示的长度
highlighter.setTextFragmenter(newSimpleFragmenter(50));
Listlist=newArrayList();
for(Documentdoc:documentList){
SearchArticleVOarticleVO=newSearchArticleVO();
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(Stringfield:fields){
//为文档生成高亮
Stringtext=doc.get(field);
//使用指定的分析器对文本进行分词
TokenStreamtokenStream=TokenSources.getTokenStream(field,text,analyzer);
//找到其中一个关键字就行了
StringbestFragment=highlighter.getBestFragment(tokenStream,text);
if(StringUtils.isNotBlank(bestFragment)){
//输出高亮结果,取第一条即可
if(field.equals(IArticleIndex.COLUMN_ARTICLE_NAME)){
articleVO.setArticleName(bestFragment);
}
if(field.equals(IArticleIndex.COLUMN_CONTENT)){
articleVO.setSummary(bestFragment);
}
}
}
list.add(articleVO);
}
}
我是一零贰肆,一个关注Java技术和记录生活的博主。
欢迎扫码关注“一零贰肆”的公众号,一起学习,共同进步,多看路,少踩坑。
服务器托管,北京服务器托管,服务器租用 http://www.fwqtg.net
机房租用,北京机房租用,IDC机房托管, http://www.fwqtg.net