springboot統合ESディスクファイルの全文検索を実現するコード例
最近ある友達はどのように大量のディスクの資料に対してカタログ、ファイル名とファイル本文を行って捜索することを実現しますか?ESを利用してドキュメントの索引と検索を実現するのは適切な選択だと思います。そこで、いくつかのコードを手書きで実現しました。設計の考え方と実現方法を紹介します。
全体構造
ディスクファイルが異なるデバイスに分布していることを考慮して、ディスクスイーププロキシのモード構築システムを採用し、スキャンサービスをプロキシ的にターゲットディスクがあるサーバに展開し、タイミングタスクとして実行し、インデックスをESに統一して構築した。もちろんES採掘用の分散式の高い配置方法は、検索サービスとスキャンエージェントは、アーキテクチャを簡略化し、分散能力を実現するために配置される。
ディスクファイルのクイック検索アーキテクチャ
部署ES
ES(elasticsearch)は、本プロジェクトの唯一の依存性を持つサードパーティソフトウェアであり、ESはdocker方式の展開をサポートしており、以下は展開プロセスである。
ESインターフェース
エンジニアリング
エンジニアリング
依存パケット
本プロジェクトは、スプリングブックのベーススターを導入するほか、ES関連パッケージを導入する必要があります。
ESのアクセスアドレスをappication.ymlに設定する必要があります。また、プログラムを簡略化するためには、スキャンディスクのルートディレクトリを配置する必要があります。後のスキャンジョブは、ディレクトリの下のすべてのインデックスファイルを繰り返し巡回します。
ファイルの所在ディレクトリ、ファイル名、ファイル本文の検索が可能であることが要求されますので、これらをインデックスフィールドとして定義し、ES clientが要求するJestIdを追加してIDを注釈します。
指定されたディレクトリのすべてのファイルをスキャンするために、再帰的な方法でこのディレクトリを巡回し、処理済みのファイルを識別して効率を向上させるために、ファイルタイプの識別には2つの方法で選択できます。一つはファイルの内容がより正確な判断(Magic)であり、一つはファイルの拡張子で大まかに判断されます。この部分はシステム全体のコアコンポーネントです。
ここにはテクニックがあります。
ターゲットファイルの内容に対してMD 5値を計算し、ファイルの指紋としてESのインデックスフィールドに格納し、インデックスを再構築するたびにこのMD 5が存在するかどうかを判断し、存在する場合はインデックスを再構築する必要はなく、ファイルインデックスの重複を回避することができ、システム再起動後にファイルを巡回することも回避できる。
ここでは、動的インクリメントのインデックス作成を実現するために、指定されたディレクトリをスキャンするタイミングタスクを使用します。
ここでは、レストフルという方法で検索サービスを提供し、キーワードを高輝度モードでフロントエンドUIに提供し、ブラウザ側は戻りJSONに従って展示することができる。
ここではswagger方式でAPIテストを行います。このうちkeywordは全文検索で検索するキーワードです。
検索結果
thymeleafを用いてUIを生成する。
統合されたthymeleafのテンプレートエンジンは、検索結果をウェブで直接表示します。テンプレートにはメイン検索ページと検索結果ページが含まれています。@Controllerの注釈とModelオブジェクトによって実現されます。
全体構造
ディスクファイルが異なるデバイスに分布していることを考慮して、ディスクスイーププロキシのモード構築システムを採用し、スキャンサービスをプロキシ的にターゲットディスクがあるサーバに展開し、タイミングタスクとして実行し、インデックスを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の展開が成功したと説明します。ESインターフェース
エンジニアリング
エンジニアリング
依存パケット
本プロジェクトは、スプリングブックのベーススターを導入するほか、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のアクセスアドレスをappication.ymlに設定する必要があります。また、プログラムを簡略化するためには、スキャンディスクのルートディレクトリを配置する必要があります。後のスキャンジョブは、ディレクトリの下のすべてのインデックスファイルを繰り返し巡回します。
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;
}
ディスクをスキャンしてインデックスを作成します。指定されたディレクトリのすべてのファイルをスキャンするために、再帰的な方法でこのディレクトリを巡回し、処理済みのファイルを識別して効率を向上させるために、ファイルタイプの識別には2つの方法で選択できます。一つはファイルの内容がより正確な判断(Magic)であり、一つはファイルの拡張子で大まかに判断されます。この部分はシステム全体のコアコンポーネントです。
ここにはテクニックがあります。
ターゲットファイルの内容に対してMD 5値を計算し、ファイルの指紋としてESのインデックスフィールドに格納し、インデックスを再構築するたびにこのMD 5が存在するかどうかを判断し、存在する場合はインデックスを再構築する必要はなく、ファイルインデックスの重複を回避することができ、システム再起動後にファイルを巡回することも回避できる。
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());
}
}
}
検索サービスここでは、レストフルという方法で検索サービスを提供し、キーワードを高輝度モードでフロントエンド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;
}
}
検索結果テストここではswagger方式でAPIテストを行います。このうちkeywordは全文検索で検索するキーワードです。
検索結果
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>
以上が本文の全部です。皆さんの勉強に役に立つように、私たちを応援してください。