springboot集成ES实现磁盘文件全文检索

spring-boot 专栏收录该内容
10 篇文章 1 订阅

image

有个朋友咨询如何实现对海量磁盘资料进行目录、文件名及文件正文进行搜索,要求实现简单高效、维护方便、成本低廉。我想了想利用ES来实现文档的索引及搜索是适当的选择,于是就着手写了一些代码来实现,下面就将设计思路及实现方法作以介绍。

整体架构

考虑到磁盘文件分布到不同的设备上,所以采用磁盘扫瞄代理的模式构建系统,即把扫描服务以代理的方式部署到目标磁盘所在的服务器上,作为定时任务执行,索引统一建立到ES中,当然ES采用分布式高可用部署方法,搜索服务和扫描代理部署到一起来简化架构并实现分布式能力。

image

部署ES

ES(elasticsearch)是本项目唯一依赖的第三方软件,ES支持docker方式部署,以下是部署过程

docker pull docker.elastic.co/elasticsearch/elasticsearch:6.3.2
docker run -e ES_JAVA_OPTS="-Xms256m -Xmx256m" -d -p 9200:9200 -p 9300:9300 --name es01 docker.elastic.co/elasticsearch/elasticsearch:6.3.2

部署完成后,通过浏览器打开http://localhost:9200,如果正常打开,出现如下界面,则说明ES部署成功。

image

工程结构

image

依赖包

本项目除了引入springboot的基础starter外,还需要引入ES相关包

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-elasticsearch</artifactId>
        </dependency>
        <dependency>
            <groupId>io.searchbox</groupId>
            <artifactId>jest</artifactId>
            <version>5.3.3</version>
        </dependency>
        <dependency>
            <groupId>net.sf.jmimemagic</groupId>
            <artifactId>jmimemagic</artifactId>
            <version>0.1.4</version>
        </dependency>
    </dependencies>

配置文件

需要将ES的访问地址配置到application.yml里边,同时为了简化程序,需要将待扫描磁盘的根目录(index-root)配置进去,后面的扫描任务就会递归遍历该目录下的全部可索引文件。

server:
  port: @elasticsearch.port@
spring:
  application:
    name: @project.artifactId@
  profiles:
    active: dev
  elasticsearch:
    jest:
      uris: http://127.0.0.1:9200
index-root: /Users/crazyicelee/mywokerspace

索引结构数据定义

因为要求文件所在目录、文件名、文件正文都有能够检索,所以要将这些内容都作为索引字段定义,而且添加ES client要求的JestId来注解id。

package com.crazyice.lee.accumulation.search.data;

import io.searchbox.annotations.JestId;
import lombok.Data;

@Data
public class Article {
    @JestId
    private Integer id;
    private String author;
    private String title;
    private String path;
    private String content;
    private String fileFingerprint;
}

扫描磁盘并创建索引

因为要扫描指定目录下的全部文件,所以采用递归的方法遍历该目录,并标识已经处理的文件以提升效率,在文件类型识别方面采用两种方式可供选择,一个是文件内容更为精准判断(Magic),一种是以文件扩展名粗略判断。这部分是整个系统的核心组件。

这里有个小技巧

对目标文件内容计算MD5值并作为文件指纹存储到ES的索引字段里边,每次在重建索引的时候判断该MD5是否存在,如果存在就不用重复建立索引了,可以避免文件索引重复,也能避免系统重启后重复遍历文件。

package com.crazyice.lee.accumulation.search.service;

import com.alibaba.fastjson.JSONObject;
import com.crazyice.lee.accumulation.search.data.Article;
import com.crazyice.lee.accumulation.search.utils.Md5CaculateUtil;
import io.searchbox.client.JestClient;
import io.searchbox.core.Index;
import io.searchbox.core.Search;
import io.searchbox.core.SearchResult;
import lombok.extern.slf4j.Slf4j;
import net.sf.jmimemagic.*;
import org.apache.poi.hwpf.extractor.WordExtractor;
import org.apache.poi.xwpf.extractor.XWPFWordExtractor;
import org.apache.poi.xwpf.usermodel.XWPFDocument;
import org.elasticsearch.index.query.QueryBuilders;
import org.elasticsearch.search.builder.SearchSourceBuilder;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;

