Lucene相关学习笔记

一.Lucene概述

LOGO:
在这里插入图片描述

  • Lucene是一套用于全文检索和搜寻的开源程序库,由Apache软件基金会支持和提供
  • Lucene提供了一个简单却强大的应用程序接口(API),能够做全文索引和搜寻,在Java开发环境里Lucene是一个成熟的免费开放源代码工具
  • Lucene并不是现成的搜索引擎产品,但可以用来制作搜索引擎产品
  • 官网:http://lucene.apache.org/

1.1 什么是全文检索?

计算机索引程序通过扫描文章中的每一个词,对每一个词建立一个索引,指明该词在文章中出现的次数位置,当用户查询时,检索程序就根据事先建立的索引进行查找,并将查找的结果反馈给用户的检索方式

何为全文检索?举个例子,比如现在要在一个文件中查找某个字符串,最直接的想法就是从头开始检索,查到了就OK,这种对于小数据量的文件来说,很实用,但是对于大数据量的文件来说,就有点吃力了。或者说找包含某个字符串的文件,也是这样,如果在一个拥有几十个 G 的硬盘中找那效率可想而知,是很低的。

文件中的数据是属于非结构化数据,也就是说它没有什么结构可言,要解决上面提到的效率问题,首先我们得将非结构化数据中的一部分信息提取出来,重新组织,使其变得有一定结构,然后对这些有一定结构的数据进行搜索,从而达到搜索相对较快的目的。这就叫全文搜索。即先建立索引,再对索引进行搜索的过程。

总结:对文档(数据)中每一个词都做索引。

1.2 Lucene 建立索引的方式

那么 Lucene 中是如何建立索引的呢?假设现在有两篇文章,内容如下:

文章1的内容为:Tom lives in Guangzhou, I live in Guangzhou too.
文章2的内容为:He once lived in Shanghai.

首先第一步是将文档传给分词组件(Tokenizer),分词组件会将文档分成一个个单词,并去除标点符号和停词。所谓的停词指的是没有特别意义的词,比如英文中的 a,the,too 等。经过分词后,得到词元(Token) 。如下:

文章1经过分词后的结果:[Tom] [lives] [Guangzhou] [I] [live] [Guangzhou]
文章2经过分词后的结果:[He] [lives] [Shanghai]

然后将词元传给语言处理组件(Linguistic Processor),对于英语,语言处理组件一般会将字母变为小写,将单词缩减为词根形式,如 ”lives” 到 ”live” 等,将单词转变为词根形式,如 ”drove” 到 ”drive” 等。然后得到词(Term)。如下:

文章1经过处理后的结果:[tom] [live] [guangzhou] [i] [live] [guangzhou]
文章2经过处理后的结果:[he] [live] [shanghai]

最后将得到的词传给索引组件(Indexer),索引组件经过处理,得到下面的索引结构:

关键词 文章号[出现频率] 出现位置
guangzhou 1[2] 3,6
he 2[1] 1
i 1[1] 4
live 1[2],2[1] 2,5,2
shanghai 2[1] 3
tom 1[1] 1

以上就是Lucene 索引结构中最核心的部分。它的关键字是按字符顺序排列的,因此 Lucene 可以用二元搜索算法快速定位关键词。实现时 Lucene 将上面三列分别作为词典文件(Term Dictionary)、频率文件(frequencies)和位置文件(positions)保存。其中词典文件不仅保存有每个关键词,还保留了指向频率文件和位置文件的指针,通过指针可以找到该关键字的频率信息和位置信息
搜索的过程是先对词典二元查找、找到该词,通过指向频率文件的指针读出所有文章号,然后返回结果,然后就可以在具体的文章中根据出现位置找到该词了。所以 Lucene 在第一次建立索引的时候可能会比较慢,但是以后就不需要每次都建立索引了,就快了。

理解了 Lucene 的分词原理,接下来我们在 Spring Boot 中集成 Lucene 并实现索引和搜索的功能。

1.3 Lucene与Solr的关系

  • Lucene:底层的API,工具包
  • Solr:基于Lucene开发的企业级的搜索引擎产品

二.Spring Boot 中集成 Lucence

