【Lucene】全文检索技术详解

1.什么是lucene
Lucene是Apache的一个全文检索引擎(核心组件)工具包(jar包、类库),通过lucene可以让程序员快速开发一个全文检索功能。它不能独立运行,不能单独对外提供服务。

2.全文检索的定义
全文检索首先对要搜索的文档进行分词,然后形成索引,通过查询索引来查询文档。比如:字典,字典的偏旁部首页,就类似于luence的索引;字典的具体内容,就类似于luence的文档内容。

3.Lucene实现全文检索的流程
这里写图片描述
全文检索的流程:索引流程、搜索流程
索引流程:采集数据—》文档处理存储到索引库中
搜索流程:输入查询条件—》通过lucene的查询器查询索引—》从索引库中取出结—》视图渲染(Lucene本身不能进行视图渲染)。

4.入门程序

4.1 需求
    使用lucene完成对数据库中图书信息的索引和搜索功能。
4.2 环境准备
    Jdk:1.7及以上
    Lucene:4.10(从4.8版本以后,必须使用jdk1.7及以上)
    Ide:indigo
    数据库:mysql 5
4.3 数据库脚本初始化
    百度网盘链接:https://pan.baidu.com/s/1nuFeiwh  密码:alzy
4.4 Lucene下载
    Lucene是开发全文检索功能的工具包,使用时从官方网站下载,并解压。
    下载地址:http://archive.apache.org/dist/lucene/java/
4.5 工程搭建

这里写图片描述

4.6 索引流程
1)采集数据的方式
对于互联网中的数据,使用爬虫工具(http工具)将网页爬取到本地
对于数据库中的数据,使用jdbc程序进行数据采集
对于文件系统的数据,使用io流采集
2)索引文件的逻辑结构

这里写图片描述

文档域:文档域存储的信息就是采集到的信息,通过Document对象来存储,具体说是通过Document对象中field域来存储数据。比如:数据库中一条记录会存储一个一个Document对象,数据库中一列会存储成Document中一个field域。文档域中,Document对象之间是没有关系的。而且每个Document中的field域也不一定一样。

索引域:索引域主要是为了搜索使用的。索引域内容是经过lucene分词之后存储的。

倒排索引表:根据内容(词语)找文档,倒排索引结构也叫反向索引结构,包括索引和文档两部分,索引即词汇表,它是在索引中匹配搜索关键字,由于索引内容量有限并且采用固定优化算法搜索速度很快,找到了索引中的词汇,词汇与文档关联,从而最终找到了文档。

这里写图片描述

3)索引
采集数据:
public class Book {
    private Integer id; //图书ID
    private String name;    //图书名称
    private Float price;    //图书价钱
    private String pic;     //图书图片
    private String description; //图书描述
    //生成getXX()和setXX().....
}

public interface IBookDao {
    List<Book> queryBooks();
}

public class IBookDaoImpl implements IBookDao {

    @Override
    public List<Book> queryBooks() {
        //数据库连接
        Connection conn = null;
        //预编译statement
        PreparedStatement ps = null;
        //结果集
        ResultSet resultSet = null;
        //图书列表
        List<Book> bookList = new ArrayList<>();

        //加载数据库驱动
        try {
            Class.forName("com.mysql.jdbc.Driver");
            //连接数据库
            conn =DriverManager.getConnection("jdbc:mysql://localhost:3306/luence", "java", "123456");
            //SQL语句
            String sql = "SELECT * FROM Book";
            //创建PreparedStatement
            ps = conn.prepareStatement(sql);
            //获取结果集
            resultSet = ps.executeQuery();
            //解析结果集
            while(resultSet.next()){
                Book book = new Book();
                book.setId(resultSet.getInt(1));
                book.setName(resultSet.getString(2));
                book.setPrice(resultSet.getFloat(3));
                book.setPic(resultSet.getString(4));
                book.setDescription(resultSet.getString(5));
                bookList.add(book);
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
        return bookList;
    }

}

4)创建索引
创建索引流程:

这里写图片描述

IndexWriter是索引过程的核心组件,通过IndexWriter可以创建新索引、更新索引、删除索引操作。IndexWriter需要通过Directory对索引进行存储操作。

Directory描述了索引的存储位置,底层封装了I/O操作,负责对索引进行存储。它是一个抽象类,它的子类常用的包括FSDirectory(在文件系统存储索引)、RAMDirectory(在内存存储索引)。

