1 Lucene介绍
1.1 什么是Lucene
Lucene是apache下的一个开源的全文检索引擎工具包。它为软件开发人员提供一个简单易用的工具包(类库),以方便的在目标系统中实现全文检索的功能。
1.2 全文检索的应用场景
1.2.1 搜索引擎
注意: Lucene和搜索引擎是不同的,Lucene是一套用java或其它语言写的全文检索的工具包。它为应用程序提供了很多个api接口去调用,可以简单理解为是一套实现全文检索的类库。搜索引擎是一个全文检索系统,它是一个单独运行的软件系统。 |
1.2.2 站内搜索(关注)
1.3 全文检索定义
全文检索首先将要查询的目标文档中的词提取出来,组成索引,通过查询索引达到搜索目标文档的目的。这种先建立索引,再对索引进行搜索的过程就叫全文检索(Full-textSearch)。
2 Lucene实现全文检索的流程
全文检索的流程分为两大部分:索引流程、搜索流程。
l 索引流程:即采集数据à构建文档对象à分析文档(分词)à创建索引。
l 搜索流程:即用户通过搜索界面à创建查询à执行搜索,搜索器从索引库搜索à渲染搜索结果。
3 入门程序
3.1 需求
使用Lucene实现电商项目中图书类商品的索引和搜索功能。
3.2 环境准备
l Jdk环境:1.7.0_72
l Ide环境:eclipse indigo
l 数据库环境:mysql5.1
l Lucene:4.10.3
3.2.1 数据库初始化
3.2.2 Lucene下载安装
Lucene是开发全文检索功能的工具包,使用时从官方网站下载,并解压。
官方网站:http://lucene.apache.org/
目前最新版本:5.2.1
下载地址:http://archive.apache.org/dist/lucene/java/
下载版本:4.10.3
JDK要求:1.7以上(从版本4.8开始,不支持1.7以下)
3.3 工程搭建(两步)
3.3.1 第一步:创建java工程
3.3.2 第二步:添加jar包
入门程序只需要添加以下jar包:
l mysql5.1驱动包:mysql-connector-java-5.1.7-bin.jar
l 核心包:lucene-core-4.10.3.jar
l 分析器通用包:lucene-analyzers-common-4.10.3.jar
l 查询解析器包:lucene-queryparser-4.10.3.jar
l junit包:junit-4.9.jar
3.4 索引流程
对文档索引的过程,就是将用户要搜索的文档内容进行索引,然后把索引存储在索引库(index)中。
3.4.1 为什么要采集数据
全文检索要搜索的数据信息格式多种多样,拿搜索引擎(百度, google)来说,通过搜索引擎网站能搜索互联网站上的网页(html)、互联网上的音乐(mp3..)、视频(avi..)、pdf电子书等。
全文检索搜索的这些数据称为非结构化数据。
什么是非结构化数据?
结构化数据:指具有固定格式或有限长度的数据,如数据库,元数据等。
非结构化数据:指不定长或无固定格式的数据,如邮件,word文档等。
如何对结构化数据搜索?
由于结构化数据是固定格式,所以就可以针对固定格式的数据设计算法来搜索,比如数据库like查询,like查询采用顺序扫描法,使用关键字匹配内容,对于内容量大的like查询速度慢。
如何对非结构化数据搜索?
需要将所有要搜索的非结构化数据通过技术手段采集到一个固定的地方,将这些非结构化的数据想办法组成结构化的数据,再以一定的算法去搜索。
3.4.2 如何采集数据
采集数据技术有哪些?
1、对于互联网上网页采用http将网页抓取到本地生成html文件。
2、如果数据在数据库中就连接数据库读取表中的数据。
3、如果数据是文件系统中的某个文件,就通过文件系统读取文件的内容。
3.4.2.1 网页采集(了解)
因为目前搜索引擎主要搜索数据的来源是互联网,搜索引擎使用一种爬虫程序抓取网页( 通过http抓取html网页信息),以下是一些爬虫项目:
Solr(http://lucene.apache.org/solr) ,solr是apache的一个子项目,支持从关系数据库、xml文档中提取原始数据。
Nutch(http://lucene.apache.org/nutch), Nutch是apache的一个子项目,包括大规模爬虫工具,能够抓取和分辨web网站数据。
jsoup(http://jsoup.org/ ),jsoup 是一款Java 的HTML解析器,可直接解析某个URL地址、HTML文本内容。它提供了一套非常省力的API,可通过DOM,CSS以及类似于jQuery的操作方法来取出和操作数据。
heritrix(http://sourceforge.net/projects/archive-crawler/files/),Heritrix 是一个由 java 开发的、开源的网络爬虫,用户可以使用它来从网上抓取想要的资源。其最出色之处在于它良好的可扩展性,方便用户实现自己的抓取逻辑。
3.4.2.2 数据库采集(掌握)
针对电商站内搜索功能,全文检索的数据源在数据库中,需要通过jdbc访问数据库中book表的内容。
3.4.2.2.1 Po
publicclass Book { // 图书ID private Integer id; // 图书名称 private String name; // 图书价格 private Float price; // 图书图片 private String pic; // 图书描述 private String description; } |
3.4.2.2.2 Dao
publicinterface BookDao { // 图书查询 public List<Book> queryBookList() throws Exception; } |
publicclass BookDaoImpl implements BookDao {
@Override public List<Book> queryBookList() throws Exception { // 数据库链接 Connection connection = null;
// 预编译statement PreparedStatement preparedStatement = null;
// 结果集 ResultSet resultSet = null;
// 图书列表 List<Book> list = new ArrayList<Book>();
try { // 加载数据库驱动 Class.forName("com.mysql.jdbc.Driver"); // 连接数据库 connection = DriverManager.getConnection( "jdbc:mysql://localhost:3306/solr", "root", "root");
// SQL语句 String sql = "SELECT * FROM book"; // 创建preparedStatement preparedStatement = connection.prepareStatement(sql);
// 获取结果集 resultSet = preparedStatement.executeQuery();
// 结果集解析 while (resultSet.next()) { Book book = new Book(); book.setId(resultSet.getInt("id")); book.setName(resultSet.getString("name")); book.setPrice(resultSet.getFloat("price")); book.setPic(resultSet.getString("pic")); book.setDescription(resultSet.getString("description")); list.add(book); }
} catch (Exception e) { e.printStackTrace(); }
return list; }
} |
可以进行单元测试一下。
3.4.3 索引文件的逻辑结构
n 文档域:
对非结构化的数据统一格式为document文档格式,一个文档有多个field域,不同的文档其field的个数可以不同,建议相同类型的文档包括相同的field。
本例子一个document对应一条 book表的记录。
n 索引域:
用于搜索,搜索程序将从索引域中搜索一个一个词,根据词找到对应的文档。
将Document中的Field的内容进行分词,将分好的词创建索引,索引=Field域名:词
n 倒排索引表
传统方法是先找到文件,如何在文件中找内容,在文件内容中匹配搜索关键字,这种方法是顺序扫描方法,数据量大就搜索慢。
倒排索引结构是根据内容(词语)找文档,倒排索引结构也叫反向索引结构,包括索引和文档两部分,索引即词汇表,它是在索引中匹配搜索关键字,由于索引内容量有限并且采用固定优化算法搜索速度很快,找到了索引中的词汇,词汇与文档关联,从而最终找到了文档。
3.4.4 创建索引
创建索引流程:
IndexWriter是索引过程的核心组件,通过IndexWriter可以创建新索引、更新索引、删除索引操作。IndexWriter需要通过Directory对索引进行存储操作。
Directory描述了索引的存储位置,底层封装了I/O操作,负责对索引进行存储。它是一个抽象类,它的子类常用的包括FSDirectory(在文件系统存储索引)、RAMDirectory(在内存存储索引)。
3.4.4.1 创建Document
采集数据的目的是为了索引,在索引前需要将原始内容创建成文档(Document),文档(Document)中包括一个一个的域(Field)。
3.4.4.1.1 代码实现
// 采集数据 BookDao dao = new BookDaoImpl(); List<Book> list = dao.queryBookList(); // Document对象集合 List<Document> docList = new ArrayList<Document>(); // Document对象 Document doc = null; for (Book book : list) { // 创建Document对象,同时要创建field对象 doc = new Document(); // 根据需求创建不同的Field Field id = new TextField("id", book.getId().toString(), Store.YES); Field name = new TextField("name", book.getName(), Store.YES); Field price = new TextField("price", book.getPrice().toString(),Store.YES); Field pic = new TextField("pic", book.getPic(), Store.YES); Field desc = new TextField("description", book.getDescription(), Store.YES);
// 把域(Field)添加到文档(Document)中 doc.add(id); doc.add(name); doc.add(price); doc.add(pic); doc.add(desc);
docList.add(doc); } |
3.4.4.2 分词
3.4.4.2.1 分词过程
在对Docuemnt中的内容索引之前需要使用分词器进行分词 ,主要过程就是分词、过虑两步。
l 分词就是将采集到的文档内容切分成一个一个的词,具体应该说是将Document中Field的value值切分成一个一个的词。
l 过虑包括去除标点符号、去除停用词(的、是、a、an、the等)、大写转小写、词的形还原(复数形式转成单数形参、过去式转成现在式。。。)等。
什么是停用词?停用词是为节省存储空间和提高搜索效率,搜索引擎在索引页面或处理搜索请求时会自动忽略某些字或词,这些字或词即被称为Stop Words(停用词)。比如语气助词、副词、介词、连接词等,通常自身并无明确的意义,只有将其放入一个完整的句子中才有一定作用,如常见的“的”、“在”、“是”、“啊”等。
Lucene作为了一个工具包提供不同国家的分词器,如下图:
注意由于语言不同分析器的切分规则也不同,本例子使用StandardAnalyzer,它可以对用英文进行分词。
如下是org.apache.lucene.analysis.standard.standardAnalyzer的部分源码:
@Override protected TokenStreamComponents createComponents(final String fieldName, final Reader reader) { final StandardTokenizer src = new src.setMaxTokenLength(maxTokenLength); TokenStream tok = new tok = new tok = new returnnew TokenStreamComponents(src, tok) { @Override protectedvoid setReader(final Reader reader) throws IOException { src.setMaxTokenLength(StandardAnalyzer.this.maxTokenLength); super.setReader(reader); } }; } |
Tokenizer是分词器,负责将reader转换为语汇单元即进行分词,Lucene提供了很多的分词器,也可以使用第三方的分词,比如IKAnalyzer一个中文分词器。
tokenFilter是分词过滤器,负责对语汇单元进行过滤,tokenFilter可以是一个过滤器链儿,Lucene提供了很多的分词器过滤器,比如大小写转换、去除停用词等。
如下图是语汇单元的生成过程:
从一个Reader字符流开始,创建一个基于Reader的Tokenizer分词器,经过三个TokenFilter生成语汇单元Token。
比如下边的文档经过分析器分析如下:
Ø 原文档内容:
Lucene is a Java full-text search engine.
|
Ø 分析后得到的语汇单元:
lucene、java、full、text、search、engine |
同一个域中相同的语汇单元(Token)对应同一个Term(词),它记录了语汇单元的内容及所在域的域名等。
l 不同的域中拆分出来的相同的单词对应不同的term。
l 相同的域中拆分出来的相同的单词对应相同的term。
例如:图书信息里面,图书名称中的java和图书描述中的java对应不同的term
3.4.4.2.2 代码实现
// 分析文档,对文档中的field域进行分词 Analyzer analyzer = new StandardAnalyzer(); |
3.4.4.3 创建索引
// 分析文档,对文档中的field域进行分词 Analyzer analyzer = new StandardAnalyzer(); // 创建索引 // a) 创建索引库目录 Directory directory = FSDirectory.open(new File("E:\\11-index\\0720")); // b) 创建IndexWriterConfig对象 IndexWriterConfig cfg = new IndexWriterConfig(Version.LATEST, analyzer); // c) 创建IndexWriter对象 IndexWriter writer = new IndexWriter(directory, cfg); // d) 通过IndexWriter对象添加文档对象(document) for (Document document : docList) { writer.addDocument(document); } // f) 关闭IndexWriter writer.close(); |
3.4.4.4 使用Luke查看索引
Luke作为Lucene工具包中的一个工具(http://www.getopt.org/luke/),可以通过界面来进行索引文件的查询、修改。
打开Luke方法:
l 命令运行:cmd运行:java -jar lukeall-4.10.3.jar
l 手动执行:双击lukeall-4.10.3.jar
3.5 搜索流程
搜索过程:
1、用户定义查询语句,用户确定查询什么内容(输入什么关键字)
指定查询语法,相当于sql语句。
2、IndexSearcher索引搜索对象,定义了很多搜索方法,程序员调用此方法搜索。
3、IndexReader索引读取对象,它对应的索引维护对象IndexWriter,IndexSearcher通过IndexReader读取索引目录中的索引文件
4、Directory索引流对象,IndexReader需要Directory读取索引库,使用FSDirectory文件系统流对象
5、IndexSearcher搜索完成,返回一个TopDocs(匹配度高的前边的一些记录)
3.5.1 输入查询语句
同数据库的sql一样,lucene全文检索也有固定的语法:
最基本的有比如:AND, OR, NOT 等
举个例子,用户想找一个description中包括java关键字和lucene关键字的文档。
它对应的查询语句:description:javaAND lucene
如下是使用luke搜索的例子:
3.5.2 搜索分词
和索引过程的分词一样,这里要对用户输入的关键字进行分词,一般情况索引和搜索使用的分词器一致。
比如:输入搜索关键字“java培训”,分词后为java和培训两个词,与java和培训有关的内容都搜索出来了,如下:
3.5.3 搜索索引
根据关键字从索引中找到对应的索引信息,即词term。term与document相关联,找到了term就找到了关联的document,从document取出Field中的信息即是要搜索的信息。
代码实现:
// 查询索引 @Test publicvoid searcherIndex() throws Exception {
// 1、创建查询(Query对象)
// 创建分析器 Analyzer analyzer = new StandardAnalyzer();
QueryParser queryParser = new QueryParser("description", analyzer); Query query = queryParser.parse("description:java AND lucene"); // 2、执行搜索 // a) 指定索引库目录 Directory directory = FSDirectory.open(new File("E:\\11-index\\0720")); // b) 创建IndexReader对象 IndexReader reader = DirectoryReader.open(directory); // c) 创建IndexSearcher对象 IndexSearcher searcher = new IndexSearcher(reader); // d) 通过IndexSearcher对象执行查询索引库,返回TopDocs对象 // 第一个参数:查询对象 // 第二个参数:最大的n条记录 TopDocs topDocs = searcher.search(query, 10); // e) 提取TopDocs对象中前n条记录 ScoreDoc[] scoreDocs = topDocs.scoreDocs; System.out.println("查询出文档个数为:" + topDocs.totalHits); for (ScoreDoc scoreDoc : scoreDocs) { // 文档对象ID int docId = scoreDoc.doc; Document doc = searcher.doc(docId); // f) 输出文档内容 System.out.println("==============================="); System.out.println("文档id:" + docId); System.out.println("图书id:" + doc.get("id")); System.out.println("图书name:" + doc.get("name")); System.out.println("图书price:" + doc.get("price")); System.out.println("图书pic:" + doc.get("pic")); System.out.println("图书description:" + doc.get("description")); } // g) 关闭IndexReader reader.close(); } |
4 Field域
4.1 Field属性
Field是文档中的域,包括Field名和Field值两部分,一个文档可以包括多个Field,Document只是Field的一个承载体,Field值即为要索引的内容,也是要搜索的内容。
l 是否分词(tokenized)
是:作分词处理,即将Field值进行分词,分词的目的是为了索引。
比如:商品名称、商品简介等,这些内容用户要输入关键字搜索,由于搜索的内容格式大、内容多需要分词后将语汇单元索引。
否:不作分词处理
比如:商品id、订单号、身份证号等
l 是否索引(indexed)
是:进行索引。将Field分词后的词或整个Field值进行索引,索引的目的是为了搜索。
比如:商品名称、商品简介分析后进行索引,订单号、身份证号不用分析但也要索引,这些将来都要作为查询条件。
否:不索引。该域的内容无法搜索到
比如:商品id、文件路径、图片路径等,不用作为查询条件的不用索引。
l 是否存储(stored)
是:将Field值存储在文档中,存储在文档中的Field才可以从Document中获取。
比如:商品名称、订单号,凡是将来要从Document中获取的Field都要存储。
否:不存储Field值,不存储的Field无法通过Document获取
比如:商品简介,内容较大不用存储。如果要向用户展示商品简介可以从系统的关系数据库中获取商品简介。
4.2 Field常用类型
下边列出了开发中常用的Filed类型,注意Field的属性,根据需求选择:
Field类 | 数据类型 | Analyzed 是否分词 | Indexed 是否索引 | Stored 是否存储 | 说明 |
StringField(FieldName, FieldValue,Store.YES)) | 字符串 | N | Y | Y或N | 这个Field用来构建一个字符串Field,但是不会进行分词,会将整个串存储在索引中,比如(订单号,身份证号等) 是否存储在文档中用Store.YES或Store.NO决定 |
LongField(FieldName, FieldValue,Store.YES) | Long型 | Y | Y | Y或N | 这个Field用来构建一个Long数字型Field,进行分词和索引,比如(价格) 是否存储在文档中用Store.YES或Store.NO决定 |
StoredField(FieldName, FieldValue) | 重载方法,支持多种类型 | N | N | Y | 这个Field用来构建不同类型Field 不分析,不索引,但要Field存储在文档中 |
TextField(FieldName, FieldValue, Store.NO) 或 TextField(FieldName, reader) | 字符串 或 流 | Y | Y | Y或N | 如果是一个Reader, lucene猜测内容比较多,会采用Unstored的策略. |
4.3 Field代码修改如下
图书id:
是否分词:不用分词,因为不会根据商品id来搜索商品
是否索引:不索引,因为不需要根据图书ID进行搜索
是否存储:要存储,因为查询结果页面需要使用id这个值。
图书名称:
是否分词:要分词,因为要将图书的名称内容分词索引,根据关键搜索图书名称抽取的词。
是否索引:要索引。
是否存储:要存储。
图书价格:
是否分词:要分词,lucene对数字型的值只要有搜索需求的都要分词和索引,因为lucene对数字型的内容要特殊分词处理,本例子可能要根据价格范围搜索,需要分词和索引。
是否索引:要索引
是否存储:要存储
图书图片地址:
是否分词:不分词
是否索引:不索引
是否存储:要存储
图书描述:
是否分词:要分词
是否索引:要索引
是否存储:因为图书描述内容量大,不在查询结果页面直接显示,不存储。
不存储是来不在lucene的索引文件中记录,节省lucene的索引文件空间,如果要在详情页面显示描述,思路:
从lucene中取出图书的id,根据图书的id查询关系数据库中book表得到描述信息。
// 图书ID // 参数:域名、域中存储的内容、是否存储 // 不分词、索引、要存储 // Field id = new TextField("id", book.getId().toString(), // Store.YES); Field id = new StoredField("id", book.getId().toString(), Store.YES); // 图书名称 // 分词、索引、存储 Field bookname = new TextField("bookname", book.getName(), Store.YES); // 图书价格 // 分词、索引、存储 Field price = new FloatField("price", book.getPrice(), Store.YES); // 图书图片 // 不分词、不索引、要存储 Field pic = new StoredField("pic", book.getPic()); // 图书描述 // 分词、索引、不存储 Field description = new TextField("description", book.getDescription(), Store.NO); |
5 索引维护
5.1 需求
管理人员通过电商系统更改图书信息,这时更新的是数据库,如果使用lucene搜索图书信息需要在数据库表book信息变化时及时更新lucene索引库。
5.2 添加索引
调用indexWriter.addDocument(doc)添加索引。
参考入门程序的创建索引。
5.3 删除索引
5.3.1 删除指定索引
根据Term项删除索引,满足条件的将全部删除。
// 删除索引 @Test publicvoid deleteIndex() throws Exception { // 1、指定索引库目录 Directory directory = FSDirectory.open(new File("E:\\11-index\\0720")); // 2、创建IndexWriterConfig IndexWriterConfig cfg = new IndexWriterConfig(Version.LATEST, new StandardAnalyzer()); // 3、创建IndexWriter IndexWriter writer = new IndexWriter(directory, cfg); // 4、通过IndexWriter来删除索引 // b)、删除指定索引 writer.deleteDocuments(new Term("filename", "apache")); // 5、关闭IndexWriter writer.close(); } |
5.3.2 删除全部索引(慎用)
将索引目录的索引信息全部删除,直接彻底删除,无法恢复。慎用!!!
// 删除索引 @Test publicvoid deleteIndex() throws Exception { // 1、指定索引库目录 Directory directory = FSDirectory.open(new File("E:\\11-index\\0720")); // 2、创建IndexWriterConfig IndexWriterConfig cfg = new IndexWriterConfig(Version.LATEST, new StandardAnalyzer()); // 3、创建IndexWriter IndexWriter writer = new IndexWriter(directory, cfg); // 4、通过IndexWriter来删除索引 // a)、删除全部索引 writer.deleteAll(); // 5、关闭IndexWriter writer.close(); } |
建议参照关系数据库基于主键删除方式,所以在创建索引时需要创建一个主键Field,删除时根据此主键Field删除。
索引删除后将放在Lucene的回收站中,Lucene3.X版本可以恢复删除的文档,3.X之后无法恢复。
5.4 修改索引
更新索引是先删除再添加,建议对更新需求采用此方法并且要保证对已存在的索引执行更新,可以先查询出来,确定更新记录存在执行更新操作。
// 修改索引 @Test publicvoid updateIndex() throws Exception { // 1、指定索引库目录 Directory directory = FSDirectory.open(new File("E:\\11-index\\0720")); // 2、创建IndexWriterConfig IndexWriterConfig cfg = new IndexWriterConfig(Version.LATEST, new StandardAnalyzer()); // 3、创建IndexWriter IndexWriter writer = new IndexWriter(directory, cfg); // 4、通过IndexWriter来修改索引 // a)、创建修改后的文档对象 Document document = new Document();
// 文件名称 Field filenameField = new StringField("filename", "updateIndex", Store.YES); document.add(filenameField);
// 修改指定索引为新的索引 writer.updateDocument(new Term("filename", "apache"), document);
// 5、关闭IndexWriter writer.close(); } |
6 搜索
6.1 创建查询的两种方法
对要搜索的信息创建Query查询对象,Lucene会根据Query查询对象生成最终的查询语法。类似关系数据库Sql语法一样,Lucene也有自己的查询语法,比如:“name:lucene”表示查询Field的name为“lucene”的文档信息。
可通过两种方法创建查询对象:
1)使用Lucene提供Query子类
Query是一个抽象类,lucene提供了很多查询对象,比如TermQuery项精确查询,NumericRangeQuery数字范围查询等。
如下代码:
Queryquery = new TermQuery(new Term("name", "lucene"));
2)使用QueryParse解析查询表达式
QueryParser会将用户输入的查询表达式解析成Query对象实例。
如下代码:
QueryParser queryParser = new QueryParser("name", new IKAnalyzer());
Queryquery = queryParser.parse("name:lucene");
6.2 通过Query子类搜索
6.2.1 TermQuery
TermQuery项查询,TermQuery不使用分析器,搜索关键词作为整体来匹配Field域中的词进行查询,比如订单号、分类ID号等。
privatevoid doSearch(Query query) { IndexReader reader = null; try { // a) 指定索引库目录 Directory indexdirectory = FSDirectory.open(new File( "E:\\11-index\\0720")); // b) 创建IndexReader对象 reader = DirectoryReader.open(indexdirectory); // c) 创建IndexSearcher对象 IndexSearcher searcher = new IndexSearcher(reader); // d) 通过IndexSearcher对象执行查询索引库,返回TopDocs对象 // 第一个参数:查询对象 // 第二个参数:最大的n条记录 TopDocs topDocs = searcher.search(query, 10); // e) 提取TopDocs对象中的文档ID,如何找出对应的文档 ScoreDoc[] scoreDocs = topDocs.scoreDocs; System.out.println("总共查询出的结果总数为:" + topDocs.totalHits); Document doc; for (ScoreDoc scoreDoc : scoreDocs) { // 文档对象ID int docId = scoreDoc.doc; doc = searcher.doc(docId); // f) 输出文档内容 System.out.println(doc.get("filename")); System.out.println(doc.get("path")); System.out.println(doc.get("size")); } } catch (IOException e) { e.printStackTrace(); } finally { if (reader != null) { try { reader.close(); } catch (IOException e) { e.printStackTrace(); } } } }
@Test publicvoid testTermQuery() throws Exception { // 1、创建查询(Query对象) Query query = new TermQuery(new Term("filename", "apache")); // 2、执行搜索 doSearch(query); } |
6.2.2 NumbericRangeQuery
NumericRangeQuery,指定数字范围查询.
@Test publicvoid testNumbericRangeQuery() throws Exception { // 创建查询 // 第一个参数:域名 // 第二个参数:最小值 // 第三个参数:最大值 // 第四个参数:是否包含最小值 // 第五个参数:是否包含最大值 Query query = NumericRangeQuery.newLongRange("size", 1l, 100l, true,true); // 2、执行搜索 doSearch(query); }
|
6.2.3 BooleanQuery
BooleanQuery,布尔查询,实现组合条件查询。
@Test publicvoid booleanQuery() throws Exception { BooleanQuery query = new BooleanQuery(); Query query1 = new TermQuery(new Term("id", "3")); Query query2 = NumericRangeQuery.newFloatRange("price", 10f, 200f, true, true);
//MUST:查询条件必须满足,相当于AND //SHOULD:查询条件可选,相当于OR //MUST_NOT:查询条件不能满足,相当于NOT非 query.add(query1, Occur.MUST); query.add(query2, Occur.SHOULD);
System.out.println(query);
search(query); } |
组合关系代表的意思如下:
1、MUST和MUST表示“与”的关系,即“并集”。
2、MUST和MUST_NOT前者包含后者不包含。
3、MUST_NOT和MUST_NOT没意义
4、SHOULD与MUST表示MUST,SHOULD失去意义;
5、SHOUlD与MUST_NOT相当于MUST与MUST_NOT。
6、SHOULD与SHOULD表示“或”的概念。
6.3 通过QueryParser搜索
通过QueryParser也可以创建Query,QueryParser提供一个Parse方法,此方法可以直接根据查询语法来查询。Query对象执行的查询语法可通过System.out.println(query);查询。
6.3.1 QueryParser
6.3.1.1 代码实现
@Test publicvoid testQueryParser() throws Exception { // 创建QueryParser // 第一个参数:默认域名 // 第二个参数:分词器 QueryParser queryParser = new QueryParser("name", new IKAnalyzer()); // 指定查询语法,如果不指定域,就搜索默认的域 Query query = queryParser.parse("lucene"); System.out.println(query); // 2、执行搜索 doSearch(query);
} |
6.3.1.2 查询语法
1、基础的查询语法,关键词查询:
域名+“:”+搜索的关键字
例如:content:java
2、范围查询
域名+“:”+[最小值 TO 最大值]
例如:size:[1 TO 1000]
注意:QueryParser不支持对数字范围的搜索,它支持字符串范围。数字范围搜索建议使用NumericRangeQuery。
3、组合条件查询
Occur.MUST 查询条件必须满足,相当于and | +(加号) |
Occur.SHOULD 查询条件可选,相当于or
| 空(不用符号) |
Occur.MUST_NOT 查询条件不能满足,相当于not非 | -(减号) |
1)+条件1 +条件2:两个条件之间是并且的关系and
例如:+filename:apache +content:apache
2)+条件1 条件2:必须满足第一个条件,忽略第二个条件
例如:+filename:apache content:apache
3)条件1 条件2:两个条件满足其一即可。
例如:filename:apache content:apache
4)-条件1 条件2:必须不满足条件1,要满足条件2
例如:-filename:apache content:apache
第二种写法:
条件1 AND 条件2
条件1 OR 条件2
条件1 NOT 条件2
6.3.2 MultiFieldQueryParser
通过MuliFieldQueryParse对多个域查询。
@Test publicvoid testMultiFieldQueryParser() throws Exception { // 可以指定默认搜索的域是多个 String[] fields = { "name", "description" }; // 创建一个MulitFiledQueryParser对象 QueryParser parser = new MultiFieldQueryParser(fields, new IKAnalyzer()); // 指定查询语法,如果不指定域,就搜索默认的域 Query query = parser.parse("lucene"); // 2、执行搜索 doSearch(query); } |
6.4 TopDocs
Lucene搜索结果可通过TopDocs遍历,TopDocs类提供了少量的属性,如下:
方法或属性 | 说明 |
totalHits | 匹配搜索条件的总记录数 |
scoreDocs | 顶部匹配记录 |
注意:
Search方法需要指定匹配记录数量n:indexSearcher.search(query, n)
TopDocs.totalHits:是匹配索引库中所有记录的数量
TopDocs.scoreDocs:匹配相关度高的前边记录数组,scoreDocs的长度小于等于search方法指定的参数n
7 相关度排序
7.1 什么是相关度排序
相关度排序是查询结果按照与查询关键字的相关性进行排序,越相关的越靠前。比如搜索“Lucene”关键字,与该关键字最相关的文章应该排在前边。
7.2 相关度打分
Lucene对查询关键字和索引文档的相关度进行打分,得分高的就排在前边。如何打分呢?Lucene是在用户进行检索时实时根据搜索的关键字计算出来的,分两步:
1)计算出词(Term)的权重
2)根据词的权重值,计算文档相关度得分。
什么是词的权重?
通过索引部分的学习,明确索引的最小单位是一个Term(索引词典中的一个词),搜索也是要从Term中搜索,再根据Term找到文档,Term对文档的重要性称为权重,影响Term权重有两个因素:
l Term Frequency (tf):
指此Term在此文档中出现了多少次。tf越大说明越重要。
词(Term)在文档中出现的次数越多,说明此词(Term)对该文档越重要,如“Lucene”这个词,在文档中出现的次数很多,说明该文档主要就是讲Lucene技术的。
l Document Frequency (df):
指有多少文档包含次Term。df 越大说明越不重要。
比如,在一篇英语文档中,this出现的次数更多,就说明越重要吗?不是的,有越多的文档包含此词(Term), 说明此词(Term)太普通,不足以区分这些文档,因而重要性越低。
7.3 设置boost值影响相关度排序
boost是一个加权值(默认加权值为1.0f),它可以影响权重的计算。
l 在索引时对某个文档中的field设置加权值高,在搜索时匹配到这个文档就可能排在前边。
l 在搜索时对某个域进行加权,在进行组合域查询时,匹配到加权值高的域最后计算的相关度得分就高。
设置boost是给域(field)或者Document设置的。
7.3.1 在创建索引时设置
如果希望某些文档更重要,当此文档中包含所要查询的词则应该得分较高,这样相关度排序可以排在前边,可以在创建索引时设定文档中某些域(Field)的boost值来实现,如果不进行设定,则Field Boost默认为1.0f。一旦设定,除非删除此文档,否则无法改变。
7.3.1.1 代码实现
@Test publicvoid setBoost4createIndex() throws Exception { // 创建分词器 Analyzer analyzer = new StandardAnalyzer();
IndexWriterConfig cfg = new IndexWriterConfig(Version.LUCENE_4_10_3, analyzer); Directory directory = FSDirectory.open(new File("E:\\11-index\\0728")); // 创建IndexWriter对象,通过它把分好的词写到索引库中 IndexWriter writer = new IndexWriter(directory, cfg);
Document doc = new Document(); Field id = new StringField("id", "11", Store.YES); Field description = new TextField("description", "测试设置BOOST值 lucene", Store.YES); // 设置boost description.setBoost(10.0f); // 把域添加到文档中 doc.add(id); doc.add(description); writer.addDocument(doc); // 关闭IndexWriter writer.close(); } |
7.3.1.2 输出
7.3.2 在查询索引时设置
7.3.2.1 代码实现
7.3.2.2 输出
未设置加权值:
8 中文分词器
8.1 什么是中文分词器
学过英文的都知道,英文是以单词为单位的,单词与单词之间以空格或者逗号句号隔开。而中文则以字为单位,字又组成词,字和词再组成句子。所以对于英文,我们可以简单以空格判断某个字符串是否为一个单词,比如I loveChina,love 和 China很容易被程序区分开来;但中文“我爱中国”就不一样了,电脑不知道“中国”是一个词语还是“爱中”是一个词语。把中文的句子切分成有意义的词,就是中文分词,也称切词。我爱中国,分词的结果是:我爱 中国。
8.2 Lucene自带的中文分词器
l StandardAnalyzer:
单字分词:就是按照中文一个字一个字地进行分词。如:“我爱中国”,
效果:“我”、“爱”、“中”、“国”。
l CJKAnalyzer
二分法分词:按两个字进行切分。如:“我是中国人”,效果:“我是”、“是中”、“中国”“国人”。
上边两个分词器无法满足需求。
8.3 第三方中文分词器
l paoding: 庖丁解牛最新版在 https://code.google.com/p/paoding/ 中最多支持Lucene 3.0,且最新提交的代码在 2008-06-03,在svn中最新也是2010年提交,已经过时,不予考虑。
l mmseg4j:最新版已从 https://code.google.com/p/mmseg4j/ 移至 https://github.com/chenlb/mmseg4j-solr,支持Lucene 4.10,且在github中最新提交代码是2014年6月,从09年~14年一共有:18个版本,也就是一年几乎有3个大小版本,有较大的活跃度,用了mmseg算法。
l IK-analyzer:最新版在https://code.google.com/p/ik-analyzer/上,支持Lucene 4.10从2006年12月推出1.0版开始, IKAnalyzer已经推出了4个大版本。最初,它是以开源项目Luence为应用主体的,结合词典分词和文法分析算法的中文分词组件。从3.0版本开 始,IK发展为面向Java的公用分词组件,独立于Lucene项目,同时提供了对Lucene的默认优化实现。在2012版本中,IK实现了简单的分词 歧义排除算法,标志着IK分词器从单纯的词典分词向模拟语义分词衍化。 但是也就是2012年12月后没有在更新。
l ansj_seg:最新版本在 https://github.com/NLPchina/ansj_seg tags仅有1.1版本,从2012年到2014年更新了大小6次,但是作者本人在2014年10月10日说明:“可能我以后没有精力来维护ansj_seg了”,现在由”nlp_china”管理。2014年11月有更新。并未说明是否支持Lucene,是一个由CRF(条件随机场)算法所做的分词算法。
l imdict-chinese-analyzer:最新版在 https://code.google.com/p/imdict-chinese-analyzer/ ,最新更新也在2009年5月,下载源码,不支持Lucene 4.10 。是利用HMM(隐马尔科夫链)算法。
l Jcseg:最新版本在git.oschina.net/lionsoul/jcseg,支持Lucene 4.10,作者有较高的活跃度。利用mmseg算法。
8.4 使用中文分词器IKAnalyzer
IKAnalyzer继承Lucene的Analyzer抽象类,使用IKAnalyzer和Lucene自带的分析器方法一样,将Analyzer测试代码改为IKAnalyzer测试中文分词效果。
如果使用中文分词器ik-analyzer,就在索引和搜索程序中使用一致的分词器ik-analyzer。
8.4.1 添加jar包
8.4.2 修改分词器代码
// 创建中文分词器 Analyzer analyzer = new IKAnalyzer(); |
8.5 扩展中文词库
从ikanalyzer包中拷贝配置文件到classpath下。
<?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">dicdata/mydict.dic</entry>
<!-- 用户可以在这里配置自己的扩展停用词字典 -->
<entry key="ext_stopwords">dicdata/ext_stopword.dic</entry>
</properties>
如果想配置扩展词和停用词,就创建扩展词的文件和停用词的文件,文件的编码要是utf-8。
注意:不要用记事本保存扩展词文件和停用词文件,那样的话,格式中是含有bom的。
添加扩展词文件:ext.dic,内容如下:
8.6 使用luke测试中文分词
使用Luke测试第三方分词器分词效果,需通过java.ext.dirs加载jar包:
可简单的将第三方分词器和lukeall放在一块儿,cmd下运行:
java -Djava.ext.dirs=. -jarlukeall-4.10.3.jar