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    新的业务需求

比如,用户在百度文本框中输入,"吃饭睡觉写程序",会出现的以下结果:

Lucene全文检索技术-LMLPHP

从结果可以看出,百度搜索具备以下明显特点:

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    搜索引擎的原理

Lucene全文检索技术-LMLPHP

*****************************************************************************************************

2.3    倒排索引

倒排索引又叫反向索引(右下图)以字或词为关键字进行索引,表中关键字所对应的记录表项记录了出现这个字或词的所有文档,一个表项就是一个字表段,它记录该文档的ID和字符在该文档中出现的位置情况。

Lucene全文检索技术-LMLPHP

在实际的运用中,我们可以对数据库中原始的数据结构(左图),在业务空闲时事先根据左图内容,创建新的倒排索引结构的数据区域(右图)。

用户有查询需求时,先访问倒排索引数据区域(右图),得出文档id后,通过文档id即可快速,准确的通过左图找到具体的文档内容。

这一过程,可以通过我们自己写程序来实现,也可以借用已经抽象出来的通用开源技术来实现。

*****************************************************************************************************

3.    Lucene 简介

3.1    什么是Lucene

Lucene全文检索技术-LMLPHP

Lucene是一套用于全文检索和搜寻的开源程序库,由Apache软件基金会支持和提供

Lucene提供了一个简单却强大的应用程序接口(API),能够做全文索引和搜寻,在Java开发环境里Lucene是一个成熟的免费开放源代码工具

Lucene并不是现成的搜索引擎产品,但可以用来制作搜索引擎产品

官网:http://lucene.apache.org/

3.2    什么是全文索引

计算机索引程序通过扫描文章中的每一个词,对每一个词建立一个索引,指明该词在文章中出现的次数和位置,当用户查询时,检索程序就根据事先建立的索引进行查找,并将查找的结果反馈给用户的检索方式

总结:对文档(数据)中每一个词都做索引。

3.3    Lucene下载

Lucene全文检索技术-LMLPHP

目前最新的版本是5.x系列,但是大多数企业中依旧使用4.x版本,比较稳定。本次课程我们使用4.10.2版本,该下载文件位于课前资料中。

3.4    Lucene和Solr关系

Lucene:底层的API,工具包

Solr:基于Lucene开发的企业级的搜索引擎产品

3.5    Lucene目录结构

Lucene全文检索技术-LMLPHP

*****************************************************************************************************

4.    创建索引

4.1    准备工作

4.1.1        建立JAVA项目

Lucene全文检索技术-LMLPHP

Lucene全文检索技术-LMLPHP

Lucene全文检索技术-LMLPHP

Lucene全文检索技术-LMLPHP

简要流程:菜单选择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    创建索引代码实现

创建索引其实就是从下图左边的原始数据,进行分词并提取出分词相关的索引信息,以右图倒排索引的方式,存放到某一位置。

Lucene全文检索技术-LMLPHP

创建索引代码流程图:

Lucene全文检索技术-LMLPHP

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();

}

运行后产生的结果:

Lucene全文检索技术-LMLPHP

*****************************************************************************************************

4.3    创建索引总结

创建索引的基本类:

1、文档对象(Document):可以把一个 Document 对象想象成数据库中的一条记录。例如原始数据(左图)中的一条记录。

2、字段类(Field):就是一条记录中的一个字段。例如原始数据(左图)一条记录中的文档编号,或者文档内容。

3、目录类(Directory):代表了 Lucene 索引库的存储位置。例如倒排索引(右图)所将要存放的位置。

4、分词器类(Analyzer):提供分词算法,针对不同的语言和应用需要选择适合的 Analyzer。

5、索引写入器配置对象(IndexWriterConfig):加载lucene版本配置和分词配置。

6、索引写入器类(IndexWriter):操作工具,把一个个的 Document 对象加到索引库中来,或进行删改。

*****************************************************Lucene全文检索技术-LMLPHP************************************************

4.4    使用lukeall工具,查看索引内部结构

4.4.1        运行luke工具命令:

Lucene全文检索技术-LMLPHP

4.4.2     Luke工具使用介绍:

Lucene全文检索技术-LMLPHP

Lucene全文检索技术-LMLPHP

*****************************************************************************************************

5.    创建索引核心API详解

5.1    Directory(目录类)

Lucene全文检索技术-LMLPHP

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();

Lucene全文检索技术-LMLPHP

基本上都不常用。

常用的是中文分词器。

中文分词器简单使用:

Google上提供IK分词器,只能支持Lucene3.x ,下载 IKAnalyzer2012FF_u1.jar (被改动过支持lucene4.x )

(1)导入jar包 IKAnalyzer2012FF_u1.jar

(2)使用IK分词器

// 创建分词器对象,这里我们使用IK中文分词器

Analyzer analyzer = new IKAnalyzer();

使用中文分词器后的分词效果:

Lucene全文检索技术-LMLPHP

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的常用子类

所支持的字段类型:

Lucene全文检索技术-LMLPHP

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

Lucene全文检索技术-LMLPHP

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. 中文分词器详解

中文分词不是技术难点,对于搜索引擎难点是:语意分词;

比如:我爱炒肉丝

目前的中文分词器:

Lucene全文检索技术-LMLPHP

11.1 IK分词器

Lucene全文检索技术-LMLPHP

官网:https://code.google.com/p/ik-analyzer/

IK分词器官方版本是不支持Lucene4.X的,有人基于IK的源码做了改造,支持了Lucene4.X:

Lucene全文检索技术-LMLPHP

11.2 IK分词器的使用

Google上提供IK分词器,只能支持Lucene3.x ,下载 IKAnalyzer2012FF_u1.jar (被改动过支持lucene4.x )

1)导入jar包 IKAnalyzer2012FF_u1.jar

    (2)使用IK分词器

// 创建分词器对象,这里我们使用IK分词器

Analyzer analyzer = new IKAnalyzer();

Lucene全文检索技术-LMLPHP

11.3 IK的自定义词库

扩展词典(新创建词功能):有些词IK分词器不识别例如"传智播客"

停用词典(停用某些词功能):有些词不需要建立索引例如:"的,哦,啊"

(1)添加各种配置文件在项目根目录下:

Lucene全文检索技术-LMLPHP

(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:自定义新词配置文件

Lucene全文检索技术-LMLPHP

(4)编辑stopword.dic:停词配置文件

Lucene全文检索技术-LMLPHP

(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    得分

得分用来表示查询词条关联性的强弱,得分越高,表示查询的匹配度就越高

得分公式:

Lucene全文检索技术-LMLPHP

  • 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.    总结

Lucene全文检索技术-LMLPHP

Lucene全文检索技术-LMLPHP

*****************************************************************************************************

04-24 13:52