@Component
@Slf4j
public class DirectoryRecurse {

    @Autowired
    private JestClient jestClient;

    //读取文件内容转换为字符串
    private String readToString(File file, String fileType) {
        StringBuffer result = new StringBuffer();
        switch (fileType) {
            case "text/plain":
            case "java":
            case "c":
            case "cpp":
            case "txt":
                try (FileInputStream in = new FileInputStream(file)) {
                    Long filelength = file.length();
                    byte[] filecontent = new byte[filelength.intValue()];
                    in.read(filecontent);
                    result.append(new String(filecontent, "utf8"));
                } catch (FileNotFoundException e) {
                    log.error("{}", e.getLocalizedMessage());
                } catch (IOException e) {
                    log.error("{}", e.getLocalizedMessage());
                }
                break;
            case "doc":
                //使用HWPF组件中WordExtractor类从Word文档中提取文本或段落
                try (FileInputStream in = new FileInputStream(file)) {
                    WordExtractor extractor = new WordExtractor(in);
                    result.append(extractor.getText());
                } catch (Exception e) {
                    log.error("{}", e.getLocalizedMessage());
                }
                break;
            case "docx":
                try (FileInputStream in = new FileInputStream(file); XWPFDocument doc = new XWPFDocument(in)) {
                    XWPFWordExtractor extractor = new XWPFWordExtractor(doc);
                    result.append(extractor.getText());
                } catch (Exception e) {
                    log.error("{}", e.getLocalizedMessage());
                }
                break;
        }
        return result.toString();
    }

    //判断是否已经索引
    private JSONObject isIndex(File file) {
        JSONObject result = new JSONObject();
        //用MD5生成文件指纹,搜索该指纹是否已经索引
        String fileFingerprint = Md5CaculateUtil.getMD5(file);
        result.put("fileFingerprint", fileFingerprint);
        SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder();
        searchSourceBuilder.query(QueryBuilders.termQuery("fileFingerprint", fileFingerprint));
        Search search = new Search.Builder(searchSourceBuilder.toString()).addIndex("diskfile").addType("files").build();
        try {
            //执行
            SearchResult searchResult = jestClient.execute(search);
            if (searchResult.getTotal() > 0) {
                result.put("isIndex", true);
            } else {
                result.put("isIndex", false);
            }
        } catch (IOException e) {
            log.error("{}", e.getLocalizedMessage());
        }
        return result;
    }

    //对文件目录及内容创建索引
    private void createIndex(File file, String method) {
        //忽略掉临时文件,以~$起始的文件名
        if (file.getName().startsWith("~$")) return;

        String fileType = null;
        switch (method) {
            case "magic":
                Magic parser = new Magic();
                try {
                    MagicMatch match = parser.getMagicMatch(file, false);
                    fileType = match.getMimeType();
                } catch (MagicParseException e) {
                    //log.error("{}",e.getLocalizedMessage());
                } catch (MagicMatchNotFoundException e) {
                    //log.error("{}",e.getLocalizedMessage());
                } catch (MagicException e) {
                    //log.error("{}",e.getLocalizedMessage());
                }
                break;
            case "ext":
                String filename = file.getName();
                String[] strArray = filename.split("\\.");
                int suffixIndex = strArray.length - 1;
                fileType = strArray[suffixIndex];
        }

        switch (fileType) {
            case "text/plain":
            case "java":
            case "c":
            case "cpp":
            case "txt":
            case "doc":
            case "docx":
                JSONObject isIndexResult = isIndex(file);
                log.info("文件名:{},文件类型:{},MD5:{},建立索引:{}", file.getPath(), fileType, isIndexResult.getString("fileFingerprint"), isIndexResult.getBoolean("isIndex"));

                if (isIndexResult.getBoolean("isIndex")) break;
                //1\. 给ES中索引(保存)一个文档
                Article article = new Article();
                article.setTitle(file.getName());
                article.setAuthor(file.getParent());
                article.setPath(file.getPath());
                article.setContent(readToString(file, fileType));
                article.setFileFingerprint(isIndexResult.getString("fileFingerprint"));
                //2\. 构建一个索引
                Index index = new Index.Builder(article).index("diskfile").type("files").build();
                try {
                    //3\. 执行
                    if (!jestClient.execute(index).getId().isEmpty()) {
                        log.info("构建索引成功!");
                    }
                } catch (IOException e) {
                    log.error("{}", e.getLocalizedMessage());
                }
                break;
        }
    }

