日萌社
人工智能AI:Keras PyTorch MXNet TensorFlow PaddlePaddle 深度学习实战(不定时更新)
搜索引擎:Elasticsearch、Solr、Lucene
- ELK中的ES:Elasticsearch
- SolrCloud 的搭建、使用
- Solr 高亮显示
- Spring Data Solr 使用
- Solr的安装与配置
- Solr 原理、API 使用
- Lucene 原理、API使用
- Lucene 得分算法
MySQL数据库的使用索引来搜索
1.MySQL数据库中的索引:
UNIQUE(唯一索引)
PRIMARY KEY(主键索引),本质是名叫PRIMARY KEY 的UNIQUE索引
INDEX(普通索引)
INDEX(组合索引)
FULLTEXT(全文索引),搜索引擎常用
2.MySQL数据库的模糊查询速度慢,效率不高。
模糊查询的用法分析:
1.模糊查询的用法:字段名 like “%搜索内容%”
2.对“已添加索引的字段”进行模糊查询的话,分以下情况:
1.使用 字段名 like “%搜索内容%”或 字段名 like “%搜索内容”的话,即模糊查询LIKE中以“%”或“_”开头的话,
是无法使用到索引进行快速查询的。
2.使用 字段名 like “搜索内容%”,是可以使用到索引进行快速查询的。
3.即使字段添加了索引,但是在SQL语句中使用了以下规则进行WHERE条件过滤的话,便不会使用上“字段中添加的索引”进行快速查询:
1.NULL值判断:因为使用NULL值判断的话,则不会使用字段中的索引。
所以字段的默认值应改为使用 0 或 -1 或 空字符串,字段的默认值不应为NULL值。
2.使用 != 或 <> 操作符:!= 和 <> 都表示不等于操作,则不会使用字段中的索引。应改为尽量使用 >、<、= 才会使用上字段中的索引。
3.模糊查询LIKE中以“%”或“_”开头:是不会使用字段中的索引的。但如果以“%”或“_”结尾的话,还是可以使用到索引的。
可改为使用FULLTEXT(全文索引),搜索引擎常用。
4.在where子句中使用 or 来连接条件:应使用UNION 或 UNION ALL
5.in 和 not in:尽量控制in的范围(1000以内)
6.在where子句中对字段进行表达式操作:在列上进行运算的话,则不会使用字段中的索引
搜索引擎原理
倒排索引/反向索引 技术
1.单词:使用索引
2.倒排列表汇中:记录着的是文档编号
3.通过索引快速定义到“单词所对应的是”哪些文档编号,
然后根据文档编号在对应的文档内容中进行搜索关键字内容。
1.倒排索引又叫反向索引(右下图)以字或词为关键字进行索引,表中关键字所对应的记录表项,
记录了出现这个字或词的所有文档,每一个表项记录该文档的ID和关键字在该文档中出现的位置情况。
2.在实际的运用中,我们可以对数据库中原始的数据结构(临时表或者商品表),在业务空闲时事先根据左图内容,创建新的文档列表(左图)及倒排索引区域(右图)。
用户有查询需求时,先访问倒排索引数据区域(右图),得出文档编号后,通过文档编号即可快速,准确的通过左图找到具体的文档内容。
这一过程,可以通过我们自己写程序来实现,也可以借用已经抽象出来的通用开源技术来实现。
1.倒排建立索引的过程:
1.将数据库表中的数据 原样保存到 索引库中(结构化数据)
2.建立索引:
1.中文分词:把一串中文内容分为多个词
2.将每个词 和 对应的文档编号 进行关联起来,建立起索引
2.根据索引进行关键字查询:
1.先根据索引进行关键字查询,如果有,获取文档编号。
2.然后根据文档编号,去到对应的文档数据内容中 进行关键字查询。
3.正排索引:根据文档编号找到文档数据内容
4.倒排索引:根据关键字找到文档编号,然后再根据文档编号找到文档数据内容
全文检索
1.计算机索引程序通过扫描文章中的每一个词,对每一个词建立一个索引,指明该词在文章中出现的次数和位置,
当用户查询时,检索程序就根据事先建立的索引进行查找,并将查找的结果反馈给用户的检索方式
2.全文检索:全部都搜索
3.通过分词和倒排索引实现全部都搜索
4.总结:对文档(数据)中每一个词都做索引。
Lucene
1.目前最新的版本是6.x系列,但是大多数企业中依旧使用4.x版本,比较稳定。本次课程我们使用4.10.2版本
2.Lucene与Solr的关系:
Lucene:底层的API,工具包,提供了很多API实现建立索引和查询索引
Solr:基于Lucene开发的企业级的搜索引擎产品
引入Lucene依赖
<dependencies>
<!-- Junit单元测试 -->
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.12</version>
</dependency>
<!-- lucene核心库 -->
<dependency>
<groupId>org.apache.lucene</groupId>
<artifactId>lucene-core</artifactId>
<version>4.10.2</version>
</dependency>
<!-- Lucene的查询解析器 -->
<dependency>
<groupId>org.apache.lucene</groupId>
<artifactId>lucene-queryparser</artifactId>
<version>4.10.2</version>
</dependency>
<!-- lucene的默认分词器库 -->
<dependency>
<groupId>org.apache.lucene</groupId>
<artifactId>lucene-analyzers-common</artifactId>
<version>4.10.2</version>
</dependency>
<!-- lucene的高亮显示 -->
<dependency>
<groupId>org.apache.lucene</groupId>
<artifactId>lucene-highlighter</artifactId>
<version>4.10.2</version>
</dependency>
<!-- 引入IK分词器 -->
<dependency>
<groupId>com.janeluo</groupId>
<artifactId>ikanalyzer</artifactId>
<version>2012_u6</version>
</dependency>
</dependencies>
<build>
<plugins>
<!-- java编译插件 -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.2</version>
<configuration>
<source>1.7</source>
<target>1.7</target>
<encoding>UTF-8</encoding>
</configuration>
</plugin>
</plugins>
</build>
创建索引
1.索引库:实际就是本地的一个文件夹目录
2.创建索引写入器配置对象:IndexWriterConfig conf = new IndexWriterConfig(Version.LATEST, new StandardAnalyzer());
第一个参数:配置Lucene的版本,可使用“VerSion.LATEST”表示最新版本
第二个参数:指定使用哪种分词器。此处创建使用默认标准的分词器对象:Analyzer analyzer = new StandardAnalyzer()
注意:默认标准的分词器是不支持中文分词的,所以使用默认标准的分词器对象的话,
那么一串的中文内容的话,会以一个一个中文字符进行分词,而不是以词的形式进行分词。
3.索引写入器对象:IndexWriter indexWriter = new IndexWriter(“索引库的存放位置”Directory目录对象, 索引写入器配置对象);
索引写入器对象写入完数据之后,必须close()关闭:indexWriter.close()
示例代码:
import java.io.File;
import java.io.IOException;
import org.apache.lucene.analysis.Analyzer;
import org.apache.lucene.analysis.standard.StandardAnalyzer;
import org.apache.lucene.document.Document;
import org.apache.lucene.document.Field.Store;
import org.apache.lucene.document.StringField;
import org.apache.lucene.document.TextField;
import org.apache.lucene.index.IndexWriter;
import org.apache.lucene.index.IndexWriterConfig;
import org.apache.lucene.store.Directory;
import org.apache.lucene.store.FSDirectory;
import org.apache.lucene.util.Version;
import org.junit.Test;
// 创建文档对象
Document document = new Document();
// 使用StringField/TextField封装字段数据:Field是一个父类,StringField或TextField都继承自Field
// StringField构造器中的3个参数:1.字段名,2.字段值,3.是否作为文档数据保存(Store.YES存储;NO不存储。)
// StringField创建索引但不分词
document.add(new StringField("id", "1", Store.YES));
// TextField构造器中的3个参数:1.字段名,2.字段值,3.是否作为文档数据保存(Store.YES存储;NO不存储。)
// TextField:创建索引并进行分词
document.add(new TextField("title", "谷歌地图之父跳槽FaceBook", Store.YES));
// 创建Directory目录对象,指定索引库的存放位置,有以下两种存放方式:
// 1.FSDirectory:文件系统目录,会把索引库保存到本地磁盘。特点:速度略慢,但是比较安全
// 2.RAMDirectory:内存目录,会把索引库保存在内存。 特点:速度快,但是不安全
Directory directory = FSDirectory.open(new File("F:\\LucenenData\\indexDir"));
// 创建分词器对象
// 默认分词器new StandardAnalyzer():默认对英文进行分词,不会对中文进行中文分词,中文只能以逐个中文字符作为分词
Analyzer analyzer = new StandardAnalyzer();
// 创建索引写入器配置对象:
// 第一个参数:Lucene版本号 VerSion.LATEST
// 第一个参数:分词器对象
IndexWriterConfig conf = new IndexWriterConfig(Version.LATEST, analyzer);
// 创建索引写入器:传入Directory目录对象 和 索引写入器配置对象
IndexWriter indexWriter = new IndexWriter(directory, conf);
// 向索引库写入文档对象
indexWriter.addDocument(document);
// 提交
indexWriter.commit();
// 关闭
indexWriter.close();
批量添加索引
List<Document> docs = new ArrayList<Document>();
for(Long i = 2L ;i < 30 ; i ++)
{
Document doc = new Document();
LongField id = new LongField("id", i, Store.YES);
doc.add(id);
StringField title = new StringField("title", "Itcast"+i, Store.YES);
doc.add(title);
TextField content = new TextField("content", "鸟哥的linux技术私房菜"+i, Store.YES);
if(i==17L){
//激励因子 改变激励因子可以改变得分
content.setBoost(10);
}
doc.add(content);
docs.add(doc);//把每个Document文档对象添加到List<Document>集合中
}
//批量添加索引:往索引写入器中传入List<Document>集合
indexWriter.addDocuments(docs);
// 提交
indexWriter.commit();
// 关闭
indexWriter.close();
Lucene索引查看工具lukeall
API详解
Document(文档类)
Field(字段类)
1.一个Document文档对象中中可以存储多个不同的字段,每一个字段都是一个Field类的对象。
2.一个Document文档对象中的字段其类型是不确定的,因此Field类就提供了各种不同的子类,来对应这些不同类型的字段。
3.此Field类的子类如下:
4.Field类的每个子类有不同的特性:
1.创建索引:
DoubleField、FloatField、IntField、LongField、StringField、TextField这些子类都一定会被创建索引。
2.创建索引,并且会自动分词:
TextField:既会创建索引,又会自动分词。
3.创建索引,但并不会分词:
StringField:会创建索引,但是不会分词。
如果不分词,会造成整个字段值作为整体一个词条,除非用户在搜索时完全匹配整个字段值,否则搜索不到。
4.自动会存储,但是一定不会创建索引,也不会分词:
StoredField:可以创建各种数据类型的字段
5.封装在Document文档对象中的字段数据作为文档内容数据存储到本地文件中:
要通过构造函数中的参数Store来指定:Store.YES/Store.NO
Store.YES:代表存储
Store.NO:代表不存储
1.如何确定一个字段是否需要存储?
如果一个字段要显示到最终的结果中展示给用户看,那么一定要存储,否则就不存储。
比如一个商品描述很长,在查询的时候又没必要显示到结果中展示给用户看的话,可以不存储。
2.Store.YES/Store.NO:
Store.YES:代表存储
Store.NO:代表不存储
3.Store.NO的使用场景:
1.我的一个Document文档对象存储有三个字段名:商品id、商品title、商品描述desc
2.id 和 title都使用Store.YES都作为文档内容进行存储,而商品描述desc使用Store.NO不作为文档内容进行存储,
那么通过索引查询出来的数据只有id 和 title值,没有商品描述desc数据。因为商品描述desc数据可能很大,
没必要作为文档内容进行存储,而且我们搜索出来的结果根据需求也没有必要显示出来。所以商品描述desc数据并没有作为文档内容进行存储。
3.既然商品描述desc数据并不作为文档内容进行存储,那为什么还要在Document文档对象中存储商品描述desc数据信息呢?
虽然商品描述desc数据并不作为文档内容进行存储,但是只要是把商品描述desc数据往Document文档对象中进行存储时,
都会自动给 商品描述desc数据进行分词并建立索引。
好处结果便是:在搜索时,通过搜索商品信息的关键字便可根据分词索引查找到对应的Document文档对象中存储的其他数据,
包括查询出商品id、商品title信息数据,那么接着便可以根据商品id到MySQL数据库中查找出其他相应的商品信息。
4.如何确定一个字段是否需要创建索引?
如果要根据这个字段进行搜索,那么这个字段就必须创建索引。
5.如何确定一个字段是否需要分词?
前提是这个字段首先要创建索引。然后如果这个字段的值是不可分割的,那么就不需要分词。例如:ID
Directory(目录类)
1.FSDirectory:文件系统目录,会把索引库保存到本地磁盘。
特点:速度略慢,但是比较安全
2.RAMDirectory:内存目录,会把索引库保存在内存。
特点:速度快,但是不安全
IndexWriterConfig(索引写入器配置类)
1.设置配置信息:Lucene的版本和分词器类型
2.设置OpenMode参数
1.OpenMode.APPEND:默认参数,一般场景都是索引追加。
索引追加:
1.当新的分词要存进索引库中时,如果索引库中已存在同名的分词时,则索引追加,在该已存在的分词对应的倒排列表中追加新的文档编号;
使用Lucene索引查看工具lukeall可以查看到 该分词所在的Freq列上的值会是追加自增
2.如果索引库中不存在同名的分词时,则索引创建,在索引库中直接添加该分词,并在新存入的分词对应的倒排列表中添加文档编号。
2.OpenMode.CREATE:
索引覆盖/索引创建:
1.当新的分词要存进索引库中时,如果索引库中已存在同名的分词时,则索引覆盖,把该已存在的分词对应的倒排列表中所有的文档编号给删除掉后,保存新的文档编号。
使用Lucene索引查看工具lukeall可以查看到 该分词所在的Freq列上的旧值会被覆盖为新值。
2.如果索引库中不存在同名的分词时,则索引创建,在索引库中直接添加该分词,并在新存入的分词对应的倒排列表中添加文档编号。
OpenMode打开模式:OpenMode枚举类。1.OpenMode.CREATE:索引覆盖。 2.OpenMode.APPEND:索引追加
IndexWriter(索引写入器类)
1.IndexWriter(索引写入器类):
索引写出工具,作用就是 实现对索引的增(创建索引)、删(删除索引)、改(修改索引)
2.索引写入工具IndexWriter的使用注意:
1.IndexWriter也是全局唯一(单例模式),则在使用IndexWriter时无需关闭
2.问题:若IndexWriter使用完毕之后不关闭,那么会导致新建的索引没有生效?
3.解决办法:使用indexWriter.commit()方法提交对索引的更改!
3.示例代码:
// 创建Directory目录对象,指定索引库的存放位置,有以下两种存放方式:
// 1.FSDirectory:文件系统目录,会把索引库保存到本地磁盘。特点:速度略慢,但是比较安全
// 2.RAMDirectory:内存目录,会把索引库保存在内存。 特点:速度快,但是不安全
Directory directory = FSDirectory.open(new File("F:\\LucenenData\\indexDir"));
// 创建分词器对象
// 默认分词器new StandardAnalyzer():默认对英文进行分词,不会对中文进行中文分词,中文只能以逐个中文字符作为分词
Analyzer analyzer = new StandardAnalyzer();
// 创建索引写入器配置对象:
// 第一个参数:Lucene版本号 VerSion.LATEST
// 第一个参数:分词器对象
IndexWriterConfig conf = new IndexWriterConfig(Version.LATEST, analyzer);
// 创建索引写入器:传入Directory目录对象 和 索引写入器配置对象
IndexWriter indexWriter = new IndexWriter(directory, conf);
// 向索引库写入文档对象
indexWriter.addDocument(Document文档对象);
// 提交
indexWriter.commit();
// 关闭
indexWriter.close();
DirectoryReader/IndexReader(索引读取工具类)
1.DirectoryReader/IndexReader(索引读取工具类)的开启和关闭:
1.开启读取(打开硬盘索引):
//第一种用法:索引读取工具DirectoryReader(DirectoryReader的父类是IndexReader)
DirectoryReader reader = DirectoryReader.open(directory);
//第二种用法:索引读取工具IndexReader(IndexReader的其中一个子类是DirectoryReader)
IndexReader reader = DirectoryReader.open(directory);
//第一种关闭用法:使用索引搜索工具IndexSearcher获取索引读取工具IndexReader对象,并调用close()方法
IndexReader reader = IndexSearcher实例对象.getIndexReader()
reader.close()
2.关闭读取(关闭硬盘索引):
//class IndexReader implements Closeable:IndexReader类中带有close()方法,那么子类DirectoryReader都继承有close()方法
//第二种关闭用法:IndexReader类或DirectoryReader类的实例对象都可以调用close()
reader.close();
2.IndexReader的使用注意:
1.IndexReader的实例化过程是一个非常耗时的过程。
在使用Lucene时,当索引的数据很大时,建议不要频繁去打开关闭硬盘索引(),这个加载过程就比较耗时了。
2.在搜索索引时,创建IndexSearcher对象即可。一般全局唯一(单例模式),无需关闭。
3.创建 索引读取工具IndexReader:
1.问题:若IndexReader全局唯一,则当我们在搜索索引的过程中,索引发生改变(比如:用IndexWriter删除某个索引),
但是搜索到的索引并没有改变?
2.原因:因为IndexReader全局唯一,它始终读取的是最开始创建的索引。
3.解决办法:
1.使用 IndexReader ir = IndexReader.openIfChanged(IndexReader oldReader),用来读取最新的索引。
2.IndexReader.openIfChanged()方法:若IndexReader打开后,如果索引发生了变化的话,
那么使用openIfChanged方法则会返回一个新的IndexReader,然后把旧的IndexReader对象关闭(旧的IndexReader对象名.close());
如果索引没有发生变化的话,则返回null。
3.IndexReader 新的IndexReader对象 = IndexReader.openIfChanged(旧的IndexReader对象)
3.示例代码1:
//创建 索引读取工具IndexReader
public IndexSearcher createIndexSearcher()
{
try
{
if (indexReader == null)
{
indexReader = IndexReader.open(directory);
}
else
{
//1.使用 IndexReader ir = IndexReader.openIfChanged(IndexReader oldReader),用来读取最新的索引。
//2.IndexReader.openIfChanged()方法:若IndexReader打开后,如果索引发生了变化的话,
// 那么使用openIfChanged方法则会返回一个新的IndexReader,然后把旧的IndexReader对象close()关闭;
// 如果索引没有发生变化的话,则返回null。
//3.IndexReader 新的IndexReader对象 = IndexReader.openIfChanged(旧的IndexReader对象)
IndexReader ir = IndexReader.openIfChanged(indexReader);
if (ir != null)
{
//将旧的IndexReader对象关闭
indexReader.close();
indexReader = ir;
}
}
}
catch (IOException e)
{
e.printStackTrace();
}
return new IndexSearcher(indexReader);
}
4.示例代码2:
//第一种查询解析器:查询单字段的查询解析器
//查询解析器对象(构造参数:1.搜索的目标字段名 2.使用何种分词器对搜索的参数进行分析)
//此处使用中文分词器IKAnalyzer 对"content"字段数据进行搜索
QueryParser queryParser = new QueryParser("content", new IKAnalyzer());
//第二种查询解析器:查询多字段的查询解析器。会把"要搜索的词句中的"每个分词赋值给多个"搜索的目标字段名"进行搜索
//MultiFieldQueryParser queryParser = new MultiFieldQueryParser(new String[]{"id","content"}, new IKAnalyzer());
//查询对象,包含要查询的关键词信息。首先会先对"要搜索的词句"进行分词,然后再把每个分词赋值给单个/多个"搜索的目标字段名"
Query query = queryParser.parse("要搜索的词句");
// 索引库目录文件夹:"c:\\index"
Directory directory = FSDirectory.open(new File("c:\\index"));
//第一种用法:索引读取工具DirectoryReader(DirectoryReader的父类是IndexReader)
DirectoryReader reader = DirectoryReader.open(directory);
//第二种用法:索引读取工具IndexReader(IndexReader的其中一个子类是DirectoryReader)
//IndexReader reader = DirectoryReader.open(directory);
// 索引搜索工具IndexSearcher
IndexSearcher indexSearcher = new IndexSearcher(reader);
//使用索引搜索工具IndexSearcher 根据Query查询对象查询获取匹配度最高的前面n条文档内容信息
//TopDocs topDocs = indexSearcher.search(Query查询对象, 获得匹配度最高的前面n条文档内容信息)
TopDocs topDocs = indexSearcher.search(query, 10)
//返回值TopDocs查询结果对象中封装了两部分数据:
// 1.TopDocs中的第一部分数据:
// int totalHits = topDocs.totalHits
// 查询到的数据库中匹配到的的总条数
// 2.TopDocs中的第二部分数据:
// ScoreDoc[] scoreDocs = topDocs.scoreDocs
// ScoreDoc数组中封装了匹配度最高的前面n条文档内容信息。
//totalHits:查询获取出来的到的总条数
int totalHits = topDocs.totalHits;
//ScoreDoc[]数组中的每个ScoreDoc对象 封装了两部分数据:
// 1.ScoreDoc对象中第一部分数据:float score = scoreDoc.score:文档得分
// 文档得分越高表示匹配度越高,文档得分越低表示匹配度越低。
// 2.ScoreDoc对象中第二部分数据:int docId = scoreDoc.doc:文档编号
ScoreDoc[] scoreDocs = topDocs.scoreDocs;
for (ScoreDoc scoreDoc : scoreDocs)
{
//ScoreDoc对象中第一部分数据:文档得分(ScoreDoc对象名.score)
float score = scoreDoc.score;
//ScoreDoc对象中第二部分数据:文档编号(ScoreDoc对象名.doc)
int docId = scoreDoc.doc;
//根据文档编号获取到Document文档对象:使用索引搜索工具IndexSearcher根据文档编号 找到对应的Document文档对象
Document document = indexSearcher.doc(docId);
//Document文档对象名.get(“字段名”):获取字段值
System.out.println(document.get("id"));
System.out.println(document.get("desc"));
System.out.println(document.get("title"));
}
//第一种关闭用法:使用索引搜索工具IndexSearcher获取索引读取工具IndexReader对象,并调用close()方法
IndexReader reader = indexSearcher.getIndexReader()
reader.close()
//class IndexReader implements Closeable:IndexReader类中带有close()方法,那么子类DirectoryReader都继承有close()方法
//第二种关闭用法:IndexReader类或DirectoryReader类的实例对象都可以调用close()
reader.close();
Analyzer分词器:IK分词器(中文分词器)
1.第一步:pom.xml中引入IK分词器(中文分词器)
<!-- 引入IK分词器 -->
<dependency>
<groupId>com.janeluo</groupId>
<artifactId>ikanalyzer</artifactId>
<version>2012_u6</version>
</dependency>
2.第二步:/resources/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>
3.第三步:/resources/ext.dic文件
一行写一个分词,如以下写法:
IK分词器(中文分词器):
1.引入IK分词器的依赖:
<!-- 引入IK分词器 -->
<dependency>
<groupId>com.janeluo</groupId>
<artifactId>ikanalyzer</artifactId>
<version>2012_u6</version>
</dependency>
2.中文分词器(IK分词器)
Analyzer analyzer = new IKAnalyzer();
3.自定义词库
1.扩展词典(新创建词功能): 有些词IK分词器不识别 例如:“传智播客”,“碉堡了”
2.停用词典(停用某些词功能): 有些词不需要建立索引 例如:“哦”,“啊”,“的”
3.IK分词器的词库有限,新增加的词条可以通过配置文件添加到IK的词库中,也可以把一些不用的词条去除:
4.扩展词典:用来引入一些自定义的新词
5.停止词典:用来停用一些不必要的词条
6.结果:分词中,加入了我们新的词,被停用的词语没有被分词:
@Test
public void indexCreate1() throws IOException
{
// 索引库位置
Directory directory = FSDirectory.open(new File("F:\\LucenenData\\indexDir"));
// 创建分词器对象
// 默认分词器new StandardAnalyzer():默认对英文进行分词,不会对中文进行中文分词,中文只能以逐个中文字符作为分词
// Analyzer analyzer = new StandardAnalyzer();
//中文分词器(IK分词器)
Analyzer analyzer = new IKAnalyzer();
// 创建索引写入器配置对象,第一个参数Lucene版本,第二个参数分词器
//Lucene最新版本:VerSion.LATEST。默认分词器:new StandardAnalyzer()
IndexWriterConfig conf = new IndexWriterConfig(Version.LATEST, analyzer);
// 创建索引写入器对象:传入Directory目录对象 和 索引写入器配置对象
IndexWriter indexWriter = new IndexWriter(directory, conf);
// 向索引库写入Document文档对象
// 准备数据 数据封装在Document对象中
Document doc = new Document();
LongField id = new LongField("id", 2L, Store.YES);
doc.add(id);
TextField title = new TextField("title", "谷歌地图之父跳槽facebook", Store.YES);
doc.add(title);
DoubleField price = new DoubleField("price", 78.26, Store.YES);
doc.add(price);
indexWriter.addDocument(doc);// 向索引库写入文档对象
indexWriter.commit();// 提交
indexWriter.close();// 关闭
}
@Test
public void indexCreateAPI() throws IOException {
// 准备数据 数据封装在Document对象中
Document doc = new Document();
LongField id = new LongField("id", 1L, Store.NO);
doc.add(id);
StringField content = new StringField("content", "程序员", Store.NO);
doc.add(content);
// 索引库位置
Directory directory = FSDirectory.open(new File("F:\\LucenenData\\indexDir"));
// 创建分词器对象
// 默认分词器new StandardAnalyzer():默认对英文进行分词,不会对中文进行中文分词,中文只能以逐个中文字符作为分词
// Analyzer analyzer = new StandardAnalyzer();
//中文分词器(IK分词器)
Analyzer analyzer = new IKAnalyzer();
// 创建索引写入器配置对象,第一个参数版本VerSion.LATEST,第二个参数分词器
//Lucene最新版本:VerSion.LATEST。默认分词器:new StandardAnalyzer()
IndexWriterConfig conf = new IndexWriterConfig(Version.LATEST,analyzer );
// 创建索引写入器对象
IndexWriter indexWriter = new IndexWriter(directory, conf);
// 向索引库写入文档对象
indexWriter.addDocument(doc);// 向索引库写入文档对象
indexWriter.commit();// 提交
indexWriter.close();// 关闭
}
@Test
public void testCreater2() throws IOException {
// 创建文档对象集合
List<Document> docs = new ArrayList<>();
// 创建文档对象
Document document1 = new Document();
document1.add(new StringField("id", "1", Store.YES));
document1.add(new TextField("title", "谷歌地图之父跳槽FaceBook", Store.YES));
docs.add(document1);
// 创建Document文档对象
Document document2 = new Document();
document2.add(new StringField("id", "2", Store.YES));
document2.add(new TextField("title", "谷歌地图之父加盟FaceBook", Store.YES));
docs.add(document2);
// 创建文档对象
Document document3 = new Document();
document3.add(new StringField("id", "3", Store.YES));
document3.add(new TextField("title", "谷歌地图创始人拉斯离开谷歌加盟Facebook", Store.YES));
docs.add(document3);
// 创建文档对象
Document document4 = new Document();
document4.add(new StringField("id", "4", Store.YES));
document4.add(new TextField("title", "谷歌地图之父跳槽Facebook与Wave项目取消有关", Store.YES));
docs.add(document4);
// 创建文档对象
Document document5 = new Document();
document5.add(new StringField("id", "5", Store.YES));
document5.add(new TextField("title", "谷歌地图之父拉斯加盟社交网站Facebook", Store.YES));
docs.add(document5);
// 索引库对象
Directory directory = FSDirectory.open(new File("F:\\LucenenData\\indexDir"));
// 创建分词器对象
// 默认分词器new StandardAnalyzer():默认对英文进行分词,不会对中文进行中文分词,中文只能以逐个中文字符作为分词
// Analyzer analyzer = new StandardAnalyzer();
//中文分词器(IK分词器)
Analyzer analyzer = new IKAnalyzer();
// 创建索引写入器配置对象,第一个参数版本VerSion.LATEST,第二个参数分词器
//Lucene最新版本:VerSion.LATEST。默认分词器:new StandardAnalyzer()
IndexWriterConfig conf = new IndexWriterConfig(Version.LATEST, analyzer);
//1.OpenMode.APPEND:默认参数,一般场景都是索引追加。
// 索引追加:
// 1.当新的分词要存进索引库中时,如果索引库中已存在同名的分词时,则索引追加,在该已存在的分词对应的倒排列表中追加新的文档编号;
// 使用Lucene索引查看工具lukeall可以查看到 该分词所在的Freq列上的值会是追加自增
// 2.如果索引库中不存在同名的分词时,则索引创建,在索引库中直接添加该分词,并在新存入的分词对应的倒排列表中添加文档编号。
//2.OpenMode.CREATE:
// 索引覆盖/索引创建:
// 1.当新的分词要存进索引库中时,如果索引库中已存在同名的分词时,则索引覆盖,把该已存在的分词对应的倒排列表中所有的文档编号给删除掉后,
// 保存新的文档编号。
// 使用Lucene索引查看工具lukeall可以查看到 该分词所在的Freq列上的旧值会被覆盖为新值。
// 2.如果索引库中不存在同名的分词时,则索引创建,在索引库中直接添加该分词,并在新存入的分词对应的倒排列表中添加文档编号。
// OpenMode打开模式:OpenMode枚举类。1.OpenMode.CREATE:索引覆盖。 2.OpenMode.APPEND:索引追加
conf.setOpenMode(OpenMode.CREATE);
// 创建索引写入器对象:传入Directory目录对象 和 索引写入器配置对象
IndexWriter indexWriter = new IndexWriter(directory, conf);
// 执行写入操作
indexWriter.addDocuments(docs);
// 提交
indexWriter.commit();
// 关闭
indexWriter.close();
}
@Test
public void indexCreateIK() throws IOException {
// 准备数据 数据封装在Document对象中
Document doc = new Document();
LongField id = new LongField("id", 5L, Store.NO);
doc.add(id);
TextField title = new TextField("title", "华为手机", Store.YES);
doc.add(title);
TextField desc = new TextField("desc", "充电一小时,通话一分钟", Store.YES);
doc.add(desc);
DoubleField price = new DoubleField("price", 9999, Store.YES);
doc.add(price);
// 索引库位置
Directory directory = FSDirectory.open(new File("F:\\LucenenData\\indexDir"));
// 创建索引写入器配置对象,第一个参数版本VerSion.LATEST,第二个参数分词器
IndexWriterConfig conf = new IndexWriterConfig(Version.LATEST, new IKAnalyzer());
// 创建索引写入器对象
IndexWriter indexWriter = new IndexWriter(directory, conf);
// 向索引库写入文档对象
indexWriter.addDocument(doc);
indexWriter.commit();
indexWriter.close();
}
@Test
public void indexCreate() throws IOException {
// 准备数据 数据封装在Document对象中
Document doc = new Document();
//数据有不同字段,字段类型也不同
//LongField IntField DoubleField....
// StringField(索引但是默认不分词,数据被当成一个词条), Term(词条)
// TextField(索引并且根据索引写入器中定义的分词器来进行分词,数据被当做多个词条)
LongField id = new LongField("id", 1L, Store.YES);
doc.add(id);
// new TextField("title", "谷歌地图之父跳槽FaceBook", Store.YES);
// TextField:创建索引并提供分词,StringField创建索引但不分词
TextField content = new TextField("content", "传智播客是个好地方,上海校区更加好", Store.YES);
doc.add(content);
// 创建目录对象,指定索引库的存放位置;FSDirectory文件系统;RAMDirectory内存
Directory directory = FSDirectory.open(new File("F:\\LucenenData\\indexDir"));
// 创建分词器对象
// StandardAnalyzer analyzer = new StandardAnalyzer();
Analyzer analyzer = new IKAnalyzer();
// 创建索引写入器配置对象,第一个参数版本VerSion.LATEST,第一个参数分词器
IndexWriterConfig conf = new IndexWriterConfig(Version.LATEST, analyzer);
// 创建索引写入器
IndexWriter indexWriter = new IndexWriter(directory , conf);
// 向索引库写入文档对象
// indexWriter.addDocument(doc);
List<Document> docs = new ArrayList<Document>();
docs.add(doc);
//批量添加索引
indexWriter.addDocuments(docs);
// 提交
indexWriter.commit();
// 关闭
indexWriter.close();
}
根据索引查询数据
DirectoryReader/IndexReader(索引读取工具类)
1.DirectoryReader/IndexReader(索引读取工具类)的开启和关闭:
1.开启读取(打开硬盘索引):
//第一种用法:索引读取工具DirectoryReader(DirectoryReader的父类是IndexReader)
DirectoryReader reader = DirectoryReader.open(directory);
//第二种用法:索引读取工具IndexReader(IndexReader的其中一个子类是DirectoryReader)
IndexReader reader = DirectoryReader.open(directory);
//第一种关闭用法:使用索引搜索工具IndexSearcher获取索引读取工具IndexReader对象,并调用close()方法
IndexReader reader = IndexSearcher实例对象.getIndexReader()
reader.close()
2.关闭读取(关闭硬盘索引):
//class IndexReader implements Closeable:IndexReader类中带有close()方法,那么子类DirectoryReader都继承有close()方法
//第二种关闭用法:IndexReader类或DirectoryReader类的实例对象都可以调用close()
reader.close();
2.IndexReader的使用注意:
1.IndexReader的实例化过程是一个非常耗时的过程。
在使用Lucene时,当索引的数据很大时,建议不要频繁去打开关闭硬盘索引(),这个加载过程就比较耗时了。
2.在搜索索引时,创建IndexSearcher对象即可。一般全局唯一,无需关闭。
3.创建 索引读取工具IndexReader:
1.问题:若IndexReader全局唯一,则当我们在搜索索引的过程中,索引发生改变(比如:用IndexWriter删除某个索引),
但是搜索到的索引并没有改变?
2.原因:因为IndexReader全局唯一,它始终读取的是最开始创建的索引。
3.解决办法:
1.使用 IndexReader ir = IndexReader.openIfChanged(IndexReader oldReader),用来读取最新的索引。
2.IndexReader.openIfChanged()方法:若IndexReader打开后,如果索引发生了变化的话,
那么使用openIfChanged方法则会返回一个新的IndexReader,然后把旧的IndexReader对象关闭(旧的IndexReader对象名.close());
如果索引没有发生变化的话,则返回null。
3.IndexReader 新的IndexReader对象 = IndexReader.openIfChanged(旧的IndexReader对象)
3.示例代码1:
//创建 索引读取工具IndexReader
public IndexSearcher createIndexSearcher()
{
try
{
if (indexReader == null)
{
indexReader = IndexReader.open(directory);
}
else
{
//1.使用 IndexReader ir = IndexReader.openIfChanged(IndexReader oldReader),用来读取最新的索引。
//2.IndexReader.openIfChanged()方法:若IndexReader打开后,如果索引发生了变化的话,
// 那么使用openIfChanged方法则会返回一个新的IndexReader,然后把旧的IndexReader对象close()关闭;
// 如果索引没有发生变化的话,则返回null。
//3.IndexReader 新的IndexReader对象 = IndexReader.openIfChanged(旧的IndexReader对象)
IndexReader ir = IndexReader.openIfChanged(indexReader);
if (ir != null)
{
//将旧的IndexReader对象关闭
indexReader.close();
indexReader = ir;
}
}
}
catch (IOException e)
{
e.printStackTrace();
}
return new IndexSearcher(indexReader);
}
4.示例代码2:
//第一种查询解析器:查询单字段的查询解析器
//查询解析器对象(构造参数:1.搜索的目标字段名 2.使用何种分词器对搜索的参数进行分析)
//此处使用中文分词器IKAnalyzer 对"content"字段数据进行搜索
QueryParser queryParser = new QueryParser("content", new IKAnalyzer());
//第二种查询解析器:查询多字段的查询解析器。会把"要搜索的词句中的"每个分词赋值给多个"搜索的目标字段名"进行搜索
//MultiFieldQueryParser queryParser = new MultiFieldQueryParser(new String[]{"id","content"}, new IKAnalyzer());
//查询对象,包含要查询的关键词信息。首先会先对"要搜索的词句"进行分词,然后再把每个分词赋值给单个/多个"搜索的目标字段名"
Query query = queryParser.parse("要搜索的词句");
// 索引库目录文件夹:"c:\\index"
Directory directory = FSDirectory.open(new File("c:\\index"));
//第一种用法:索引读取工具DirectoryReader(DirectoryReader的父类是IndexReader)
DirectoryReader reader = DirectoryReader.open(directory);
//第二种用法:索引读取工具IndexReader(IndexReader的其中一个子类是DirectoryReader)
//IndexReader reader = DirectoryReader.open(directory);
// 索引搜索工具IndexSearcher
IndexSearcher indexSearcher = new IndexSearcher(reader);
//使用索引搜索工具IndexSearcher 根据Query查询对象查询获取匹配度最高的前面n条文档内容信息
//TopDocs topDocs = indexSearcher.search(Query查询对象, 获得匹配度最高的前面n条文档内容信息)
TopDocs topDocs = indexSearcher.search(query, 10)
//返回值TopDocs查询结果对象中封装了两部分数据:
// 1.TopDocs中的第一部分数据:
// int totalHits = topDocs.totalHits
// 查询到的数据库中匹配到的的总条数
// 2.TopDocs中的第二部分数据:
// ScoreDoc[] scoreDocs = topDocs.scoreDocs
// ScoreDoc数组中封装了匹配度最高的前面n条文档内容信息。
//totalHits:查询获取出来的到的总条数
int totalHits = topDocs.totalHits;
//ScoreDoc[]数组中的每个ScoreDoc对象 封装了两部分数据:
// 1.ScoreDoc对象中第一部分数据:float score = scoreDoc.score:文档得分
// 文档得分越高表示匹配度越高,文档得分越低表示匹配度越低。
// 2.ScoreDoc对象中第二部分数据:int docId = scoreDoc.doc:文档编号
ScoreDoc[] scoreDocs = topDocs.scoreDocs;
for (ScoreDoc scoreDoc : scoreDocs)
{
//ScoreDoc对象中第一部分数据:文档得分(ScoreDoc对象名.score)
float score = scoreDoc.score;
//ScoreDoc对象中第二部分数据:文档编号(ScoreDoc对象名.doc)
int docId = scoreDoc.doc;
//根据文档编号获取到Document文档对象:使用索引搜索工具IndexSearcher根据文档编号 找到对应的Document文档对象
Document document = indexSearcher.doc(docId);
//Document文档对象名.get(“字段名”):获取字段值
System.out.println(document.get("id"));
System.out.println(document.get("desc"));
System.out.println(document.get("title"));
}
//第一种关闭用法:使用索引搜索工具IndexSearcher获取索引读取工具IndexReader对象,并调用close()方法
IndexReader reader = indexSearcher.getIndexReader()
reader.close()
//class IndexReader implements Closeable:IndexReader类中带有close()方法,那么子类DirectoryReader都继承有close()方法
//第二种关闭用法:IndexReader类或DirectoryReader类的实例对象都可以调用close()
reader.close();
1.查询解析器
1.QueryParser(单一字段的查询解析器)
1.第一步:先把parse(“要搜索的词句”)中传入的“要搜索的词句”进行分词。
2.第二步:把每个分好的词分别依次赋值给“title”字段名然后根据索引进行查询。
2.MultiFieldQueryParser(多字段的查询解析器)
1.第一步:先把parse(“要搜索的词句”)中传入的“要搜索的词句”进行分词
2.第二步:把每个分好的词分别依次赋值给指定的“id”、“title”进行查询,
即同一个分词会分别赋值给“id”和“title”进行查询。
3.第三步:查询出的数据为并集:把A集合 与 B集合 合并在一起组成的新集合。
//第一种查询解析器:查询单字段的查询解析器
//查询解析器对象(构造参数:1.搜索的目标字段名 2.使用何种分词器对搜索的参数进行分析)
//此处使用中文分词器IKAnalyzer 对"content"字段数据进行搜索
QueryParser queryParser = new QueryParser("content", new IKAnalyzer());
//第二种查询解析器:查询多字段的查询解析器。会把"要搜索的词句中的"每个分词赋值给多个"搜索的目标字段名"进行搜索
//MultiFieldQueryParser queryParser = new MultiFieldQueryParser(new String[]{"id","content"}, new IKAnalyzer());
//查询对象,包含要查询的关键词信息。首先会先对"要搜索的词句"进行分词,然后再把每个分词赋值给单个/多个"搜索的目标字段名"
Query query = queryParser.parse("要搜索的词句");
2.查询对象
1.Query(查询对象,包含要查询的关键词信息)
通过QueryParser(单一字段的查询解析器)解析关键字,得到查询对象。
1.第一步:先把parse(“要搜索的词句”)中传入的“要搜索的词句”进行分词。
2.第二步:把每个分好的词分别依次赋值给“title”字段名然后根据索引进行查询。
2.自定义查询对象(特殊查询)
1.我们可以通过Query的子类,直接创建查询对象,实现高级查询。
2.Query的子类如下:
3.举例部分的Query的子类:
1.TermQuery:直接对词条进行等值查询
2.WildcardQuery:不分词直接对词条进行通配符查询
“?”:匹配一个任意字符
“*”:匹配多个任意字符
3.FuzzyQuery:不分词直接对词条进行容错查询
4.NumericRangeQuery:数值范围查询
5.BooleanQuery:多个query组合到一起搜索
6.MatchAllDocsQuery:查询所有的文档
3.IndexSearcher(索引搜索对象,执行搜索功能)
1.IndexSearcher:快速搜索、排序等功能。IndexSearcher需要依赖IndexReader类。
// 索引库位置:"c:\\index"
Directory directory = FSDirectory.open(new File("c:\\index"));
// 索引读取工具
DirectoryReader reader = DirectoryReader.open(directory);
// 索引搜索工具
IndexSearcher indexSearcher = new IndexSearcher(reader);
2.查询后得到的结果,就是打分排序后的前N名结果。N可以通过第2个参数来指定:
4.TopDocs(查询结果对象)
1.通过索引搜索工具IndexSearcher可以搜索到结果:TopDocs对象
在TopDocs对象中,包含两部分信息:
int totalHits:查询到的数据库中匹配到的的总条数
ScoreDoc[] scoreDocs:把“指定要获取的匹配度最高的前面n条文档内容信息”封装到数组中
//使用索引搜索工具IndexSearcher 根据Query查询对象查询获取匹配度最高的前面n条文档内容信息
//TopDocs topDocs = indexSearcher.search(Query查询对象, 获得匹配度最高的前面n条文档内容信息)
TopDocs topDocs = indexSearcher.search(query, 10)
//返回值TopDocs查询结果对象中封装了两部分数据:
// 1.TopDocs中的第一部分数据:
// int totalHits = topDocs.totalHits
// 查询到的数据库中匹配到的的总条数
// 2.TopDocs中的第二部分数据:
// ScoreDoc[] scoreDocs = topDocs.scoreDocs
// ScoreDoc数组中封装了匹配度最高的前面n条文档内容信息。
//totalHits:查询获取出来的到的总条数
int totalHits = topDocs.totalHits;
//ScoreDoc[]数组中的每个ScoreDoc对象 封装了两部分数据:
// 1.ScoreDoc对象中第一部分数据:float score = scoreDoc.score:文档得分
// 文档得分越高表示匹配度越高,文档得分越低表示匹配度越低。
// 2.ScoreDoc对象中第二部分数据:int docId = scoreDoc.doc:文档编号
ScoreDoc[] scoreDocs = topDocs.scoreDocs;
5.ScoreDoc(得分文档对象)
ScoreDoc是得分文档对象,包含两部分数据:
int doc:文档的编号。拿到文档编号后,然后可以使用使用索引搜索工具IndexSearcher根据文档编号 找到对应的Document文档对象
float score:文档的得分(文档得分越高表示匹配度越高,文档得分越低表示匹配度越低)
//使用索引搜索工具IndexSearcher 根据Query查询对象查询获取匹配度最高的前面n条文档内容信息
//TopDocs topDocs = indexSearcher.search(Query查询对象, 获得匹配度最高的前面n条文档内容信息)
TopDocs topDocs = indexSearcher.search(queryParser.parse("要搜索的词句"), 10)
//返回值TopDocs查询结果对象中封装了两部分数据:
// 1.TopDocs中的第一部分数据:
// int totalHits = topDocs.totalHits
// 查询到的数据库中匹配到的的总条数
// 2.TopDocs中的第二部分数据:
// ScoreDoc[] scoreDocs = topDocs.scoreDocs
// ScoreDoc数组中封装了匹配度最高的前面n条文档内容信息。
//totalHits:查询获取出来的到的总条数
int totalHits = topDocs.totalHits;
//ScoreDoc[]数组中的每个ScoreDoc对象 封装了两部分数据:
// 1.ScoreDoc对象中第一部分数据:float score = scoreDoc.score:文档得分
// 文档得分越高表示匹配度越高,文档得分越低表示匹配度越低。
// 2.ScoreDoc对象中第二部分数据:int docId = scoreDoc.doc:文档编号
ScoreDoc[] scoreDocs = topDocs.scoreDocs;
for (ScoreDoc scoreDoc : scoreDocs)
{
//ScoreDoc对象中第一部分数据:文档得分(ScoreDoc对象名.score)
float score = scoreDoc.score;
//ScoreDoc对象中第二部分数据:文档编号(ScoreDoc对象名.doc)
int docId = scoreDoc.doc;
//根据文档编号获取到Document文档对象:使用索引搜索工具IndexSearcher根据文档编号 找到对应的Document文档对象
Document document = indexSearcher.doc(docId);
//Document文档对象名.get(“字段名”):获取字段值
System.out.println(document.get("id"));
System.out.println(document.get("desc"));
System.out.println(document.get("title"));
}
示例代码
// 第一种查询解析器:查询单字段的查询解析器
// 查询解析器对象(构造参数:1.搜索的目标字段名 2.使用何种分词器对搜索的参数进行分析)
@Test
public void searchIndex() throws Exception {
// 构建查询对象
QueryParser parser = new QueryParser("desc", new IKAnalyzer());
Query query = parser.parse("一分钟");
// 创建索引查询对象
DirectoryReader reader = DirectoryReader.open(FSDirectory.open(new File("F:\\LucenenData\\indexDir")));
IndexSearcher indexSearcher = new IndexSearcher(reader);
// 查询
TopDocs topDocs = indexSearcher.search(query, 4);
int totalHits = topDocs.totalHits;
System.out.println(totalHits);
ScoreDoc[] scoreDocs = topDocs.scoreDocs;
for (ScoreDoc scoreDoc : scoreDocs) {
int docId = scoreDoc.doc;
Document document = indexSearcher.doc(docId);
System.out.println(document.get("id"));
System.out.println(document.get("desc"));
System.out.println(document.get("title"));
System.out.println("---------------");
float score = scoreDoc.score;
System.out.println();
}
indexSearcher.getIndexReader().close();
}
// 第二种查询解析器:查询多字段的查询解析器。会把"要搜索的词句中的"每个分词赋值给多个"搜索的目标字段名"进行搜索
@Test
public void searchIndex2() throws Exception {
// 构建查询对象
MultiFieldQueryParser parser = new MultiFieldQueryParser(new String[] { "title", "desc" }, new IKAnalyzer());
Query query = parser.parse("小米 雷军");
// 创建索引查询对象
DirectoryReader reader = DirectoryReader.open(FSDirectory.open(new File("F:\\LucenenData\\indexDir")));
IndexSearcher indexSearcher = new IndexSearcher(reader);
// 查询
TopDocs topDocs = indexSearcher.search(query, 10);
ScoreDoc[] scoreDocs = topDocs.scoreDocs;
for (ScoreDoc scoreDoc : scoreDocs) {
int docId = scoreDoc.doc;
Document document = indexSearcher.doc(docId);
System.out.println(document.get("title"));
System.out.println(document.get("desc"));
}
indexSearcher.getIndexReader().close();
}
@Test
public void searchIndex() throws IOException, ParseException{
//查询解析器对象 构造参数:1.搜索的目标字段名称 2.使用何种分词器对搜索的参数进行分析
// QueryParser queryParser = new QueryParser("content", new IKAnalyzer());
//同时查询多字段的查询解析器
MultiFieldQueryParser parser = new MultiFieldQueryParser(new String[]{"id","content"}, new IKAnalyzer());
// 对搜索的参数进行解析 解析后得到Query对象
Query query = parser.parse("linux");
DirectoryReader reader = DirectoryReader.open(FSDirectory.open(new File("F:\\LucenenData\\indexDir")));
//创建索引查询对象
IndexSearcher indexSearcher = new IndexSearcher(reader);
//topDocs:排名前 n 的结果集
TopDocs topDocs = indexSearcher.search(query, Integer.MAX_VALUE);
//得分文档集合
ScoreDoc[] scoreDocs = topDocs.scoreDocs;
for(ScoreDoc sd : scoreDocs){
Integer docID = sd.doc;
Document document = indexSearcher.doc(docID);
System.out.println(sd.score);
System.out.println("搜索到的结果集id = " + document.get("id"));
System.out.println("搜索到的结果集title = " + document.get("title"));
System.out.println("搜索到的结果集content = " + document.get("content"));
}
indexSearcher.getIndexReader().close();
}
多域查询
查询所有的文档、TermQuery词条查询、WildcardQuery通配符查询、FuzzyQuery模糊查询/容错查询、NumericRangeQuery数值范围查询、BooleanQuery组合查询
1.抽取公用的搜索方法:
public void search(Query query) throws Exception
{
// 创建目录对象
Directory directory = FSDirectory.open(new File("C:\\tmp\\indexDir"));
// 索引的读取对象
IndexReader indexReader = DirectoryReader.open(directory);
// 索引的搜索工具
IndexSearcher searcher = new IndexSearcher(indexReader);
// search:1.参数1:query查询对象,2.参数2:查询的条数 3.返回的是指定查询条数的文档数据。
// TopDocs对象:包含所匹配到的文档的总条数,和包含指定查询条数的文档编号的数组
TopDocs topDocs = searcher.search(query, 10);
System.out.println("搜索的命中总条数:" + topDocs.totalHits);
// 获取得分文档的数组,得分文档包含文档编号以及得分
ScoreDoc[] scoreDocs = topDocs.scoreDocs;
for (ScoreDoc scoreDoc : scoreDocs)
{
System.out.println("文档编号" + scoreDoc.doc);
System.out.println("文档得分" + scoreDoc.score);
// 根据编号查询文档
Document document = indexReader.document(scoreDoc.doc);
System.out.println(document.get("id"));
System.out.println(document.get("title"));
}
}
2.MatchAllDocsQuery(查询所有的文档)
@Test
public void matchAllDocsQuery()
{
//查询所有的文档对象数据
Query query = new MatchAllDocsQuery();
baseSearch(query);
}
3.TermQuery(词条查询)
//TermQuery(词条查询)
//1.查询内容不会进行分词,查询内容直接就作为一个词条。查询条件必须是最小粒度不可再分割的内容
//2.查询内容作为一个整体,也即作为一个词条,进行等值匹配,即完全匹配,查询内容(词条)必须和库中的分词完全匹配。
//场景:不可分割的字段可以采用,比如id
//缺点:只能查询一个词,例如可以查询"谷歌",不能查询"谷歌地图"
@Test
public void testTermSearcher() throws IOException, ParseException
{
// 创建查询对象
Query query = new TermQuery(new Term("title", "谷歌"));
// 执行搜索操作
searcher(query);
}
//TermQuery(词条查询):直接对词条进行等值查询
@Test
public void termQuery() {
Term term = new Term("desc", "三星炸了");
Query query = new TermQuery(term);
baseSearch(query);
}
//TermQuery(词条查询):单个词条的搜索,输入的内容会被当做一个完整的词条,不会再对搜索参数进行分词
public void termQuery()
{
//Term term = new Term("查询的目标字段", "查询的参数");
Term term = new Term("content", "程序");
Query query = new TermQuery(term);
try {
baseSearch(query);
} catch (Exception e) {
e.printStackTrace();
}
}
4.WildcardQuery(通配符查询)
WildcardQuery(通配符查询):
1.“?”表示匹配一个中文字符
2.“*”表示匹配任意多个中文字符
@Test
public void testWildCardQuery() throws Exception
{
// 查询条件对象(通配符):
// ?:通配一个字符
// *:通配多个字符
Query query = new WildcardQuery(new Term("title", "*歌*"));
search(query);
}
//WildcardQuery(通配符查询):不分词直接对词条进行通配符查询
//?:匹配一个任意字符。 *:匹配多个任意字符
@Test
public void wildcardQuery()
{
Term term = new Term("desc", "电*");
Query query = new WildcardQuery(term);
baseSearch(query);
}
5.FuzzyQuery(模糊查询/容错查询)
1.不显式写出“FuzzyQuery(Term对象, 容错两次)”:默认允许容错两次,实际指的是最大编辑次数(纠错次数、补位次数、移动位置的次数)
2.显式写出“FuzzyQuery(Term对象, 容错两次)”:
比如库中的词条是Itcast,而我要搜索的内容是Itacts,那么通过移动字母位置的方式,a和c移位,t和s移位,一共两次移动位置,照样可以配制出库中的词条Itcast。
@Test
public void testFuzzyQuery() throws Exception
{
// 查询条件对象(模糊查询/容错查询)
// 参数:1.词条,查询字段及关键词,关键词允许写错;2.允许写错的最大编辑距离,并且不能大于2(0~2)。自动补齐或切换位置,至多两次机会
// 最大编辑距离:facebool-->facebook需要编辑的次数1次,包括大小写
Query query = new FuzzyQuery(new Term("title", "facebook"), 1);
search(query);
}
//FuzzyQuery(模糊查询/容错查询):不分词直接对词条进行容错查询
@Test
public void fuzzyQuery()
{
Term term = new Term("title", "iyhony");
Query query = new FuzzyQuery(term, 2);
baseSearch(query);
}
6.NumericRangeQuery(数值范围查询)
@Test
public void testNumericRangeQuery() throws Exception
{
// 查询条件对象(数值范围查询)
// 查询非String类型的数据或者说是一些继承Numeric类的对象的查询
// 1.字段;2.最小值;3.最大值;4.是否包含最小值自身;5.是否包含最大值自身;
// NumericRangeQuery.newLongRange("搜索的目标字段", 起始范围, 结束范围, 是否包含最小, 是否包含最大);
Query query = NumericRangeQuery.newLongRange("id", 2l, 4l, true, true);
search(query);
}
//NumericRangeQuery(数值范围查询)
@Test
public void numericRangeQuery()
{
// NumericRangeQuery.newLongRange("搜索的目标字段", 起始范围, 结束范围, 是否包含最小, 是否包含最大);
Query query = NumericRangeQuery.newDoubleRange("price", 50.88, 9999.00, true, true);
baseSearch(query);
}
7.BooleanQuery(组合查询)
// 交集(两个集合的相同部分):Occur.MUST + Occur.MUST
// 并集(两个集合合并为一个整体新的集合):Occur.SHOULD + Occur.SHOULD
// 差集(集合A 减 集合B;包含集合A 但不包含B 所构成的集合):Occur.MUST + Occur.MUST_NOT
// 非(不包含该集合):Occur.MUST_NOT
// 包含(包含该集合):Occur.MUST
@Test
public void testBooleanQuery() throws Exception
{
Query query1 = NumericRangeQuery.newLongRange("id", 2l, 4l, true, true);//NumericRangeQuery(数值范围查询)
Query query2 = NumericRangeQuery.newLongRange("id", 0l, 3l, true, true);//NumericRangeQuery(数值范围查询)
// boolean查询本身没有查询条件,它可以组合其他查询
BooleanQuery query = new BooleanQuery();
//求 query1 和 query2 之间的 并集
query.add(query1, Occur.SHOULD);
query.add(query2, Occur.SHOULD);
search(query);
}
@Test
public void booleanQueryTest()
{
BooleanQuery query = new BooleanQuery();
// 交集: A结果集must(必须) + B结果集must(必须) = A和B之间共同的部分
Term term1 = new Term("title", "高富帅");
TermQuery termQuery1 = new TermQuery(term1, 1);
query.add(termQuery1, Occur.MUST);
Term term2 = new Term("title", "白富美");
TermQuery termQuery2 = new TermQuery(term2);
query.add(termQuery2, Occur.MUST);
baseSearch(query);
//-------------------------------------------------------
// 并集: should + should = A和B的结果集合并
Term term1 = new Term("title", "高富帅");
TermQuery termQuery1 = new TermQuery(term1, 1);
query.add(termQuery1, Occur.SHOULD);
Term term2 = new Term("title", "白富美");
TermQuery termQuery2 = new TermQuery(term2); query.add(termQuery2, Occur.SHOULD);
baseSearch(query);
//-------------------------------------------------------
// 差集(集合A 减 集合B;包含集合A 但不包含B 所构成的集合):排除。MUST+MUST_NOT表示包含前者并且排除后者的集合。
Term term1 = new Term("title", "高富帅");
TermQuery termQuery1 = new TermQuery(term1, 1);
query.add(termQuery1, Occur.MUST);
Term term2 = new Term("title", "白富美");
TermQuery termQuery2 = new TermQuery(term2);
query.add(termQuery2, Occur.MUST_NOT);
baseSearch(query);
// MUST_NOT+MUST_NOT,什么都查询不出来(违规条件)
// SHOULD+MUST,只查MUST条件的查询结果,SHOULD条件失效(违规条件)
// SHOULD+MUST_NOT 与 MUST+MUST_NOT 等效
}
TermQuery(词条查询)
WildcardQuery(通配符查询)
模糊查询/容错查询
1.不写“FuzzyQuery(Term对象, 容错两次)”:默认允许容错两次,实际指的是最大编辑次数(纠错次数、补位次数、移动位置的次数)
2.显式写出“FuzzyQuery(Term对象, 容错两次)”:
比如库中的词条是Itcast,而我要搜索的内容是Itacts,那么通过移动字母位置的方式,a和c移位,t和s移位,一共两次移动位置,
照样可以配制出库中的词条Itcast。
NumericRangeQuery(数值范围查询)
BooleanQuery(组合查询)
修改索引、删除索引
更新索引
//本质先删除再添加。先删除所有满足条件的文档,再把新的文档对象Document存入进去。
//因此,更新索引通常要根据唯一字段。
@Test
public void testUpdate() throws IOException{
// 创建文档对象
Document document = new Document();
document.add(new StringField("id", "9", Store.YES));
document.add(new TextField("title", "谷歌地图之父跳槽FaceBook", Store.YES));
// 索引库对象
Directory directory = FSDirectory.open(new File("C:\\tmp\\index"));
// 索引写入器配置对象
IndexWriterConfig conf = new IndexWriterConfig(Version.LATEST, new IKAnalyzer());
// 索引写入器对象
IndexWriter indexWriter = new IndexWriter(directory, conf);
// 执行更新操作
indexWriter.updateDocument(new Term("id", "1"), document);
// 提交
indexWriter.commit();
// 关闭
indexWriter.close();
}
//修改索引的原理:先删除根据条件查询的所有的结果,然后再添加一个新的文档对象Document
@Test
public void updateIndex() throws IOException{
IndexWriterConfig conf = new IndexWriterConfig(Version.LATEST, new IKAnalyzer());
IndexWriter indexWriter = new IndexWriter(FSDirectory.open(new File(INDEX_PATH)), conf);
Term term = new Term("content", "传智播客");
Document doc = new Document();
LongField id = new LongField("id", 30L, Store.YES);
doc.add(id);
TextField title = new TextField("title", "谷歌地图之父跳槽FaceBook", Store.YES);
doc.add(title);
TextField content = new TextField("content", "程序员是未来的社会栋梁", Store.YES);
doc.add(content);
// 根据指定的词条进行搜索,所有与词条匹配的内容都会被指定的新文档对象Document所覆盖
indexWriter.updateDocument(term, doc);
indexWriter.commit();
indexWriter.close();
}
删除索引
@Test
public void testDelete() throws IOException {
// 创建目录对象
Directory directory = FSDirectory.open(new File("C:\\tmp\\indexDir"));
// 创建索引写入器配置对象
IndexWriterConfig conf = new IndexWriterConfig(Version.LATEST, new IKAnalyzer());
// 创建索引写入器对象
IndexWriter indexWriter = new IndexWriter(directory, conf);
// 执行删除操作(根据词条),要求id字段必须是字符串类型
// indexWriter.deleteDocuments(new Term("id", "5"));
// 根据数值范围删除多个文档对象
// indexWriter.deleteDocuments(NumericRangeQuery.newLongRange("id", 2l, 4l, true, false));
// 删除所有
indexWriter.deleteAll();
// 提交
indexWriter.commit();
// 关闭
indexWriter.close();
}
@Test
public void deleteIndex() throws IOException{
IndexWriterConfig conf = new IndexWriterConfig(Version.LATEST, new IKAnalyzer());
IndexWriter indexWriter = new IndexWriter(FSDirectory.open(new File(INDEX_PATH)), conf);
//根据指定的词条进行删除
indexWriter.deleteDocuments(new Term("content","linux"));
indexWriter.deleteAll();//删除所有
indexWriter.commit();
indexWriter.close();
}
Lucene的高级使用:高亮显示、排序、分页
高亮显示
1.高亮显示原理:
给所有关键字加上一个HTML标签。
给这个特殊的标签设置CSS样式。
2.pom.xml中配置依赖:
<!-- lucene的高亮显示 -->
<dependency>
<groupId>org.apache.lucene</groupId>
<artifactId>lucene-highlighter</artifactId>
<version>4.10.2</version>
</dependency>
3.示例代码1:
//高亮查询:高亮就是搜索的参数(关键词高亮显示)
@Test
public void searchhighlighter() throws IOException, ParseException, InvalidTokenOffsetsException{
//查询解析器对象 构造参数:1.搜索的目标字段名称 2.使用何种分词器对搜索的参数进行分析
//QueryParser queryParser = new QueryParser("content", new IKAnalyzer());
//同时查询多字段的查询解析器
MultiFieldQueryParser parser = new MultiFieldQueryParser(new String[]{"id","content"}, new IKAnalyzer());
// 对搜索的参数进行解析 解析后得到Query对象
Query query = parser.parse("传智播客在哪里?");
DirectoryReader reader = DirectoryReader.open(FSDirectory.open(new File(INDEX_PATH)));
//创建索引查询对象
IndexSearcher indexSearcher = new IndexSearcher(reader);
//第一个参数:高亮显示的文字的前缀。第二个参数:高亮显示的文字的后缀。
//<em color='red'>高亮显示的文字</em>
Formatter formatter = new SimpleHTMLFormatter("<em color='red'>", "</em>");
//把Query查询对象传入,最终目的用于高亮显示分词
Scorer fragmentScorer = new QueryScorer(query);
//创建高亮显示处理对象
Highlighter highlighter = new Highlighter(formatter, fragmentScorer);
//topDocs:排名前 n 的结果集
TopDocs topDocs = indexSearcher.search(query, Integer.MAX_VALUE);
//得分文档集合
ScoreDoc[] scoreDocs = topDocs.scoreDocs;
for(ScoreDoc sd : scoreDocs){
Integer docID = sd.doc;
Document document = indexSearcher.doc(docID);
System.out.println(sd.score);
System.out.println("搜索到的结果集id = " + document.get("id"));
System.out.println("搜索到的结果集title = " + document.get("title"));
String content = document.get("content");
//对结果集进行高亮处理:只会对content字段数据中的匹配到的分词进行高亮显示
String highlighterContent = highlighter.getBestFragment(new IKAnalyzer(), "content", content);
System.out.println("搜索到的结果集content = " + highlighterContent);
}
indexSearcher.getIndexReader().close();
}
4.示例代码2:
// 高亮显示
@Test
public void testHighlighter() throws Exception {
// 目录对象
Directory directory = FSDirectory.open(new File("indexDir"));
// 创建读取工具
IndexReader reader = DirectoryReader.open(directory);
// 创建搜索工具
IndexSearcher searcher = new IndexSearcher(reader);
QueryParser parser = new QueryParser("title", new IKAnalyzer());
Query query = parser.parse("谷歌地图");
// 格式化器
Formatter formatter = new SimpleHTMLFormatter("<em>", "</em>");
Scorer scorer = new QueryScorer(query);
// 准备高亮工具
Highlighter highlighter = new Highlighter(formatter, scorer);
// 搜索
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"));
String title = doc.get("title");
// 用高亮工具处理普通的查询结果,参数:分词器,要高亮的字段的名称,高亮字段的原始值
String hTitle = highlighter.getBestFragment(new IKAnalyzer(), "title", title);
System.out.println("title: " + hTitle);
// 获取文档的得分
System.out.println("得分:" + scoreDoc.score);
}
}
排序
1.示例代码1:
//对搜索的结果集进行排序
public void sortSearch() throws IOException, ParseException{
DirectoryReader reader = DirectoryReader.open(FSDirectory.open(new File(INDEX_PATH)));
//创建索引查询对象
IndexSearcher indexSearcher = new IndexSearcher(reader);
org.apache.lucene.queryparser.classic.QueryParser parser = new org.apache.lucene.queryparser.classic.QueryParser("content", new IKAnalyzer());
Query query = parser.parse("linux的优秀的操作系统");
//SortField:
// 第一个参数:指定排序字段
// 第二个参数:指定字段类型
// 第三个参数:指定使用的排序规则:默认为 false(升序)。可另外修改指定为 true(降序)
Sort sort = new Sort(new SortField("id", Type.LONG,true));
//topDocs:排名前 n 的结果集
TopDocs topDocs = indexSearcher.search(query, 30, sort);
//得分文档集合
ScoreDoc[] scoreDocs = topDocs.scoreDocs;
for(ScoreDoc sd : scoreDocs){
Integer docID = sd.doc;
Document document = indexSearcher.doc(docID);
System.out.println("文档的得分:"+sd.score);
System.out.println("搜索到的结果集id = " + document.get("id"));
System.out.println("搜索到的结果集title = " + document.get("title"));
System.out.println("搜索到的结果集content = " + document.get("content"));
}
indexSearcher.getIndexReader().close();
}
2.示例代码2:
// 排序
@Test
public void testSortQuery() throws Exception {
// 目录对象
Directory directory = FSDirectory.open(new File("indexDir"));
// 创建读取工具
IndexReader reader = DirectoryReader.open(directory);
// 创建搜索工具
IndexSearcher searcher = new IndexSearcher(reader);
QueryParser parser = new QueryParser("title", new IKAnalyzer());
Query query = parser.parse("谷歌地图");
// 创建排序对象,需要使用排序字段SortField。
//参数:字段的名称。字段的类型。默认false(升序),可使用true(降序)。
Sort sort = new Sort(new SortField("id", Type.LONG, true));
// 搜索
TopDocs topDocs = searcher.search(query, 10, sort);
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"));
}
}
分页
1.分页分两种:
1.物理分页:limit
2.逻辑分页:代码
2.示例代码1:
//分页查询,并根据id降序排列
@Test
public void pageSortSearch() throws IOException, ParseException{
int pageNum = 3;//页码
int pageSize = 10;//页面数据数量
int start = (pageNum -1) * pageSize;//每页的起始位置
int end = pageNum * pageSize;//每页的结束位置
DirectoryReader reader = DirectoryReader.open(FSDirectory.open(new File(INDEX_PATH)));
//创建索引查询对象
IndexSearcher indexSearcher = new IndexSearcher(reader);
QueryParser parser = new QueryParser("content", new IKAnalyzer());
Query query = parser.parse("linux的优秀的操作系统");
// 创建排序对象,需要使用排序字段SortField。
//参数:字段的名称。字段的类型。默认false(升序),可使用true(降序)。
Sort sort = new Sort(new SortField("id", Type.LONG,true));
//topDocs:获取匹配到的前end条匹配度最高的数据,查询0~end条。
TopDocs topDocs = indexSearcher.search(query, end, sort);
//得分文档集合
ScoreDoc[] scoreDocs = topDocs.scoreDocs;
//start:每页的起始位置
for(int i=start ; i<scoreDocs.length;i++)
{
ScoreDoc sd = scoreDocs[i];
Integer docID = sd.doc;
Document document = indexSearcher.doc(docID);
System.out.println("文档的得分:"+sd.score);
System.out.println("搜索到的结果集id = " + document.get("id"));
System.out.println("搜索到的结果集title = " + document.get("title"));
System.out.println("搜索到的结果集content = " + document.get("content"));
}
indexSearcher.getIndexReader().close();
}
3.示例代码2:
// 分页
@Test
public void testPageQuery() throws Exception {
// 实际上Lucene本身不支持分页。因此我们需要自己进行逻辑分页。我们要准备分页参数:
int pageSize = 2;// 每页条数
int pageNum = 3;// 当前页码
int start = (pageNum - 1) * pageSize;// 当前页的起始条数
int end = start + pageSize;// 当前页的结束条数(不能包含)
// 目录对象
Directory directory = FSDirectory.open(new File("indexDir"));
// 创建读取工具
IndexReader reader = DirectoryReader.open(directory);
// 创建搜索工具
IndexSearcher searcher = new IndexSearcher(reader);
QueryParser parser = new QueryParser("title", new IKAnalyzer());
Query query = parser.parse("谷歌地图");
// 创建排序对象,需要使用排序字段SortField。
//参数:字段的名称。字段的类型。默认false(升序),可使用true(降序)。
Sort sort = new Sort(new SortField("id", Type.LONG, false));
// 搜索数据,查询0~end条
TopDocs topDocs = searcher.search(query, end, sort);
System.out.println("本次搜索共" + topDocs.totalHits + "条数据");
ScoreDoc[] scoreDocs = topDocs.scoreDocs;
//start:每页的起始位置
for (int i = start; i < end; i++) {
ScoreDoc scoreDoc = scoreDocs[i];
// 获取文档编号
int docID = scoreDoc.doc;
Document doc = reader.document(docID);
System.out.println("id: " + doc.get("id"));
System.out.println("title: " + doc.get("title"));
}
}