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

整体架构

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

部署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部署成功。

工程结构

依赖包

本项目除了引入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: develasticsearch: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 {@JestIdprivate 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 {@Autowiredprivate 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 {@Autowiredprivate 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 {@Autowiredprivate 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是全文检索中要搜索的关键字。

使用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、回复 “代金券” 免费获取腾讯云和阿里云代金券

推荐阅读:
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 **
欢迎关注

springboot集成ES实现磁盘文件全文检索相关推荐

  1. SpringBoot集成Es使用ElasticSearchTemplate7.x版本自动注入失败解决

    SpringBoot集成Es使用ElasticSearchTemplate7.x版本自动注入失败解决 错误: Caused by: org.springframework.beans.factory. ...

  2. Java使用Springboot集成Es官方推荐(RestHighLevelClient)

    SpringBoot集成ElasticSearch的四种方式(主要讲解ES官方推荐方式) TransportClient:这种方式即将弃用 官方将在8.0版本彻底去除 Data-Es:Spring提供 ...

  3. SpringBoot 集成 ES 7.6.2 并对字段进行中文和拼音分词处理

    前言 在最近做的流媒体项目中需要集成 ES 搜索引擎,目前 ES 最新版本为 7.x 版本,在以往的项目中我都采用的是 spring 集成的 spring-data-es, 使用自定义类集成 elas ...

  4. ElasticSearch - SpringBoot集成ES

    文章目录 ElasticSearch - SpringBoot集成ES 1.整体设计思路(仿NBA中国官网) 2.项目搭建 3.ES API的基本使用 3.1 新增球员信息 3.2 查看球员信息 3. ...

  5. SpringBoot集成ES 7.6.2 并对字段进行中文和拼音分词处理

    文章目录 前言 一.为什么不用spring封装的spring-data-es? 二.springboot集成es的两种方式 1.spring-data-es使用elasticsearch 2.doc对 ...

  6. SpringBoot 集成ES集群CRUD及分页解决方案

    1 SpringBoot 集成ES集群 1.2 pom <parent><groupId>org.springframework.boot</groupId>< ...

  7. 从ElasticSearch 认识到实战(SpringBoot集成ES)

    ElasticSearch 认识到实战 目录 搜索引擎介绍 ElasticSearch知识 安装 使用restful风格查询ES SpringBoot配置ES SpringBoot集成使用 一.搜索引 ...

  8. SpringBoot集成ES+京东搜索

    SpringBoot集成elasticsearch 引入依赖 <dependency><groupId>com.alibaba</groupId><artif ...

  9. Springboot集成ES启动报错

    报错内容 None of the configured nodes are available elasticsearch.yml配置 cluster.name: ftest node.name: n ...

最新文章

  1. 一个利用System.gc和finalize研究Java垃圾回收机制的练习
  2. 【leetcode 简单】 第一百一十题 分发饼干
  3. android webview rem,Android部分webview rem计算误差记录
  4. JavaScript总结(3)
  5. python网络框架生产环境_配置Django框架为生产环境的注意事项(DEBUG=False)
  6. RabbitMQ学习之messageconver插件实现(Gson)
  7. java mongo忽略大小写_Java Spring Mongo排序忽略大小写问题
  8. 项目管理笔记(观念)
  9. WebMvcConfigurerAdapter
  10. 手机输入法带拼音声调_分享4种给拼音加声调的方法,让你的word更有灵魂
  11. 十分钟学会摩尔斯密码
  12. 纯小白Python爬取东方财富网研报内容并通过机器学习的SVM模型进行文本分析(四)
  13. MySQL8 免安装版安装
  14. ASP.NET Session详细介绍
  15. Authing 背后的计算哲学
  16. 5月6日地图下载。同学们要练习!
  17. 泰森多边形(Voronoi图)
  18. matlab---矩阵运算函数
  19. 美版饿了么上市:美国外卖行业为何落后?有哪些挑战和机遇?
  20. ROS SMACH个人学习记录

热门文章

  1. 电子签名生成的图片为空白,以及生成透明底签名图片转为白色底
  2. 第05章 Go语言函数(Go语言func)
  3. 使用NAT打造FTP服务新法
  4. 在Matplotlib中将图片导出
  5. 实战篇:手动编译安装微软 Linux 开源版 CBL-Mariner
  6. 一个故事讲完进程、线程和协程
  7. iOS | 模拟器调试Web控制台空白问题及解决
  8. 广域网、城域网、局域网、个人区域网的不同
  9. 如何给电脑重装系统--一点通
  10. 2022年小额贷款行业研究报告