    /**
     * 添加索引
     * @throws Exception
     */
    @Test
    public void createIndex() throws Exception {
        //采集数据
        IBookDao dao = new IBookDaoImpl();
        List<Book> list = dao.queryBooks();
        //将采集到的数据封装到Document对象中
        List<Document> docList = new ArrayList<>();
        Document document = null;

        for (Book book : list) {
            document = new Document();
            //store:如果是yes,则说明存储到文档域中
            //不分词、索引、存储StringField
            Field id = new StringField("id", book.getId().toString(), Store.YES);
            //分词、索引、存储TextField
            Field name = new TextField("name", book.getName(), Store.YES);
            //分词、索引、存储但是是数字类型,所有使用FloatField
            Field price = new FloatField("price", book.getPrice(), Store.YES);
            //不分词、不索引、存储StoredField
            Field pic = new StoredField("pic", book.getPic());
            //分词、索引、不存储TextField
            Field description = new TextField("description", book.getDescription(), Store.NO);
            //创建索引时设置boost值
            if(book.getId() == 1){
                description.setBoost(100f);
            }

            //将field域设置到Document对象中
            document.add(id);
            document.add(name);
            document.add(price);
            document.add(pic);
            document.add(description);
            docList.add(document);
        }

        //创建分词器,标准分词器
        Analyzer analyzer = new StandardAnalyzer();
        //创建IndexWriter
        IndexWriterConfig indexWriterConfig = new IndexWriterConfig(Version.LUCENE_4_10_3, analyzer);
        //指定索引库的地址
        Directory directory = FSDirectory.open(new File("G:\\JAVA JELLO\\IndexFile"));
        IndexWriter indexWriter = new IndexWriter(directory, indexWriterConfig);

        //通过IndexWriter对象将Document写入到索引库中
        for (Document doc : docList) {
            indexWriter.addDocument(doc);
        }

        indexWriter.close();
    }

分词:
Lucene中分词主要分为两个步骤:分词、过滤。
分词:将field域中的内容一个个的分词。
过滤:将分好的词进行过滤,比如去掉标点符号、大写转小写、词的型还原(复数转单数、过去式转成现在式)、停用词过滤
停用词:单独应用没有特殊意义的词。比如的、啊、等,英文中的this is a the等等。

语汇单元的生成过程:

这里写图片描述

从一个Reader字符流开始,创建一个基于Reader的Tokenizer分词器,经过三个TokenFilter生成语汇单元Token。
同一个域中相同的语汇单元(Token)对应同一个Term(词),它记录了语汇单元的内容及所在域的域名等,还包括来该token出现的频率及位置。
不同的域中拆分出来的相同的单词对应不同的term。
相同的域中拆分出来的相同的单词对应相同的term。

5)使用luke工具查看索引

这里写图片描述
这里写图片描述

6)搜索流程
输入查询语句,同数据库的sql一样,lucene全文检索也有固定的语法:
最基本的有比如:AND, OR, NOT 等
举个例子,用户想找一个description中包括java关键字和lucene关键字的文档。
它对应的查询语句:description:java AND lucene
如下是使用luke搜索的例子:

这里写图片描述

这里写图片描述

    public void doSerach(Query query) throws Exception{
        Directory directory = FSDirectory.open(new File("G:\\JAVA JELLO\\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();
    }

    /**
     * 通过QueryParser创建查询对象
     * @throws Exception
     */
    @Test
    public void indexSearch() throws Exception{
        // 创建query对象
        // 使用QueryParser搜索时,需要指定分词器,搜索时的分词器要和索引时的分词器一致
        // 第一个参数:默认搜索的域的名称
        QueryParser queryParser = new QueryParser("description", new StandardAnalyzer());
        // 通过queryparser来创建query对象
        // 参数:输入的lucene的查询语句(关键字一定要大写)
        Query query = queryParser.parse("name:java OR description:luence");
        // 创建IndexSearcher
        // 指定索引库的地址
        doSerach(query);
    }

5.Field域

5.1 Field的属性
是否分词(Tokenized)
是:对该field存储的内容进行分词,分词的目的,就是为了索引。
比如:商品名称、商品描述、商品价格
否:不需要对field存储的内容进行分词,不分词,不代表不索引,而是将整个内容进行索引。
比如:商品id

是否索引(Indexed)
是:将分好的词进行索引,索引的目的,就是为了搜索。
比如:商品名称、商品描述、商品价格、商品id
否:不索引,也就是不对该field域进行搜索。

是否存储(Stored)
是:将field域中的内容存储到文档域中。存储的目的,就是为了搜索页面显示取值用的。
比如:商品名称、商品价格、商品id、商品图片地址
否:不将field域中的内容存储到文档域中。不存储,则搜索页面中没法获取该field域的值。
比如:商品描述,由于商品描述在搜索页面中不需要显示,再加上商品描述的内容比较多,所以就不需要进行存储。
如果需要商品描述,则根据搜索出的商品ID去数据库中查询,然后显示出商品描述信息即可。

5.2 Field的常用类型
下边列出了开发中常用 的Filed类型,注意Field的属性,根据需求选择:

这里写图片描述

6.索引维护

1)删除索引
根据条件删除:Term是索引域中最小的单位。根据条件删除时,建议根据唯一键来进行删除。在solr中就是根据ID来进行删除和修改操作的。
    /**
     * 删除索引
     * @throws IOException 
     */
    @Test 
    public void deleteIndex() throws IOException{
        //创建标准分词器
        Analyzer analyzer = new StandardAnalyzer();
        //创建IndexWriter
        IndexWriterConfig cfg = new IndexWriterConfig(Version.LUCENE_4_10_3, analyzer);
        Directory directory = FSDirectory.open(new File("G:\\JAVA JELLO\\IndexFile"));
        IndexWriter indexWriter = new IndexWriter(directory, cfg);
        //Terms
        indexWriter.deleteDocuments(new Term("id","1"));
        //indexWriter.deleteAll();
        indexWriter.close();
    }
2)修改索引
    /**
     * 修改索引
     * @throws Exception 
     */
    @Test
    public void updateIndex() throws Exception{
        Analyzer analyzer = new StandardAnalyzer();
        IndexWriterConfig cfg = new IndexWriterConfig(Version.LUCENE_4_10_3, analyzer);
        Directory directory = FSDirectory.open(new File("G:\\JAVA JELLO\\IndexFile"));
        IndexWriter indexWriter = new IndexWriter(directory, cfg);

        //修改时如果根据查询条件,可以查询出结果,则将以前的删除掉,然后覆盖新的Document对象,如果没有查询出结果,则新增一个Document
        //修改流程:先查询、再删除、再添加
        Document doc = new Document();
        doc.add(new TextField("name", "lisi", Store.YES));
        indexWriter.updateDocument(new Term("name", "zhangsan"), doc);//第一个参数:指定查询条件  第二个参数:修改之后对象

        indexWriter.close();
    }

