Lucene全文检索技术
今日大纲
● 搜索的概念、搜索引擎原理、倒排索引
● 全文索引的概念
● 使用Lucene对索引进行CRUD操作
● Lucene常用API详解
● 分词器、高亮、分页、得分、排序
*****************************************************************************************************
1. 搜索的概念
1.1 什么是搜索
简单的说,搜索就是搜寻、查找,在IT行业中就是指用户输入关键字,通过相应的算法,查询并返回用户所需要的信息。
1.2 普通的数据库搜索
类似:select * from 表名 where 字段名 like '%关键字%'
例如:select * from article where content like '%here%'
结果: where here shere
1.3 新的业务需求
比如,用户在百度文本框中输入,"吃饭睡觉写程序",会出现的以下结果:
从结果可以看出,百度搜索具备以下明显特点:
1、即使在相关结果数量接近500万时,也能快速得出结果。
2、搜索的结果不仅仅局限于完整的"吃饭睡觉写程序"这一短语,而是将此短语拆分成,"写程序","吃饭","睡觉","程序"等关键字。
3、对拆分后的搜索关键字进行标红显示。
4、......
问题:上述功能,使用大家以前学过的数据库搜索能够方便实现吗?
1.4 普通的数据库搜索的缺陷
类似:select * from 表名 where 字段名 like '%关键字%'
例如:select * from article where content like '%here%'
结果: where here shere
1、因为没有通过高效的索引方式,所以查询的速度在大量数据的情况下是很慢。
2、搜索效果比较差,只能对用户输入的完整关键字首尾位进行模糊匹配。用户搜索的结果误多输入一个字符,可能就导致查询出的结果远离用户的预期。
*****************************************************************************************************
2. 搜索技术
2.1 搜索引擎的种类
搜索引擎按照功能通常分为垂直搜索和综合搜索。
1、垂直搜索是指专门针对某一类信息进行搜索。例如:会搜网主要做商务搜索的,并且提供商务信息。除此之外还有爱看图标网、职友集等。
2、搜索等。
*****************************************************************************************************
2.2 搜索引擎的原理
*****************************************************************************************************
2.3 倒排索引
倒排索引又叫反向索引(右下图)以字或词为关键字进行索引,表中关键字所对应的记录表项记录了出现这个字或词的所有文档,一个表项就是一个字表段,它记录该文档的ID和字符在该文档中出现的位置情况。
在实际的运用中,我们可以对数据库中原始的数据结构(左图),在业务空闲时事先根据左图内容,创建新的倒排索引结构的数据区域(右图)。
用户有查询需求时,先访问倒排索引数据区域(右图),得出文档id后,通过文档id即可快速,准确的通过左图找到具体的文档内容。
这一过程,可以通过我们自己写程序来实现,也可以借用已经抽象出来的通用开源技术来实现。
*****************************************************************************************************
3. Lucene 简介
3.1 什么是Lucene
Lucene是一套用于全文检索和搜寻的开源程序库,由Apache软件基金会支持和提供
Lucene提供了一个简单却强大的应用程序接口(API),能够做全文索引和搜寻,在Java开发环境里Lucene是一个成熟的免费开放源代码工具
Lucene并不是现成的搜索引擎产品,但可以用来制作搜索引擎产品
官网:http://lucene.apache.org/
3.2 什么是全文索引
计算机索引程序通过扫描文章中的每一个词,对每一个词建立一个索引,指明该词在文章中出现的次数和位置,当用户查询时,检索程序就根据事先建立的索引进行查找,并将查找的结果反馈给用户的检索方式
总结:对文档(数据)中每一个词都做索引。
3.3 Lucene下载
目前最新的版本是5.x系列,但是大多数企业中依旧使用4.x版本,比较稳定。本次课程我们使用4.10.2版本,该下载文件位于课前资料中。
3.4 Lucene和Solr关系
Lucene:底层的API,工具包
Solr:基于Lucene开发的企业级的搜索引擎产品
3.5 Lucene目录结构
*****************************************************************************************************
4. 创建索引
4.1 准备工作
4.1.1 建立JAVA项目
简要流程:菜单选择file -> new -> project -> java project -> next -> 输入工程名称 -> next -> create new source folder -> 输入folder name 为 lib -> finish -> libraries -> Add libraries -> Junit -> 选择Junit libray version :Junit 4 -> finish
4.1.2 导入Lucene相关jar包
基础包: lucene-core-4.x.jar
查询包: lucene-queryparser-4.x.jar
分词器: lucene-analyzers-common-4.10.2.jar
导入步骤:
1、将jar包复制到项目中lib目录下。
2、选中jar包点击右键 Build path ==> add to Build path。
****************************************************************************************************
4.2 创建索引代码实现
创建索引其实就是从下图左边的原始数据,进行分词并提取出分词相关的索引信息,以右图倒排索引的方式,存放到某一位置。
创建索引代码流程图:
1、新建package:cn.itcast.lucene 包。
2、在cn.itcast.lucene包下新建类。
3、编写方法createIndex如下:
/**
* 创建索引
*
* @throws IOException
*/
@Test
public void createIndex() throws IOException {
// 创建文档对象(document),可以对应着数据库中某一条记录
Document document = new Document();
// 为文档对象添加一条记录数据,Store.YES表示存储原始数据内容(查询时可以显示出原始内容)
document.add(new StringField("id", "1", Store.YES));
document.add(new TextField("content", "谷歌地图之父跳槽FaceBook", Store.YES));
// 定义目录对象(directory)用来指定索引数据所存在的文件目录
Directory directory = FSDirectory.open(new File("D:\\indexDir"));
// 创建分词器对象,这里我们使用标准分词器
Analyzer analyzer = new StandardAnalyzer();
// 创建索引写入器配置对象(IndexWriterConfig):用来指定lucene版本,使用何种分词器
IndexWriterConfig indexWriterConfig = new IndexWriterConfig(
Version.LATEST, analyzer);
// 创建索引写入器对象(IndexWriter),并指定目录对象和索引写入器配置对象
IndexWriter indexWriter = new IndexWriter(directory, indexWriterConfig);
// 通过索引写入器对象(IndexWriter)添加文档对象
indexWriter.addDocument(document);
// 通过索引写入器对象(IndexWriter)提交写入信息
indexWriter.commit();
// 关闭索引写入器对象(IndexWriter)
indexWriter.close();
}
运行后产生的结果:
*****************************************************************************************************
4.3 创建索引总结
创建索引的基本类:
1、文档对象(Document):可以把一个 Document 对象想象成数据库中的一条记录。例如原始数据(左图)中的一条记录。
2、字段类(Field):就是一条记录中的一个字段。例如原始数据(左图)一条记录中的文档编号,或者文档内容。
3、目录类(Directory):代表了 Lucene 索引库的存储位置。例如倒排索引(右图)所将要存放的位置。
4、分词器类(Analyzer):提供分词算法,针对不同的语言和应用需要选择适合的 Analyzer。
5、索引写入器配置对象(IndexWriterConfig):加载lucene版本配置和分词配置。
6、索引写入器类(IndexWriter):操作工具,把一个个的 Document 对象加到索引库中来,或进行删改。
*****************************************************************************************************
4.4 使用lukeall工具,查看索引内部结构
4.4.1 运行luke工具命令:
4.4.2 Luke工具使用介绍:
*****************************************************************************************************
5. 创建索引核心API详解
5.1 Directory(目录类)
Directory 索引路径,常用的两种:文件和内存。内存路径查询速度更快,但无法永久保存。
// FSDirectory 文件磁盘路径
FSDirectory directory = FSDirectory.open(new File("D:\\indexDir"));
// RAMDirectory 内存路径
RAMDirectory ramDirectory = new RAMDirectory(directory, new IOContext());
5.2 Analyzer(分词器类)
分词器的作用:输入的搜索关键字或存储的文档数据做分词。
// 创建分词器对象,这里我们使用标准分词器
Analyzer analyzer = new StandardAnalyzer();
基本上都不常用。
常用的是中文分词器。
中文分词器简单使用:
Google上提供IK分词器,只能支持Lucene3.x ,下载 IKAnalyzer2012FF_u1.jar (被改动过支持lucene4.x )
(1)导入jar包 IKAnalyzer2012FF_u1.jar
(2)使用IK分词器
// 创建分词器对象,这里我们使用IK中文分词器
Analyzer analyzer = new IKAnalyzer();
使用中文分词器后的分词效果:
5.3 Document(文档类)
Document相当于数据库中的一条数据,包含很多字段。
// 创建文档对象(document),可以对应着数据库中某一条记录
Document document = new Document();
// 为文档对象添加一条记录数据,Store.YES表示存储原始数据内容(查询时可以显示出原始内容)
document.add(new StringField("id", "1", Store.YES));
document.add(new TextField("content", "谷歌地图之父跳槽FaceBook", Store.YES));
5.4 Field(字段类)
// 为文档对象添加一条记录数据,Store.YES表示存储原始数据内容(查询时可以显示出原始内容)
document.add(new StringField("id", "1", Store.YES));
document.add(new TextField("content", "谷歌地图之父跳槽FaceBook", Store.YES));
以上是简化写法形式,标准写法如下:
// 定义字段类型特征对象
FieldType fieldType = new FieldType();
// 是否做索引
fieldType.setIndexed(true);
// 是否做分词
fieldType.setTokenized(false);
// 是否存储
fieldType.setStored(true);
// 根据字段类型特征创建字段对象
Field field = new Field("id", "1", fieldType);
// 添加该字段到文档对象中
document.add(field);
Field的常用子类
所支持的字段类型:
StringField: 做索引但是不做分词;
TextFiled: 做索引和分词;
Store:是否存储
存储:在索引库中会将该数据保存下来
不存储:该数据只做索引,但是不存储到索引库中
如何判断字段是否存储?
判断依据:在搜索结果中是否返回,如果返回,需要存储,如不返回,不需要存储;
如果有的字段不需要建立索引:
//不为该字段创建索引
FieldType fieldType = new FieldType();
fieldType.setIndexed(false);
fieldType.setStored(true);
document.add(new Field("id", "1", fieldType));
document.add(new TextField("content", "谷歌地图之父跳槽FaceBook", Store.YES));
5.5 IndexWriterConfig(索引写入器配置类)
指定了Lucene版本和分词器。
// 创建索引写入器(IndexWriter)配置对象:用来指定lucene版本,使用何种分词器
IndexWriterConfig indexWriterConfig = new IndexWriterConfig(Version.LATEST, analyzer);
//CREATE:先清空原有索引数据,再写入
indexWriterConfig.setOpenMode(OpenMode.CREATE);
5.6 IndexWriter(索引写入器类)
负责将数据文档添加(更新,删除)到索引库中。
// 通过索引写入器对象(IndexWriter)添加文档对象
indexWriter.addDocument(document);
// 通过索引写入器对象(IndexWriter)一次添加多个文档对象
// 伪代码
List<Document> docs = new ArrayList<Document>();
// 中间缺将多个文档对象添加到集合
indexWriter.addDocuments(docs);
完整代码:
/**
* alt+shift+j 添加文档注释
*
* 创建索引 小加强版
*
* @throws IOException
*/
@Test
public void createIndex() throws IOException {
// 文档集合
List<Document> docs = new ArrayList<Document>();
// 创建文档对象--相当于数据库中表中的每条记录
Document document1 = new Document();
// 为文档对象加入字段
// Ctrl+T 看子类 Store.YES表示将原始数据内容存储到索引库中
// // 定义字段类型特征对象
// FieldType fieldType = new FieldType();
//
// fieldType.setIndexed(false);// 是否做索引
// fieldType.setTokenized(false);// 是否做分词
// fieldType.setStored(true);// 是否存储
//
// // 创建字段
// Field field = new Field("id", "1", fieldType);
//
// // 添加刚刚定义的字段
// document.add(field);
document1.add(new StringField("id", "1", Store.YES));
document1.add(new TextField("content", "谷歌地图之父跳槽FaceBook,这是真的吗?",
Store.YES));
// 创建文档对象--相当于数据库中表中的每条记录
Document document2 = new Document();
document2.add(new StringField("id", "2", Store.YES));
document2.add(new TextField("content", "谷歌地图之父加盟FaceBook",
Store.YES));
// 创建文档对象--相当于数据库中表中的每条记录
Document document3 = new Document();
document3.add(new StringField("id", "3", Store.YES));
document3.add(new TextField("content", "谷歌地图创始人拉斯离开谷歌加盟Facebook",
Store.YES));
// 创建文档对象--相当于数据库中表中的每条记录
Document document4 = new Document();
document4.add(new StringField("id", "4", Store.YES));
document4.add(new TextField("content", "谷歌地图之父跳槽Facebook与Wave项目取消有关",
Store.YES));
// 创建文档对象--相当于数据库中表中的每条记录
Document document5 = new Document();
document5.add(new StringField("id", "5", Store.YES));
document5.add(new TextField("content", "谷歌地图之父拉斯加盟社交网站Facebook",
Store.YES));
// 创建目录对象(索引库),并指定位置
Directory directory = FSDirectory.open(new File("d://indexDir"));
// 创建分词器 标准分词器
// Analyzer analyzer = new StandardAnalyzer();
// 中文分词器
Analyzer analyzer = new IKAnalyzer();
// 创建索引写入器的配置对象 参数(版本号,分词器对象)
IndexWriterConfig indexWriterConfig = new IndexWriterConfig(
Version.LATEST, analyzer);
// 先删除原有索引库的内容,再重新写入
indexWriterConfig.setOpenMode(OpenMode.CREATE);
// 创建索引写入器对象 参数(目录对象,引写入器的配置对象)
IndexWriter indexWriter = new IndexWriter(directory, indexWriterConfig);
//将各个文档添加到文档集合中
docs.add(document1);
docs.add(document2);
docs.add(document3);
docs.add(document4);
docs.add(document5);
// 将文档对象集合添加到索引写入器中
indexWriter.addDocuments(docs);
// 将文档内容写入到索引库中
indexWriter.commit();// 提交
indexWriter.close();// 关闭
}
*****************************************************************************************************
6. 查询索引
/**
* 查询索引
*
* @throws ParseException
* @throws IOException
*/
@Test
public void queryIndex() throws ParseException, IOException {
// 创建查询解析器对象 两个构造参数 1、查询的字段名称 2、分词器对象
QueryParser queryParser = new QueryParser("content", new IKAnalyzer());
// 通过查询解析器对象获得查询对象 将查询的内容作为参数
Query query = queryParser.parse("谷歌");
// 创建 索引搜索对象,并指定索引库的位置
IndexSearcher indexSearcher = new IndexSearcher(
DirectoryReader.open(FSDirectory.open(new File("d://indexDir"))));
//通过 索引搜索对象 开始查询 参数 1、查询对象 2、查询的最大结果数量,并返回得分排名文档集合
//得分:根据查询的匹配度会对每个查询结果进行打分,匹配越高的,得分越高
TopDocs topDocs = indexSearcher.search(query, Integer.MAX_VALUE);
//打印出查询命中的文档数量
System.out.println("查询命中的文档数量:"+topDocs.totalHits);
//通过得分排名文档获得 分数文档数组
ScoreDoc[] myScoreDocs = topDocs.scoreDocs;
//遍历得分文档数组
for (ScoreDoc scoreDoc : myScoreDocs) {
//打印出分数文档的内置id
System.out.println("内置id"+scoreDoc.doc);
//根据内置id 通过 索引搜索对象 获得文档对象
Document doc = indexSearcher.doc(scoreDoc.doc);
//打印出相应的内容
System.out.println(doc.get("id"));
System.out.println(doc.get("content"));
}
}
}
通过查询功能可以看出
关于Store.YES和Store.NO的区别
、Store.YES 采用保存模式,可以查询到,并且可以打印出原始内容
、Store.NO 采用不保存模式,可以查询到,但是不能打印出原始内容
*****************************************************************************************************
7. 查询索引核心API详解
7.1 QueryParser(查询分析器类)
针对单一字段
// 创建查询解析器(QueryParser),两个构造参数的含义(字段名称,分析器示例)
QueryParser queryParser = new QueryParser("content",new IKAnalyzer());
7.2 MultiFieldQueryParser(多列查询分析器类)
针对多个字段
// 创建多列查询解析器(QueryParser),两个构造参数的含义(字段名称数组,分析器示例)
QueryParser queryParser = new MultiFieldQueryParser(new String[]{"id","content"}, new IKAnalyzer()) ;
7.3 Query(查询对象)
// 定义查询对象(Query),指定查询字关键字内容
Query query = queryParser.parse("谷歌吃饭");
7.4 IndexSeacher(索引搜索对象)
// 通过索引搜索对象(IndexSearcher)执行查询,返回得分排名文档集合(TopDocs:指向相匹配的搜索条件的前N个搜索结果,按得分排名)
TopDocs topDocs = indexSearcher.search(query, Integer.MAX_VALUE);
7.5 TopDocs(查询结果排名前N的文档集合)
System.out.println("命中的查询的总数:" + topDocs.totalHits);// 命中的查询的总数
totalHits属性:代表查询的总结果数
scoreDocs属性:返回分数文档数组
7.6 ScoreDoc(得分文档对象)
//通过排名文档对象获得 分数文档数组
ScoreDoc[] myScoreDocs = topDocs.scoreDocs;
//遍历得分文档数组
for (ScoreDoc scoreDoc : myScoreDocs) {
//打印出分数文档的内置id
System.out.println("内置id"+scoreDoc.doc);
//根据内置id 通过 索引搜索对象 获得文档对象
Document doc = indexSearcher.doc(scoreDoc.doc);
//打印出相应的内容
System.out.println(doc.get("id"));
System.out.println(doc.get("content"));
}
score属性:查询得分
doc属性:文档内部编号
/**
* 查询索引 api详解 小加强版
*
* @throws ParseException
* @throws IOException
*/
@Test
public void queryIndex() throws ParseException, IOException {
// 创建查询解析器
// QueryParser queryParser = new QueryParser("content", new
// IKAnalyzer());
// 针对多个字段的查询
QueryParser queryParser = new MultiFieldQueryParser(new String[] {
"id", "content" }, new IKAnalyzer());
// 获得查询对象
Query query = queryParser.parse("谷歌之父");
// 创建查询搜索器对象
IndexSearcher indexSearcher = new IndexSearcher(
DirectoryReader.open(FSDirectory.open(new File("d://indexDir"))));
// 开始查询 返回的是排名文档集合
TopDocs topDocs = indexSearcher.search(query, Integer.MAX_VALUE);
System.out.println("命中数" + topDocs.totalHits);
// 获得得分文档数组
ScoreDoc[] scoreDocs = topDocs.scoreDocs;
for (ScoreDoc scoreDoc : scoreDocs) {
System.out.println("内置id:" + scoreDoc.doc);
//匹配度
System.out.println("得分:"+scoreDoc.score);
// 通过内置id 获得单个文档对象
Document doc = indexSearcher.doc(scoreDoc.doc);
System.out.println(doc.get("id"));
System.out.println(doc.get("content"));
}
}
*****************************************************************************************************
8. 高级查询功能
8.1 词条查询(TermQuery)
//词条就是索引库最小搜索单位,不可再分,搜索无结果
Query query = new TermQuery(new Term("content", "谷歌吃饭"));
8.2 模糊搜索(WildcardQuery)
? 代表一个字符
* 代表任意多个字符
//使用*号可以匹配N个字符
Query query = new WildcardQuery(new Term("content", "*"+"歌"+"*"));
8.3 相似度搜索(FuzzyQuery)
//允许用户输入有错误,显示相似单词搜索结果,默认编辑两次
Query query = new FuzzyQuery(new Term("content","faccbook"));
//指定编辑次数
);
)。
、2 三个整数,最多可以编辑两次。
8.4 数字范围搜索(NumericRangeQuery)
//指定搜索字段,搜索范围(开始和结束),最后两个参数表示是否包含开始节点和结束节点
Query query = NumericRangeQuery.newLongRange("id", 7L, 30L, true, true);
8.5 查询索引库所有内容(MatchAllDocsQuery)
//查询索引库所有内容,没有查询条件
Query query = new MatchAllDocsQuery();
8.6 组合查询(BooleanQuery)
提供 add,用于添加其它Query
occur可以取值 MUST 必须满足、 MUST_NOT 必须不满足, SHOULD 可以满足
//组合查询(BooleanQuery)
//范围查询
Query query1 = NumericRangeQuery.newLongRange("id",1L,2L,true,true);
Query query2 = NumericRangeQuery.newLongRange("id",2L,4L,true,true);
//加入组合条件
BooleanQuery query = new BooleanQuery();
query.add(query1, Occur.MUST_NOT);
query.add(query2, Occur.MUST);
完整代码:
/**
* 查询索引 高级查询功能
*
* @throws ParseException
* @throws IOException
*/
@Test
public void queryIndex() throws ParseException, IOException {
// 创建查询解析器
QueryParser queryParser = new QueryParser("content", new IKAnalyzer());
// 根据查询解析器 获得 查询对象
// Query query = queryParser.parse("谷歌");
// 词条查询(TermQuery) 词条就是索引库最小搜索单位,不可再分,搜索无结果
// Query query = new TermQuery(new Term("content","谷歌拉斯"));
// 模糊搜索(WildcardQuery) 使用*号可以匹配N个字符
// Query query = new WildcardQuery(new Term("content","*歌*"));
// 相似度搜索(FuzzyQuery) 2:编辑次数
//Query query = new FuzzyQuery(new Term("content", "faccbok"), 2);
// 数字范围搜索(NumericRangeQuery)
// 指定搜索字段,搜索范围(开始和结束),最后两个参数表示是否包含开始节点和结束节点
//Query query = NumericRangeQuery.newLongRange(
// "id", 1L, 4L, true, true);
//查询索引库所有内容(MatchAllDocsQuery)
//Query query = new MatchAllDocsQuery();
//组合查询(BooleanQuery)
//范围查询
Query query1 = NumericRangeQuery.newLongRange("id", 1L, 2L, true, true);
Query query2 = NumericRangeQuery.newLongRange("id", 2L, 4L, true, true);
BooleanQuery query = new BooleanQuery();
query.add(query1, Occur.SHOULD);
query.add(query2, Occur.SHOULD);
// 创建索引搜索器对象 并指定索引库的位置
IndexSearcher indexSearcher = new IndexSearcher(
DirectoryReader.open(FSDirectory.open(new File("d://indexDir"))));
// 开始查询 返回的是一个排名文档集合
TopDocs topDocs = indexSearcher.search(query, Integer.MAX_VALUE);
// 打印命中数
System.out.println("命中数:" + topDocs.totalHits);
// 获得分数文档集合
ScoreDoc[] myScoreDocs = topDocs.scoreDocs;
// 遍历得分数文档集合
for (ScoreDoc scoreDoc : myScoreDocs) {
// 内置id
System.out.println("内置id" + scoreDoc.doc);
System.out.println("匹配度得分" + scoreDoc.score);
// 根据内置id获得具体的文档对象
Document document = indexSearcher.doc(scoreDoc.doc);
// 打印出内容
System.out.println(document.get("id"));
System.out.println(document.get("content"));
}
}
/**
* 创建索引 多个文档 id是long型
*
* @throws IOException
*/
@Test
public void createIndex() throws IOException {
// 文档集合
List<Document> docs = new ArrayList<Document>();
// 创建文档
Document document1 = new Document();
document1.add(new LongField("id", 1, Store.YES));
document1.add(new TextField("content", "谷歌地图之父跳槽FaceBook", Store.YES));
Document document2 = new Document();
document2.add(new LongField("id", 2, Store.YES));
document2.add(new TextField("content", "谷歌地图之父加盟FaceBook", Store.YES));
Document document3 = new Document();
document3.add(new LongField("id", 3, Store.YES));
document3.add(new TextField("content", "谷歌地图创始人拉斯离开谷歌加盟Facebook",
Store.YES));
Document document4 = new Document();
document4.add(new LongField("id", 4, Store.YES));
document4.add(new TextField("content", "谷歌地图之父跳槽Facebook与Wave项目取消有关",
Store.YES));
Document document5 = new Document();
document5.add(new LongField("id", 5, Store.YES));
document5.add(new TextField("content", "谷歌地图之父拉斯加盟社交网站Facebook",
Store.YES));
// 将文档对象放入文档集合中
docs.add(document1);
docs.add(document2);
docs.add(document3);
docs.add(document4);
docs.add(document5);
// 先删除原有索引库的内容,再重新写入
IndexWriterConfig indexWriterConfig = new IndexWriterConfig(
Version.LATEST, new IKAnalyzer());
indexWriterConfig.setOpenMode(OpenMode.CREATE);
// 获得索引写入器
IndexWriter indexWriter = new IndexWriter(FSDirectory.open(new File(
"d://indexDir")), indexWriterConfig);
// 将文档集合添加到索引写入器中
indexWriter.addDocuments(docs);
indexWriter.commit();
indexWriter.close();
}
*****************************************************************************************************
9. 更新索引
//通过索引写入器对象(IndexWriter)修改文档对象
indexWriter.updateDocument(new Term("id", "1"), document);
注意:Lucene中的indexWriter.updateDocument有如下限制:
、设置的Term对应的Field必须是字符串类型。
、若id是整型的,则不能直接用updateDocument方法来更新,必须先删除后创建。
完整代码:
/**
* 更新索引
* @throws IOException
*/
@Test
public void updateIndex() throws IOException {
// 创建新的文档对象
Document document = new Document();
document.add(new StringField("id", "4", Store.YES));
document.add(new TextField("content",
"谷歌地图之父跳槽Facebook与Wave项目取消有关,才不是这样呢", Store.YES));
//创建索引写入器
IndexWriter indexWriter = new IndexWriter(FSDirectory.open(new File("d://indexDir")),
new IndexWriterConfig(Version.LATEST, new IKAnalyzer()));
//更新索引
indexWriter.updateDocument(new Term("id","4"), document);
indexWriter.commit();//提交
indexWriter.close();//关闭
}
*****************************************************************************************************
10. 删除索引
/**
* 删除索引
* @throws IOException
*/
@Test
public void deleteIndex() throws IOException {
// 定义目录对象(directory)用来指定索引数据所存在的文件目录
Directory directory = FSDirectory.open(new File("D:\\indexDir"));
// 创建索引写入器对象(IndexWriter),并指定目录对象和索引写入器配置对象
IndexWriter indexWriter = new IndexWriter(directory,
new IndexWriterConfig(Version.LATEST, new StandardAnalyzer()));
// 删除索引 new Term("id", "1")表示删除哪一条
indexWriter.deleteDocuments(new Term("id", "1"));
indexWriter.commit();
indexWriter.close();
}
/**
* 删除所有索引
* @throws IOException
*/
@Test
public void deleteAllIndex() throws IOException {
// 定义目录对象(directory)用来指定索引数据所存在的文件目录
Directory directory = FSDirectory.open(new File("D:\\indexDir"));
// 创建索引写入器对象(IndexWriter),并指定目录对象和索引写入器配置对象
IndexWriter indexWriter = new IndexWriter(directory,
new IndexWriterConfig(Version.LATEST, new StandardAnalyzer()));
//删除所有索引
indexWriter.deleteAll();
indexWriter.commit();
indexWriter.close();
}
*****************************************************************************************************
11. 中文分词器详解
中文分词不是技术难点,对于搜索引擎难点是:语意分词;
比如:我爱炒肉丝
目前的中文分词器:
11.1 IK分词器
官网:https://code.google.com/p/ik-analyzer/
IK分词器官方版本是不支持Lucene4.X的,有人基于IK的源码做了改造,支持了Lucene4.X:
11.2 IK分词器的使用
Google上提供IK分词器,只能支持Lucene3.x ,下载 IKAnalyzer2012FF_u1.jar (被改动过支持lucene4.x )
(1)导入jar包 IKAnalyzer2012FF_u1.jar
(2)使用IK分词器
// 创建分词器对象,这里我们使用IK分词器
Analyzer analyzer = new IKAnalyzer();
11.3 IK的自定义词库
扩展词典(新创建词功能):有些词IK分词器不识别例如"传智播客"
停用词典(停用某些词功能):有些词不需要建立索引例如:"的,哦,啊"
(1)添加各种配置文件在项目根目录下:
(2)编辑IKAnalyzer.cfg.xml:IK分词器总配置文件
<?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)编辑ext.dic:自定义新词配置文件
(4)编辑stopword.dic:停词配置文件
(5)重新写入索引并测试。
*****************************************************************************************************
12. lucene4.x 查询结果的高亮显示
12.1 高亮结果显示的原理
1、对搜索结果中关键字(搜索词),两端添加HTML标签
2、通过CSS样式,对特有标签进行修饰,显示为高亮
lucene在对目标内容进行高亮显示时,会截取目标内容中一段文本(摘要),再对摘要中关键字进行高亮(两端添加HTML)
12.2 通过lucene完成高亮结果处理
12.2.1 第一步:引入高亮器的jar包
导入 lucene-highlighter-4.10.2.jar
12.2.2 第二步:定义和配置高亮器对象
// 定义查询对象(Query),指定查询字关键字内容
Query query = queryParser.parse("谷歌吃饭");
//定义高亮格式
Formatter formatter = new SimpleHTMLFormatter("<font color='red'>","</font>");
//加载高亮格式对象(formatter)和查询对象(query)到高亮对象(highlighter)中
//query需要封装QueryScorer后再作为参数加载
Highlighter highlighter = new Highlighter(formatter, new QueryScorer(query));
12.2.3 第三步:对查询结果高亮标记
//该代码写在遍历结果的for循环里面的最后
//通过高亮对象(highlighter)的getBestFragment()方法获得已经加入高亮html标签的内容。
//有三个参数:分析器、要解析的字段名、要解析的数据
String content2 = highlighter.getBestFragment(new IKAnalyzer(),"content", docResult.get("content"));
System.out.println("content:" + content2);
完整代码:
/**
* 查询索引 高亮功能
*
* @throws ParseException
* @throws IOException
* @throws InvalidTokenOffsetsException
*/
@Test
public void queryIndex() throws ParseException, IOException, InvalidTokenOffsetsException {
// 创建查询解析器 两个构造参数 1、查询的字段名称 2、分词器对象
QueryParser queryParser = new QueryParser("content", new IKAnalyzer());
// 获得查询对象,并指定查询的内容
Query query = queryParser.parse("谷歌之父");
//定义高亮的格式
Formatter formatter = new SimpleHTMLFormatter("<font color='red'>","</font>");
//加载高亮格式对象(formatter)和查询对象(query)到高亮对象(highlighter)中
//query需要封装QueryScorer后再作为参数加载
Highlighter highlighter = new Highlighter(formatter, new QueryScorer(query));
// 创建索引搜索器对象,并指定索引库的位置
IndexSearcher indexSearcher = new IndexSearcher(
DirectoryReader.open(FSDirectory.open(new File("d://indexDir"))));
//通过索引搜索器对象开始查询 返回文档排名集合
TopDocs topDocs = indexSearcher.search(query, Integer.MAX_VALUE);
//查询出结果的数量
System.out.println("命中数:"+topDocs.totalHits);
//通过得分排名文档获得 分数文档数组
ScoreDoc[] scoreDocs = topDocs.scoreDocs;
//遍历该数据
for (ScoreDoc scoreDoc : scoreDocs) {
System.out.println("内置id:"+scoreDoc.doc);
//根据内置id 通过 索引搜索对象 获得文档对象
Document doc = indexSearcher.doc(scoreDoc.doc);
//打印文档对象的内容
System.out.println(doc.get("id"));
//System.out.println(doc.get("content"));
//显示高亮的结果
String bestFragment = highlighter.getBestFragment(new IKAnalyzer(), "content", doc.get("content"));
System.out.println(bestFragment);
}
}
*****************************************************************************************************
13. lucene4.x 分页
通过IndexSearcher提供 search(query, n) 这里n 代表查询前多少条信息
//分页需求共有3页,每页2条: 我要查出第二页的2条数据
int totalPage = 3;//总页数
int pageCounts= 2;//每页显示数量
int pageIndex = 2;//第几页
int begin = (totalPage-pageIndex)*pageCounts;//计算出起点
int end = pageIndex*pageCounts;//计算出终点
完整代码:
/**
* 查询索引 分页
* @throws ParseException
* @throws IOException
*/
@Test
public void queryIndex() throws ParseException, IOException {
//分页需求 共有3页,每页2条: 我要查出第二页的2条数据
int totalPage = 3;//总页数
int pageCounts= 2;//每页显示数量
int pageIndex = 2;//第几页
int begin = (totalPage-pageIndex)*pageCounts;//计算出起点
int end = pageIndex*pageCounts;//计算出终点
//创建查询解析器对象
QueryParser queryParser = new QueryParser("content", new IKAnalyzer());
//根据查询解析器对象 创建查询对象 并载入查询的内容
Query query = queryParser.parse("谷歌");
//创建了索引搜索器对象,并指定了目录(索引库)的位置
IndexSearcher indexSearcher = new IndexSearcher(DirectoryReader.open(FSDirectory.open(new File("d://indexDir"))));
//开始查询 参数1、查询对象 2、最大返回结果条数
//返回值:排名结果文档集合
//得分:根据查询的匹配度会对每个查询结果进行打分,匹配越高的,得分越高
TopDocs topDocs = indexSearcher.search(query,end);
//查询结果的数量
System.out.println("查询结果的命中数量:"+topDocs.totalHits);
//获得分数文档的集合
ScoreDoc[] myScoreDocs = topDocs.scoreDocs;
//遍历分数文档的集合
// for (ScoreDoc scoreDoc : myScoreDocs) {
//
// //打印分数文档的在索引库中的内置id
// System.out.println("内置id"+scoreDoc.doc);
//
// //通过内置id找到具体的文档对象
// Document document = indexSearcher.doc(scoreDoc.doc);
//
// //打印出内容
// System.out.println(document.get("id"));
// System.out.println(document.get("content"));
//
// }
for (int i = begin; i < myScoreDocs.length; i++) {
//打印分数文档的在索引库中的内置id
System.out.println("内置id"+myScoreDocs[i].doc);
//通过内置id找到具体的文档对象
Document document = indexSearcher.doc(myScoreDocs[i].doc);
//打印出内容
System.out.println(document.get("id"));
System.out.println(document.get("content"));
}
}
*****************************************************************************************************
14. lucene4.x 得分
得分用来表示查询词条关联性的强弱,得分越高,表示查询的匹配度就越高
得分公式:
- tf词频:查找的词在某个文档中查询词语出现次数
- idf 反转文档频率:文档总数/词在多少文档出现
例子:
例1.有很多不同的数学公式可以用来计算TF-IDF。这边的例子以上述的数学公式来计算。词频 (TF) 是一词语出现的次数除以该文件的总词语数。假如一篇文件的总词语数是100个,而词语"母牛"出现了3次,那么"母牛"一词在该文件中的词频就是3/100=0.03。一个计算文件频率 (DF) 的方法是测定有多少份文件出现过"母牛"一词,然后除以文件集里包含的文件总数。所以,如果"母牛"一词在1,000份文件出现过,而文件总数是10,000,000份的话,其逆向文件频率就是 lg(10,000,000 / 1,000)=4。最后的TF-IDF的分数为0.03 * 4=0.12。
- ,可以手动调整
- lengthNorm :和Field分词数量(Terms)成反比,默认没有分词,值是 1
通过设置Boost参数,改变搜索得分
Document document3 = new Document();
document3.add(new StringField("id", "3", Store.YES));
TextField field = new TextField("content","谷歌地图创始人拉斯离开谷歌加盟Facebook", Store.YES);
field.setBoost(10);// 设置激励因子,数值越大,得分越高,默认排序时候越靠前
document3.add(field);
Lucene默认是根据得分来进行排序的,修改某条文档字段的激励因子(Boost)可以快速改变该字段的排序位置
完整代码:
/**
* alt+shift+j 添加文档注释
*
* 创建索引 小加强版
*
* @throws IOException
*/
@Test
public void createIndex() throws IOException {
// 文档集合
List<Document> docs = new ArrayList<Document>();
// 创建文档对象--相当于数据库中表中的每条记录
Document document1 = new Document();
// 为文档对象加入字段
// Ctrl+T 看子类 Store.YES表示将原始数据内容存储到索引库中
// // 定义字段类型特征对象
// FieldType fieldType = new FieldType();
//
// fieldType.setIndexed(false);// 是否做索引
// fieldType.setTokenized(false);// 是否做分词
// fieldType.setStored(true);// 是否存储
//
// // 创建字段
// Field field = new Field("id", "1", fieldType);
//
// // 添加刚刚定义的字段
// document.add(field);
document1.add(new StringField("id", "1", Store.YES));
document1.add(new TextField("content", "谷歌地图之父跳槽FaceBook,这是真的吗?",
Store.YES));
// 创建文档对象--相当于数据库中表中的每条记录
Document document2 = new Document();
document2.add(new StringField("id", "2", Store.YES));
document2.add(new TextField("content", "谷歌地图之父加盟FaceBook",
Store.YES));
// 创建文档对象--相当于数据库中表中的每条记录
Document document3 = new Document();
document3.add(new StringField("id", "3", Store.YES));
Field field = new TextField("content", "谷歌地图创始人拉斯离开谷歌加盟Facebook",
Store.YES);
//作弊 改变激励因子
field.setBoost(10);
document3.add(field);
// 创建文档对象--相当于数据库中表中的每条记录
Document document4 = new Document();
document4.add(new StringField("id", "4", Store.YES));
document4.add(new TextField("content", "谷歌地图之父跳槽Facebook与Wave项目取消有关",
Store.YES));
// 创建文档对象--相当于数据库中表中的每条记录
Document document5 = new Document();
document5.add(new StringField("id", "5", Store.YES));
document5.add(new TextField("content", "谷歌地图之父拉斯加盟社交网站Facebook",
Store.YES));
Document document6 = new Document();
document6.add(new StringField("id", "6", Store.YES));
document6.add(new TextField("content", "传智播客碉堡了,学完了我要迎娶白富美",
Store.YES));
// 创建目录对象(索引库),并指定位置
Directory directory = FSDirectory.open(new File("d://indexDir"));
// 创建分词器 标准分词器
// Analyzer analyzer = new StandardAnalyzer();
// 中文分词器
Analyzer analyzer = new IKAnalyzer();
// 创建索引写入器的配置对象 参数(版本号,分词器对象)
IndexWriterConfig indexWriterConfig = new IndexWriterConfig(
Version.LATEST, analyzer);
// 先删除原有索引库的内容,再重新写入
indexWriterConfig.setOpenMode(OpenMode.CREATE);
// 创建索引写入器对象 参数(目录对象,引写入器的配置对象)
IndexWriter indexWriter = new IndexWriter(directory, indexWriterConfig);
//将各个文档添加到文档集合中
docs.add(document1);
docs.add(document2);
docs.add(document3);
docs.add(document4);
docs.add(document5);
docs.add(document6);
// 将文档对象集合添加到索引写入器中
indexWriter.addDocuments(docs);
// 将文档内容写入到索引库中
indexWriter.commit();// 提交
indexWriter.close();// 关闭
}
*****************************************************************************************************
15. lucene4.x 排序
Sort sort = new Sort(new SortField("id", Type.LONG, false));// false为升序排列
TopDocs topDocs = indexSearcher.search(query, Integer.MAX_VALUE, sort);
注意:数据类型Type.LONG 最好要与Field的类型一致
完整代码:
/**
* 查询索引 排序
* @throws ParseException
* @throws IOException
*/
@Test
public void queryIndex() throws ParseException, IOException {
//创建查询解析器对象
QueryParser queryParser = new QueryParser("content", new IKAnalyzer());
//根据查询解析器对象 创建查询对象 并载入查询的内容
Query query = queryParser.parse("谷歌");
//创建了索引搜索器对象,并指定了目录(索引库)的位置
IndexSearcher indexSearcher = new IndexSearcher(DirectoryReader.open(FSDirectory.open(new File("d://indexDir"))));
//开始查询 参数1、查询对象 2、最大返回结果条数
//返回值:排名结果文档集合
//得分:根据查询的匹配度会对每个查询结果进行打分,匹配越高的,得分越高
//排序
Sort sort = new Sort(new SortField("id",Type.LONG,true));//false为升序排列
TopDocs topDocs = indexSearcher.search(query, Integer.MAX_VALUE,sort);
//查询结果的数量
System.out.println("查询结果的命中数量:"+topDocs.totalHits);
//获得分数文档的集合
ScoreDoc[] myScoreDocs = topDocs.scoreDocs;
//遍历分数文档的集合
for (ScoreDoc scoreDoc : myScoreDocs) {
//打印分数文档的在索引库中的内置id
System.out.println("内置id"+scoreDoc.doc);
//通过内置id找到具体的文档对象
Document document = indexSearcher.doc(scoreDoc.doc);
//打印出内容
System.out.println(document.get("id"));
System.out.println(document.get("content"));
}
}
*****************************************************************************************************
16. 总结
*****************************************************************************************************