前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >Lucene初步学习及在博客系统中应用demo

Lucene初步学习及在博客系统中应用demo

作者头像
呼延十
发布2019-06-26 17:50:51
7810
发布2019-06-26 17:50:51
举报
文章被收录于专栏:呼延呼延呼延

目录

Lucene 简介

简介

Lucene是一套用于全文检索和搜索的开放源码程序库,由Apache软件基金会支持和提供。Lucene提供了一个简单却强大的应用程序接口,能够做全文索引和搜索,在Java开发环境里Lucene是一个成熟的免费开放源代码工具;就其本身而论,Lucene是现在并且是这几年,最受欢迎的免费Java信息检索程序库。

Lucene 能够为文本类型的数据建立索引,所以你只要能把你要索引的数据格式转化的文本的,Lucene 就能对你的文档进行索引和搜索。比如你要对一些 HTML 文档,PDF 文档进行索引的话你就首先需要把 HTML 文档和 PDF 文档转化成文本格式的,然后将转化后的内容交给 Lucene 进行索引,然后把创建好的索引文件保存到磁盘或者内存中,最后根据用户输入的查询条件在索引文件上进行查询。不指定要索引的文档的格式也使 Lucene 能够几乎适用于所有的搜索应用程序。

现在很流行的SolrElasticsearch,都是基于Lucene开发的.此外,Eclipse的帮助系统的搜索也是基于Lucene实现的.

流程图

使用lucene构建的搜索程序的流程图如下(图源:Lucene In Action书中配图,博主绘制):

2019-05-23-17-13-18
2019-05-23-17-13-18

红框中的部分可以由Lucene完成,其余需要自己编码或者借助其他开源框架.

使用准备

简单的使用Lucene,只需要引入Jar包,然后编写对应的生成索引和查找代码即可.

在本文的示例中,我使用Lucene给我的博客建立一个简单的搜索系统,因为之前的搜索系统是在前端完成的,这次学习的Lucene正好可以拿来完成一个后端的搜索系统.

实现思路:

  1. 对博客目录下的所有已md结尾的文件建立索引.并将索引写在硬盘上的某个目录下.
  2. 提供重建索引的API,因为文章可能会修改,以及新增.
  3. 提供根据关键字查找的API.
  4. 根据查找API返回的结果在前端进行渲染.

第四步就不说了,我凑活写了一点JS代码,能够比较丑的渲染出来.

对应于上面的架构图,这里详细写一下博客搜索的实现方法: 首先是建索引的过程,对应图中由下到上.

  1. Row Data为在磁盘上存储的博客文件.
  2. acqure content以及build document为自己编码实现,主要是扫面磁盘文件,并且将其转换成Lucene Document的格式.
  3. Analyzs Document使用了Lucene的分词机制,使用的分词器为IKAnalyzier.
  4. index document使用lucene的索引API.

然后是搜索过程:

  1. search ui是由前端完成,直接传入搜索字符串.
  2. 使用lucene分词机制对搜索词进行分词
  3. 调用lucene查询API,查询文件
  4. 调用lucene解析结果的API拿到结果,封装,返回至前端.
  5. 前端进行渲染.

添加依赖

使用Maven管理项目的话:

        <!-- https://mvnrepository.com/artifact/org.apache.lucene/lucene-core -->
        <dependency>
            <groupId>org.apache.lucene</groupId>
            <artifactId>lucene-core</artifactId>
            <version>8.0.0</version>
        </dependency>

        <!-- https://mvnrepository.com/artifact/org.apache.lucene/lucene-queryparser -->
        <dependency>
            <groupId>org.apache.lucene</groupId>
            <artifactId>lucene-queryparser</artifactId>
            <version>8.0.0</version>
        </dependency>

        <!-- https://mvnrepository.com/artifact/org.apache.lucene/lucene-highlighter -->
        <dependency>
            <groupId>org.apache.lucene</groupId>
            <artifactId>lucene-highlighter</artifactId>
            <version>8.0.0</version>
        </dependency>

        <!-- https://mvnrepository.com/artifact/org.apache.lucene/lucene-analyzers-common -->
        <dependency>
            <groupId>org.apache.lucene</groupId>
            <artifactId>lucene-analyzers-common</artifactId>
            <version>8.0.0</version>
        </dependency>

