序言
之前在工作上,碰到一个需求就是读取文件内容存储到es中,然后利用es的特性的进行文件的检索,所以今天抽出一些时间来写这篇文章
项目已经推送到了我的GitHub:github.com/maoshengyzx…
SpringBoot依赖版本为 2.3.4.RELEASE
,Minio依赖版本为 8.0.2
1.下载Minio
Minio是一个高新能、分布式的对象存储服务器,其是开源免费的,官方地址:github.com/minio/minio
Minio下载后会是一个minio.exe的文件,我们使用cmd命令在当前文件夹下打开,然后输入命令
minio.exe server D:\
,将 D:\
替换为你实际想要存储Minio文件的存储目录,启动完成后,访问http://127.0.0.1:9000, 账号密码都为 minioadmin。
2.配置Minio
下载启动完成之后,我们需要再Java当中配置Minio的信息,这样才能使Java程序连接到Minio服务器
YAML 代码解读复制代码minio:
endpoint: http://127.0.0.1:9000 # 替换为自己的服务地址
accessKey: 3x2K4oPLqIzvrEL6w2ya # 公钥
secretKey: xPx2PYoNbbxRmEYXgJVUmsPr5zf8OycyiJQJWzKl # 私钥
配置完成后,创建MinioProp类,用于读取Minio配置
JAVA 代码解读复制代码@Data
@Component
@ConfigurationProperties(prefix = "minio")
public class MinioProp {
// minio 连接地址
private String endpoint;
// 公钥
private String accessKey;
// 私钥
private String secretKey;
}
然后创建Minio的配置类
Java 代码解读复制代码@Configuration
public class MinioConfig {
private final MinioProp minioProp;
public MinioConfig(MinioProp minioProp) {
this.minioProp = minioProp;
}
/**
* minio客户端
*
* @return
*/
@Bean
public MinioClient minioClient() {
return MinioClient.builder()
.endpoint(minioProp.getEndpoint())
.credentials(minioProp.getAccessKey(), minioProp.getSecretKey())
.build();
}
}
最后封装Minio的工具类,用于简化代码操作
JAVA 代码解读复制代码@Component
public class MinioUtils {
private final MinioClient minioClient;
public MinioUtils(MinioClient minioClient) {
this.minioClient = minioClient;
}
/**
* 创建桶
*
* @param bucketName
*/
@SneakyThrows
public void createBucket(String bucketName) {
boolean found =
minioClient.bucketExists(BucketExistsArgs.builder().bucket(bucketName).build());
if (!found) {
minioClient.makeBucket(
MakeBucketArgs.builder()
.bucket(bucketName)
.region("cn-beijing")
.build());
}
}
/**
* 删除桶
*
* @param bucketName 桶名称
*/
@SneakyThrows
public void removeBucket(String bucketName) {
minioClient.removeBucket(RemoveBucketArgs.builder().bucket(bucketName).build());
}
/**
* 上传文件
*
* @param bucketName 桶名称
* @param objectName 文件名
* @param stream 流
* @param fileSize 文件大小
* @param type 文件类型
* @throws Exception
*/
public void putObject(String bucketName, String objectName, InputStream stream, Long fileSize, String type) throws Exception {
minioClient.putObject(
PutObjectArgs.builder().bucket(bucketName).object(objectName).stream(
stream, fileSize, -1)
.contentType(type)
.build());
}
/**
* 判断文件夹是否存在
*
* @param bucketName 桶名称
* @param prefix 文件夹名字
* @return
*/
@SneakyThrows
public Boolean folderExists(String bucketName, String prefix) {
Iterable> results = minioClient.listObjects(ListObjectsArgs.builder().bucket(bucketName)
.prefix(prefix).recursive(false).build());
for (Result- result : results) {
Item item = result.get();
if (item.isDir()) {
return true;
}
}
return false;
}
/**
* 创建文件夹
*
* @param bucketName 桶名称
* @param path 路径
*/
@SneakyThrows
public void createFolder(String bucketName, String path) {
minioClient.putObject(PutObjectArgs.builder().bucket(bucketName).object(path)
.stream(new ByteArrayInputStream(new byte[]{}), 0, -1).build());
}
/**
* 获取文件在minio在服务器上的外链
*
* @param bucketName 桶名称
* @param objectName 文件名
* @return
*/
@SneakyThrows
public String getObjectUrl(String bucketName, String objectName) {
return minioClient.getPresignedObjectUrl(
GetPresignedObjectUrlArgs.builder()
.method(Method.GET)
.bucket(bucketName)
.object(objectName)
.build());
}
/**
* 从minio下载文件为流
*
* @param bucketName 桶名称
* @param objectName 文件名称
* @return
*/
@SneakyThrows
public InputStream downloadObjectAsStream(String bucketName, String objectName) {
GetObjectArgs objectArgs = GetObjectArgs.builder().bucket(bucketName).object(objectName).build();
return minioClient.getObject(objectArgs);
}
}
到此Minio的准备工作就已经初步完成了,我们现在开始ElasticSearch的配置
3.ES配置
方便大家查看,我这里将YML文件展示出来
YAML 代码解读复制代码spring:
application:
name: SpringBoot-ElasticSearch
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
username: root
password: root
url: jdbc:mysql:///db7?useUnicode=true&characterEncoding=utf-8&serverTimezone=Asia/Shanghai
data:
elasticsearch:
client:
reactive:
endpoints: 127.0.0.1:9200
servlet:
multipart:
max-file-size: 20MB
max-request-size: 20MB
# MyBatis配置
mybatis:
# 搜索指定包别名
typeAliasesPackage: com.ruoyi.**.domain
# 配置mapper的扫描,找到所有的mapper.xml映射文件
mapperLocations: classpath*:mapper/**/*Mapper.xml
server:
port: 8080
minio:
endpoint: http://127.0.0.1:9000
accessKey: 3x2K4oPLqIzvrEL6w2ya
secretKey: xPx2PYoNbbxRmEYXgJVUmsPr5zf8OycyiJQJWzKl
ES导入依赖和配置文件后,就已经可以使用了,具体的API大家可以参考官方文档 docs.spring.io/spring-data… 接下来,我们就需要来配置ES中索引、文档等信息了,好在Spring为这些信息的创建提供了简介的方式--只需要在类和字段上添加注解就可以了
Java 代码解读复制代码@Data
@Document(indexName = "file_table")
public class FileTable implements Serializable {
private static final long serialVersionUID = -84031829975035928L;
/**
* 文件表主键,自增 ID
*/
@Id
private Long id;
/**
* 文件名
* text 可分词,不可以聚合
*/
@Field(name = "fileName", type = FieldType.Text, analyzer = "ik_smart", searchAnalyzer = "ik_smart")
private String fileName;
/**
* 文件内容
*/
@Field(name = "fileContent", type = FieldType.Text, analyzer = "ik_max_word", searchAnalyzer = "ik_max_word")
private String fileContent;
/**
* 文件类型
* keyword 不可分词 但是可以聚合
*/
@Field(name = "fileType", type = FieldType.Keyword)
private String fileType;
/**
* 文件大小
*/
@Field(name = "fileSize", type = FieldType.Long)
private Long fileSize;
/**
* 文件路径
*/
@Field(name = "filePath", type = FieldType.Text, index = false)
private String filePath;
/**
* 逻辑删除标志,0 未删除 1 已删除
*/
private Long isDeleted;
}
实体类创建完毕后,我们需要创键FileTableRepository
接口来继承
PagingAndSortingRepository
(由Spring提供的一个和ES交互操作的接口,提供了基本的CRUD操作)
Java 代码解读复制代码public interface FileTableRepository extends PagingAndSortingRepository {
// 可以自定义方法(无需提供实现),但是一般用不到
}
4.代码
以上操作都完成后,就已经可以开发代码了,FileTable的控制层代码如下:
Java 代码解读复制代码@RestController
@RequestMapping("/file")
public class FileController {
private final FileTableService fileTableService;
public FileController(MinioUtils minioUtils, FileTableService fileTableService) {
this.fileTableService = fileTableService;
}
/**
* 文件上传
* @param file 文件
* @param bucketName 桶名称
* @return
*/
@GetMapping("/uploadFile")
public String uploadFile(@RequestParam("file") MultipartFile file, String bucketName) {
fileTableService.uploadFile(file, bucketName);
return "文件上传成功";
}
/**
* 简单测试一个查询高亮
*
* @param id
* @return
*/
@GetMapping("/getInfoHighlight")
public List getInfoHighlight(Long id) {
return fileTableService.getInfoHighlight(id);
}
}
sevice实现如下:
Java 代码解读复制代码@Service("fileTableService")
@AllArgsConstructor
public class FileTableServiceImpl implements FileTableService {
private final FileTableMapper fileTableMapper;
private final FileTableRepository fileTableRepository;
private final ElasticsearchOperations elasticsearchOperations;
private final MinioUtils minioUtils;
/**
* 实例化完成后创建索引
*/
@PostConstruct
public void createIndex() {
IndexOperations operations = elasticsearchOperations.indexOps(FileTable.class);
if (!operations.exists()) {
operations.create();
}
Document document = operations.createMapping();
operations.putMapping(document);
}
/**
* 新增数据
*
* @param fileTable 实例对象
* @return 实例对象
*/
@Override
public FileTable insert(FileTable fileTable) {
this.fileTableMapper.insert(fileTable);
return fileTable;
}
/**
* 获取file的文件内容,上次到es中来做检索
*
* @param file 文件对象
* @param bucketName 桶名称
*/
@Override
@SneakyThrows
public void uploadFile(MultipartFile file, String bucketName) {
minioUtils.createBucket(bucketName);
// 生产服务器文件名
String objectName = bucketName + UUID.randomUUID().getLeastSignificantBits() + "_" + file.getOriginalFilename();
Assert.notNull(file.getOriginalFilename(), "文件名不能为空");
String fileType = file.getOriginalFilename().split("\\.")[1];
minioUtils.putObject(bucketName, objectName, file.getInputStream(), file.getSize(), fileType);
// 插入数据库数据
String fileUrl = minioUtils.getObjectUrl(bucketName, objectName);
FileTable fileTable = new FileTable();
fileTable.setFileName(objectName);
fileTable.setFilePath(fileUrl);
fileTable.setIsDeleted(0L);
fileTable.setFileType(fileType);
fileTable.setFileSize(file.getSize());
fileTableMapper.insert(fileTable);
// 读取文件内容,上传到es,方便后续的检索 可以考虑使用消息队列,提高效率 因为读取文件内容比较耗时
// 这里为了演示,直接读取文件内容,上传到es
String fileContent = FileUtils.readFileContent(file.getInputStream(), fileType);
fileTable.setFileContent(fileContent);
fileTableRepository.save(fileTable);
}
@Override
public List getInfoHighlight(Long id) {
NativeSearchQueryBuilder queryBuilder = new NativeSearchQueryBuilder();
queryBuilder.withQuery(QueryBuilders.multiMatchQuery("手册", "fileName", "fileContent"));
queryBuilder.withQuery(QueryBuilders.termQuery("id", id));
// 设置高亮
HighlightBuilder highlightBuilder = new HighlightBuilder();
String[] fieldNames = {"fileName", "fileContent"};
for (String fieldName : fieldNames) {
highlightBuilder.field(fieldName);
}
highlightBuilder.preTags("");
highlightBuilder.postTags("");
highlightBuilder.order();
queryBuilder.withHighlightBuilder(highlightBuilder);
// queryBuilder.withHighlightFields(new HighlightBuilder.Field("fileName"));
// 也可以添加分页和排序
SortBuilder sortBuilder = new FieldSortBuilder("fileSize").order(SortOrder.DESC);
queryBuilder.withSort(sortBuilder).withPageable(PageRequest.of(0, 10)); // 表示第一页,每页10条
NativeSearchQuery nativeSearchQuery = queryBuilder.build();
SearchHits searchHits = elasticsearchOperations.search(nativeSearchQuery, FileTable.class);
searchHits.forEach(item -> {
FileTable fileTable = item.getContent();
System.out.println("Highlighted FileName: " + item.getHighlightFields().get("fileName"));
System.out.println("Highlighted FileContent: " + item.getHighlightFields().get("fileContent"));
});
ArrayList fileTables = new ArrayList<>();
searchHits.forEach(item -> {
fileTables.add(item.getContent());
});
return fileTables;
}
}
FileUtils工具类代码,用于读取文件的内容
Java 代码解读复制代码public class FileUtils {
private static final List FILE_TYPE;
static {
FILE_TYPE = Arrays.asList("pdf", "doc", "docx", "text");
}
@SneakyThrows
public static String readFileContent(InputStream inputStream, String fileType) {
if (!FILE_TYPE.contains(fileType)) {
return null;
}
// 使用PdfBox读取pdf文件内容
if ("pdf".equalsIgnoreCase(fileType)) {
return readPdfContent(inputStream);
} else if ("doc".equalsIgnoreCase(fileType) || "docx".equalsIgnoreCase(fileType)) {
return readDocOrDocxContent(inputStream);
} else if ("tex".equalsIgnoreCase(fileType)) {
return readTextContent(inputStream);
}
return null;
}
@SneakyThrows
private static String readPdfContent(InputStream inputStream) {
// 加载PDF文档
PDDocument pdDocument = PDDocument.load(inputStream);
// 创建PDFTextStripper对象, 提取文本
PDFTextStripper textStripper = new PDFTextStripper();
// 提取文本
String content = textStripper.getText(pdDocument);
// 关闭PDF文档
pdDocument.close();
return content;
}
private static String readDocOrDocxContent(InputStream inputStream) {
try {
// 加载DOC文档
XWPFDocument document = new XWPFDocument(inputStream);
// 2. 提取文本内容
XWPFWordExtractor extractor = new XWPFWordExtractor(document);
return extractor.getText();
} catch (IOException e) {
e.printStackTrace();
return null;
}
}
private static String readTextContent(InputStream inputStream) {
StringBuilder content = new StringBuilder();
try (InputStreamReader isr = new InputStreamReader(inputStream, StandardCharsets.UTF_8)) {
int ch;
while ((ch = isr.read()) != -1) {
content.append((char) ch);
}
} catch (IOException e) {
e.printStackTrace();
return null;
}
return content.toString();
}
}
5.总结
我这个地方高亮查询的时候不知道为什么没有生效,如果大家知道的话,希望可以告诉我一下,谢谢啦。
评论记录:
回复评论: