springboot 통합 ES 디스크 파일 전체 텍스트 검색 을 위 한 예제 코드

최근 한 친구 가 대량의 디스크 자 료 를 디 렉 터 리,파일 이름과 파일 본문 을 검색 하 는 방법 을 문의 하여 간단 하고 효율 적 이 며 유지 가 편리 하 며 원가 가 저렴 하 다 고 요구 했다.저 는 ES 를 이용 하여 문서 의 색인 과 검색 을 실현 하 는 것 이 적당 한 선택 이 라 고 생각 했 습 니 다.그래서 손 으로 코드 를 써 서 이 루어 졌 습 니 다.다음은 디자인 사고 와 실현 방법 을 소개 하 겠 습 니 다.
전체 구조
디스크 파일 이 서로 다른 장치 에 분포 되 어 있 는 것 을 고려 하여 디스크 스 캔 프 록 시 모드 로 시스템 을 구축 합 니 다.즉,스 캔 서 비 스 를 대상 디스크 가 있 는 서버 에 프 록 시 방식 으로 배치 하고 정시 작업 으로 실행 하 며 색인 을 ES 에 통일 적 으로 구축 합 니 다.물론 ES 는 분포 식 고 사용 가능 한 배치 방법 을 사용 합 니 다.검색 서비스 와 스캐닝 에이 전 트 를 함께 배치 하여 구 조 를 간소화 하고 분포 식 능력 을 실현 한다.

디스크 파일 빠 른 검색 구조
배치 ES
ES(elasticsearch)는 본 프로젝트 가 유일 하 게 의존 하 는 제3자 소프트웨어 로 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 배치 성공 을 설명 합 니 다.

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: 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 테스트 를 진행 합 니 다.그 중에서 키 워드 는 전체 텍스트 검색 에서 검색 할 키워드 입 니 다.

검색 결과
thymeleaf 를 사용 하여 UI 생 성
thymeleaf 를 통합 한 템 플 릿 엔진 은 검색 결 과 를 웹 으로 직접 보 여 줍 니 다.템 플 릿 은 메 인 검색 페이지 와 검색 결과 페이지 를 포함 하고@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>

이상 이 바로 본 고의 모든 내용 입 니 다.여러분 의 학습 에 도움 이 되 고 저 희 를 많이 응원 해 주 셨 으 면 좋 겠 습 니 다.

좋은 웹페이지 즐겨찾기