2.1 依赖导入


 		<!--thymeleaf 模板 这里导入模板是因为后面测试需要用到-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-thymeleaf</artifactId>
        </dependency>
		<!-- Lucence核心包 -->
        <dependency>
            <groupId>org.apache.lucene</groupId>
            <artifactId>lucene-core</artifactId>
            <version>4.10.2</version>
        </dependency>

        <!-- Lucene查询解析包 -->
        <dependency>
            <groupId>org.apache.lucene</groupId>
            <artifactId>lucene-queryparser</artifactId>
            <version>4.10.2</version>
        </dependency>

        <!-- 常规的分词(英文) -->
        <dependency>
            <groupId>org.apache.lucene</groupId>
            <artifactId>lucene-analyzers-common</artifactId>
            <version>4.10.2</version>
        </dependency>

        <!--支持分词高亮  -->
        <dependency>
            <groupId>org.apache.lucene</groupId>
            <artifactId>lucene-highlighter</artifactId>
            <version>4.10.2</version>
        </dependency>

        <!--支持中文分词  -->
        <dependency>
            <groupId>org.apache.lucene</groupId>
            <artifactId>lucene-analyzers-smartcn</artifactId>
            <version>4.10.2</version>
        </dependency>

        <!--引入IK分词器 -->
        <dependency>
            <groupId>com.janeluo</groupId>
            <artifactId>ikanalyzer</artifactId>
            <version>2012_u6</version>
        </dependency>

2.2 创建索引

/**
     * 新建索引库
     * @param location 索引库位置
     * @throws IOException
     */
    public static void createIndex(String location) throws IOException {

        long startTime = System.currentTimeMillis();

        //创建目录对象 指定索引库的存放位置
        Directory dir = FSDirectory.open(new File(location));

        //创建默认分词器对象
        //Analyzer analyzer = new StandardAnalyzer();

        //IK中文分词器
        Analyzer analyzer = new IKAnalyzer();

        //创建索引配置对象
        IndexWriterConfig config = new IndexWriterConfig(Version.LATEST, analyzer);

        //设置打开模式,默认为Append(追加索引) Create(覆盖索引)
        //config.setOpenMode(IndexWriterConfig.OpenMode.CREATE);

        //创建索引写入器对象
        IndexWriter indexWriter = new IndexWriter(dir, config);

        //创建文档对象
        Document document1 = new Document();

        //StringField等会创建索引,但是不会被分词。如果不分词,会造成整个字段作为一个词条,除非用户完全匹配,否则搜索不到
        document1.add(new StringField("id", "11", Field.Store.YES)); //YES表示存储到文档列表,NO表示不存储

        //TextField即创建索引,又会被分词。
        document1.add(new TextField("title", "我靠,碉堡了哈", Field.Store.YES));

        //向索引库写入文档对象
        //indexWriter.addDocument(document);
        Document document2 = new Document();
        document2.add(new StringField("id", "12", Field.Store.YES)); //YES表示存储到文档列表,NO表示不存储
        document2.add(new TextField("title", "hello,传智播客", Field.Store.YES));

        //批量写入文档
        List<Document> docs = new ArrayList<>();

        for (int i = 0; i < 10; i++) {
            Document document3 = new Document();
            LongField id = new LongField("id", i, Field.Store.YES);//YES表示存储到文档列表,NO表示不存储
            TextField title = new TextField("title", "hello,传智播客" + i, Field.Store.YES);
            if (i==3){
                //为该条数据增加权重,加上权重后查询时就会按权重出现在最前面(类似于百度的竞价排名)
                title.setBoost(100f);
            }
            document3.add(id);
            document3.add(title);
            docs.add(document3);
        }
        docs.add(document1);
        docs.add(document2);
        indexWriter.addDocuments(docs);

        //提交
        indexWriter.commit();
        //关闭
        indexWriter.close();

        long endTime = System.currentTimeMillis();

        log.info("创建索引共耗时{}ms", endTime - startTime);
    }
    
	public static void main(String[] args) throws Exception{
        //createIndex(LOCATION); //新建索引
        queryIndex(LOCATION, "传智播客", "title"); //查询索引数据
        //updateIndex(LOCATION); //更新索引
        //deleteIndex(LOCATION); //删除索引
    }

创建好索引后,就可以直接查看索引了,为了方便,我们这里先直接用索引查看工具查看

2.3 索引查看工具
在这里插入图片描述
Lucene相关学习笔记_第1张图片
Lucene相关学习笔记_第2张图片
注: lukeall的版本一定要和maven坐标的版本一致,否则将打不开查看工具,会报错

问题1:如何确定一个字段是否需要存储?
如果一个字段要显示到最终的结果中,那么一定要存储,否则就不存储
问题2:如何确定一个字段是否需要创建索引?
如果要根据这个字段进行搜索,那么这个字段就必须创建索引。
问题3:如何确定一个字段是否需要分词?
前提是这个字段首先要创建索引。然后如果这个字段的值是不可分割的,那么就不需要分词。例如:ID

