参考链接
https://blog.csdn.net/forfuture1978/article/details/4711308
https://blog.51cto.com/u_15133569/5895049
https://blog.csdn.net/qq_41861558/article/details/102792470
https://www.bilibili.com/video/BV1Na411h7kk?p=2&vd_source=ed806c6daa358b1d73b17235aad841ba
Lucene 是什么
Lucene是apache软件基金会 jakarta项目组的一个子项目,是一个开放源代码的全文检索引擎工具包,但它不是一个完整的全文检索引擎,而是一个全文检索引擎的架构,提供了完整的查询引擎和索引引擎,部分文本分析引擎。方便软件开发人员是以此为基础建立起完整的全文检索引擎。
Lucene提供了一个简单却强大的应用程式接口,能够做全文索引和搜寻。在Java开发环境里Lucene是一个成熟的免费开源工具。
为什么要用 Lucene
结构化数据: 指具有固定格式或有限长度的数据,如数据库,元数据等。
非结构化数据(全文数据): 指不定长或无固定格式的数据,如邮件,word文档等。
对非结构化数据的搜索有一种简单原始的方法:
顺序扫描法 (Serial Scanning): 比如要从全部文档里找一个字符串,就按文档顺序遍历每一个文档,每一个文档都从头到尾去找字符串,直到扫描完所有的文档。
优点:准确率高;缺点:数据量大之后速度慢。
例如:数据库中的like关键字模糊查询;ctrl+F 的文本查找。
但是数据量过多时,查询速度会变得非常慢,需要使用更好的解决方案来分担查询的压力。
全文检索
索引 :为了提升非结构化数据的检索速度,从非结构化数据中提取出“有结构”的然后重新组织的信息,称之索引。比如字典,字典的拼音表和部首检字表就相当于字典的索引。
全文检索(Full-text Search):先建立索引,再对索引进行搜索的过程就叫全文检索。
全文检索大体分两个过程,索引创建 (Indexing) 和搜索索引 (Search) 。
索引创建:将现实世界中所有的结构化和非结构化数据提取信息,创建索引的过程。
搜索索引:就是得到用户的查询请求,搜索创建的索引,然后返回结果的过程。
索引里面究竟存些什么?(Index)
简单来说,保存从字符串到文件的映射。由于这是文件到字符串映射的反向过程,于是保存这种信息的索引称为反向索引。
左边保存的是一系列字符串,称为词典。
每个字符串都指向包含此字符串的文档(Document)链表,此文档链表称为倒排表(Posting List)。
检索的时候只需要检索词典就能获取文档链表,通过合并多个链表就能快速获取检索结果。
建立索引是需要花时间的,不过以后便是一劳永逸了。一次索引,多次使用。
如何创建索引?(Indexing)
- 将原文档(Document)传给分词组件(Tokenizer),经过分词后得到的结果称为词元(Token)。
分词(中文分词会比英文分词麻烦);除去标点符号;停用词 - 将得到的词元(Token)传给语言处理组件(Linguistic Processor),对得到的词元(Token)做一些同语言相关的处理得到结果称为词(Term)。
英文变为小写(Lowercase);单词还原,如“cars”到“car”等(stemming),如“drove”到“drive”等(lemmatization)。 - 将得到的词(Term)传给索引组件(Indexer),生成文档倒排(Posting List)链表。
利用得到的词(Term)创建一个字典,并对字典进行排序。最后合并相同的词(Term)成为文档倒排(Posting List)链表
Document Frequency 即文档频次,表示总共有多少文件包含此词(Term)。
Frequency 即词频率,表示此文件中包含了几个此词(Term)。
如何对索引进行搜索?(Search)
- 用户输入查询语句
查询语句的语法根据全文检索系统的实现而不同。最基本的有比如:AND, OR, NOT等 - 对查询语句进行词法分析,语法分析,及语言处理。
词法分析主要用来识别单词和关键字
语法分析主要是根据查询语句的语法规则来形成一棵语法树
语言处理同索引过程中的语言处理(Linguistic Processor)几乎相同 - 搜索索引,得到符合语法树的文档
在反向索引表中,分别找出包含关键字的文档链表。对找出的链表进行交,差,并等操作,得到最终符合要求的文档。 - 根据得到的文档和查询语句的相关性,对结果进行排序。
找出词(Term)对文档的重要性的过程称为计算词的权重(Term weight)的过程。
词的权重(Term weight)表示此词(Term)在此文档中的重要程度,越重要的词(Term)有越大的权重(Term weight),因而在计算文档之间的相关性中将发挥更大的作用。有一种叫做向量空间模型的算法(Vector Space Model),可以判断词(Term)之间的关系从而得到文档相关性。实现全文检索系统的人可以自己的实现相关性排序算法。
影响一个词(Term)在一篇文档中的重要性主要有两个因素:- Term Frequency (tf):即此Term在此文档中出现了多少次。tf 越大说明越重要。
- Document Frequency (df):即有多少文档包含次Term。df 越大说明越不重要。
搭建Lucene简单框架
以下代码只是简单的移植,并不完整,且没有经过测试
项目初始化的时候要初始化Lucene
public LuceneService() throws IOException {
//RAMDriectory: 内存目录, 将索引库信息存放到内存中
//FSDirectory用来指定文件系统的目录, 将索引信息保存到磁盘上
FSDirectory luceneDB;
WhitespaceAnalyzer whitespaceAnalyzer = new WhitespaceAnalyzer();
//Lucene存储库的位置
File fileIdx = new File("存储路径,可以写在配置文件中去调用");
try {
//打开Lucene存储库
luceneDB = FSDirectory.open(fileIdx.toPath());
//IndexWriterConfig: 索引写入器的配置类,需要传递Lucene的版本和分词器
IndexWriterConfig writerConfig = new IndexWriterConfig(whitespaceAnalyzer);
//IndexWriter:索引写入器对象,功能为:添加索引、修改索引和删除索引。 需要传入Directory和indexWriterConfig对象 ,即索引的目录和配置
indexWriter = new IndexWriter(luceneDB, writerConfig);
//QueryParser:查询解析器
//LuceneName是自己建立规范名称的类
objectParser = new QueryParser(LuceneName.IDX_OBJECT_NAME, whitespaceAnalyzer);
log.info("索引服务初始化完成,索引目录为 " + fileIdx.toPath().toString());
} catch (IOException e) {
log.error("无法初始化索引,请检查提供的索引目录是否可用:[{}]", sysConfig.getIdx());
throw e;
}
}
可以建立一个类【LuceneName】去规范化Lucene名称
public class LuceneName {
public static final String comma_seg=",";
public static final String TERM_TRUE = "true";
public static final String TERM_FALSE = "false";
public static final AhoCorasickDoubleArrayTrie boolTrie = new AhoCorasickDoubleArrayTrie<>();
static {
Map convertable = new HashMap<>();
convertable.put("真", LuceneName.TERM_TRUE);
convertable.put("假", LuceneName.TERM_FALSE);
convertable.put("是", LuceneName.TERM_TRUE);
convertable.put("否", LuceneName.TERM_FALSE);
convertable.put("true", LuceneName.TERM_TRUE);
convertable.put("false", LuceneName.TERM_FALSE);
convertable.put("1", LuceneName.TERM_TRUE);
convertable.put("0", LuceneName.TERM_FALSE);
boolTrie.build(convertable);
}
//----------------------------java对象------------------------------------------------------
/**
* java对象的某个字段在索引中的字段名
* String
*/
public static final String IDX_OBJECT_ID = "OBJECT_ID";
/**
* java对象的某个字段在索引中的字段名
* String
*/
public static final String IDX_OBJECT_NAME = "OBJECT_NAME";
/**
* java对象的某个字段在索引中的内容
* String
*/
public static final String IDX_OBJECT_CONTENT = "OBJECT_CONTENT";
//.......
}
创建倒排索引
//创建多个文档的文件
@Test
public boolean lunceneWriter(Object object) {
if (object == null) {
log.error("输入的新闻为空,无法添加到索引中");
return false;
}
try {
//1 创建文档对象集合
// Collection documents = new ArrayList();
// Document ducument = new Document();
// 添加字段信息。参数:字段的名称、字段的值、是否存储,yes储存,no不储存
// ducument.add(new StringField("id", "1", Field.Store.YES));
// title字段用TextField,创建索引被分词。StringField创建索引,但不会被分词
// ducument.add(new TextField("title", "谷歌地图之父跳槽facebook", Field.Store.YES));
// documents.add(ducument);
Document doc = new Document();
if (!StringUtil.isEmpty(object.getContent()) {
String content = object.getContent();
// todo 可以手动对文本进行分词,然后中间添加空格。后续就直接用lucene的空格分词,有利于全文检索。后续检索出来展示时还需要删除空格保证文档易读
// 添加字段信息。参数:字段的名称、字段的值、是否存储,yes储存,no不储存
doc.add(new TextField(LuceneName.OBJECT_CONTENT, content, Field.Store.YES));
}
if (!StringUtil.isEmpty(object.getTime()) {
//对于时间戳的存储
doc.add(new LongPoint(LuceneName.IDX_OBJECT_DATE, object.getTime()));
//LongPoint类型字段是无法存储的,因此需要单独设置一个域用于存储发布时间的原始值
doc.add(new StoredField(LuceneName.IDX_OBJECT_DATE_STORED, String.valueOf(object.getTime()));
//NumericDocValuesField用于排序
doc.add(new NumericDocValuesField(LuceneName.IDX_OBJECT_DATE,object.getTime()));
}
indexWriter.updateDocument(new Term(LuceneName.IDX_OBJECT_ID, object.getId()), doc);
indexWriter.commit();
return true;
//2 创建存储目录,本地
// FSDirectory directory = FSDirectory.open(new File("e:/hadoop/hdpdata/lucene"));
//3 创建分词器
// StandardAnalyzer analyzer = new StandardAnalyzer();
// IKAnalyzer ikAnalyzer = new IKAnalyzer();
//4 创建索引写入器的配置对象
// IndexWriterConfig config = new IndexWriterConfig(Version.LATEST, analyzer);
//如果有数据,覆盖
// config.setOpenMode(IndexWriterConfig.OpenMode.CREATE);
//5 创建索引写入器对象
// IndexWriter indexWriter = new IndexWriter(directory, config);
//6 将文档交给索引写入器
// indexWriter.addDocuments(documents);
//7 提交
// indexWriter.commit();
//8 关闭
// indexWriter.close();
}catch (Exception e){
e.printStackTrace();
log.error("无法更新索引,异常为:[{}]", e.getMessage());
}
return false;
}
lucene检索
public SearchResult queryNews(String keyword, long startDate, long endDate, int results, int page) {
SearchResult result = new SearchResult();
IndexSearcher searcher = null;
BooleanQuery.Builder builder = null;
try {
log.debug("执行基于关键词的检索," +
"检索关键词为:[{}], " +
"时间范围为:[{}={}]" , keyword, startDate, endDate);
IndexReader reader = DirectoryReader.open(indexWriter);
searcher = new IndexSearcher(reader);
//查询结果的object ID列表
builder = new BooleanQuery.Builder();
//添加关键词检索
if (!StringUtil.isEmpty(keyword)) {
//用的HanLP包
List keyWords = HanLP.segment(keyword);
for (com.hankcs.hanlp.seg.common.Term keyWord : keyWords) {
BooleanQuery.Builder blder = new BooleanQuery.Builder();
if (keyWord.word.equals(" ") || keyWord.word.contains(" ")) {
continue;
}
/**
* 转义特殊字符
*/
String thisWord = QueryParser.escape(keyWord.word);
/**
* 查OBJECT的名字
*/
Query titleQuery = objectParser.parse(thisWord);
/**
/**
* 查OBJECT的内容
*/
Query contentQuery = objectParser.parse(LuceneName.IDX_OBJECT_CONTENT + ":" + thisWord);
blder.add(new BoostQuery(titleQuery, 25.0f), BooleanClause.Occur.SHOULD);
blder.add(contentQuery, BooleanClause.Occur.SHOULD);
builder.add(blder.build(), BooleanClause.Occur.MUST);
}
}
//添加时间检索
Query pubRangeQuery = null;
if(!(startDate ==0 && endDate == 0)){
pubRangeQuery = LongPoint.newRangeQuery(LuceneName.IDX_OBJECT_DATE, startDate, endDate);
builder.add(pubRangeQuery, BooleanClause.Occur.MUST);
}
} catch (IOException | ParseException e) {
e.printStackTrace();
}
try {
//分页查询 根据校对事件进行排序
if(StringUtil.isEmpty(keyword)){
Sort sortByProofDate = new Sort(new SortField(LuceneName.IDX_NEWS_PROOFDATE, SortField.Type.LONG, true));
result = LuceneUtils.LuceneSearchSortBySortField(results, page, searcher, builder.build(),sortByProofDate);
}else{
result = LuceneUtils.LuceneSearch(results, page, searcher, builder.build());
}
} catch (IOException e) {
e.printStackTrace();
}
return result;
}
删除索引
public boolean onNewsDelete(String objectId) {
try {
indexWriter.deleteDocuments(new Term(LuceneName.IDX_OBJECT_ID, objectId));
indexWriter.commit();
return true;
} catch (IOException e) {
e.printStackTrace();
return false;
}
}