2.1.开发环境准备 2.1.1.数据库jar包 我们这里可以尝试着从数据库中采集数据,因此需要连接数据库,我们一直用MySQL,所以这里需要MySQL的jar包 2.1.2.MyBatis的jar包(可选) 从数据库采集数据就需要查询数据库,我们可以用jdbc原生的写DAO,还可以使用我们之前学习过的MyBatis动态代理DAO,因此可能需要MyBatis的jar包 2.1.3.数据库环境 数据库脚本:【资料数据库ook.sql】,创建一个lucene数据库(utf-8),然后导入这个脚本。 2.1.4.新建java工程 由于是模拟练习,所以主要是学习Lucene的开发jar包的使用,所以普通的Java工程就可以。 2.2.开发代码准备 2.2.1.MyBatis持久层开发 1. 创建pojo package cn.baidu.pojo; public class Book { // 图书ID private Integer id; // 图书名称 private String name; // 图书价格 private Float price; // 图书图片 private String pic; // 图书描述 private String desc; getter/setter方法。。。。。。 } 2. 创建DAO接口 package cn.baidu.dao; import java.util.List; import cn.baidu.pojo.Book; public interface BookDao { public List<Book> queryBookList() throws Exception; } 3. 创建映射文件 <?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd"> <mapper namespace="cn.baidu.dao.BookDao"> <select id="queryBookList" resultType="cn.baidu.pojo.Book"> select id, name, price, pic, description as `desc` from book </select> </mapper> 2.2.2.创建索引的准备代码 在【cn.baidu.test】中创建【CreateIndexTest.java】 /** * 使用中文分析器IKAnalyzer创建索引 * * @author Derek Sun */ public class CreateIndexTest { /** * 创建索引的准备工作(使用IK分析器) */ private IndexWriter createIndexWriter(String indexRepositoryPath) throws Exception { // 创建Directory对象 Directory dir = FSDirectory.open(new File(indexRepositoryPath)); // 创建一个标准分析器 Analyzer analyzer = new IKAnalyzer(); // 创建IndexWriterConfig对象 // 参数1: Lucene的版本信息, 可以选择对应的Lucene版本也可以使用LATEST // 参数2: 分析器对象 IndexWriterConfig config = new IndexWriterConfig(Version.LATEST, analyzer); // 创建IndexWriter对象 return new IndexWriter(dir, config); } /** * 从数据库采集数据 */ private List<Book> getBookInfoFromDB() throws Exception { SqlSession sqlSession = null; try { InputStream inputStream = Resources.getResourceAsStream("MyBatisConfig.xml"); SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream); sqlSession = sqlSessionFactory.openSession(); BookDao bookDao = sqlSession.getMapper(BookDao.class); List<Book> bookList = bookDao.queryBookList(); return bookList; } catch(Exception e) { e.printStackTrace(); throw e; } finally { sqlSession.close(); } } /** * 把数据库中的数据创建索引 */ @Test public void testCreateIndex() throws Exception { // 。。。。。。 } } 3.Field域 3.1.Field的属性 Field域是Document文档的基本构成元素,和数据库表的字段类似,用于存储不同的数据,包括Field名和Field值两部分。Field域和数据库表字段一样也有不同类型的Field域。 我们先不讨论Field域的类型,首先不管是什么类型的Field域都会有三个共同的属性: 1.是否分词(tokenized):是否对域的内容进行分词处理。 前提:域内容需要查询时,内容多的分,内容少的不分;域内容不需要查询时不分。 比如:商品名称、商品描述等,这些内容都是查询信息的重点部分,而且内容多,因此需要分词 比如:商品id、订单号、身份证号等,这些内容也是查询的部分,但不需要分词。 2.是否索引(indexed):将Field分析后的词或整个Field的值进行索引,只有索引方可搜索到。 前提:域内容需要查询时索引,不需要查询时不索引。 比如:商品名称、商品描述分析后进行索引;商品id、订单号、身份证号不用分词但也要索引,这些将来都要作为查询条件。 比如:图片路径、文件路径等,不用作为查询条件就不用索引。 3.是否存储(stored):将Field值存储在文档中,存储在文档中的Field才可以从Document中获取。 前提:是否要在搜索结果中将内容展示给用户。 比如:商品名称、订单号,凡是将来要从Document中获取的内容都要存储。 比如:商品描述,内容较大不用存储,可以节省lucene的索引文件空间。如果要向用户展示商品描述可以从系统的关系数据库中获取。 3.2.常用Field类型 说完三个共同的属性后,我们再来看看Lucene的Field都有哪些常用类型: 不同类型的Field的上面三个共同属性的值会不同,用户可以根据此选用不同类型的Field完成业务需求。 Field类 数据类型 tokenized是否分词 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的策略. 3.3.修改Field 3.3.1.修改分析 对昨天的创建索引的代码中创建的Field对象进行类型的修改,根据不同的字段使用合适类型的Field类型: 1.图书id 是否分词:不分词,因为不会根据商品id来搜索商品 是否索引:索引,因为可能需要根据图书ID进行搜索 是否存储:要存储,因为查询结果页面需要使用id这个值。 使用Field:StringField(FieldName, FieldValue, Store.YES) 2.图书名称 是否分词:要分词,因为要根据图书名称的关键词搜索。 是否索引:要索引。 是否存储:要存储。 使用Field:TextField(FieldName, FieldValue, Store.YES) 3.图书价格 是否分词:要分词,lucene对数字型的值只要有搜索需求的都要分词和索引,因为lucene对数字型的内容要特殊 分词处理,需要分词和索引。 是否索引:要索引 是否存储:要存储 使用Field:FloatField(FieldName, FieldValue, Store.YES) 4.图书图片地址 是否分词:不分词 是否索引:不索引 是否存储:要存储 使用Field:StoredField(FieldName, FieldValue) 5.图书描述 是否分词:要分词 是否索引:要索引 是否存储:因为图书描述内容量大,不在查询结果页面直接显示,不存储(不存储是指不在lucene的Field域中保存) 使用Field:TextField(FieldName, FieldValue, Store.NO) 如果要在详情页面显示详细的描述内容信息,解决方案: 从lucene中取出图书的id,根据图书的id查询关系数据库(MySQL)中book表得到描述信息。 3.3.2.代码修改 对之前编写的testCreateIndex()方法进行修改。让不同的列根据需求使用合适类型的Field类 代码片段 // Document文档中添加Field域 // 图书id(不分词、不索引、只存储) document.add(new StringField("id", book.getId().toString(), Store.YES)); // 图书名称(分词、索引、存储) document.add(new TextField("name", book.getName().toString(), Store.YES)); // 图书价格(分词、索引、存储) document.add(new FloatField("price", book.getPrice(), Store.YES)); // 图书图片地址(不分词、不索引、只存储) document.add(new StoredField("pic", book.getPic().toString())); // 图书描述(分词、索引、不存储) document.add(new TextField("desc", book.getDescription().toString(), Store.NO)); 图片地址没有作为索引条目: 4.索引维护 4.1.需求 当采集的原数据发生变化的时候,Lucene创建的索引库也要跟着同步变化。比如:管理人员通过电商系统更改图书信息,这时更新的是关系数据库,如果使用lucene搜索图书信息,需要在数据库表book信息变化时及时同步更新到lucene的索引库。 4.2.添加索引 4.2.1.事前准备 1.添加新索引前目录【C:mydir 3_workspaceluceneindexDB】 2.用Luke查看【name】Field域的term词项,结果如下:(添加前) 查看document对象的数量是五个 3. 先在扩展词库中增加两个扩展词: 注意:词库文件在eclipse不能直接打开,如果在eclipse中打开会直接用记事本打开,但是记事本一保存可能会保存出带bom头的utf-8格式的文件,所以必须在外面通过专业记事本修改,但要注意这时候要直接去该编译后路径下的文件【bin】下的,如果你改的还是【config】下的就不会被自动编译到bin下,就不会起作用,但可以你还很纳闷为什么不好用。 在bin下打开【ext.dic】,增加两个扩展词 4.2.2.代码实现 @Test public void testAddIndex() throws Exception { // 第一步:创建IndexWriter IndexWriter indexWriter = createIndexWriter("C:\mydir\03_workspace\lucene\indexDB"); // 创建两个文档对象 Document doc1 = new Document(); Document doc2 = new Document(); // 给第一个文档对象添加域 // id doc1.add(new StringField("id", "6", Store.YES)); // 图书名称 doc1.add(new TextField("name", "传智播客", Store.YES)); // 图书描述 doc1.add(new TextField("desc", "新增document2", Store.NO)); // 给第二个文档对象添加域 // id doc2.add(new StringField("id", "7", Store.YES)); // 图书名称 doc2.add(new TextField("name", "baidu", Store.YES)); // 图书描述 doc2.add(new TextField("desc", "新增document3", Store.NO)); // 创建索引 indexWriter.addDocument(doc1); indexWriter.addDocument(doc2); // 关闭IndexWriter对象 indexWriter.close(); } 4.2.3.测试 1. 查看索引库目录:红框中的是新添加的索引文件 注意:新增的索引和document对象和原来的不是在同一个文件中。 2. Luke重新读入索引库目 增加了两个document对象: 4.3.删除索引 4.3.1.删除指定的文档对象 根据term项删除指定的文档对象,同时会让对应的索引项失效,索引失效但不会被一同删除,仍然保留在索引表中: 代码【DeleteIndexTest.java】 public class DeleteIndexTest { /** * 根据Term删除 * @throws Exception */ @Test public void test() throws Exception { // 创建目录对象,指定索引路径 Directory dir = FSDirectory.open(new File("C:\mydir\03_workspace\lucene\indexDB")); // 创建分析器 Analyzer analyzer = new IKAnalyzer(); // 创建写入配置 IndexWriterConfig config = new IndexWriterConfig(Version.LUCENE_4_10_3, analyzer); // 创建写入对象 IndexWriter indexWriter = new IndexWriter(dir, config); // 根据一个term对象删除索引 indexWriter.deleteDocuments(new Term("name", "baidu")); // 释放资源 indexWriter.close(); } } 执行后,用Luke查看结果: term索引项仍然存在,没有发生变化,但已经失效了,原因是对应的document对象已经被删除了。 使用【name:baidu】条件查询没有搜索到,说明这个索引已经失效 此时的document对象没有被真正的删除而是放到了类似windows回收站的删除文件中了: 注意:放到删除文件中的ducoment对象在Lucene3.X版本可以恢复删除的文档,3.X之后无法恢复。 如果想要彻底删除指定的索引,需要强制清空“回收站”:执行【DeleteIndexTest.java】的【test2】 /** * 普通删除+强制情况回收站 * * @throws Exception */ @Test public void test2() throws Exception { // 创建目录对象,指定索引路径 Directory dir = FSDirectory.open(new File("C:\mydir\03_workspace\lucene\indexDB")); // 创建分析器 Analyzer analyzer = new IKAnalyzer(); // 创建写入配置 IndexWriterConfig config = new IndexWriterConfig(Version.LUCENE_4_10_3, analyzer); // 创建写入对象 IndexWriter indexWriter = new IndexWriter(dir, config); // 根据一个term对象删除索引 indexWriter.deleteDocuments(new Term("name", "baidu")); // 强制清空回收站 indexWriter.forceMergeDeletes(); // 释放资源 indexWriter.close(); } 强制清空回收站后索引库文件发生了变化: 用Luke查看删除后的索引库:name:baidu的term索引项已经没有了: Document就剩六个了,而且docID的位置也没给留,原先的docID为6的就是name:baidu的那条,已经被删除了: 用【name:baidu】条件查询自然也是什么都查不到了: 4.3.2.删除全部索引(慎用) 将索引目录的索引信息全部删除,直接彻底删除,无法恢复。 执行【DeleteIndexTest.java】的【test3】 /** * 全部删除 * @throws Exception */ @Test public void test3() throws Exception { // 创建目录对象,指定索引路径 Directory dir = FSDirectory.open(new File("C:\mydir\03_workspace\lucene\indexDB")); // 创建分析器 Analyzer analyzer = new IKAnalyzer(); // 创建写入配置 IndexWriterConfig config = new IndexWriterConfig(Version.LUCENE_4_10_3, analyzer); // 创建写入对象 IndexWriter indexWriter = new IndexWriter(dir, config); // 根据一个term对象删除索引 indexWriter.deleteAll(); // 释放资源 indexWriter.close(); } 删除后的索引目录: 索引域数据清空 文档域数据也清空 说明: 建议参照关系数据库的根据主键删除的方式,所以索引中创建document对象时,需要创建一个主键Field,删除时根据此主键Field删除。这样比较安全。 4.3.3.删除小结 1.上面使用IndexWriter的deleteDocuments(term)方法的删除就是Lucene的普通删除方法。只是将document对象删除同时使对应的索引项失效,并没有真的删除。如果想真正的删除就要强制清空回收站,即执行:forceMergeDeletes()方法。 2.慎用deleteAll()。 4.4.修改索引 更新索引采用的是先删后增的方式,即先删除符合条件的索引(即上面刚学完的普通删除),然后再添加新的索引,生成新的索引文件。更新条件依然是根据term词项更新。 更新前索引库的目录: Document还在: 执行【UpdateIndexTest.java】 package cn.baidu.test; import java.io.File; import org.apache.lucene.analysis.Analyzer; import org.apache.lucene.document.Document; import org.apache.lucene.document.Field.Store; import org.apache.lucene.document.StoredField; import org.apache.lucene.document.TextField; import org.apache.lucene.index.IndexWriter; import org.apache.lucene.index.IndexWriterConfig; import org.apache.lucene.index.Term; import org.apache.lucene.store.Directory; import org.apache.lucene.store.FSDirectory; import org.apache.lucene.util.Version; import org.junit.Test; import org.wltea.analyzer.lucene.IKAnalyzer; public class UpdateIndexTest { @Test public void test() throws Exception { // 创建分析器对象 Analyzer analyzer = new IKAnalyzer(); // 创建目录对象,指定索引库目录 Directory dir = FSDirectory.open(new File("C:\mydir\03_workspace\lucene\indexDB")); // 创建写入的配置信息 IndexWriterConfig config = new IndexWriterConfig(Version.LUCENE_4_10_3, analyzer); // 创建写入对象 IndexWriter indexWriter = new IndexWriter(dir, config); // 创建一个新的document对象 Document doc = new Document(); doc.add(new StringField("id", 8)); doc.add(new TextField("name", "luceneUpdateTest", Store.YES)); // 根据term词项更新 indexWriter.updateDocument(new Term("name", "java"), doc); // 释放资源 indexWriter.close(); } } 更新后索引库的目录: Luke查看索引库:name:java的term索引项还在,又新增了name:luceneupdatetest(这里经过分析器的过滤器后大写转变为小写了): 原来的文档已经不存在了,进了“回收站”: 新增了一个document: 注意:如果更新索引的目标文档对象不存在,则直接执行添加。 建议: 为了避免误更新,建议对更新需求采用先查询出来后,确定要更新的记录存在的前提下再执行更新操作。 5.Lucene索引库查询(重点) 5.1.创建查询的两种方法 Lucene要搜索信息需要通过Query查询对象进行。Lucene会根据Query查询对象生成最终的查询语法,类似SQL语法一样。可通过两种方法创建查询对象: 1.通过Query子类搜索 2.使用QueryParse解析查询表达式同时生成Query对象 5.2.通过Query子类查询 Query是一个抽象类,lucene提供了很多查询对象,比如TermQuery项精确查询,NumericRangeQuery数字范围查询等。 程序员可以利用Query的子类手动创建查询对象,但这种查询对象是没有分析器的,因此程序员写了什么样的条件就直接用什么样的条件进行查询,而不会进行词汇分析、语言分析等处理。所以通过Query子类的查询在实际项目中使用的不多。但学习它主要的目的是通过这种最基本的查询对象的学习,让搜索技术的初学者对查询索引有一个基础的认识,为后面学习复杂的查询奠定基础。 抽取搜索逻辑: private void doSearch(Query query) throws IOException { System.out.println("实际的查询条件:" + query); // 创建Directory对象 Directory directory = FSDirectory.open(new File("C:\mydir\03_workspace\lucene\indexDB")); // 创建IndexReader对象 IndexReader reader = DirectoryReader.open(directory); // 创建IndexSearcher对象 IndexSearcher searcher = new IndexSearcher(reader); // 执行搜索,返回结果集TopDocs // 参数1:查询条件对象,参数2:返回的数据条数,指定查询结果最顶部的n条数据返回 TopDocs topDocs = searcher.search(query, 10); System.out.println("查询到的数据总条数是:" + topDocs.totalHits); // 获取查询结果集 ScoreDoc[] docs = topDocs.scoreDocs; // 遍历结果集 for (ScoreDoc scoreDoc : docs) { // 根据文档对象ID取得文档对象 int docID = scoreDoc.doc; Document doc = searcher.doc(docID); System.out.println("======================================"); System.out.println("docID:" + docID); System.out.println("bookId:" + 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("desc:" + doc.get("desc")); } // 3. 关闭IndexReader对象,释放资源 reader.close(); } 5.2.1.全部索引查询——MatchAllDocsQuery 查询索引目录下所有文档的全部内容:相当于【*:*】 // 用MatchAllDocsQuery对象查询 @Test public void testMatchAllDocsQuery() throws Exception { Query query = new MatchAllDocsQuery(); doSearch(query); } 结果: 实际的查询条件:*:* 查询到的数据总条数是:5 ====================================== docID:0 bookId:1 name:java 编程思想 price:71.5 pic:23488292934.jpg ====================================== docID:1 bookId:2 name:apache lucene price:66.0 pic:77373773737.jpg ====================================== docID:2 bookId:3 name:mybatis price:55.0 pic:88272828282.jpg ====================================== docID:3 bookId:4 name:spring price:56.0 pic:83938383222.jpg ====================================== docID:4 bookId:5 name:solr price:78.0 pic:99999229292.jpg 5.2.2.精确指定索引词项查询——TermQuery TermQuery:通过Term项查询。 TermQuery不使用分析器,所以不进行分析,是精确匹配,大小写敏感。所以建议匹配不分词的Field域查询,比如订单号、分类ID号等。而且只接受一个Term对象 【查询name域中包含luceneupdatetest的文档】 // 用TermQuery对象查询 @Test public void testByTermQuery() throws Exception { Query query = new TermQuery(new Term("name", "mybatis")); doSearch(query); } 结果: 实际的查询条件:name:mybatis 查询到的数据总条数是:1 ====================================== docID:2 bookId:3 name:mybatis price:55.0 pic:88272828282.jpg 因为在创建索引时经过分析name Filed域已经把大写转小写了,所以索引表中的term此项是【mybatis】而不是【MyBatis】 如果把条件改成:Query query = new TermQuery(new Term("name", "MyBatis")); 结果: 实际的查询条件:name:MyBatis 查询到的数据总条数是:0 5.2.3.数值范围查询——NumericRangeQuery NumericRangeQuery指定数字范围的查询,五个参数分别是:域名、最小值、最大值、是否包含最小值、是否包含最大值: /** * 使用NumericRangeQuery */ @Test public void testByNumericRangeQuery() throws Exception { Query query = NumericRangeQuery.newFloatRange("price", 55f, 66f, true, false); doSearch(query); System.out.println(" "); query = NumericRangeQuery.newFloatRange("price", 55f, 66f, false, true); doSearch(query); System.out.println(" "); query = NumericRangeQuery.newFloatRange("price", 55f, 66f, true, true); doSearch(query); } 结果:用方括号表示包含,用大括号表示不包含 实际的查询条件:price:[55.0 TO 66.0} 查询到的数据总条数是:2 ====================================== docID:2 bookId:3 name:mybatis price:55.0 pic:88272828282.jpg ====================================== docID:3 bookId:4 name:spring price:56.0 pic:83938383222.jpg 实际的查询条件:price:{55.0 TO 66.0] 查询到的数据总条数是:2 ====================================== docID:1 bookId:2 name:apache lucene price:66.0 pic:77373773737.jpg ====================================== docID:3 bookId:4 name:spring price:56.0 pic:83938383222.jpg 实际的查询条件:price:[55.0 TO 66.0] 查询到的数据总条数是:3 ====================================== docID:1 bookId:2 name:apache lucene price:66.0 pic:77373773737.jpg ====================================== docID:2 bookId:3 name:mybatis price:55.0 pic:88272828282.jpg ====================================== docID:3 bookId:4 name:spring price:56.0 pic:83938383222.jpg 5.2.4.组合条件查询——BooleanQuery BooleanQuery布尔查询,实现组合条件查询:它可以组合多个其他类型的query对象。 参数1:被组合的一个query对象 参数2:每加一个条件要指定是MUST,还是MUST_NOT,还是SHOULD,是对参数1条件的逻辑控制。 参数2的值: Occur.MUST:必须满足此条件 Occur.SHOULD:应该满足此条件(也可以不满足) Occur.MUST_NOT:必须不满足此条件 MUST、SHOULD、MUST_NOT的使用规则: MUST:在任何时候,与其他条件组合都有效。 MUST_NOT: 不能与MUST_NOT组合,否则没有任何结果返回 与其他条件组合都有效。 SHOULD: 不能与MUST组合,否则SHOULD条件失效 与其他条件组合都有效。 常用组合关系代表的意思如下: 1、MUST和MUST表示“与”的关系,即“交集”。 2、SHOULD与SHOULD表示“或”的关系,即“并集”。 3、MUST和MUST_NOT前者包含后者不包含。 4、MUST_NOT和MUST_NOT,什么都查询不出来 5、SHOULD与MUST,SHOULD控制的条件失效不被查询考虑,只查询MUST控制的条件。 6、SHOULD与MUST_NOT先查SHOULD,然后再用MUST_NOT过滤。 注意:从上面的组合可以得出,正常情况下SHOULD不应该跟MUST或MUST_NOT组合使用,SHOULD就跟SHOULD组合使用,这样才是正常的检索。MUST_NOT和MUST_NOT也是扯淡的。 示例【MUST与MUST】 /** * 使用BooleanQuery:MUST与MUST */ @Test public void testByBooleanQuery1() throws Exception { Query query1 = new TermQuery(new Term("name", "apache")); Query query2 = NumericRangeQuery.newFloatRange("price", 55f, 66f, false, true); BooleanQuery booleanQuery = new BooleanQuery(); booleanQuery.add(query1, Occur.MUST); // 书名带有apache的必须包含进来 booleanQuery.add(query2, Occur.MUST); // 价格大于55,小于等于66范围内的书籍必须包含进来 System.out.println("实际的查询条件:" + booleanQuery); // and相当于求它们的交际 doSearch(booleanQuery); } 结果: 实际的查询条件:+name:apache +price:{55.0 TO 66.0] 查询到的数据总条数是:1 ====================================== docID:1 bookId:2 name:apache lucene price:66.0 pic:77373773737.jpg 示例【SHOULD与SHOULD】:并集 @Test public void testByBooleanQuery6() throws Exception { Query query1 = new TermQuery(new Term("name", "lucene")); Query query2 = NumericRangeQuery.newFloatRange("price", 55f, 66f, true, false); BooleanQuery booleanQuery = new BooleanQuery(); booleanQuery.add(query1, Occur.SHOULD); // 书名带有mybatis的应该包含进来 booleanQuery.add(query2, Occur.SHOULD); // 价格大于等于55,小于66范围内的书籍应该包含 System.out.println("实际的查询条件:" + booleanQuery); // and相当于求它们的交际 doSearch(booleanQuery); } 结果: 实际的查询条件:name:lucene price:[55.0 TO 66.0} 查询到的数据总条数是:3 ====================================== docID:1 bookId:2 name:apache lucene price:66.0 pic:77373773737.jpg ====================================== docID:2 bookId:3 name:mybatis price:55.0 pic:88272828282.jpg ====================================== docID:3 bookId:4 name:spring price:56.0 pic:83938383222.jpg 示例【MUST与MUST_NOT】:前者包含后者不包含 /** * 使用BooleanQuery */ @Test public void testByBooleanQuery2() throws Exception { Query query1 = new TermQuery(new Term("name", "mybatis")); Query query2 = NumericRangeQuery.newFloatRange("price", 55f, 66f, true, false); BooleanQuery booleanQuery = new BooleanQuery(); booleanQuery.add(query1, Occur.MUST_NOT); // 书名带有mybatis的不能包含进来 booleanQuery.add(query2, Occur.MUST); // 价格大于等于55,小于66范围内的书籍 System.out.println("实际的查询条件:" + booleanQuery); // and相当于求它们的交际 doSearch(booleanQuery); } 结果: 实际的查询条件:-name:mybatis +price:[55.0 TO 66.0} 查询到的数据总条数是:1 ====================================== docID:3 bookId:4 name:spring price:56.0 pic:83938383222.jpg 示例【MUST_NOT和MUST_NOT】:没有意义,什么都查询不到 @Test public void testByBooleanQuery3() throws Exception { Query query1 = new TermQuery(new Term("name", "apache")); Query query2 = NumericRangeQuery.newFloatRange("price", 55f, 56f, true, true); BooleanQuery booleanQuery = new BooleanQuery(); booleanQuery.add(query1, Occur.MUST_NOT); // 书名带有apache的必须不包含进来 booleanQuery.add(query2, Occur.MUST_NOT); // 价格大于等于55,小于等于56范围内的书籍必须不包含进来 System.out.println("实际的查询条件:" + booleanQuery); // and相当于求它们的交际 doSearch(booleanQuery); } 结果: 实际的查询条件:-name:apache -price:[55.0 TO 56.0] 查询到的数据总条数是:0 MUST_NOT必须在它之前有条件确定下来一个固定范围内再进行排除条件过滤,但是两个MUST_NOT,谁也没有给定范围,所以最终查不到结果。 【SHOULD与MUST】:SHOULD控制的条件失效不被查询考虑,只查询MUST控制的条件。 @Test public void testByBooleanQuery4() throws Exception { Query query1 = new TermQuery(new Term("name", "lucene")); Query query2 = NumericRangeQuery.newFloatRange("price", 55f, 56f, true, true); BooleanQuery booleanQuery = new BooleanQuery(); booleanQuery.add(query1, Occur.SHOULD); // 书名带有lucene的应该包含进来(SHOULD控制的条件失效) booleanQuery.add(query2, Occur.MUST); // 价格大于等于55,小于等于56范围内的书籍必须包含进来 System.out.println("实际的查询条件:" + booleanQuery); // and相当于求它们的交际 doSearch(booleanQuery); } 结果: 实际的查询条件:name:lucene +price:[55.0 TO 56.0] 查询到的数据总条数是:2 ====================================== docID:2 bookId:3 name:mybatis price:55.0 pic:88272828282.jpg ====================================== docID:3 bookId:4 name:spring price:56.0 pic:83938383222.jpg 示例【SHOULD与MUST_NOT】:SHOULD条件有效,在SHOULD查询结果范围内用MUST_NOT过滤。 @Test public void testByBooleanQuery5() throws Exception { Query query1 = new TermQuery(new Term("name", "mybatis")); Query query2 = NumericRangeQuery.newFloatRange("price", 55f, 66f, true, false); BooleanQuery booleanQuery = new BooleanQuery(); booleanQuery.add(query1, Occur.MUST_NOT); // 书名带有mybatis的不能包含进来 booleanQuery.add(query2, Occur.SHOULD); // 价格大于等于55,小于66范围内的书籍应该包含 System.out.println("实际的查询条件:" + booleanQuery); // and相当于求它们的交际 doSearch(booleanQuery); } 结果: 实际的查询条件:-name:mybatis price:[55.0 TO 66.0} 查询到的数据总条数是:1 ====================================== docID:3 bookId:4 name:spring price:56.0 pic:83938383222.jpg 因为MUST_NOT需要一个结果范围,所以SHOULD条件必须被执行,并固定了范围,然后再用MUST_NOT排除。 5.3.通过QueryParser搜索 我们可以直接写类似上面打印出来的查询语句,通过QueryParser的Parse方法进行解析生成Query对象。 如果使用QueryParser需要的jar包就是: 5.3.1.QueryParser QueryParser对象的创建就是直接new,参数1:默认的搜索域,参数2:一个分析器对象。 5.3.1.1.基础查询 也叫关键词查询。 语法:域名+“:”+搜索的关键字。 例如:name:java 示例:由于有分析器,所以条件大小写都是一样的,经过分析器后都会处理成小写。 @Test public void testByQueryParser1() throws Exception { // 创建分析器 Analyzer analyzer = new StandardAnalyzer(); // 创建查询解析器 QueryParser queryParser = new QueryParser("desc", analyzer); // 根据查询解析器常见查询对象 Query query = queryParser.parse("name:Mybatis"); // 进行查询 doSearch(query); System.out.println(" "); // 根据查询解析器常见查询对象 query = queryParser.parse("name:mybatis"); // 进行查询 doSearch(query); } 结果: 实际的查询条件:name:mybatis 查询到的数据总条数是:1 ====================================== docID:2 bookId:3 name:mybatis price:55.0 pic:88272828282.jpg 实际的查询条件:name:mybatis 查询到的数据总条数是:1 ====================================== docID:2 bookId:3 name:mybatis price:55.0 pic:88272828282.jpg 5.3.1.2.范围查询 域名+“:”+[最小值 TO 最大值](用[表示小于等于,{表示小于,]表示大于等于,}表示大于,TO要大写) 例如:size:[1 TO 1000] 注意:QueryParser不支持对数字范围的搜索,它支持字符串范围。数字范围搜索建议使用NumericRangeQuery。虽然上面的NumericRangeQuery在测试时候打印出来的条件和这个写法是一样的,但是使用QueryParser时候就不能这样写,但是字符串范围查询是可以的。 在solr中可以支持这样的数值范围查询语法。 5.3.1.3.组合条件查询 在QueryParser对象中写条件时可以使用:AND,OR,NOT要大写 必须满足此条件 AND或+(加号) 相当于Occur.MUST 应该满足此条件 OR或空(不用符号) 相当于Occur.SHOULD 必须不满足此条件 NOT或-(减号) 相当于Occur.MUST_NOT 前提:QueryParser queryParser = new QueryParser("desc", analyzer); 1)+条件1 +条件2:两个条件都必须满足(相当于MUST与MUST) queryParser.parse("+mybatis +lucene") 或 queryParser.parse("mybatis AND lucene") 2)条件1 条件2:两个条件满足其一即可(相当于SHOULD与SHOULD) queryParser.parse("mybatis lucene") queryParser.parse("mybatis OR lucene") 3)+条件1 -条件2:条件1必须满足,条件2必须不满足(相当于MUST与MUST_NOT) queryParser.parse("+lucene -mybatis") 4)-条件1 +条件2:条件1必须不满足,条件2必须满足(相当于MUST_NOT与MUST) queryParser.parse("-lucene +mybatis") queryParser.parse("NOT lucene AND mybatis") 5)+条件1 条件2:必须满足条件1,条件2失效(相当于MUST与SHOULD) queryParser.parse("+lucene mybatis"),必须包含lucene的,忽略mybatis的。 ╳ queryParser.parse("AND lucene mybatis"),最前面不能写AND或OR,但可以写NOT 6)条件1 +条件2:条件1失效,必须满足条件2(相当于SHOULD与MUST) queryParser.parse("mybatis +lucene") ╳ queryParser.parse("OR lucene AND mybatis") 7)-条件1 条件2:相当于MUST_NOT与SHOULD queryParser.parse("-lucene mybatis") queryParser.parse("NOT lucene mybatis") 5.3.1.4.默认搜索域对查询条件的影响 设置了默认搜索域后,查询条件中必须明确给出要搜索的域才会按照给出的域搜索,否则都会默认认为是搜索默认搜索域。假设:QueryParser queryParser = new QueryParser("desc", analyzer);表明设置了desc为默认搜索域。 比如: Query query = queryParser.parse("java AND lucene"); 被解释成:desc:java AND desc:lucene,条件中没有明确指定时就用默认的域desc。 Query query = queryParser.parse("name:java AND lucene"); 被解释成:name:java AND desc:lucene,而不是name:java AND name:lucene。 Query query = queryParser.parse("name:lucene AND java"); 被解释成:name:lucene AND desc:java,而不是name:lucene AND name:java。 以上两种条件最终的查询结果是不同的,但如果理解错误很容易误解为查询结果应该是一样的。 Query query = queryParser.parse("desc:java AND lucene"); 被解释成:desc:java AND desc:lucene,第二个条件其实使用的是默认域,只不过默认域和前面指定 的域名一致而已 Query query = queryParser.parse("name:java"); 还可以指定一个新的Field域,完全跟默认的搜索域没有任何关系,这时只会搜索name域,desc域与本次查询没有任何关系。 5.3.2.MultiFieldQueryParser 通过MultiFieldQueryParse对多个域查询,两个域之间相当于用或连接: /** * 使用MultiFieldQueryParser多域查询解析器进行查询 */ @Test public void testByMultiFieldQueryParser() throws Exception { // 创建分析器 Analyzer analyzer = new StandardAnalyzer(); String[] fields = {"name", "desc"}; // 创建多域查询解析器 MultiFieldQueryParser queryParser = new MultiFieldQueryParser(fields, analyzer); // 根据查询条件解析出对应的查询对象 Query query = queryParser.parse("lucene"); // 打印查询对象 System.out.println(query); // 进行查询 doSearch(query); } 结果: name:lucene desc:lucene 查询到的数据总条数是:2 ====================================== docID:1 bookId:2 name:apache lucene price:66.0 pic:77373773737.jpg ====================================== docID:4 bookId:5 name:solr price:78.0 pic:99999229292.jpg 生成的查询语句: name:lucene desc:lucene 没有+和-,说明是或条件,即name:lucene OR desc:lucene 5.4.TopDocs Lucene搜索结果可通过TopDocs遍历,TopDocs类提供了少量的属性,如下: 方法或属性 说明 totalHits 匹配搜索条件的总记录数 scoreDocs 顶部匹配记录 注意: Search方法需要指定匹配记录数量n:indexSearcher.search(query, n) TopDocs.totalHits:是匹配索引库中所有记录的数量 TopDocs.scoreDocs:匹配相关度高的前边记录数组,scoreDocs的长度小于等于search方法指定的参数n 6.相关度排序(了解) 6.1.什么是相关度排序 相关度排序是查询结果按照与查询关键字的相关性进行排序,越相关的越靠前。比如搜索“Lucene”关键字,与该关键字最相关的文章应该排在前边。 6.2.相关度打分 Lucene对查询的关键字与包含这个关键字的文档的相关度进行打分,得分高的就排在前边。如何打分呢?Lucene是在用户进行检索时实时根据搜索的关键字计算出来的,分两步: 1)计算出词(Term)的权重 2)根据词的权重值,计算文档相关度得分。 什么是词的权重? 通过索引部分的学习,明确索引的最小单位是一个Term(索引词典中的一个词)。搜索也是从索引域中查询Term,再根据Term找到文档。Term对文档的重要性称为权重,影响Term权重有两个因素: Term Frequency (tf): 指此Term在此文档中出现了多少次。tf 越大说明越重要。 词(Term)在文档中出现的次数越多,说明此词(Term)对该文档越重要,如“Lucene”这个词,在文档中出现的次数很多,说明该文档主要就是讲Lucene技术的。 Document Frequency (df): 指有多少文档包含此Term。df 越大说明越不重要。 比如,在多篇英语文档中,this出现的次数更多,就说明越重要吗?不是的,有越多的文档包含此词(Term), 说明此词(Term)太普通,不足以区分这些文档,因而重要性越低。 6.3.设置boost值影响相关度排序 boost是一个加权值(默认加权值为1.0f),它可以影响权重的计算。在索引时对某个文档中的field设置加权值,设置越高,在搜索时匹配到这个文档就可能排在前边。 未设置权重: 当未设置boost加权时name为spring的排名是最后一名,希望把name为spring的排名提高。 先清空索引库,然后可以重新做一个类试验加权值boost的,就是在创建索引的代码中追加加权值的设置逻辑: package cn.baidu.test2; import java.io.File; import java.io.InputStream; import java.util.ArrayList; import java.util.List; import org.apache.ibatis.io.Resources; import org.apache.ibatis.session.SqlSession; import org.apache.ibatis.session.SqlSessionFactory; import org.apache.ibatis.session.SqlSessionFactoryBuilder; import org.apache.lucene.analysis.Analyzer; import org.apache.lucene.document.Document; import org.apache.lucene.document.Field.Store; import org.apache.lucene.document.FloatField; import org.apache.lucene.document.StoredField; 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.Before; import org.junit.Test; import org.wltea.analyzer.lucene.IKAnalyzer; import cn.baidu.dao.BookDao; import cn.baidu.pojo.Book; /** * 增加加权值boost * * @author Derek Sun * */ public class CreateIndexTest3 { private SqlSessionFactory sqlSessionFactory = null; @Before public void init() throws Exception { InputStream inputStream = Resources.getResourceAsStream("MyBatisConfig.xml"); sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream); } @Test public void test() throws Exception { SqlSession sqlSession = null; try { sqlSession = sqlSessionFactory.openSession(); BookDao bookDao = sqlSession.getMapper(BookDao.class); // 1. 从数据库采集数据 List<Book> bookList = bookDao.queryBookList(); // 2. 创建Document文档对象 List<Document> documents = new ArrayList<Document>(); for (Book book : bookList) { Document document = new Document(); // Document文档中添加Field域 // 图书id // 不分词、不索引、只存储 document.add(new StoredField("id", book.getId().toString())); // 图书名称 // 分词、索引、存储 document.add(new TextField("name", book.getName().toString(), Store.YES)); // 图书价格 // 分词、索引、存储 document.add(new FloatField("price", book.getPrice(), Store.YES)); // 图书图片地址 // 不分词、不索引、只存储 document.add(new StoredField("pic", book.getPic().toString())); // 图书描述 // 分词、索引、不存储 TextField detailField = new TextField("desc", book.getDescription().toString(), Store.NO); // 判断是不是spring的那一条,如果是就增加它的加权值 if (book.getId() == 4) { detailField.setBoost(100f); } document.add(detailField); // 把Document放到list中 documents.add(document); } // 3. 创建Analyzer分词器(分析文档,对文档进行切分词) Analyzer analyzer = new IKAnalyzer(); // 4. 创建索引 // 4-1. 创建Directory对象,声明索引库位置 Directory dir = FSDirectory.open(new File("C:\mydir\03_workspace\lucene\indexDB")); // 4-2. 创建IndexWriteConfig对象,写入索引需要的配置 IndexWriterConfig config = new IndexWriterConfig(Version.LUCENE_4_10_3, analyzer); // 4-3. 创建IndexWriter写入对象 IndexWriter indexWriter = new IndexWriter(dir, config); // 4-4. 把Document写入到索引库,通过IndexWriter对象添加文档对象document for (Document doc : documents) { indexWriter.addDocument(doc); } // 4-5. 释放资源(释放资源同时还有commit操作) indexWriter.close(); } catch(Exception e) { e.printStackTrace(); throw e; } finally { sqlSession.close(); } } } 执行创建索引的逻辑,使用luke重载新生成的索引库,再次查询spring在第一: 7.Solr介绍 7.1.什么是solr Solr是Apache下的一个顶级开源项目,采用Java开发,它是基于Lucene的全文搜索服务。Solr是一个可以独立运行的搜索服务器,可以独立运行在Jetty、Tomcat等这些Servlet容器中。使用solr进行全文检索服务的话,只需要通过http请求访问该服务器即可。 Solr提供了比Lucene更为丰富的查询语言,同时实现了文档的Field域的可配置、可扩展,并对索引、搜索性能进行了优化。 使用Solr 进行创建索引和搜索索引的实现方法很简单,如下: 创建索引:客户端(可以是浏览器可以是Java程序)用 POST 方法向 Solr 服务器发送一个描述 Field 及其内容的 XML 文档,Solr服务器根据xml文档添加、删除、更新索引 。 搜索索引:客户端(可以是浏览器可以是Java程序)用 GET方法向 Solr 服务器发送请求,然后对 Solr服务器返回Xml、json等格式的查询结果进行解析。Solr不提供构建页面UI的功能。Solr提供了一个管理界面,通过管理界面可以查询Solr的配置和运行情况。 7.2.Solr和Lucene的区别 Lucene是一个开放源代码的全文检索引擎开发工具包,它不是一个完整的全文检索应用,不能独立运行。我们可以借助它在企业的系统中实现全文检索搜索功能,或者以Lucene为基础构建一个独立的全文检索搜索引擎应用,像百度,谷歌一样。 Solr是用Lucene开发的一个全文检索搜索引擎服务,它是一个完整的全文检索应用,可以独立运行,企业中可以利用solr的搜索引擎服务非常快速的构建企业自己的搜索引擎,也可以通过Solr高效的完成站内搜索功能。 说直白一些就是:Lucene不能拿来直接用,要在它基础上开发;Solr就是用Lucene开发的搜索引擎服务,可以拿来直接用。 8.Solr安装配置 8.1.下载solr Solr和lucene的版本是同步更新的,本课程使用的版本:4.10.3 下载地址:http://archive.apache.org/dist/lucene/solr/ Linux下需要solr-4.10.3.tgz,windows下需要solr-4.10.3.zip。 解压solr-4.10.3.zip: 目录说明: example:solr工程的实例目录: example/solr: 该目录是一个标准的SolrHome,它包含一个默认配置信息的SolrCore目录。 example/multicore: 该目录包含了在Solr的multicore中设置的多个Core目录。 example/webapps: 该目录中包括一个solr.war,该war可作为solr的运行实例工程。 8.2.SolrHome和SolrCore SolrHome就是MySQL的数据库主机。 SolrCore就是MySQL的数据库主机下的一个数据库。这里面叫做索引库。MySQL的数据库是相互独立的,SolrCore同样也是相互独立的。 SolrHome是Solr索引库的主目录,一个SolrHome可以包括多个SolrCore(Solr实例),每个SolrCore就是一个索引库,提供单独的搜索和索引服务,有自己独立的配置文件和数据文件。 在【资料solrsolr-4.10.3example】目录下,【solr】是一个SolrHome目录结构,如下: 上图中“collection1”是一个SolrCore(Solr实例)目录 ,目录内容如下所示: 8.3.SolrCore创建与配置 Solr中许多功能都是配置就可以使用的,不需要做太多的实现编码,我们要做的只是配置好,然后调用即可。而且SolrCore的创建就是拷贝官方提供的一个实例,然后改一改配置即可。 8.3.1.创建SolrCore 最快的创建即拷贝解压缩包中的例子,拷到Solr工作目录:【C:mydir 3_workspacesolr】。拷贝【solr-4.10.3examplesolr】文件夹到Solr工作目录,并改名为【solrhome】(改名不是必须的,只是为了便于理解),这是一个SolrHome目录,它里面自带一个collection1就是一个SolrCore。 8.3.1.配置SolrCore 一个SolrCore的配置都是自己独立的。主要的配置文件在SolrCore目录下的conf/solrconfig.xml。 这个文件配置SolrCore实例的相关信息。主要是Solr索引库依赖的lib(lib标签)、索引文件存放的路径(datadir标签)、Solr提供的请求访问URL配置(requestHandler标签)。如果使用默认配置可以不用做任何修改。 8.3.1.1.lib 标签 在solrconfig.xml中默认配置了【contrib】和【dist】两个目录下的jar包,这些jar目录都在解压后的文件夹中,所以这两个文件夹也需要拷贝过来: 【solr.install.dir】表示当前的SolrCore的目录位置,即【C:mydir 3_workspacesolr solrhomecollection1】,冒号:后面的【../../..】是针对【solr.install.dir】的相对路径,这个相对路径取决于我们把这两个文件夹放哪。这里还可以直接指定绝对路径,但不推荐使用绝对路径。 比如:我们把【contrib】与【dist】复制到【C:mydir 3_workspacesolr】目录下: 那么【solr.install.dir:】后的【../../..】应该改为【../..】: (./ 表示当前目录 ../表示上一级目录) 8.3.1.2.datadir标签 配置SolrCore的data目录。data目录用来存放SolrCore的索引文件和tlog日志文件 【solr.data.dir】默认路径是【collection1data】文件夹,如果没有data,solr会自动创建。 如果不想使用默认的目录也可以通过solrconfig.xml更改索引目录 ,例如: (建议不修改,因为各自的索引文件和日志放在各自的SolrCore目录下更好,而且当配置多个SolrCore会报错) 8.3.1.3.requestHandler标签 requestHandler请求处理器,定义了索引和搜索的URL访问方式。官方提供的配置文件中就已经给我们配置了许多url访问方式,基本不用改。requestHandler也是可以根据自己的要求自定义。 /update:维护索引使用的url,可以完成索引的添加、修改、删除操作。 /select:查询索引使用的url。 设置搜索参数完成搜索,搜索参数也可以设置一些默认值,如下: <requestHandler name="/select" class="solr.SearchHandler"> <!-- 设置默认的参数值,可以在请求地址中修改这些参数--> <lst name="defaults"> <str name="echoParams">explicit</str> <int name="rows">10</int><!--显示数量--> <str name="wt">json</str><!--显示格式--> <str name="df">text</str><!--默认搜索字段--> </lst> </requestHandler> 8.4.Solr运行环境 solr 需要运行在一个Servlet容器中,Solr4.10.3要求jdk使用1.7以上,Solr默认提供Jetty(java写的Servlet容器), 使用jetty启动:使用cmd命令行,进入example文件夹启动。启动命令java -jar start.jar 启动后访问地址:http://127.0.0.1:8983/solr 但是企业中一般使用Tomcat作为服务器,本课程也是一样, 相关环境如下: Solr:4.10.3 Jdk环境:1.7(solr4.10 不能使用jdk1.7以下) 服务器:Tomcat 7 8.5.Solr服务部署 上面拷贝并配置的不是SolrWeb服务,那只是一个个的Solr索引库。 我们现在要部署的才是真正的Solr Web服务。 由于在项目中用到的web服务器大多数是用的Tomcat,所以就进行solr和Tomcat的整合。 8.5.1.安装Tomcat 复制自己的Tomcat7到这里 删除不用的应用(可以不删) 修改server.xml配置文件里面的端口号(否则后面eclipse使用Tomcat会冲突) 修改以下三个端口号 8.5.2.部署solr.war到Tomcat中 1、从solr解压包下的solr-4.10.3examplewebapps目录中拷贝solr.war 复制solr.war 粘贴到自己Tomcat的webapps里 在Tomcat的webapps里,把war解压到当前路径,并删除solr.war 效果: 8.5.3.添加solr服务的扩展jar包(日志包) 把solr解压包下solr-4.10.3examplelibext目录下的所有jar包拷贝到Tomcat部署的solr的WEB-INF/lib文件夹: 复制扩展jar包 粘贴到Tomcat的webapps的solr工程的WEB-INFlib目录 8.5.4.配置solr应用的web.xml 需要修改web.xml,让Tomcat使用JNDI的方式告诉solr服务器SolrHome在哪。 修改内容:第42行的Solr/home名称必须是固定的,修改第43行,如下图 8.5.5.启动Tomcat进行访问 访问:http://localhost:8081/solr/,出现以下界面则说明solr安装成功!!! 8.6.管理界面功能介绍 8.6.1.Dashboard 仪表盘,显示了该Solr实例开始启动运行的时间、版本、系统资源、jvm等信息。 8.6.2.Logging Solr运行日志信息 8.6.3.Cloud Cloud即SolrCloud,即Solr云(集群),当使用Solr Cloud模式运行时会显示此菜单,该部分功能在第二个项目,即电商项目会演示。 8.6.4.Core Admin Solr Core的管理界面。在这里可以添加SolrCore实例(有bug,不推荐使用浏览器界面添加SolrCore)。 推荐使用手动添加solrcore: 第一步:复制collection1改名为collection2 第二步:修改core.properties。name=collection2 第三步:重启tomcat 8.6.5.java properties Solr在JVM 运行环境中的属性信息,包括类路径、文件编码、jvm内存设置等信息。 8.6.6.Tread Dump 显示Solr Server中当前活跃线程信息,同时也可以跟踪线程运行栈信息。 8.6.7.Core selector(重点) 选择一个SolrCore进行详细操作,如下: 8.6.7.1.Analysis 通过此界面可以测试索引分析器和搜索分析器的具体分析执行结果 Solr中自带了许多已经定义好的Field,而且Solr的Field中都带有分析器可以对设置的内容进行分析处理,这一点比Lucene要高级,因为Lucene的Field对象不能指定分析器。 在这个界面中可以选择不同的Field域对你输入的内容进行分析测试,用于查看某个Field的分词情况,从而我们可以在开发中选用正确的Field。 1. 我们先选用【_root_】域: 结果:没有分词 我们发现【_root_】的实际类型是【StrField】,String类型的Field是不分词的,这个和Lucene的类似。 2. 我们选用一个可能有分词功能的Field【content】 结果:分词了,但是分的不好 【content】的Field类型是TextField,它使用的分析器是solr标准分析器,只能对英文分词,所以对汉字分的不好。 结论: 虽然solr提供了许多可用的Field,但是这些Field对汉语支持不好,所以在实际开发是我们需要自定义支持汉语的Field才可以进行我们的系统开发。Solr对Field域定义非常灵活、强大,这个明天具体学习。 8.6.7.2.dataimport 可以定义数据导入处理器,从关系数据库将数据创建索引并导入到Solr索引库中。默认没有配置,需要手工配置。 8.6.7.3.Document 通过此菜单可以创建索引、更新索引、删除索引等操作。 通过/update表示更新索引,solr默认根据id(唯一约束)域来更新Document的内容,如果根据id值搜索不到id域则会执行添加操作,如果找到则更新。 overwrite="true" : solr在做索引的时候,如果文档已经存在,就用xml中的文档进行替换 commitWithin="1000" : solr 在做索引的时候,每隔1000(1秒)毫秒,做一次文档提交。为了方便测试也可以在Document中立即提交,</doc>后添加“<commit/>” 1. 添加索引 id域=c001的Document不存在时,创建Document对象,有两个域:id域和title域,查看结果: Solr会针对你添加的Document对象自动创建索引。 2. 更新索引 Solr只能更新已经存在的索引 查询结果: 3. 如果使用了solr不存在的Field,是不允许的: 4. 如果不带id域也是不允许的: 5. 根据id删除索引: 查询删除结果: 6. 根据条件删除: 先添加两个Document: 查询结果: 执行条件删除: 再查询:只有c002的数据了 8.6.7.4.Query 通过/select执行搜索索引,必须指定“q”查询条件方可搜索。这个我们明天还要好好学习,今天就简单了解。 9.Solrj的使用 9.1.什么是solrj solrj是访问Solr服务的java客户端,提供索引和搜索的请求方法,如下图: Solrj和图形界面操作的区别就类似于数据库中使用jdbc和mysql客户端的区别一样。 9.2.需求 使用solrj调用solr服务实现对索引库的增删改查操作。 9.3.环境准备 Solr:4.10.3 Jdk环境:1.7 IDE环境:Eclipse Mars2 9.4.工程搭建 9.4.1.创建java工程 9.4.2.添加jar Solrj的包,solr-4.10.3dist目录下 solrj依赖包,solr-4.10.3distsolrj-lib Solr服务的日志依赖包,solrexamplelibext 9.4.3.添加日志配置文件 创建【config】: 加入log4j.properties。 9.5.代码实现 9.5.1.添加&修改索引 9.5.1.1.步骤 1、创建(new)HttpSolrServer对象,通过它和Solr服务器建立连接,需要提供URL 2、创建SolrInputDocument对象,然后通过它的addField(域名,域值)来添加域。 3、通过HttpSolrServer对象的add()方法将SolrInputDocument添加到索引库。 4、HttpSolrServer的commit(提交)。 9.5.1.2.代码 说明:根据id(唯一约束)域来更新Document的内容,如果根据id值搜索不到id域则会执行添加操作,如果找到则更新。 @Test public void testCreateAndUpdateIndex() throws Exception { // 1. 创建HttpSolrServer对象 // 设置solr服务接口,浏览器客户端地址http://127.0.0.1:8081/solr/ // 如果想上面不指定某个Solr实例默认使用第一个,但最好还是指定一个 String baseURL = "http://127.0.0.1:8081/solr/collection1"; HttpSolrServer httpSolrServer = new HttpSolrServer(baseURL); // 2. 创建SolrInputDocument对象 SolrInputDocument document = new SolrInputDocument(); document.addField("id", "c1001"); document.addField("content ", "Hello world!"); // 3. 把SolrInputDocument对象添加到索引库中 httpSolrServer.add(document); // 4. 提交 httpSolrServer.commit(); } 9.5.1.查询索引 抽取HttpSolrServer 的创建代码 private HttpSolrServer httpSolrServer; // 提取HttpSolrServer创建 @Before public void init() { // 1. 创建HttpSolrServer对象 // 设置solr服务接口,浏览器客户端地址http://127.0.0.1:8081/solr/#/ String baseURL = "http://127.0.0.1:8081/solr/"; this.httpSolrServer = new HttpSolrServer(baseURL); } 根据条件的简单查询(查询全部) /** * 简单搜索 * * @throws Exception */ @Test public void testSearchIndex1() throws Exception { // 创建搜索对象 SolrQuery query = new SolrQuery(); // 设置搜索条件 query.setQuery("*:*"); // 发起搜索请求 QueryResponse response = this.httpSolrServer.query(query); // 处理搜索结果 SolrDocumentList results = response.getResults(); System.out.println("搜索到的结果总数:" + results.getNumFound()); // 遍历搜索结果 for (SolrDocument solrDocument : results) { System.out.println("----------------------------------------------------"); System.out.println("id:" + solrDocument.get("id")); System.out.println("content" + solrDocument.get("content")); } } 9.5.2.删除索引 删除索引逻辑,两种: 根据id删除 根据条件删除,根据条件删除,可以使用*:*作为条件,就是删除所有数据(慎用) @Test public void testDeleteIndex() throws Exception { // 根据id删除索引数据 // this.httpSolrServer.deleteById("c1001"); // 根据条件删除(如果是*:*就表示全部删除,慎用) //this.httpSolrServer.deleteByQuery("*:*"); this.httpSolrServer.deleteByQuery("id:c1001"); // 提交 this.httpSolrServer.commit(); }