lucene入门

什么是Lucene?

Lucene是apache下的一个开放源代码的全文检索引擎工具包。提供了完整的查询引擎和索引引擎,
部分文本分析引擎。Lucene的目的是为软件开发人员提供一个简单易用的工具包,以方便的在目标系统中实现全文检索的功能。

Lucene与搜索引擎的区别

全文检索系统是按照全文检索理论建立起来的用于提供全文检索服务的软件系统。全文检索系统是一个可以运行的系统,
包括建立索引、处理查询返回结果集、增加索引、优化索引结构等功能。例如:百度搜索、eclipse帮助搜索、淘宝网商品搜索。

搜索引擎是全文检索技术最主要的一个应用,例如百度。搜索引擎起源于传统的信息全文检索理论,
即计算机程序通过扫描每一篇文章中的每一个词,建立以词为单位的倒排文件,
检索程序根据检索词在每一篇文章中出现的频率和每一个检索词在一篇文章中出现的概率,对包含这些检索词的文章进行排序,
最后输出排序的结果。全文检索技术是搜索引擎的核心支撑技术。

Lucene和搜索引擎不同,Lucene是一套用java写的全文检索的工具包,为应用程序提供了很多个api接口去调用,
可以简单理解为是一套实现全文检索的类库,搜索引擎是一个全文检索系统,它是一个单独运行的软件。

Lucene实现全文检索的流程

全文检索的流程分为两大部分:索引流程、搜索流程。