2.4 Analyzer分词器

因为Lucene默认的分词器是不支持中文分词的,所以我们这里要引入第3方的IK分词器

		//创建默认分词器对象
        //Analyzer analyzer = new StandardAnalyzer();

        //用IK中文分词器替换掉默认分词器即可
        Analyzer analyzer = new IKAnalyzer();

中文分词更专业:
在这里插入图片描述
2.4.1 自定义词库

扩展词典(新创建词功能):有些词IK分词器不识别例如:“蓝瘦”,“香菇”
停用词典(停用某些词功能):有些词不需要建立索引例如:“哦”,“啊”,“的”

IK分词器的词库有限,新增加的词条可以通过配置文件添加到IK的词库中,也可以把一些不用的词条去除:

xml配置文件:


  
<properties>  
	<comment>IK Analyzer 扩展配置comment>
	
	<entry key="ext_dict">IK/ext.dic;entry>
	
	
	
properties>

新建扩展词典ext.dic 和停用词典stopword.dic
Lucene相关学习笔记_第3张图片
这样就加入了我们新的词,被停用的词语没有被分词

2.5. 查询索引数据

 /**
     * 查询索引
     *
     * @param location    索引库位置
     * @param searchWord  检索词
     * @param searchFiled 需要检索的字段
     * @throws IOException
     */
    public static List<String> queryIndex(String location, String searchWord, String searchFiled) throws IOException,
        ParseException, InvalidTokenOffsetsException {

        // 实际上Lucene本身不支持分页。因此我们需要自己进行逻辑分页。我们要准备分页参数:
        int pageNum = 1;// 当前页码
        int pageSize = 1;// 每页条数
        int start = (pageNum - 1) * pageSize;// 当前页的起始条数
        int end = start + pageSize;// 当前页的结束条数(不能包含)

        // 初始化索引库对象
        Directory directory = FSDirectory.open(new File(location));

        // 索引读取工具
        IndexReader indexReader = DirectoryReader.open(directory);
        // 索引搜索对象
        IndexSearcher indexSearcher = new IndexSearcher(indexReader);

        IKAnalyzer analyzer = new IKAnalyzer();
        // 1.普通查询解析器对象
        QueryParser parser = new QueryParser(searchFiled, analyzer);

        //2.多字段查询解析器(可以根据多个字段查询)
        //MultiFieldQueryParser parser = new MultiFieldQueryParser(new String[]{"id","title"},new IKAnalyzer());
        // 创建查询对象
        Query query = parser.parse(searchWord);

        // 格式化器
        SimpleHTMLFormatter formatter = new SimpleHTMLFormatter("", "");
        QueryScorer scorer = new QueryScorer(query);

        //根据这个得分计算出一个片段
        Fragmenter fragmenter = new SimpleSpanFragmenter(scorer);

        // 准备高亮工具
        Highlighter highlighter = new Highlighter(formatter, scorer);

        //设置一下要显示的片段
        highlighter.setTextFragmenter(fragmenter);

        //3.词条查询(只能查询一个词,例如可以查询"谷歌",不能查询"谷歌地图",这是2个词)
        //Query query = new TermQuery(new Term(searchFiled, searchWord));

        //4.通配符
        //?:通配一个字符
        //*:通配多个字符
        //Query query = new WildcardQuery(new Term(searchFiled, "*"+searchWord+"*"));

        //5.模糊查询
        //Query query = new FuzzyQuery(new Term(searchFiled, searchWord), 2);//允许写错的最大编辑距离

        //6.数值范围查询(查询非String类型的数据或者说是一些继承Numeric类的对象的查询)
        //Query query = NumericRangeQuery.newLongRange("id", 2L, 9L, true, true);

        //7.组合查询
        // 交集:Occur.MUST + Occur.MUST
        // 并集:Occur.SHOULD + Occur.SHOULD
        // 非:Occur.MUST_NOT
        //BooleanQuery query = new BooleanQuery();
        //Query query1 = NumericRangeQuery.newLongRange("id", 2L, 4L, true, true);
        //Query query2 = NumericRangeQuery.newLongRange("id", 0L, 3L, true, true);
        //query.add(query1, BooleanClause.Occur.SHOULD);
        //query.add(query2, BooleanClause.Occur.SHOULD);

        // 创建排序对象,需要排序字段SortField,参数:字段的名称、字段的类型、是否反转如果是false,升序。true降序
        Sort sort = new Sort(new SortField("id", SortField.Type.LONG, false));

        // 执行搜索操作,返回值topDocs包含命中数,得分文档
        TopDocs topDocs = indexSearcher.search(query, end, sort);//MAX_VALUE:返回排名靠前的多少条字段 sort:排序
        // 打印命中数
        System.out.println("一共命中:" + topDocs.totalHits + "条数据");
        // 获得得分文档数组对象,得分文档对象包含得分和文档编号
        ScoreDoc[] scoreDocs = topDocs.scoreDocs;
        List<String> list=new ArrayList<>();
        for (int i = start; i < end; i++) {
            ScoreDoc scoreDoc = scoreDocs[i];
            System.out.println("得分:" + scoreDoc.score);
            // 文档的编号
            int doc = scoreDoc.doc;
            System.out.println("编号:" + doc);
            // 获取文档对象,通过索引读取工具
            Document document = indexReader.document(doc);
            System.out.println("id:" + document.get("id"));
            String title = document.get(searchFiled);
            if (title!=null){
                 //高亮显示
                TokenStream tokenStream = analyzer.tokenStream(searchFiled, new StringReader(title));
                String heightTitle = highlighter.getBestFragment(tokenStream, title);
                 //将查询到的数据放入list返回
                list.add(heightTitle);
                System.out.println(String.format("高亮显示后的{%s}为{%s}", searchFiled, heightTitle));
            }
        }
        indexReader.close();
        return list;
    }