建立索引代码

    /**
     * 使用IndexWriter对数据创建索引
     */
    public void create() throws IOException {

        if (deleteAllIndex()) {
            logger.info("clear the index.");
        } else {
            logger.info("cleat the index error, stop create index.");
            return;
        }

        // 索引存放的位置...
        Directory d = FSDirectory.open(FileSystems.getDefault().getPath(indexPath));

        logger.info("start create index for blog.");
        // 索引写入的配置
        Analyzer analyzer = new JcsegAnalyzer(1);// 分词器
        IndexWriterConfig conf = new IndexWriterConfig(analyzer);
        // 构建用于操作索引的类
        IndexWriter indexWriter = new IndexWriter(d, conf);
        int curFileNum = FILE_NUM.get();
        findFile(blogPath, indexWriter);
        logger.info("add {} file into index.", FILE_NUM.get() - curFileNum);

        indexWriter.close();
    }

    private boolean deleteAllIndex() {
        File indexDir = new File(indexPath);
        if (indexDir.isDirectory()) {
            File[] files = indexDir.listFiles();
            if (files == null || files.length == 0) {
                logger.info("index dir is em[ty.");
                return true;
            }
            for (File file : files) {
                if (!file.delete()) {
                    logger.info("delete some index error, file = {}", file.getAbsolutePath());
                    return false;
                }
            }
            return true;
        }
        logger.info("delete index error, because the file is not a dir.");
        return false;
    }

    private void findFile(String blogPath, IndexWriter indexWriter) throws IOException {
        File file = new File(blogPath);
        if (file.exists()) {
            File[] files = file.listFiles();
            if (files == null || files.length == 0) {
                logger.info("this file is empty, path = {}", file.getAbsolutePath());
            } else {
                for (File file2 : files) {
                    if (file2.isDirectory()) {
                        logger.info(" {} is a directory, find in it.", file2.getAbsolutePath());
                        findFile(file2.getAbsolutePath(), indexWriter);
                    } else {
                        if (file2.getName().endsWith(".md")) {
                            logger.info("find a md file = {} , make index", file2.getAbsolutePath());
                            FILE_NUM.incrementAndGet();
                            addFile(indexWriter, file2);
                        }
                    }
                }
            }
        } else {
            logger.warn("file is not exist");
        }
    }

    private void addFile(IndexWriter indexWriter, File file) throws IOException {
        // 通过IndexWriter来创建索引
        // 索引库里面的数据 要遵守一定的结构(索引结构,document)
        Document doc = new Document();
        String titleValue = file.getName().replace(".md", "");
        IndexableField title = new TextField("title", titleValue, Field.Store.YES);

        String contentValue = readToString(file);
        IndexableField content = new TextField(
                "content", contentValue, Field.Store.YES);

        Map<String, List<String>> profiles = getTagsAndCategoriesFromContent(file, contentValue);

        List<String> tagList = profiles.get(TAGS_KEY);
        String tagStr = String.join(",", tagList);
        IndexableField tags = new TextField("tags", tagStr, Field.Store.YES);

        List<String> categoriesList = profiles.get(CATEGORIES_KEY);
        String categoriesStr = String.join(",", categoriesList);
        IndexableField categories = new TextField("categories", categoriesStr, Field.Store.YES);


        doc.add(title);
        doc.add(content);
        doc.add(tags);
        doc.add(categories);
        // document里面也有很多字段
        indexWriter.addDocument(doc);
    }

这里面主要实现了四个方法:

  1. 创建索引的入口
  2. 删除当前的所有索引
  3. 遍历查找文件
  4. 将查找到的文件读取并调用Lucene API建立索引.

查找API