索引流程:即采集数据 -> 构建文档对象  -> 分析文档(分词 -> 创建索引。
搜索流程:即用户通过搜索界面 -> 创建查询 -> 执行搜索,搜索器从索引库搜索 -> 渲染搜索结果。

入门程序

使用Lucene实现电商项目中图书类商品的索引和搜索功能。

环境准备

Jdk环境:1.7.0_72
Ide环境:eclipse indigo
数据库环境:mysql 5.1
Lucene:4.10.3

Lucene下载安装

Lucene是开发全文检索功能的工具包,使用时从官方网站下载,并解压。

官方网站:http://lucene.apache.org/ 
目前最新版本:5.2.1

下载地址:http://archive.apache.org/dist/lucene/java/

下载版本:4.10.3
JDK要求:1.7以上(从版本4.8开始,不支持1.7以下)

工程搭建

1.创建java工程
2.添加jar包,入门程序只需要添加以下jar包:

    mysql5.1驱动包:mysql-connector-java-5.1.7-bin.jar
    核心包:lucene-core-4.10.3.jar
    分析器通用包:lucene-analyzers-common-4.10.3.jar
    查询解析器包:lucene-queryparser-4.10.3.jar
    junit包:junit-4.9.jar

索引流程

对文档索引的过程,就是将用户要搜索的文档内容进行索引,然后把索引存储在索引库(index)中。

为什么要采集数据?

全文检索要搜索的数据信息格式多种多样,拿搜索引擎(百度, google)来说,
通过搜索引擎网站能搜索互联网站上的网页(html)、互联网上的音乐(mp3..)、视频(avi..)、pdf电子书等。
全文检索搜索的这些数据称为非结构化数据。

什么是非结构化数据?

结构化数据:指具有固定格式或有限长度的数据,如数据库,元数据等。
非结构化数据:指不定长或无固定格式的数据,如邮件,word文档等。

如何对结构化数据搜索?

由于结构化数据是固定格式,所以就可以针对固定格式的数据设计算法来搜索,
比如数据库like查询,like查询采用顺序扫描法,使用关键字匹配内容,对于内容量大的like查询速度慢。

如何对非结构化数据搜索?

需要将所有要搜索的非结构化数据通过技术手段采集到一个固定的地方,
将这些非结构化的数据想办法组成结构化的数据,再以一定的算法去搜索。

如何采集数据?

采集数据技术有哪些?
1、对于互联网上网页采用http将网页抓取到本地生成html文件。 
2、如果数据在数据库中就连接数据库读取表中的数据。
3、如果数据是文件系统中的某个文件,就通过文件系统读取文件的内容。

网页采集(常用solr)

因为目前搜索引擎主要搜索数据的来源是互联网,搜索引擎使用一种爬虫程序抓取网页( 通过http抓取html网页信息),
以下是一些爬虫项目:

Solr(http://lucene.apache.org/solr) ,solr是apache的一个子项目,支持从关系数据库、xml文档中提取原始数据。

Nutch(http://lucene.apache.org/nutch), Nutch是apache的一个子项目,
包括大规模爬虫工具,能够抓取和分辨web网站数据。

jsoup(http://jsoup.org/ ),jsoup 是一款Java 的HTML解析器,可直接解析某个URL地址、HTML文本内容。
它提供了一套非常省力的API,可通过DOM,CSS以及类似于jQuery的操作方法来取出和操作数据。

heritrix(http://sourceforge.net/projects/archive-crawler/files/),Heritrix 是一个由 java 开发的、开源的网络爬虫,
用户可以使用它来从网上抓取想要的资源。其最出色之处在于它良好的可扩展性,方便用户实现自己的抓取逻辑。

数据库采集

针对电商站内搜索功能,全文检索的数据源在数据库中,需要通过jdbc访问数据库中book表的内容。

po

    public class Book {
    // 图书ID
    private Integer id;
    // 图书名称
    private String name;
    // 图书价格
    private Float price;
    // 图书图片
    private String pic;
    // 图书描述
    private String description;
}

Dao

    public interface BookDao {
        // 图书查询
        public List<Book> queryBookList() throws Exception;
        }


    public class BookDaoImpl implements BookDao {

    @Override
    public List<Book> queryBookList() throws Exception {
        // 数据库链接
        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/lucene", "root", "root");

            // 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;
    }

}

索引文件的逻辑结构

文档域:
对非结构化的数据统一格式为document文档格式,一个文档有多个field域,
不同的文档其field的个数可以不同,建议相同类型的文档包括相同的field。 
本例子一个document对应一 条 book表的记录。

索引域:
用于搜索,搜索程序将从索引域中搜索一个一个词,根据词找到对应的文档。
将Document中的Field的内容进行分词,将分好的词创建索引,索引=Field域名:词

倒排索引表

传统方法是先找到文件。如何在文件中找内容,在文件内容中匹配搜索关键字,
这种方法是顺序扫描方法,数据量大就搜索慢。

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

创建索引

创建索引流程:

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

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

创建Document

采集数据的目的是为了索引,在索引前需要将原始内容创建成文档(Document),文档(Document)中包括一个一个的域(Field)。

代码实现

@Test
public void createIndex() throws Exception{
//1.采集数据
BookDao dao = new BookDaoImpl();
//2.采集的数据封装成dacoument对象
List<Book> list = dao.queryBookList();

List<Document> docList = new ArrayList<Document>();
Document doc;
for (Book book : list) {
    doc = new Document();

    //图书id
    //不分词 、 不索引、要存储
    Field idField = new StoredField("id", book.getId().toString());

    //图书名称
    //分词、索引、存储
    Field nameField = new TextField("name", book.getName().toString(),Store.YES);

    //图书价格
    //分词、索引、存储
    Field priceField = new FloatField("price", book.getPrice(),Store.YES);

    //图书图片地址
    //不分词 、 不索引、要存储
    Field picField = new StoredField("pic", book.getPic());

    //图片描述
    //分词、索引、不存储
    Field desField = new TextField("description", book.getDescription().toString(),Store.NO);

    doc.add(desField);
    doc.add(picField);
    doc.add(priceField);
    doc.add(nameField);
    doc.add(idField);

    docList.add(doc);
}

分词

分词过程

在对Docuemnt中的内容索引之前需要使用分词器进行分词 ,主要过程就是分词、过虑两步。

1.分词就是将采集到的文档内容切分成一个一个的词,具体应该说是将Document中Field的value值切分成一个一个的词。

2.过虑包括去除标点符号、去除停用词(的、是、a、an、the等)、
大写转小写、词的形还原(复数形式转成单数形参、过去式转成现在式。。。)等。 

什么是停用词?

停用词是为节省存储空间和提高搜索效率,
搜索引擎在索引页面或处理搜索请求时会自动忽略某些字或词,这些字或词即被称为Stop Words(停用词)。

比如语气助词、副词、介词、连接词等,
通常自身并无明确的意义,只有将其放入一个完整的句子中才有一定作用,如常见的“的”、“在”、“是”、“啊”等。

分词器

Tokenizer是分词器,负责将reader转换为语汇单元即进行分词,
Lucene提供了很多的分词器,也可以使用第三方的分词,比如IKAnalyzer一个中文分词器。

tokenFilter是分词过滤器,负责对语汇单元进行过滤,
tokenFilter可以是一个过滤器链儿,Lucene提供了很多的分词器过滤器,比如大小写转换、去除停用词等。

如下图是语汇单元的生成过程:

比如下边的文档经过分析器分析如下:

原文档内容:
Lucene is a Java full-text search engine.  

分析后得到的语汇单元:
lucene、java、full、text、search、engine

同一个域中相同的语汇单元(Token)对应同一个Term(词),它记录了语汇单元的内容及所在域的域名等。
不同的域中拆分出来的相同的单词对应不同的term。
相同的域中拆分出来的相同的单词对应相同的term。

例如:图书信息里面,图书名称中的java和图书描述中的java对应不同的term

代码实现

    //3.进行分词
//    Analyzer analyzer = new StandardAnalyzer();
    Analyzer analyzer = new IKAnalyzer();

    //4.创建indexWriter
    //4.1创建Directory 目录流
    IndexWriterConfig indexconfig = new IndexWriterConfig(Version.LUCENE_4_10_3, analyzer);

    //Directory 是抽象类,有两个实现类,FSDirectory(文件系统) RAMDirectory(内存)
    Directory directory = FSDirectory.open(new File("F:\\index"));
    IndexWriter writer = new IndexWriter(directory,indexconfig);

    //5.通过inexWriter将document对象写入索引库
    for (Document document : docList) {
        writer.addDocument(document);
    }
    //6.关闭IndexWriter
    writer.close();
}

搜索流程

1、用户定义查询语句,用户确定查询什么内容(输入什么关键字),指定查询语法,相当于sql语句。
2、IndexSearcher索引搜索对象,定义了很多搜索方法,程序员调用此方法搜索。
3、IndexReader索引读取对象,它对应的索引维护对象IndexWriter,IndexSearcher通过IndexReader读取索引目录中的索引文件
4、Directory索引流对象,IndexReader需要Directory读取索引库,使用FSDirectory文件系统流对象
5、IndexSearcher搜索完成,返回一个TopDocs(匹配度高的前边的一些记录)

输入查询语句

同数据库的sql一样,lucene全文检索也有固定的语法:
最基本的有比如:AND, OR, NOT 等

用户想找一个description中包括java关键字和lucene关键字的文档。
它对应的查询语句:description:java AND lucene

搜索分词

和索引过程的分词一样,这里要对用户输入的关键字进行分词,一般情况索引和搜索使用的分词器一致。
比如:输入搜索关键字“java培训”,分词后为java和培训两个词,与java和培训有关的内容都搜索出来了。

搜索索引

根据关键字从索引中找到对应的索引信息,即词term。term与document相关联,
找到了term就找到了关联的document,从document取出Field中的信息即是要搜索的信息。

代码实现

    private void doSearch(Query query) {

    try {
        // 2.创建搜索器IndexSearch
        // 2.1创建IndexReader
        Directory directory = FSDirectory.open(new File("F:\\index"));
        IndexReader reader = DirectoryReader.open(directory);
        // 2.2创建Directroy

        IndexSearcher searcher = new IndexSearcher(reader);

        // 3.通过indexSearch来查询索引库
        TopDocs topDocs;
        topDocs = searcher.search(query, 10);
        // 4.处理结果
        // 根据匹配条件查询出来的数据总数
        long count = topDocs.totalHits;
        System.out.println("根据匹配条件查询出来的数据总数: " + count);

        ScoreDoc[] scoreDocs = topDocs.scoreDocs;
        for (ScoreDoc scoreDoc : scoreDocs) {
            int docId = scoreDoc.doc;
            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("商品pic: " + doc.get("pic"));
        }
        // 5.关闭IndexReader
        reader.close();
    } catch (IOException e) {
        // TODO Auto-generated catch block
        e.printStackTrace();
    }

}


    @Test
    public void search() throws ParseException, IOException {
        // 1.创建查询对象
        // 1.1创建QueryParser对象

        Analyzer analyzer = new StandardAnalyzer();
        // 默认搜索的域的域名
        String field = "name";
        QueryParser parser = new QueryParser(field, analyzer);
        Query query = parser.parse("name: 花");

        doSearch(query);

    }