0.Lucene介绍
0.1 全文检索的实现
创建索引: 原始文档 --> 创建索引【获得文档->构建文档对象->分析文档(分词)->创建索引】–> 放入索引库(Directory对象指向本地文件夹)–>
查询索引: 用户查询接口–> 创建查询–> 执行查询,在索引库找结果并返回结果–> 渲染结果–> 返回给用户
0.2 lucene8.11.0依赖
<!-- lucene核心库 -->
<dependency>
<groupId>org.apache.lucene</groupId>
<artifactId>lucene-core</artifactId>
<!--<version>4.10.2</version>-->
<version>8.11.0</version>
</dependency>
<!-- Lucene的查询解析器 -->
<dependency>
<groupId>org.apache.lucene</groupId>
<artifactId>lucene-queryparser</artifactId>
<version>8.11.0</version>
</dependency>
<!-- lucene的默认分词器库 -->
<dependency>
<groupId>org.apache.lucene</groupId>
<artifactId>lucene-analyzers-common</artifactId>
<version>8.11.0</version>
</dependency>
<!-- lucene的高亮显示 -->
<dependency>
<groupId>org.apache.lucene</groupId>
<artifactId>lucene-highlighter</artifactId>
<version>8.11.0</version>
</dependency>
1.Lucene工作原理 △
1.1 关于文档
1.获取原始内容的目的就是为了索引,
在创建索引前需要将原始内容创建成文档Document,
一个Document可以add多个Field对象,
每个Field对象就是结构化的数据内容。
类似某个音频视频文件与它的元数据的关系。
2.每个Document可以有多个Field,
不同的Document可以有不同的Field对象,
也可以有完全相同的Field(包括域名和域值都相同)
3.每个Document都会有单独的编号,就是文档ID
将原始内容创建为包含多个Field的Document,
需要再对Field中的内容进行分析,
分析的过程是对原始文档进行提词、EN还包括大写转小写(标准化)、
去空去标点去停词等,形成语汇单元(一个个关键词单词或者说中文)
比如下边的文档经过分析如下:
原始文档内容:
Lunece is a Java full-text search rngine.
Lunch is not a comlete application,
but rather a code library and API that can
easily be used to add search capabilities to applications.
分析后得到的语汇单元:
Lunece、Java、full、search、engine 。。。
分析好后的语汇单元的每个单词叫做一个Term,
多个terms可以代表一个document,
不同的域中拆分出来的相同的单词是不同的term,
term中包含两部分属性,一部分是所属的Document的路径,
另一部分是单词的具体内容
比如文件名中包含apache和文件内容中包含apache是不同的term
但是比如订单编号,身份证号本身就是一个整体,
作为单独的一个Field的时候不能进行分词,分词了就是去了意义
1.2 关于索引
1.对所有文档分析得到的语汇单元进行索引创建,
索引的目的是为了搜索出被索引的语汇单元中指定的Document
2.创建索引是对语汇单词进行指向而不是对document进行创建索引,
全文检索是通过词语找文档,而不是对文档内容进行模糊匹配。
这种索引的结构胶倒排索引结构,因为是根据文本的二次分析内容
倒回去找文本所在位置。
3.传统方法是顺序扫描法,贼慢的逐字遍历匹配,
而倒排索引法明显需要匹配的字符更少
4.倒排索引结构也叫反向索引结构,包括索引和文档两部分,
索引也就是词汇表,索引找语汇单元,语汇单元本身指向某个文档,
但是索引的词汇表本身规模小,而文档集合较大,所以该方法会更加高效。
用户输入searchMsg,根据searchMsg中的关键字进行匹配索引,
根据索引找文档,根据文档返回内容
索引可以认为是词表,term语汇单元可以认为是倒排表
1.3 关于查询
要导入用户的关键字到lunece进行索引查询,首先要创建一个查询对象,
查询对象可以指定要查询的field域,规定在所有文档的该域上进行查询。
根据查询语法在倒排索引词表中找出用户输入的关键字对应的索引,
从而找到索引所链接的文档列表
先把域选出来,决定在所有文档的哪个域中查询,
然后在所有文档的该域中把关键字进行索引的匹配,
把该索引执行所有的term所指向的文档地址获取出列表,
返回多份文档内容的列表作为返回结果渲染。
索引可以认为是词表,term语汇单元可以认为是倒排表,如图
2.Lucene初步指导使用
2.1 创建单个索引
@Test
public void testCreate() throws Exception{
Document document = new Document();
document.add(new StringField("id", "1", Field.Store.YES));
document.add(new TextField("title", "谷歌地图之父跳槽facebook", Field.Store.YES));
Directory directory = FSDirectory.open(new File("d:\\indexDir"));
Analyzer analyzer = new StandardAnalyzer();
IndexWriterConfig conf = new IndexWriterConfig(Version.LATEST, analyzer);
IndexWriter indexWriter = new IndexWriter(directory, conf);
indexWriter.addDocument(document);
indexWriter.commit();
indexWriter.close();
}
2.2 创建多个索引并设置多个字段
@Test
public void testCreate2() throws Exception{
Collection<Document> docs = new ArrayList<>();
Document document1 = new Document();
document1.add(new StringField("id", "1", Field.Store.YES));
document1.add(new TextField("title", "谷歌地图之父跳槽facebook", Field.Store.YES));
docs.add(document1);
Document document2 = new Document();
document2.add(new StringField("id", "2", Field.Store.YES));
document2.add(new TextField("title", "谷歌地图之父加盟FaceBook", Field.Store.YES));
docs.add(document2);
Document document3 = new Document();
document3.add(new StringField("id", "3", Field.Store.YES));
document3.add(new TextField("title", "谷歌地图创始人拉斯离开谷歌加盟Facebook", Field.Store.YES));
docs.add(document3);
Document document4 = new Document();
document4.add(new StringField("id", "4", Field.Store.YES));
document4.add(new TextField("title", "谷歌地图之父跳槽Facebook与Wave项目取消有关", Field.Store.YES));
docs.add(document4);
Document document5 = new Document();
document5.add(new StringField("id", "5", Field.Store.YES));
document5.add(new TextField("title", "谷歌地图之父拉斯加盟社交网站Facebook", Field.Store.YES));
docs.add(document5);
Directory directory = FSDirectory.open(new File("d:\\indexDir"));
Analyzer analyzer = new IKAnalyzer();
IndexWriterConfig conf = new IndexWriterConfig(Version.LATEST, analyzer);
conf.setOpenMode(IndexWriterConfig.OpenMode.CREATE);
IndexWriter indexWriter = new IndexWriter(directory, conf);
indexWriter.addDocuments(docs);
indexWriter.commit();
indexWriter.close();
}
2.3 查询索引数据
@Test
public void search() throws Exception {
Directory directory = FSDirectory.open(new File("d:\\indexDir"));
IndexReader reader = DirectoryReader.open(directory);
IndexSearcher searcher = new IndexSearcher(reader);
QueryParser parser = new QueryParser("title", new StandardAnalyzer());
Query query = parser.parse("谷歌");
TopDocs topDocs = searcher.search(query, 10);
System.out.println("本次搜索共找到" + topDocs.totalHits + "条数据");
ScoreDoc[] scoreDocs = topDocs.scoreDocs;
for (ScoreDoc scoreDoc : scoreDocs) {
int docID = scoreDoc.doc;
Document doc = reader.document(docID);
System.out.println("id: " + doc.get("id"));
System.out.println("title: " + doc.get("title"));
System.out.println("得分: " + scoreDoc.score);
}
}
2.3.1 正常词条查询
Query query = new TermQuery(new Term("title", "谷歌地图"));
search(query);
2.3.2 通配符查询【模糊查询】
Query query = new WildcardQuery(new Term("title", "*歌*"));
search(query);
2.3.3 数值范围查询
Query query = NumericRangeQuery.newLongRange("id", 2L, 2L, true, true);
search(query);
2.3.4 组合查询
Query query1 = NumericRangeQuery.newLongRange("id", 1L, 3L, true, true);
Query query2 = NumericRangeQuery.newLongRange("id", 2L, 4L, true, true);
BooleanQuery query = new BooleanQuery();
query.add(query1, BooleanClause.Occur.MUST_NOT);
query.add(query2, BooleanClause.Occur.SHOULD);
search(query);
2.4 修改索引
Directory directory = FSDirectory.open(new File("indexDir"));
IndexWriterConfig conf = new IndexWriterConfig(Version.LATEST, new IKAnalyzer());
IndexWriter writer = new IndexWriter(directory, conf);
Document doc = new Document();
doc.add(new StringField("id","1",Store.YES));
doc.add(new TextField("title","谷歌地图之父跳槽facebook ",Store.YES));
writer.updateDocument(new Term("id","1"), doc);
writer.commit();
writer.close();
2.5 删除索引
Directory directory = FSDirectory.open(new File("indexDir"));
IndexWriterConfig conf = new IndexWriterConfig(Version.LATEST, new IKAnalyzer());
IndexWriter writer = new IndexWriter(directory, conf);
writer.deleteAll();
writer.commit();
writer.close();
3.Lucene工作步骤详解
参考资料: (17条消息) Lunece介绍和简单实用_heshendian的博客-CSDN博客
创建索引实现:
2、创建一个indexWriter对象。
? 指定索引库的存放位置Directory对象(FSDirectory.open 来获取)
? 指定一个IndexWriterConfig对象(IndexWriterConfig需要analyzer对象)。
3、创建document对象。
4、创建field对象,将field对象添加到document对象中。
5、使用indexWriter对象将document对象写入索引库,此过程进行索引创建,并将索引和docuemnt对象写入索引库
6、关闭indexWriter对象。
查询索引实现:
1.创建一个Directory对象,也就是索引库存的位置。
2.创建一个IndexReader对象,需要指定Directory对象。
3.创建一个indexSearcher对象,需要指定IndexReader对象。
4.创建一个TermQuery对象,指定查询的域和查询的关键词。
5.执行查询
6.返回查询结果,遍历查询结果并输出
7.关闭indexReader对象
3.3 Document和Field
Document document = new Document();
document.add(new StringField("gid",gid, Field.Store.YES));
document.add(new TextField("ginfo",getInfo,Field.Store.YES));
-
Field
Document是Field的承载体,一个Document由多个Field组成
Field由名称和值两部分组成,Field的值是要索引的内容,即目标
还需要对Field进行提词形成倒排表,然后对倒排表建立索引(词表)
形成一一对应,索引--语汇单元列表--多个Document--返回文本结果
但不是所有的Field都需要进行提词term的,比如身份证号,编号等
不分词的部分需要全文完美匹配,并且索引和term是一致的,且是一对一关系
// 以下三种不是必要选项,不同情况选择不同
1.分词tokenized
分词:分词目的是为了索引,比如商品名称,商品描述这些,用户输入的关键字可能不全,匹配语汇单元会更加符合实际。
不分词:如订单编号,身份证号是一个整体,分词就失去了意义
2.索引indexed
索引:用户可能输入的关键词等涉及到的Field都需要建立索引
不索引:如商品图片路径,不会作为查询条件,不需要建立
3.存储stored
存储:比如商品名称,商品价格,凡是将来需要在结果列表中展现给用户的内容,都要在这个时候存储,提前返回不依赖数据库
不存储:比如商品描述,内容多,格式大,不需要直接在搜索结果展示,不做存储,用户点击结果进入详情页面的时候再去关系数据进行索要
Field类型 | 数据类型 | 是否分词 | 是否索引 | 是否存储 | 说明 |
---|
StringField(FieldName, FieldValue, Store.YES) | 字符串 | N | Y | Y/N | 字符串类型Field, 不分词, 作为一个整体进行索引 (如: 身份证号, 订单编号), 是否需要存储由Store.YES或Store.NO决定 | FloatPoint(FieldName,FieldValue) | float | Y | Y | N | 构建一个Float数字类型Field进行分词和索引,不存储,(价格) | DoublePoint(FieldName,FieldValue) | double | Y | Y | N | 构建一个double数字类型Field进行分词和索引,不存储,(价格) | IntPoint(FieldName,FieldValue) | integer型 | Y | Y | N | 构建一个double数字类型Field进行分词和索引,不存储,(价格) | LongPoint(FieldName, FieldValue) | 数值型代表 | Y | Y | N | Long数值型Field代表, 分词并且索引(如: 价格,身份证), 是否需要存储由Store.YES或Store.NO决定 | StoredField(FieldName, FieldValue) | 重载方法, 支持多种类型 | N | N | Y | 构建不同类型的Field, 不分词, 不索引, 要存储. (如: 商品图片路径) | TextField(FieldName, FieldValue, Store.NO) | 文本类型 | Y | Y | Y/N | 文本类型Field, 分词并且索引, 是否需要存储由Store.YES或Store.NO决定 |
3.4 Directory
Path path = Paths.get("D:\\indexdir");
Directory directory = FSDirectory.open(path);
3.5 Analyzer
3.5.1 StanderAnalyzer(Lucene)
3.5.2 SmartChineseAnalzer
- 先分句,再分词
- 对中文支持较好,但是扩展性差,扩展词库,禁用词库和同义词库等不好处理
需要加依赖
<dependency>
<groupId>org.apache.lucene</groupId>
<artifactId>lucene-analyzers-smartcn</artifactId>
<version>7.3.0</version>
</dependency>
3.5.3 IKAnalyzer
参考资料: (17条消息) 大数据工具:IKAnalyzer分词工具介绍与使用_maoyuanming0806的博客-CSDN博客_ikanalyzer
IK分词器介绍
为什么要分词?该工具在大数据处理中提取语句特征值进行向量计算中十分有用,并且是开源项目,能够实现基本的分词需求
IK是基于java开发的轻量级中文分词工具包,以开源项目lucene为主体,结合词典分词和文法分析算法的中文分词组件。在2012版本中就实现了简单的分词歧义排除算法
优势:
1.IK采用特有的“正向迭代最细粒度切分算法”,支持细粒度和智能分词两种切分模式
2.处理能力高速
3.采用多子处理器分析模式,支持英文 字母 数字 中文
4.优化词典存储,更小内存占用
重要词典:
1.扩展词典:
扩展词典:为的是让需要切分的字符串的词语 根据扩展词典里的词,不要切分开来。
例如:扩展词典中有:中国的台湾 。那么原本会切分成:中国 的 台湾 在 东海 。会切分成:中国的台湾 在 东海
2.停止词典:
对比停止词典,直接删掉停止词典中出现的词语
使用方法:
<!-- https://mvnrepository.com/artifact/com.janeluo/ikanalyzer -->
<dependency>
<groupId>com.janeluo</groupId>
<artifactId>ikanalyzer</artifactId>
<version>2012_u6</version>
</dependency>
# 只有2012版本的
- 2、把配置文件和扩展词典和停用词词典添加到classpath下:
注意:hotword.dic和ext——stopword.dic文件格式为UTF-8,注意是无BOM的UTF-8编码。
也就是说禁止使用windows记事本编辑扩展词典文件
IKAnalyzer analyzer = new IKAnalyzer();
//使用智能分词
//ik2012和ik3.0,3.0没有这个方法
//analyzer.setUseSmart(true);
try {
return printAnalyzerResult(analyzer, line);
} catch (IOException e) {
e.printStackTrace();
}
return null;
}
public static String printAnalyzerResult(Analyzer analyzer, String keyword) throws IOException {
String resultData = "";
String infoData = "";
TokenStream tokenStream = analyzer.tokenStream("content",new StringReader(keyword));
tokenStream.addAttribute(CharTermAttribute.class);
while(tokenStream.incrementToken()){
CharTermAttribute charTermAttribute = tokenStream.getAttribute(CharTermAttribute.class);
infoData = infoData+ " "+charTermAttribute.toString();
}
if(!"".equals(infoData)){
resultData = resultData + infoData.trim()+"\r\n";
}else{
resultData = "";
}
return resultData;
}
public static void main(String[] args) {
String line = "这是一个粗糙的栅栏,浪费钱,我想要一堵巨大的墙!”网友Mary说,还附上了“理想”中的边境墙照片";
String s = IKAnalyzerTest.beginAnalyzer(line);
System.out.println(s);
这是一个 这是 粗糙 栅栏 浪费 费钱 我 想要 一堵 巨大的墙 巨大 网友 mary 说 还 附上 上了 理想 中 边境 墙 照片
3.6 IndexWriter和IndexReader
Analyzer analyzer = new StandardAnalyzer();
IndexWriterConfig conf = new IndexWriterConfig(analyzer);
IndexWriter indexWriter = new IndexWriter(directory,conf);
indexWriter.addDocument(document);
【indexWriter.deleteAll();
【indexWriter.deleteDocuments(new TermQuery(new Term("name","张三")))
Document document = new Document();
document.add(new TextField("filename","要更新的文档",Field.Store.YES))
【indexWriter.updateDocument(new Term("name","zs"),document)】
indexWriter.commit();
indexWriter.close();
Directory directory =FSDirectory.open(Paths.get("path"));
IndexReader indexReader=DirectoryReader.open(directory);
IndexSearcher indexSearcher =new IndexSearcher(indexReader);
indexReader.close();
3.7 Query
3.6.1 queryParser
- 需要使用analyzer分析器,查询的索引对应的是多个语汇单元
// 创建查询
QueryParser queryParser=new QueryParser("name",new IKAnalyzer());
Query query= queryParser.parse("张三");
IndexSearcher indexSearcher=new IndexSearcher(DirectoryReader.open(FSDirectory.open(Paths.get(CreateIndexes.path))));
TopDocs topDocs = indexSearcher.search(query, 10);
//共查询到的document个数
System.out.println("查询结果总数量:" + topDocs.totalHits);
//遍历查询结果
for (ScoreDoc scoreDoc : topDocs.scoreDocs) {
Document document = indexSearcher.doc(scoreDoc.doc);
System.out.println(document.get("filename"));
//System.out.println(document.get("content"));
System.out.println(document.get("path"));
System.out.println(document.get("size"));
}
//关闭indexreader
indexSearcher.getIndexReader().close();
3.6.2 TermQuery
- 通过项单元查询,TermQuery不适用分析器,所以建议匹配不分词的Field域查询,比如订单号,价格等。查询的索引对应的
Directory directory =FSDirectory.open(Paths.get("path"));
//* 创建一个IndexReader对象,需要指定Directory对象。
IndexReader indexReader=DirectoryReader.open(directory);
//* 创建一个indexSearcher对象,需要指定IndexReader对象。
IndexSearcher indexSearcher =new IndexSearcher(indexReader);
//* 创建一个TermQuery对象,指定查询的域和查询的关键词。
Query query = new TermQuery(new Term("name","张"));
//* 执行查询
TopDocs topDocs = indexSearcher.search(query, 10);
System.out.println("查询结果的总条数:"+topDocs.totalHits);
//* 返回查询结果,遍历查询结果并输出
for(ScoreDoc scoreDoc:topDocs.scoreDocs){
//scoreDoc.doc属性就是document对象的id
//根据document的id找到document对象
Document document = indexSearcher.doc(scoreDoc.doc);
System.out.println(document.get("id"));
//System.out.println(document.get("content"));
System.out.println(document.get("name"));
System.out.println(document.get("address"));
System.out.println(document.get("hobby"));
System.out.println("-------------------------");
}
//* 关闭indexReader对象
indexReader.close();
3.8 TopDocs
3.9 ToitalHits
4.已成功实现功能的平台搜索引擎代码
package com.sysugoods.util;
import com.sysugoods.domain.Good;
import org.apache.lucene.analysis.Analyzer;
import org.apache.lucene.analysis.cn.smart.SmartChineseAnalyzer;
import org.apache.lucene.analysis.standard.StandardAnalyzer;
import org.apache.lucene.document.*;
import org.apache.lucene.index.*;
import org.apache.lucene.queryparser.classic.QueryParser;
import org.apache.lucene.search.*;
import org.apache.lucene.store.Directory;
import org.apache.lucene.store.FSDirectory;
import java.nio.file.*;
import java.util.ArrayList;
import java.util.List;
public class LuceneUtil {
public static void createIndex(Good good){
Document document = new Document();
String gid = String.valueOf(good.getGid());
document.add(new StringField("gid",gid, Field.Store.YES));
document.add(new TextField("gname",good.getGname(), Field.Store.YES));
document.add(new StringField("gprice",good.getGprice(), Field.Store.YES));
document.add(new StringField("uname",good.getUname(), Field.Store.YES));
document.add(new StringField("gstatus",good.getGstatus(), Field.Store.YES));
document.add(new StoredField("gpath",good.getGpaths()));
document.add(new TextField("gintroduce",good.getGintroduce(),Field.Store.NO));
Directory directory = null;
IndexWriter indexWriter = null;
Path path = null;
try{
path = Paths.get("D:\\indexdir");
directory = FSDirectory.open(path);
Analyzer analyzer = new StandardAnalyzer();
IndexWriterConfig conf = new IndexWriterConfig(analyzer);
indexWriter = new IndexWriter(directory,conf);
indexWriter.addDocument(document);
indexWriter.commit();
indexWriter.close();
}catch (Exception e){
e.printStackTrace();
}
}
public void modifyIndex(){
}
public void deleteIndex(){
}
public static List<Document> queryIndex(String searchMsg){
List<Document> result = new ArrayList();
Document doc = null;
try{
Path path = Paths.get("D:\\indexdir");
Directory directory = FSDirectory.open(path);
IndexReader reader = DirectoryReader.open(directory);
IndexSearcher searcher = new IndexSearcher(reader);
Analyzer analyzer = new SmartChineseAnalyzer();
QueryParser parser2 = new QueryParser("gname",analyzer);
QueryParser parser3 = new QueryParser("gintroduce",analyzer);
TermQuery query1 = new TermQuery(new Term("uname",searchMsg));
Query query2 = parser2.parse(searchMsg);
Query query3 = parser3.parse(searchMsg);
TopDocs topDocs1 = searcher.search(query1,150);
TopDocs topDocs2 = searcher.search(query2,150);
TopDocs topDocs3 = searcher.search(query3,150);
TotalHits result_num1 = topDocs1.totalHits;
TotalHits result_num2 = topDocs2.totalHits;
TotalHits result_num3 = topDocs3.totalHits;
System.out.println("uname找到一共" +result_num1.value);
System.out.println("gname找到一共"+result_num2.value);
System.out.println("gintroduce找到一共"+result_num3.value);
if(result_num1.value!=0){
ScoreDoc[] scoreDocs = topDocs1.scoreDocs;
for(ScoreDoc scoreDoc : scoreDocs){
int docId = scoreDoc.doc;
doc = reader.document(docId);
if(doc != null){
//换gid
result.add(doc);
}
}
}
if(result_num2.value!=0){
ScoreDoc[] scoreDocs = topDocs2.scoreDocs;
for(ScoreDoc scoreDoc : scoreDocs){
int docId = scoreDoc.doc;
doc = reader.document(docId);
if(doc != null){
//换gid
result.add(doc);
}
}
}
if(result_num3.value!=0){
ScoreDoc[] scoreDocs = topDocs3.scoreDocs;
for(ScoreDoc scoreDoc : scoreDocs){
int docId = scoreDoc.doc;
doc = reader.document(docId);
if(doc != null){
//换gid
result.add(doc);
}
}
}
System.out.println("共找到"+result.size());
reader.close();
}catch (Exception e){
e.printStackTrace();
}
// int result_num = topDocs.totalHits;
// System.out.println(result_num);
return result;
}
}
该模块的个人学习完毕,已成功实现所需功能 效果图测试如下图所示: 【前端是vuecli脚手架随便写的,后端用ssm框架对接】 也就是输入你好搜索,下面展示的商品的商品名或者商品描述会和“你好”相关,并且你好两个字不分顺序。 【所用的数据是通过mockjs配合javascript脚本进行模拟的,有需要可以看我的mockjs的个人学习总结】
|