public List<SearchArticleVO> search(String target) throws IOException {

        List<SearchArticleVO> result = new ArrayList<>();
        String[] fields = {"title", "tags", "content"};
        for (String field : fields) {
            paddingResultByField(target, result, field);
            if (result.size() >= 10) {
                break;
            }
        }

        return result;
    }

    private void paddingResultByField(String target, List<SearchArticleVO> result, String field) throws IOException {
        // 索引存放的位置...
        Directory d = FSDirectory.open(FileSystems.getDefault().getPath(indexPath));

        // 通过indexSearcher去检索索引目录
        IndexReader indexReader = DirectoryReader.open(d);
        IndexSearcher indexSearcher = new IndexSearcher(indexReader);

        // 这是一个搜索条件,根据这个搜索条件我们来进行查找
        // term是根据哪个字段进行检索,以及字段对应值
        //================================================
        Query query = new TermQuery(new Term(field, target));

        // 搜索先搜索索引目录
        // 找到符合条件的前100条数据
        TopDocs topDocs = indexSearcher.search(query, 100);
        logger.info("search keyword = {}, getNum = {}", target, topDocs.totalHits);
        ScoreDoc[] scoreDocs = topDocs.scoreDocs;
        for (ScoreDoc scoreDoc : scoreDocs) {
            //得分采用的是VSM算法
            logger.info("score = {}", scoreDoc.score);
            //获取查询结果的文档的惟一编号,只有获取惟一编号,才能获取该编号对应的数据
            int doc = scoreDoc.doc;
            //使用编号,获取真正的数据
            Document document = indexSearcher.doc(doc);

            String title = document.get("title").substring(11);
            logger.info("get article name = {}", title);

            String tagStr = document.get("tags");
            String content = document.get("content");
            String categoriesStr = document.get("categories");

            List<String> tagList = Stream.of(tagStr.split(",")).collect(Collectors.toList());
            List<String> caList = Stream.of(categoriesStr.split(",")).collect(Collectors.toList());


            String cateUrl = String.join("/", caList) + "/";


            String dateUrl = document.get("title").substring(0, 11).replace("-", "/");


            String url = "http://huyan.couplecoders.tech/" + cateUrl.toLowerCase() + dateUrl + title;

            int firstIndex = content.indexOf(target);

            String targetStr = content.substring(firstIndex - 15 < 0 ? 0 : firstIndex - 15, firstIndex + 15 > content.length() ? content.length() - 1 : firstIndex + 15).replaceAll("\n", "");

            result.add(SearchArticleVO.builder()
                    .title(title).content(content)
                    .tags(tagList)
                    .url(url)
                    .categories(caList)
                    .targetStr(targetStr)
                    .build());

        }
    }

    private Map<String, List<String>> getTagsAndCategoriesFromContent(File file, String content) {
        Map<String, List<String>> map = new HashMap<>();

        Pattern r = Pattern.compile("---\n.*---\n", Pattern.CASE_INSENSITIVE | Pattern.DOTALL);
        Matcher m = r.matcher(content);
        if (m.find()) {
            String s = m.group();

            //tag
            List<String> tags = new ArrayList<>();
            Pattern p1 = Pattern.compile("- .*\n");
            Matcher m1 = p1.matcher(s);
            while (m1.find()) {
                tags.add(m1.group(0).replace("\n", "").replace("- ", ""));
            }
            map.put(TAGS_KEY, tags);
            logger.info("load {} tags from {} done.", tags.size(), file.getAbsolutePath() + file.getName());

            //category
            List<String> categories = new ArrayList<>();
            Pattern p2 = Pattern.compile("category:.*\n");
            Matcher m2 = p2.matcher(s);
            while (m2.find()) {
                List<String> tmp = Stream.of(m2.group()
                        .replace("category: ", "")
                        .replace("]", "")
                        .replace("\n", "")
                        .replace("[", "")
                        .split(",")).collect(Collectors.toList());
                categories.addAll(tmp);
            }
            map.put(CATEGORIES_KEY, categories);
            logger.info("load {} categories from {} done.", categories.size(), file.getAbsolutePath() + file.getName());

        } else {
            logger.warn("not match tags or categories from {}, warning.", file.getAbsolutePath() + file.getName());
        }


        return map;
    }

这里主要实现的是:

  • 根据传入嗯关键字进行查找,然后获取真正的内容进行返回.
  • 返回的时候需要拼装多个字段,在上面的代码中有很大一部分进行了这个操作.

实现的效果

这个是纯后端的一个项目,实现的效果较为难以量化.

体验地址

在博客的SEARCH页面中添加了入口,可以输入关键字进行搜索.

搜索效率比较高,我在后台实际测试在毫秒级.

关于重建索引的说明

Lucene其实是支持增量的添加索引的,否则在数据量极其大的情况下,每次都全量的重建索引是不科学的.

但是在本文中,我采用的是每次清空索引,全部重建,原因主要有以下:

  1. 我的数据量很小,1k篇文章最多了.
  2. 我只是写个Demo,增量添加索引打算后续研究以下.
  3. 每次不止是添加文章,还可能对已有的文章进行了一些修改,所以在这个情况下的增量添加索引我没整明白.

存在的问题

  1. 就像上面写的,需要解决增量添加索引的问题,全量更新不是长久之计.
  2. 我测试的效率还不错,但是远没有达到预期,因为在我的数据量下需要100ms,那么在真正的应用场景中,这个延迟肯定是不能接受的,所以还有优化的空间.
  3. Lucene的Collector功能十分丰富,应该是有提供更科学一点的方式来进行结果数据的组装,我现在的手动组装太愚蠢了.
  4. Lucene应该支持目标内容高亮,目前我在前端实现的目标高亮算法实在是…,所以需要切换一下高亮内容的获取方法.

这些问题将在后续的文章中一一解决.

参考文章

Lucene-维基百科

基于Java的全文检索引擎简介

完。

ChangeLog

2019-04-07 完成

以上皆为个人所思所得,如有错误欢迎评论区指正。

欢迎转载,烦请署名并保留原文链接。

联系邮箱:huyanshi2580@gmail.com

更多学习笔记见个人博客——>呼延十

var gitment = new Gitment({ id: 'Lucene初步学习及在博客系统中应用demo', // 可选。默认为 location.href owner: 'hublanker', repo: 'blog', oauth: { client_id: '2297651c181f632a31db', client_secret: 'a62f60d8da404586acc965a2ba6a6da9f053703b', }, }) gitment.render('container')



本文参与 腾讯云自媒体分享计划,分享自作者个人站点/博客。
如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 目录
  • Lucene 简介
    • 简介
      • 流程图
      • 使用准备
        • 添加依赖
          • 建立索引代码
            • 查找API
              • ChangeLog
          • 实现的效果
          • 关于重建索引的说明
          • 存在的问题
          • 参考文章
          领券
          问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档