基于 lucene 8
lucene是apache下的一个开源的全文检索引擎工具包。
全文检索就是先分词创建索引,再执行搜索的过程。分词就是将一段文字分成一个个单词。全文检索就将一段文字分成一个个单词去查询数据
全文检索的流程分为两大部分:索引流程、搜索流程。
使用lucene实现电商项目中图书类商品的索引和搜索功能。
步骤说明:
lucene全文检索,不是直接查询数据库,所以需要先将数据采集出来。
package jdbc.dao; import jdbc.pojo.book; import jdbc.util.jdbcutils; import java.sql.connection; import java.sql.preparedstatement; import java.sql.resultset; import java.sql.sqlexception; import java.util.arraylist; import java.util.list; public class bookdao { public list<book> listall() { //创建集合 list<book> books = new arraylist<>(); //获取数据库连接 connection conn = jdbcutils.getconnection(); string sql = "select * from `book`"; preparedstatement preparedstatement = null; resultset resultset = null; try { //获取预编译语句 preparedstatement = conn.preparestatement(sql); //获取结果集 resultset = preparedstatement.executequery(); //结果集解析 while (resultset.next()) { books.add(new book(resultset.getint("id"), resultset.getstring("name"), resultset.getfloat("price"), resultset.getstring("pic"), resultset.getstring("description"))); } } catch (sqlexception e) { e.printstacktrace(); } finally { //关闭资源 if (null != resultset) { try { resultset.close(); } catch (sqlexception e) { e.printstacktrace(); } finally { if (preparedstatement != null) { try { preparedstatement.close(); } catch (sqlexception e) { e.printstacktrace(); } finally { if (null != conn) { try { conn.close(); } catch (sqlexception e) { e.printstacktrace(); } } } } } } } return books; } }
lucene是使用文档类型来封装数据的,所有需要先将采集的数据转换成文档类型。其格式为:
修改bookdao,新增一个方法,转换数据
public list<document> getdocuments(list<book> books) { //创建集合 list<document> documents = new arraylist<>(); //循环操作 books 集合 books.foreach(book -> { //创建 document 对象,document 内需要设置一个个 field 对象 document doc = new document(); //创建各个 field field id = new textfield("id", book.getid().tostring(), field.store.yes); field name = new textfield("name", book.getname(), field.store.yes); field price = new textfield("price", book.getprice().tostring(), field.store.yes); field pic = new textfield("id", book.getpic(), field.store.yes); field description = new textfield("description", book.getdescription(), field.store.yes); //将 field 添加到文档中 doc.add(id); doc.add(name); doc.add(price); doc.add(pic); doc.add(description); documents.add(doc); }); return documents; }
lucene是在将文档写入索引库的过程中,自动完成分词、创建索引的。因此创建索引库,从形式上看,就是将文档写入索引库!
package jdbc.test; import jdbc.dao.bookdao; import org.apache.lucene.analysis.standard.standardanalyzer; 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.junit.test; import java.io.file; import java.io.ioexception; public class lucenetest { /** * 创建索引库 */ @test public void createindex() { bookdao dao = new bookdao(); //该分词器用于逐个字符分词 standardanalyzer standardanalyzer = new standardanalyzer(); //创建索引 //1. 创建索引库存储目录 try (directory directory = fsdirectory.open(new file("c:\\users\\carlo\\onedrive\\workspace\\ideaprojects\\lucene-demo01-start\\lucene").topath())) { //2. 创建 indexwriterconfig 对象 indexwriterconfig ifc = new indexwriterconfig(standardanalyzer); //3. 创建 indexwriter 对象 indexwriter indexwriter = new indexwriter(directory, ifc); //4. 通过 indexwriter 对象添加文档 indexwriter.adddocuments(dao.getdocuments(dao.listall())); //5. 关闭 indexwriter indexwriter.close(); system.out.println("完成索引库创建"); } catch (ioexception e) { e.printstacktrace(); } } }
可以通过 luke 工具查看结果
搜索的时候,需要指定搜索哪一个域(也就是字段),并且,还要对搜索的关键词做分词处理。
@test public void searchtest() { //1. 创建查询(query 对象) standardanalyzer standardanalyzer = new standardanalyzer(); // 参数 1 指定搜索的 field queryparser queryparser = new queryparser("name", standardanalyzer); try { query query = queryparser.parse("java book"); //2. 执行搜索 //a. 指定索引库目录 directory directory = fsdirectory.open(new file("c:\\users\\carlo\\onedrive\\workspace\\ideaprojects\\lucene-demo01-start\\lucene").topath()); //b. 创建 indexreader 对象 indexreader reader = directoryreader.open(directory); //c. 创建 indexsearcher 对象 indexsearcher searcher = new indexsearcher(reader); /** * d. 通过 indexsearcher 对象查询索引库,返回 topdocs 对象 * 参数 1:查询对象(query) * 参数 2:前 n 条数据 */ topdocs topdocs = searcher.search(query, 10); //e. 提取 topdocs 对象中的的查询结果 scoredoc[] scoredocs = topdocs.scoredocs; system.out.println("查询结果个数为:" + topdocs.totalhits); //循环输出数据对象 for (scoredoc scoredoc : scoredocs) { //获得文档对象 id int docid = scoredoc.doc; //通过 id 获得具体对象 document document = searcher.doc(docid); //输出图书的书名 system.out.println(document.get("name")); } //关闭 indexreader reader.close(); } catch (parseexception | ioexception e) { e.printstacktrace(); } }
结果
对lucene分词的过程,我们可以做如下总结:
从上图中,我们发现:
我们已经知道,lucene是在写入文档时,完成分词、索引的。那lucene是怎么知道如何分词的呢?lucene是根据文档中的域的属性来确定是否要分词、是否创建索引的。所以,我们必须搞清楚域有哪些属性。
只有设置了分词属性为true,lucene才会对这个域进行分词处理。
在实际的开发中,有一些字段是不需要分词的,比如商品id,商品图片等。而有一些字段是必须分词的,比如商品名称,描述信息等。
只有设置了索引属性为true,lucene才为这个域的term词创建索引。
在实际的开发中,有一些字段是不需要创建索引的,比如商品的图片等。我们只需要对参与搜索的字段做索引处理。
只有设置了存储属性为true,在查找的时候,才能从文档中获取这个域的值。
在实际开发中,有一些字段是不需要存储的。比如:商品的描述信息。因为商品描述信息,通常都是大文本数据,读的时候会造成巨大的io开销。而描述信息是不需要经常查询的字段,这样的话就白白浪费了cpu的资源了。因此,像这种不需要经常查询,又是大文本的字段,通常不会存储到索引库。
域的常用类型有很多,每一个类都有自己默认的三大属性。如下:
public list<document> getdocuments(list<book> books) { //创建集合 list<document> documents = new arraylist<>(); //循环操作 books 集合 books.foreach(book -> { //创建 document 对象,document 内需要设置一个个 field 对象 document doc = new document(); //创建各个 field //存储但不分词、不索引 field id = new storedfield("id", book.getid()); //存储、分词、索引 field name = new textfield("name", book.getname(), field.store.yes); //存储但不分词、不索引 field price = new storedfield("price", book.getprice()); //存储但不分词、不索引 field pic = new storedfield("pic", book.getpic()); //分词、索引,但不存储 field description = new textfield("description", book.getdescription(), field.store.no); //将 field 添加到文档中 doc.add(id); doc.add(name); doc.add(price); doc.add(pic); doc.add(description); documents.add(doc); }); return documents; }
结果
数据库中新上架了图书,必须把这些图书也添加到索引库中,不然就搜不到该新上架的图书了。
调用 indexwriter.adddocument(doc)添加索引。(参考入门示例中的创建索引)
某些图书不再出版销售了,我们需要从索引库中移除该图书。
@test public void deleteindex() throws ioexception { //1.指定索引库目录 directory directory = fsdirectory.open(new file("c:\\users\\carlo\\onedrive\\workspace\\ideaprojects\\lucene-demo01-start\\lucene").topath()); //2.创建 indexwriterconfig indexwriterconfig indexwriterconfig = new indexwriterconfig(new standardanalyzer()); //3.创建 indexwriter indexwriter indexwriter = new indexwriter(directory, indexwriterconfig); //4.删除指定索引 indexwriter.deletedocuments(new term("name", "java")); //5.关闭 indexwriter indexwriter.close(); }
@test public void deleteallindex() throws ioexception { //1.指定索引库目录 directory directory = fsdirectory.open(new file("c:\\users\\carlo\\onedrive\\workspace\\ideaprojects\\lucene-demo01-start\\lucene").topath()); //2.创建 indexwriterconfig indexwriterconfig indexwriterconfig = new indexwriterconfig(new standardanalyzer()); //3.创建 indexwriter indexwriter indexwriter = new indexwriter(directory, indexwriterconfig); //4.删除所有索引 indexwriter.deleteall(); //5.关闭 indexwriter indexwriter.close(); }
lucene更新索引比较特殊,是先删除满足条件的文档,再添加新的文档。
@test public void updateindex() throws ioexception { //1.指定索引库目录 directory directory = fsdirectory.open(new file("c:\\users\\carlo\\onedrive\\workspace\\ideaprojects\\lucene-demo01-start\\lucene").topath()); //2.创建 indexwriterconfig indexwriterconfig indexwriterconfig = new indexwriterconfig(new standardanalyzer()); //3.创建 indexwriter indexwriter indexwriter = new indexwriter(directory, indexwriterconfig); //4.创建新加的文档对象 document document = new document(); document.add(new textfield("name", "testupdate", field.store.yes)); //5.修改指定索引为新的索引 indexwriter.updatedocument(new term("name", "java"), document); //6.关闭 indexwriter indexwriter.close(); }
问题:我们在入门示例中,已经知道lucene是通过indexsearcher对象,来执行搜索的。在实际的开发中,我们的查询的业务是相对复杂的,比如我们在通过关键词查找的时候,往往进行价格、商品类别的过滤。而lucene提供了一套查询方案,供我们实现复杂的查询。
执行查询之前,必须创建一个查询query查询对象。query自身是一个抽象类,不能实例化,必须通过其它的方式来实现初始化。在这里,lucene提供了两种初始化query查询对象的方式。
query是一个抽象类,lucene提供了很多查询对象,比如termquery项精确查询,numericrangequery数字范围查询等。
queryparser会将用户输入的查询表达式解析成query对象实例。如下代码:
queryparser queryparser = new queryparser("name", new standardanalyzer()); query query = queryparser.parse("name:lucene");
特点:查询的关键词不会再做分词处理,作为整体来搜索。代码如下:
@test public void querybytermquery() throws ioexception { query query = new termquery(new term("name", "java")); doquery(query); } private void doquery(query query) throws ioexception { //指定索引库 directory directory = fsdirectory.open(new file("c:\\users\\carlo\\onedrive\\workspace\\ideaprojects\\lucene-demo01-start\\lucene").topath()); //创建读取流 directoryreader reader = directoryreader.open(directory); //创建执行搜索对象 indexsearcher searcher = new indexsearcher(reader); //执行搜索 topdocs topdocs = searcher.search(query, 10); system.out.println("共搜索结果:" + topdocs.totalhits); //提取文档信息 //score即相关度。即搜索的关键词和 图书名称的相关度,用来做排序处理 scoredoc[] scoredocs = topdocs.scoredocs; for (scoredoc scoredoc : scoredocs) { int docid = scoredoc.doc; system.out.println("索引库编号:" + docid); //提取文档信息 document doc = searcher.doc(docid); system.out.println(doc.get("name")); system.out.println(doc.get("id")); system.out.println(doc.get("pricevalue")); system.out.println(doc.get("pic")); system.out.println(doc.get("description")); //关闭读取流 reader.close(); } }
使用通配符查询
/** * 通过通配符查询所有文档 * @throws ioexception */ @test public void querybywildcardquery() throws ioexception { query query = new wildcardquery(new term("name", "*")); doquery(query); } private void doquery(query query) throws ioexception { //指定索引库 directory directory = fsdirectory.open(new file("c:\\users\\carlo\\onedrive\\workspace\\ideaprojects\\lucene-demo01-start\\lucene").topath()); //创建读取流 directoryreader reader = directoryreader.open(directory); //创建执行搜索对象 indexsearcher searcher = new indexsearcher(reader); //执行搜索 topdocs topdocs = searcher.search(query, 10); system.out.println("共搜索结果:" + topdocs.totalhits); //提取文档信息 //score即相关度。即搜索的关键词和 图书名称的相关度,用来做排序处理 scoredoc[] scoredocs = topdocs.scoredocs; for (scoredoc scoredoc : scoredocs) { int docid = scoredoc.doc; system.out.println("索引库编号:" + docid); //提取文档信息 document doc = searcher.doc(docid); system.out.println(doc.get("name")); system.out.println(doc.get("id")); system.out.println(doc.get("pricevalue")); system.out.println(doc.get("pic")); system.out.println(doc.get("description")); } //关闭读取流 reader.close(); }
指定数字范围查询.(创建field类型时,注意与之对应),修改建立索引时的 price
/** * 将 book 集合封装成 document 集合 * @param books book集合 * @return document 集合 */ public list<document> getdocuments(list<book> books) { //创建集合 list<document> documents = new arraylist<>(); //循环操作 books 集合 books.foreach(book -> { //创建 document 对象,document 内需要设置一个个 field 对象 document doc = new document(); //创建各个 field //存储但不分词、不索引 field id = new storedfield("id", book.getid()); //存储、分词、索引 field name = new textfield("name", book.getname(), field.store.yes); //float 数字存储、索引 field price = new floatpoint("price", book.getprice()); //用于数字的区间查询,不会存储,需要额外的 storedfield field pricevalue = new storedfield("pricevalue", book.getprice());//用于存储具体价格 //存储但不分词、不索引 field pic = new storedfield("pic", book.getpic()); //分词、索引,但不存储 field description = new textfield("description", book.getdescription(), field.store.no); //将 field 添加到文档中 doc.add(id); doc.add(name); doc.add(price); doc.add(pricevalue); doc.add(pic); doc.add(description); documents.add(doc); }); return documents; }
使用对应的 floatpoint 的静态方法,获得 rangequery
/** * float 类型的范围查询 * @throws ioexception */ @test public void querybynumricrangequery() throws ioexception { query query = floatpoint.newrangequery("price", 60, 80); doquery(query); } private void doquery(query query) throws ioexception { //指定索引库 directory directory = fsdirectory.open(new file("c:\\users\\carlo\\onedrive\\workspace\\ideaprojects\\lucene-demo01-start\\lucene").topath()); //创建读取流 directoryreader reader = directoryreader.open(directory); //创建执行搜索对象 indexsearcher searcher = new indexsearcher(reader); //执行搜索 topdocs topdocs = searcher.search(query, 10); system.out.println("共搜索结果:" + topdocs.totalhits); //提取文档信息 //score即相关度。即搜索的关键词和 图书名称的相关度,用来做排序处理 scoredoc[] scoredocs = topdocs.scoredocs; for (scoredoc scoredoc : scoredocs) { int docid = scoredoc.doc; system.out.println("索引库编号:" + docid); //提取文档信息 document doc = searcher.doc(docid); system.out.println(doc.get("name")); system.out.println(doc.get("id")); system.out.println(doc.get("pricevalue")); system.out.println(doc.get("pic")); system.out.println(doc.get("description")); } //关闭读取流 reader.close(); }
booleanquery,布尔查询,实现组合条件查询。
@test public void querybybooleanquery() throws ioexception { query pricequery = floatpoint.newrangequery("price", 60, 80); query namequery = new termquery(new term("name", "java")); //通过 builder 创建 query booleanquery.builder booleanquerybuilder = new booleanquery.builder(); //至少有一个时 occur.must,不然结果为空 booleanquerybuilder.add(namequery, booleanclause.occur.must_not); booleanquerybuilder.add(pricequery, booleanclause.occur.must); booleanquery query = booleanquerybuilder.build(); doquery(query); } private void doquery(query query) throws ioexception { //指定索引库 directory directory = fsdirectory.open(new file("c:\\users\\carlo\\onedrive\\workspace\\ideaprojects\\lucene-demo01-start\\lucene").topath()); //创建读取流 directoryreader reader = directoryreader.open(directory); //创建执行搜索对象 indexsearcher searcher = new indexsearcher(reader); //执行搜索 topdocs topdocs = searcher.search(query, 10); system.out.println("共搜索结果:" + topdocs.totalhits); //提取文档信息 //score即相关度。即搜索的关键词和 图书名称的相关度,用来做排序处理 scoredoc[] scoredocs = topdocs.scoredocs; for (scoredoc scoredoc : scoredocs) { int docid = scoredoc.doc; system.out.println("索引库编号:" + docid); //提取文档信息 document doc = searcher.doc(docid); system.out.println(doc.get("name")); system.out.println(doc.get("id")); system.out.println(doc.get("pricevalue")); system.out.println(doc.get("pic")); system.out.println(doc.get("description")); } //关闭读取流 reader.close(); }
对搜索的关键词,做分词处理。
域名:关键字
如: name:java
例如: query query = queryparser.parse("java not 编");
@test public void querybyqueryparser() throws ioexception, parseexception { //创建分词器 standardanalyzer standardanalyzer = new standardanalyzer(); /** * 创建查询解析器 * 参数一: 默认搜索的域。 * 如果在搜索的时候,没有特别指定搜索的域,则按照默认的域进行搜索 * 指定搜索的域的方式: 域名:关键词 如: name:java * 参数二: 分词器,对关键词做分词处理 */ queryparser queryparser = new queryparser("description", standardanalyzer); query query = queryparser.parse("java 教程"); doquery(query); }
通过mulitfieldqueryparse对多个域查询。
@test public void querybymultifieldqueryparser() throws parseexception, ioexception { //1.定义多个搜索的域 string[] fields = {"name", "description"}; //2.加载分词器 standardanalyzer standardanalyzer = new standardanalyzer(); //3.创建 multifieldqueryparser 实例对象 multifieldqueryparser multifieldqueryparser = new multifieldqueryparser(fields, standardanalyzer); query query = multifieldqueryparser.parse("java"); doquery(query); }
学过英文的都知道,英文是以单词为单位的,单词与单词之间以空格或者逗号句号隔开。标准分词器,无法像英文那样按单词分词,只能一个汉字一个汉字来划分。所以需要一个能自动识别中文语义的分词器。
单字分词:就是按照中文一个字一个字地进行分词。如:“我爱中国”
效果:“我”、“爱”、“中”、“国”。
二分法分词:按两个字进行切分。如:“我是中国人”
效果:“我是”、“是中”、“中国”“国人”。
官方提供的智能中文识别,需要导入新的 jar 包
@test public void createindexbychinese () { bookdao dao = new bookdao(); //该分词器用于中文分词 smartchineseanalyzer smartchineseanalyzer = new smartchineseanalyzer(); //创建索引 //1. 创建索引库存储目录 try (directory directory = fsdirectory.open(new file("c:\\users\\carlo\\onedrive\\workspace\\ideaprojects\\lucene-demo01-start\\lucene").topath())) { //2. 创建 indexwriterconfig 对象 indexwriterconfig ifc = new indexwriterconfig(smartchineseanalyzer); //3. 创建 indexwriter 对象 indexwriter indexwriter = new indexwriter(directory, ifc); //4. 通过 indexwriter 对象添加文档 indexwriter.adddocuments(dao.getdocuments(dao.listall())); //5. 关闭 indexwriter indexwriter.close(); system.out.println("完成索引库创建"); } catch (ioexception e) { e.printstacktrace(); } }
效果如图:
如对本文有疑问, 点击进行留言回复!!
解决idea中出现“illegal character U+200B” 问题
荐 为什么加了@Transactional注解,事务没有回滚?
Attribute ‘sklearn.linear_model._logistic.LogisticRegression.multi_class‘ must be explicitly set to
Java/Python实现 LeetCode剑指Offer 14-I.剪绳子(动态规划)
网友评论