7.搜索

1)创建查询对象的方式
##通过Query子类来创建查询对象
Query子类常用的有:TermQuery、NumericRangeQuery、BooleanQuery,不能输入lucene的查询语法,不需要指定分词器。

##通过QueryParser来创建查询对象(常用)
QueryParser、MultiFieldQueryParser,可以输入lucene的查询语法、可以指定分词器。

##通过Query子类来创建查询对象
    /**
     *  1.TermQuery:精确的词项查询
     * @throws Exception 
     */
    @Test
    public void termQuery() throws Exception{
        Query query = new TermQuery(new Term("description", "java"));
        doSerach(query);
    }

    /**
     *  2.NumericRangeQuery:数字范围查询
     * @throws Exception 
     */
    @Test
    public void numericRangeQuery() throws Exception{
        //创建NumericRangeQuery对象
        //参数:域的名称,最小值,最大值,是否包含最小值,是否包含最大值
        Query query = NumericRangeQuery.newFloatRange("price", 55f, 60f, true, true);
        doSerach(query);
    }

    /**
     * 3.组合查询BooleanQuery
     * @throws Exception 
     */
    @Test
    public void booleanQuery() throws Exception{
        BooleanQuery query = new BooleanQuery();
        TermQuery q1 = new TermQuery(new Term("description", "luence"));
        Query q2 = NumericRangeQuery.newFloatRange("price", 55f, 60f, true, true);

        /**
         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表示“或”的概念。
         */
        query.add(q1, Occur.SHOULD);
        query.add(q2, Occur.SHOULD);
        doSerach(query);        
    }

##通过QueryParser创建查询对象
    /**
     * MultiFieldQueryParser:多域查询
     * @throws Exception 
     */
    @Test
    public void multiFieldQueryParser() throws Exception{
        String[] fields = {"name","description"};
//      Analyzer analyzer = new StandardAnalyzer();
        //使用ikAnalyzer分词器
        IKAnalyzer analyzer = new IKAnalyzer();
        //在MultiFieldQueryParser创建时设置boost值。
        Map<String, Float> boosts = new HashMap<String, Float>();
        boosts.put("name", 200f);
        MultiFieldQueryParser multiFieldQueryParser = new MultiFieldQueryParser(fields, analyzer,boosts );
        Query query = multiFieldQueryParser.parse("java");//等同于 multiFieldQueryParser.parse("name:luence OR description:luence");
        System.out.println("查询语句:"+query);
        doSerach(query);
    }

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

3)TopDocs
Lucene搜索结果可通过TopDocs遍历,TopDocs类提供了少量的属性,如下:
方法或属性   说明
totalHits   匹配搜索条件的总记录数
scoreDocs   顶部匹配记录
注意:
Search方法需要指定匹配记录数量n:indexSearcher.search(query, n)
TopDocs.totalHits:是匹配索引库中所有记录的数量
TopDocs.scoreDocs:匹配相关度高的前边记录数组,scoreDocs的长度小于等于search方法指定的参数n

8.中文分词器

1)什么是中文分词器
对于英文,是安装空格、标点符号进行分词
对于中文,应该安装具体的词来分,中文分词就是将词,切分成一个个有意义的词。
2)Lucene自带的中文分词器
StandardAnalyzer单字分词:就是按照中文一个字一个字地进行分词。如:“我爱中国”,
效果:“我”、“爱”、“中”、“国”。
CJKAnalyzer二分法分词:按两个字进行切分。如:“我是中国人”,效果:“我是”、“是中”、“中国”“国人”。
上边两个分词器无法满足需求。
3)第三方中文分词器IK-analyzer

这里写图片描述

4)代码
//使用ikAnalyzer分词器
IKAnalyzer analyzer = new IKAnalyzer();
5)扩展中文词库
将以下文件拷贝到config目录下

这里写图片描述

IKAnalyzer.cfg.xml内容如下:
<?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">ext.dic;</entry> 

    <!--用户可以在这里配置自己的扩展停止词字典-->
    <entry key="ext_stopwords">stopword.dic;</entry>    
</properties>
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值