    public void find(String pathName) throws IOException {
        //获取pathName的File对象
        File dirFile = new File(pathName);

        //判断该文件或目录是否存在,不存在时在控制台输出提醒
        if (!dirFile.exists()) {
            log.info("do not exit");
            return;
        }

        //判断如果不是一个目录,就判断是不是一个文件,时文件则输出文件路径
        if (!dirFile.isDirectory()) {
            if (dirFile.isFile()) {
                createIndex(dirFile, "ext");
            }
            return;
        }

        //获取此目录下的所有文件名与目录名
        String[] fileList = dirFile.list();

        for (int i = 0; i < fileList.length; i++) {
            //遍历文件目录
            String string = fileList[i];
            File file = new File(dirFile.getPath(), string);
            //如果是一个目录,输出目录名后,进行递归
            if (file.isDirectory()) {
                //递归
                find(file.getCanonicalPath());
            } else {
                createIndex(file, "ext");
            }
        }
    }
}

扫描任务

这里采用定时任务的方式来扫描指定目录以实现动态增量创建索引。

package com.crazyice.lee.accumulation.search.service;

import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;

import java.io.IOException;

@Configuration
@Component
@Slf4j
public class CreateIndexTask {
    @Autowired
    private DirectoryRecurse directoryRecurse;

    @Value("${index-root}")
    private String indexRoot;

    @Scheduled(cron = "* 0/5  * * * ?")
    private void addIndex(){
        try {
            directoryRecurse.find(indexRoot);
            directoryRecurse.writeIndexStatus();
        } catch (IOException e) {
            log.error("{}",e.getLocalizedMessage());
        }
    }
}

搜索服务

这里以restFul的方式提供搜索服务,将关键字以高亮度模式提供给前端UI,浏览器端可以根据返回的JSON进行展示。

package com.crazyice.lee.accumulation.search.web;

import com.alibaba.fastjson.JSONObject;
import com.crazyice.lee.accumulation.search.data.Article;
import io.searchbox.client.JestClient;
import io.searchbox.core.Search;
import io.searchbox.core.SearchResult;
import io.swagger.annotations.ApiImplicitParam;
import io.swagger.annotations.ApiImplicitParams;
import io.swagger.annotations.ApiOperation;
import lombok.extern.slf4j.Slf4j;
import org.elasticsearch.index.query.BoolQueryBuilder;
import org.elasticsearch.index.query.QueryBuilders;
import org.elasticsearch.search.builder.SearchSourceBuilder;
import org.elasticsearch.search.fetch.subphase.highlight.HighlightBuilder;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.lang.NonNull;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;

import java.io.IOException;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

@RestController
@Slf4j
public class Controller {
    @Autowired
    private JestClient jestClient;

