黑马程序员技术交流社区

标题: 【上海校区】Lucene 全文检索 [打印本页]

作者: 梦缠绕的时候    时间: 2019-8-13 10:14
标题: 【上海校区】Lucene 全文检索
1 Lucene简介
Lucene是apache下的一个开源的全文检索引擎工具包。
1.1 全文检索(Full-text Search)
全文检索就是先分词创建索引,再执行搜索的过程。分词就是将一段文字分成一个个单词。全文检索就将一段文字分成一个个单词去查询数据
1.2 Lucene实现全文检索的流程
全文检索的流程分为两大部分:索引流程、搜索流程。
2 入门示例2.1 需求
使用Lucene实现电商项目中图书类商品的索引和搜索功能。
2.2 配置步骤说明2.3 配置步骤2.3.1 第一部分:搭建环境(创建项目,导入包)
2.3.2 第二部分:创建索引
步骤说明:
2.3.2.1 第一步:采集数据
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;    }}2.3.2.2 第二步:将数据转换成Lucene文档
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;}2.3.2.3 第三步:创建索引库
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 工具查看结果
2.3.3 第三部分:搜索索引2.3.3.1 说明
搜索的时候,需要指定搜索哪一个域(也就是字段),并且,还要对搜索的关键词做分词处理。
2.3.3.2 执行搜索@Testpublic 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();    }}
结果
3 分词
对Lucene分词的过程,我们可以做如下总结:
从上图中,我们发现:
4 Field域
我们已经知道,Lucene是在写入文档时,完成分词、索引的。那Lucene是怎么知道如何分词的呢?Lucene是根据文档中的域的属性来确定是否要分词、是否创建索引的。所以,我们必须搞清楚域有哪些属性。
4.1 域的属性4.1.1 三大属性4.1.1.1 是否分词(tokenized)
只有设置了分词属性为true,lucene才会对这个域进行分词处理。
在实际的开发中,有一些字段是不需要分词的,比如商品id,商品图片等。而有一些字段是必须分词的,比如商品名称,描述信息等。
4.1.1.2 是否索引(indexed)
只有设置了索引属性为true,lucene才为这个域的Term词创建索引。
在实际的开发中,有一些字段是不需要创建索引的,比如商品的图片等。我们只需要对参与搜索的字段做索引处理。
4.1.1.3 是否存储(stored)
只有设置了存储属性为true,在查找的时候,才能从文档中获取这个域的值。
在实际开发中,有一些字段是不需要存储的。比如:商品的描述信息。因为商品描述信息,通常都是大文本数据,读的时候会造成巨大的IO开销。而描述信息是不需要经常查询的字段,这样的话就白白浪费了cpu的资源了。因此,像这种不需要经常查询,又是大文本的字段,通常不会存储到索引库。
4.1.2 特点4.2 Field常用类型
域的常用类型有很多,每一个类都有自己默认的三大属性。如下:
4.3 改造入门示例中的域类型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;}
结果
5 索引库维护]5.1 添加索引(文档)5.1.1 需求
数据库中新上架了图书,必须把这些图书也添加到索引库中,不然就搜不到该新上架的图书了。
5.1.2 代码实现
调用 indexWriter.addDocument(doc)添加索引。(参考入门示例中的创建索引)
5.2 删除索引(文档)5.2.1需求
某些图书不再出版销售了,我们需要从索引库中移除该图书。
5.2.2 代码实现@Testpublic 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();}5.2.3 清空索引代码实现@Testpublic 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();}5.3 更新索引(文档)5.3.1 说明
Lucene更新索引比较特殊,是先删除满足条件的文档,再添加新的文档。
5.3.2 代码实现@Testpublic 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();}6 搜索
问题:我们在入门示例中,已经知道Lucene是通过IndexSearcher对象,来执行搜索的。在实际的开发中,我们的查询的业务是相对复杂的,比如我们在通过关键词查找的时候,往往进行价格、商品类别的过滤。而Lucene提供了一套查询方案,供我们实现复杂的查询。
6.1 创建查询的两种方法
执行查询之前,必须创建一个查询Query查询对象。Query自身是一个抽象类,不能实例化,必须通过其它的方式来实现初始化。在这里,Lucene提供了两种初始化Query查询对象的方式。
6.1.1 使用Lucene提供Query子类
Query是一个抽象类,lucene提供了很多查询对象,比如TermQuery项精确查询NumericRangeQuery数字范围查询等。
6.1.2 使用QueryParse解析查询表达式
QueryParser会将用户输入的查询表达式解析成Query对象实例。如下代码:
QueryParser queryParser = new QueryParser("name", new StandardAnalyzer());Query query = queryParser.parse("name:lucene");6.2 常用的Query子类搜索6.2.1 TermQuery
特点:查询的关键词不会再做分词处理,作为整体来搜索。代码如下:
@Testpublic 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();    }}6.2.2 WildCardQuery
使用通配符查询
/** * 通过通配符查询所有文档 * @throws IOException */@Testpublic 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();}6.2.3 数字类型的 RangeQuery
指定数字范围查询.(创建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 */@Testpublic 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();}6.2.4 BooleanQuery
BooleanQuery,布尔查询,实现组合条件查询。
@Testpublic 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();}6.3 通过QueryParser搜索6.3.1 特点
对搜索的关键词,做分词处理。
6.3.2 语法6.3.2.1 基础语法
域名:关键字 如: name:java
6.3.2.2 组合条件语法
例如: Query query = queryParser.parse("java NOT 编");
6.3.3 QueryParser@Testpublic void queryByQueryParser() throws IOException, ParseException {    //创建分词器    StandardAnalyzer standardAnalyzer = new StandardAnalyzer();    /**     * 创建查询解析器     * 参数一: 默认搜索的域。     *         如果在搜索的时候,没有特别指定搜索的域,则按照默认的域进行搜索     *         指定搜索的域的方式:   域名:关键词  如:  name:java     * 参数二: 分词器,对关键词做分词处理     */    QueryParser queryParser = new QueryParser("description", standardAnalyzer);    Query query = queryParser.parse("java 教程");    doQuery(query);}6.3.4 MultiFieldQueryParser
通过MulitFieldQueryParse对多个域查询。
@Testpublic 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);}7 中文分词器7.1 什么是中文分词器
学过英文的都知道,英文是以单词为单位的,单词与单词之间以空格或者逗号句号隔开。标准分词器,无法像英文那样按单词分词,只能一个汉字一个汉字来划分。所以需要一个能自动识别中文语义的分词器。
7.2 Lucene自带的中文分词器7.2.1 StandardAnalyzer:
单字分词:就是按照中文一个字一个字地进行分词。如:“我爱中国”
效果:“我”、“爱”、“中”、“国”。
7.2.2 CJKAnalyzer
二分法分词:按两个字进行切分。如:“我是中国人”
效果:“我是”、“是中”、“中国”“国人”。
7.2.3 SmartChineseAnalyzer
官方提供的智能中文识别,需要导入新的 jar 包
@Testpublic 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();    }}
效果如图:


作者: 梦缠绕的时候    时间: 2019-8-13 10:14
有任何问题欢迎在评论区留言
作者: 梦缠绕的时候    时间: 2019-8-13 10:15
或者添加学姐微信
DKA-2018





欢迎光临 黑马程序员技术交流社区 (http://bbs.itheima.com/) 黑马程序员IT技术论坛 X3.2