完成需求:使用Lucene完成对数据库中图书信息的索引和搜索功能。
1. 环境准备及工程搭建
1.1 环境准备
mysql5.5+java8+lucene4.10.3(目前最新7.0.1,这里够用就好)
需要注意:lucene从4.8版本以后,必须使用jdk1.7及以上。
1.2 工程搭建
- Mysql驱动包
- Analysis的包
- Core包
- QueryParser包
- Junit包(非必须)
2. 索引
2.1 采集数据
Book.java(省略get&set方法)
public class Book { // 图书ID private Integer id; // 图书名称 private String name; // 图书价格 private Float price; // 图书图片 private String pic; // 图书描述 private String description; ······ ····· }
BookDaoImpl.java(实现数据库连接和查询)
public class BookDaoImpl implements BookDao { @Override public List<Book> queryBooks() { // 数据库链接 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", "123"); // 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; } }
2.2 创建索引
创建索引流程
IndexWriter是索引过程的核心组件,通过IndexWriter可以创建新索引、更新索引、删除索引操作。IndexWriter需要通过Directory对索引进行存储操作。
Directory描述了索引的存储位置,底层封装了I/O操作,负责对索引进行存储。它是一个抽象类,它的子类常用的包括FSDirectory(在文件系统存储索引)、RAMDirectory(在内存存储索引)。
@Test public void createIndex() throws Exception{ //采集数据 BookDao dao = new BookDaoImpl(); List<Book> list = dao.queryBooks(); //将采集到的数据封装到Document对象中 List<Document> docList = new ArrayList<>(); Document document; for (Book book : list) { document = new Document(); // store:如果是yes,则说明存储到文档域中 // 图书ID 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 description = new TextField("description", book.getDescription(), Store.YES); // 将field域设置到Document对象中 document.add(id); document.add(name); document.add(price); document.add(pic); document.add(description); docList.add(document); } // a)创建分词器,标准分词器(分析文档,对文档中的Field域进行分词) Analyzer analyzer = new StandardAnalyzer(); // b)创建IndexWriterConfig对象 IndexWriterConfig cfg = new IndexWriterConfig(Version.LUCENE_4_10_3, analyzer); // c)创建索引库目录,指定索引库的地址 File indexFile = new File("D:\\DBIndex\\"); Directory directory = FSDirectory.open(indexFile); // d)创建IndexWriter对象 IndexWriter writer = new IndexWriter(directory, cfg); // e)通过IndexWriter对象将Document写入到索引库中 for (Document doc : docList) { writer.addDocument(doc); } // f)关闭writer writer.close(); }
2.3 分词
2.3.1 Lucene中分词主要分为两个步骤:分词、过滤
分词:将field域中的内容一个个的分词。
过滤:将分好的词进行过滤,比如去掉标点符号、大写转小写、词的型还原(复数转单数、过去式转成现在式)、停用词过滤
停用词:单独应用没有特殊意义的词。比如的、啊、等,英文中的this is a the等等。
例:要分词的内容
Lucene is a Java full-text search engine.
经过分词后:
lucene java full text search engine
2.3.2 参考org.apache.lucene.analysis.standard.standardAnalyzer的部分源码了解分词过程
@Override protected TokenStreamComponents createComponents(final String fieldName, final Reader reader) { final StandardTokenizer src = new StandardTokenizer(getVersion(), reader); src.setMaxTokenLength(maxTokenLength); TokenStream tok = new StandardFilter(getVersion(), src); tok = new LowerCaseFilter(getVersion(), tok); tok = new StopFilter(getVersion(), tok, stopwords); return new TokenStreamComponents(src, tok) { @Override protected void setReader(final Reader reader) throws IOException { src.setMaxTokenLength(StandardAnalyzer.this.maxTokenLength); super.setReader(reader); } }; }
2.3.3 语汇单元的生成过程
从一个Reader字符流开始,创建一个基于Reader的Tokenizer分词器,经过三个TokenFilter生成语汇单元Token。
同一个域中相同的语汇单元(Token)对应同一个Term(词),它记录了语汇单元的内容及所在域的域名等,还包括来该token出现的频率及位置。
- 不同的域中拆分出来的相同的单词对应不同的term。
- 相同的域中拆分出来的相同的单词对应相同的term。
例如:图书信息里面,图书名称中的java和图书描述中的java对应不同的term
2.4 使用luke工具查看索引
Luke作为Lucene工具包中的一个工具(http://www.getopt.org/luke/),可以通过界面来进行索引文件的查询、修改。
打开Luke方法:
- 命令运行:cmd运行:java -jar lukeall-4.10.3.jar
- 手动执行:双击lukeall-4.10.3.jar
创建索引后,打开Luke,Path选为索引库的地址,确定即能查看到索引
luke应用
2.5 搜索流程
同数据库的sql一样,lucene全文检索也有固定的语法。 最基本的有比如:AND, OR, NOT 等(需要大写)
举个例子,用户想找一个description中包括java关键字和lucene关键字的文档。
它对应的查询语句:description:java AND lucene
2.5.1 使用luke搜索的例子
2.5.2 代码实现
@Test public void indexSearch() throws Exception { // 创建query对象 // 使用QueryParser搜索时,需要指定分词器,搜索时的分词器要和索引时的分词器一致 // 第一个参数:默认搜索的域的名称 QueryParser parser = new QueryParser("description", new StandardAnalyzer()); // 通过queryparser来创建query对象 // 参数:输入的lucene的查询语句(关键字一定要大写) Query query = parser.parse("description:java AND lucene"); // 创建IndexSearcher // 指定索引库的地址 File indexFile = new File("D:\\DBIndex\\"); Directory directory = FSDirectory.open(indexFile); IndexReader reader = DirectoryReader.open(directory); IndexSearcher searcher = new IndexSearcher(reader); // 通过searcher来搜索索引库 // 第二个参数:指定需要显示的顶部记录的N条 TopDocs topDocs = searcher.search(query, 10); // 根据查询条件匹配出的记录总数 int count = topDocs.totalHits; System.out.println("匹配出的记录总数:" + count); // 根据查询条件匹配出的记录 ScoreDoc[] scoreDocs = topDocs.scoreDocs; for (ScoreDoc scoreDoc : scoreDocs) { // 获取文档的ID int docId = scoreDoc.doc; // 通过ID获取文档 Document doc = searcher.doc(docId); System.out.println("商品ID:" + doc.get("id")); System.out.println("商品名称:" + doc.get("name")); System.out.println("商品价格:" + doc.get("price")); System.out.println("商品图片地址:" + doc.get("pic")); System.out.println("=========================="); // System.out.println("商品描述:" + doc.get("description")); } // 关闭资源 reader.close(); }