    @RequestMapping(value = "/search/{keyword}",method = RequestMethod.GET)
    @ApiOperation(value = "全部字段搜索关键字",notes = "es验证")
    @ApiImplicitParams(
            @ApiImplicitParam(name = "keyword",value = "全文检索关键字",required = true,paramType = "path",dataType = "String")
    )
    public List search(@PathVariable String keyword){
        SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder();
        searchSourceBuilder.query(QueryBuilders.queryStringQuery(keyword));

        HighlightBuilder highlightBuilder = new HighlightBuilder();
        //path属性高亮度
        HighlightBuilder.Field highlightPath = new HighlightBuilder.Field("path");
        highlightPath.highlighterType("unified");
        highlightBuilder.field(highlightPath);
        //title字段高亮度
        HighlightBuilder.Field highlightTitle = new HighlightBuilder.Field("title");
        highlightTitle.highlighterType("unified");
        highlightBuilder.field(highlightTitle);
        //content字段高亮度
        HighlightBuilder.Field highlightContent = new HighlightBuilder.Field("content");
        highlightContent.highlighterType("unified");
        highlightBuilder.field(highlightContent);

        //高亮度配置生效
        searchSourceBuilder.highlighter(highlightBuilder);

        log.info("搜索条件{}",searchSourceBuilder.toString());

        //构建搜索功能
        Search search = new Search.Builder(searchSourceBuilder.toString()).addIndex( "gf" ).addType( "news" ).build();
        try {
            //执行
            SearchResult result = jestClient.execute( search );
            return result.getHits(Article.class);
        } catch (IOException e) {
            log.error("{}",e.getLocalizedMessage());
        }
        return null;
    }
}

搜索restFul结果测试

这里以swagger的方式进行API测试。其中keyword是全文检索中要搜索的关键字。

image

使用thymeleaf生成UI

集成thymeleaf的模板引擎直接将搜索结果以web方式呈现。模板包括主搜索页和搜索结果页,通过@Controller注解及Model对象实现。

<body>
    <div class="container">
        <div class="header">
            <form action="./search" class="parent">
                <input type="keyword" name="keyword" th:value="${keyword}">
                <input type="submit" value="搜索">
            </form>
        </div>

        <div class="content" th:each="article,memberStat:${articles}">
            <div class="c_left">
                <p class="con-title" th:text="${article.title}"/>
                <p class="con-path" th:text="${article.path}"/>
                <p class="con-preview" th:utext="${article.highlightContent}"/>
                <a class="con-more">更多</a>
            </div>
            <div class="c_right">
                <p class="con-all" th:utext="${article.content}"/>
            </div>
        </div>

        <script language="JavaScript">
            document.querySelectorAll('.con-more').forEach(item => {
                item.onclick = () => {
                item.style.cssText = 'display: none';
                item.parentNode.querySelector('.con-preview').style.cssText = 'max-height: none;';
            }});
        </script>
    </div>

回复关键字:

1、回复 “10” 查看 最有价值的10个spring boot开源项目

2、回复 “国旗” 获取国旗头像教程**

3、回复 “Ubuntu” 获取****100 个最佳 Ubuntu 应用 和 linux神器

4、回复 “idea” 获取**最新idea破解教程 和 装逼神奇

5、回复 “ssh” 获取史上最好的 ssh工具 支持mac

6、回复 “代金券” 免费获取腾讯云和阿里云代金券

image

推荐阅读:
oauth2 认证服务器 资源服务器分离 使用Redis存储Token

MySQL优化-一篇文章就够了(转发加收藏吧)

Spring Boot最核心的27个注解,你了解多少?

程序员一般可以从什么平台接私活?

看完这14张思维导图,你的python才算入门

手把手讲解 OkHttp硬核知识点(1)

Python 爬取微信公众号文章和评论 (有源码)

Java 开发人员常用的服务配置(Nginx、Tomcat、JVM、Mysql、Redis)

腾讯电话面试总结—Linux运维工程师

python爬虫:(嘿嘿嘿)爬你喜欢的照片

面试官问我:一个 TCP 连接可以发多少个 HTTP 请求?我竟然回答不上来…

教你迅雷&百度非会员也能享受不限速的特权

Chrome开发者工具(DevTools)使用技巧