2.6 修改更新索引

 /**
     * 更新索引
     *
     * @param location
     * @throws IOException
     * @throws ParseException
     */
    public static void updateIndex(String location) throws IOException,
        ParseException {
        // 创建文档对象
        Document document = new Document();
        document.add(new StringField("id", "9", Field.Store.YES));
        document.add(new TextField("title", "谷歌地图之父跳槽FaceBook", Field.Store.YES));

        // 索引库对象
        Directory directory = FSDirectory.open(new File(location));
        // 索引写入器配置对象
        IndexWriterConfig conf = new IndexWriterConfig(Version.LATEST, new IKAnalyzer());
        // 索引写入器对象
        IndexWriter indexWriter = new IndexWriter(directory, conf);

        // 执行更新操作(将查询到碉堡所有的数据全部更新为document)
        indexWriter.updateDocument(new Term("title", "碉堡"), document);
        // 提交
        indexWriter.commit();
        // 关闭
        indexWriter.close();

    }

2.7 删除索引

/**
     * 删除索引
     *
     * @param location
     * @throws IOException
     * @throws ParseException
     */
    public static void deleteIndex(String location) throws IOException,
        ParseException {
        // 创建目录对象
        Directory directory = FSDirectory.open(new File(location));
        // 创建索引写入器配置对象
        IndexWriterConfig conf = new IndexWriterConfig(Version.LATEST, new IKAnalyzer());
        // 创建索引写入器对象
        IndexWriter indexWriter = new IndexWriter(directory, conf);

        // 执行删除操作(根据词条),要求id字段必须是字符串类型
        // indexWriter.deleteDocuments(new Term("id", "5"));
        // 根据查询条件删除
        // indexWriter.deleteDocuments(NumericRangeQuery.newLongRange("id", 2l, 4l, true, false));
        // 删除所有
        indexWriter.deleteAll();

        // 提交
        indexWriter.commit();
        // 关闭
        indexWriter.close();

    }

2.8 高亮显示测试

准备thyleaf模板页面result.html


<html lang="en" xmlns:th="http://www.w3.org/1999/xhtml">
<head>
  <meta charset="UTF-8">
  <title>Titletitle>
head>
<body>
<div th:each="desc : ${list}">
  <div th:utext="${desc}">div>
div>
body>
html>

准备后端controller

	@GetMapping("/lucene")
    public String test(Model model) throws ParseException, InvalidTokenOffsetsException, IOException {

        String indexDir="D:\\index";

        List<String> list = IndexUtil.queryIndex(indexDir, "南京文化", "title");
        model.addAttribute("list",list);
        return "result";
    }

测试结果
在这里插入图片描述
注:模板文件必须要放到resources/templates目录下
Lucene相关学习笔记_第4张图片
spring boot项目只有src目录,没有webapp目录,由于我们应用了Web模块,因此产生了 static目录与templates目录,前者用于存放静态资源,如图片、CSS、JavaScript等;后者用于存放Web页面的模板文件

你可能感兴趣的:(全文检索引擎)