100个最有价值的开源项目–微信开发系列

IDEA 2019 最新激活教程

一台Linux服务器可以负载多少个连接?(底部有福利)

免责声明:

1.本公众号所转载文章均来自公开网络。

2.如果出处标注有误或侵犯到原著作者权益,请联系删除。

3.转载本公众号中的文章请注明原文链接和作者,否则产生的任何版权纠纷均与本公众号无关。
我的官网
在这里插入图片描述

我的官网http://guan2ye.com

我的CSDN地址http://blog.csdn.net/chenjianandiyi

我的简书地址http://www.jianshu.com/u/9b5d1921ce34

我的githubhttps://github.com/javanan

我的码云地址https://gitee.com/jamen/

阿里云优惠券https://promotion.aliyun.com/ntms/yunparter/invite.html?userCode=vf2b5zld

** 个人微信公众号: dou_zhe_wan **
欢迎关注
在这里插入图片描述

  • 2
    点赞
  • 0
    评论
  • 14
    收藏
  • 一键三连
    一键三连
  • 扫一扫,分享海报

<p> <br /> </p> <p style=&quot;color:#333333;&quot;> <strong>Elasticsearch 简介</strong> </p> <p style=&quot;color:#333333;&quot;> ElasticSearch是一个基于Lucene的搜索服务器。它提供了一个分布式多用户能力的全文搜索引擎,基于RESTful web接口。Elasticsearch是用Java开发的,并作为Apache许可条款下的开放源码发布,是当前流行的企业级搜索引擎。设计用于云计算中,能够达到实时搜索,稳定,可靠,快速,安装使用方便。 </p> <p style=&quot;color:#333333;&quot;> <br /> </p> <p style=&quot;color:#333333;&quot;> <strong>elasticSearch 的使用场景</strong> </p> <p style=&quot;color:#333333;&quot;> 1、在海量数据前提下,对数据进行检索。比如:京东,淘宝等电商项目 </p> <p style=&quot;color:#333333;&quot;> <br /> </p> <p style=&quot;color:#333333;&quot;> <strong>课程目标:</strong> </p> <p style=&quot;color:#333333;&quot;> 1. 了解企业级搜索引擎 </p> <p style=&quot;color:#333333;&quot;> 2. 安装elasticsearch {linux 系统} </p> <p style=&quot;color:#333333;&quot;> 3. 安装kibana并利用kibana对Elasticsearch 索引中的数据进行搜索、查看、交互操作 </p> <p style=&quot;color:#333333;&quot;> 4. 项目实战{ELK} </p> <p style=&quot;color:#333333;&quot;> <br /> </p> <p style=&quot;color:#333333;&quot;> <strong>课程目录:</strong> </p> <p style=&quot;color:#333333;&quot;> 01 课程介绍<br /> 02 elasticsearch 简介<br /> 03 elasticsearch 使用场景<br /> 04 安装elasticsearch 之前先安装jdk<br /> 05 安装elasticsearch<br /> 06 测试elasticsearch是否安装成功&nbsp;<br /> 07 安装kibana<br /> 08 elasticsearch 基本认识 以及添加索引和删除索引<br /> 09 elasticsearch 添加查询数据<br /> 10 elasticsearch 修改删除数据<br /> 11 elasticsearch 有条件的查询<br /> 12 分词子属性fuzzy查询<br /> 13 elasticsearch 过滤使用<br /> 14 elasticsearch 排序与分页<br /> 15 elasticsearch 如何查询指定的字段<br /> 16 elasticsearch 高亮显示<br /> 17 elasticsearch 聚合<br /> 18 elasticsearch mapping 概念<br /> 19 elasticsearch 的中文词库<br /> 20 elasticsearch 中文词库安装测试<br /> 21 elasticsearch 中文词库的使用案例<br /> 22 elasticsearch 自定义词库配置<br /> 23 安装nginx 配置中文词库<br /> 24 测试elasticsearch 自定义中文词库<br /> 25 搭建项目父工程<br /> 26 搭建项目bean-interface-common<br /> 27 搭建search 的service web 项目<br /> 28 测试项目是否能与elasticsearch联通<br /> 29 创建数据库并搭建首页<br /> 30 数据上传功能的实现类完成<br /> 31 数据上传控制器完成<br /> 32 dubbo 介绍以及安装zookeeper<br /> 33 将数据从mysql 上传到elasticsearch 中<br /> 34 elasticsearch查询功能分析<br /> 35 编写业务需求的dsl 语句<br /> 36 编写输入参数返回结果集的实体类<br /> 37 实现类编写<br /> 38 编写实现类中dsl 语句<br /> 39 返回集结果转换<br /> 40 结果测试<br /> 41 测试通过输入查询条件并将数据显示到页面 </p>
<p> <span style=&quot;color:#666666;font-size:14px;background-color:#FFFFFF;&quot;>【讲师介绍】<br /> &nbsp; &nbsp; &nbsp; 现某知名大型互联网公司资深架构师,技术总监,职业规划师,面试官,曾在某上市培训机构,高校任教多年。<br /> &nbsp; &nbsp; Array老师 10多年互联网公司实战经验,知名的大型互联网公司的架构师,高管等职,在企业长期从事于技术的源码阅读和新技术的研究;从事面试官多年 。<br /> 技术选型<br /> 开发环境:Eclipse/Idea ,JDK 1.8及其以上&nbsp;<br /> 后端技术<br /> 核心框架:SpringBoot2.x框架系列(同样适用Springcloud F版本以后的版本),如下(节选): &nbsp; &nbsp;<br /> 持久层框架:RestHighLevelClient(官方推荐)<br /> 工具类:Apache Commons、Jackson 、fastjson、Gson<br /> 有四种不同的方式来产生JSON格式的文档(document)<br /> 手动方式,使用原生的byte[]或者String<br /> 使用Map方式,会自动转换成与之等价的JSON<br /> 使用第三方库来生成序列化beans,如JackJSON、FastJSON等<br /> 使用内置的帮助类XContentFactory.jsonBuilder()<br /> <br /> 前端技术 &nbsp;:<br /> ElasticSearch-head插件(NodeJs版本)<br /> 课程内容:<br /> springboot2.1.8+Es7.3.2的完美结合,让大数据PB级别的数据查询,瞬间展示和释放生产力;<br /> Es7.x(elasticsearch7.x)目前最新官方版本的集群搭建&nbsp;<br /> 绚丽的可视化工具结合展示&nbsp;<br /> 中文分词器让你搜索无忧,一次搞定新技术<br /> </span> </p> <p> <br /> </p> <p> 课程截图 </p> <p> <br /> </p> <p> <img src=&quot;https://img-bss.csdn.net/201909260449249479.png&quot; alt=&quot;&quot; /> </p> <p> <img src=&quot;https://img-bss.csdn.net/201909260449426487.png&quot; alt=&quot;&quot; /> </p> <p> <img src=&quot;https://img-bss.csdn.net/201909260453327299.png&quot; alt=&quot;&quot; /> </p> <p> <img src=&quot;https://img-bss.csdn.net/201909260454128125.png&quot; alt=&quot;&quot; /><img src=&quot;https://img-bss.csdn.net/201909260454259302.png&quot; alt=&quot;&quot; /><img src=&quot;https://img-bss.csdn.net/201909260454323952.png&quot; alt=&quot;&quot; /><img src=&quot;https://img-bss.csdn.net/201909260454395056.png&quot; alt=&quot;&quot; /><img src=&quot;https://img-bss.csdn.net/201909260454473650.png&quot; alt=&quot;&quot; /><img src=&quot;https://img-bss.csdn.net/201909260454584201.png&quot; alt=&quot;&quot; /> </p>
©️2021 CSDN 皮肤主题: 游动-白 设计师:白松林 返回首页
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值