JavaSpringBootElasticsearch
简单介绍一下需求

  1. 能支持文件的上传,下载
  2. 要能根据关键字,搜索出文件,要求要能搜索到文件里的文字,文件类型要支持word,pdf,txt

文件上传,下载比较简单,要能检索到文件里的文字,并且要尽量精确,这种情况下很多东西就需要考虑进去了。这种情况下,决定使用Elasticsearch来实现。

Elasticsearch简介

Elasticsearch是一个开源的搜索文献的引擎,大概含义就是通过Rest请求告诉它关键字,他返回对应的内容,就这么简单。
Elasticsearch封装了Lucene,Lucene是apache软件基金会一个开放源代码的全文检索引擎工具包。Lucene的调用比较复杂,所以Elasticsearch就再次封装了一层,并且提供了分布式存储等一些比较高级的功能。
基于Elasticsearch有很多的插件,这次用到的主要有两个,一个是kibana,一个是Elasticsearch-head。

  • kibana主要用来构建请求,它提供了很多自动补全的功能。
  • Elasticsearch-head主要用来可视化Elasticsearch。

    开发环境

    首先安装Elasticsearch,Elasticsearch-head,kibana,三个东西都是开箱即用,双击运行 。需要注意的是kibana的版本要和Elasticsearch的版本对应。
    Elasticsearch-head是Elasticsearch的可视化界面,Elasticsearch是基于Rest风格的API来操作的,有了可视化界面,就不用每次都使用Get操作来查询了,能提升开发效率。
    Elasticsearch-head是使用node.js开发的,在安装过程中可能会遇到跨域的问题:Elasticsearch的默认端口是9200,而Elasticsearch-head的默认端口是9100,需要改一下配置文件,具体怎么改就不详细说啦,毕竟有万能的搜索引擎。
    Elasticsearch安装完成之后,访问端口,就会出现版本信息。

    核心问题

    有两个需要解决的核心问题,文件上传和输入关键词查询。

    文件上传

    首先对于txt这种纯文本的形式来说,比较简单,直接将里面的内容传入即可。但是对于pdf,word这两种特殊格式,文件中除了文字之外有很多无关的信息,比如图片,pdf中的标签等这些信息。这就要求对文件进行预处理。
    Elasticsearch5.x以后提供了名为ingest node的功能,ingest node可以对输入的文档进行预处理。如图,PUT请求进入后会先判断有没有pipline,如果有的话会进入Ingest Node进行处理,之后才会正式被处理。
    2022-05-12-13-52-14-299592.png
    Ingest Attachment Processor Plugin是一个文本抽取插件,本质上是利用了Elasticsearch的ingest node功能,提供了关键的预处理器attachment。在安装目录下运行以下命令即可安装。
    1. ./bin/elasticsearch-plugin install ingest-attachment

    定义文本抽取管道

    1. PUT /_ingest/pipeline/attachment
    2. {
    3. "description": "Extract attachment information",
    4. "processors": [
    5. {
    6. "attachment": {
    7. "field": "content",
    8. "ignore_missing": true
    9. }
    10. },
    11. {
    12. "remove": {
    13. "field": "content"
    14. }
    15. }
    16. ]
    17. }
    在attachment中指定要过滤的字段为content,所以写入Elasticsearch时需要将文档内容放在content字段。
    运行结果如图:
    定义文本抽取管道

    建立文档结构映射

    文本文件通过预处理器上传后以何种形式存储,需要建立文档结构映射来定义。PUT定义文档结构映射的时候就会自动创建索引,所以先创建一个docwrite的索引,用于测试。
    1. PUT /docwrite
    2. {
    3. "mappings": {
    4. "properties": {
    5. "id":{
    6. "type": "keyword"
    7. },
    8. "name":{
    9. "type": "text",
    10. "analyzer": "ik_max_word"
    11. },
    12. "type":{
    13. "type": "keyword"
    14. },
    15. "attachment": {
    16. "properties": {
    17. "content":{
    18. "type": "text",
    19. "analyzer": "ik_smart"
    20. }
    21. }
    22. }
    23. }
    24. }
    25. }
    在ElasticSearch中增加了attachment字段,这个字段是attachment命名pipeline抽取文档附件中文本后自动附加的字段。这是一个嵌套字段,其包含多个子字段,包括抽取文本 content 和一些文档信息元数据。
    同是对文件的名字name指定分析器analyzer为ik_max_word,以让ElasticSearch在建立全文索引时对它们进行中文分词。
    建立文档结构

    测试

    经过上面两步,进行简单的测试。因为ElasticSearch是基于JSON格式的文档数据库,所以附件文档在插入ElasticSearch之前必须进行Base64编码。先通过下面的网站将一个pdf文件转化为base64的文本。PDF to Base64
    测试文档如图:
    测试文档
    然后通过以下请求上传上去,找了一个很大的pdf文件。需要指定的是刚创建的pipeline,结果如图所示。
    文件上传测试
    原来的索引有个type类型,新版本后面会被弃用,默认的版本都是_doc
    然后通过GET操作看看文档是否上传成功。可以看到已经被解析成功。
    文件上传结果查看
    如果不指定pipline的话,就会出现无法解析的情况。
    没有指定pipeline的情况
    根据结果可以看到,PDF文件已经通过自行定义的pipline,然后才正式进入索引数据库docwrite。

    关键字查询

    关键字查询即对输入的文字,能进行一定的分词处理。比如说对于“数据库计算机网络我的电脑”这一串词来说,要能将其分为“数据库”,“计算机网络”,“我的电脑”三个关键词,然后分别根据关键字查询。
    Elasticsearch自带了分词器,支持所有的Unicode字符,但是它只会做最大的划分,比如对于进口红酒这四个字,会被分为“进”,“口”,“红”,“酒”这四个字,这样查询出来的结果就会包括“进口”,“口红”,“红酒”。
    默认分词器
    这并不是想要的结果。想要的结果是,只分为“进口”,“红酒”这两段,然后查询相应的结果。这就需要使用支持中文的分词器了。

    ik分词器

    ik分词器是开源社区比较流行的中文分词插件,首先安装ik分词器,注意以下代码不能直接使用。
    1. ./bin/elasticsearch-plugin install https://github.com/medcl/elasticsearch-analysis-ik/releases/download/...这里找你的版本
    ik分词器包括两种模式。
  1. ik_max_word会把中文尽可能的拆分。
  2. ik_smart会根据常用的习惯进行划分,比如”进口红酒”会被划分为“进口”,“红酒”。

ik_smart模式
在使用查询时,指定ik分词器进行查询文档,比如对于插入的测试文档,使用ik_smart模式搜索,结果如图。

  1. GET /docwrite/_search
  2. {
  3. "query": {
  4. "match": {
  5. "attachment.content": {
  6. "query": "实验一",
  7. "analyzer": "ik_smart"
  8. }
  9. }
  10. }
  11. }

可以指定Elasticsearch中的高亮,来为筛选到的文字添加标签。这样的话文字前后都会被添加上标签。如图。
highlight效果

编码

编码使用Idea+maven的开发环境,首先导入依赖,依赖一定要与Elasticsearch的版本相对应。

导入依赖

Elstacisearch对于Java来说有两个API,使用的封装的比较完善的高级API。

  1. <dependency>
  2. <groupId>org.elasticsearch.client</groupId>
  3. <artifactId>elasticsearch-rest-high-level-client</artifactId>
  4. <version>7.9.1</version>
  5. </dependency>

文件上传

先建立一个与上文对应的fileObj对象

  1. public class FileObj {
  2. String id; //用于存储文件id
  3. String name; //文件名
  4. String type; //文件的type,pdf,word,or txt
  5. String content; //文件转化成base64编码后所有的内容。
  6. }

首先根据上文所诉,要先将文件以字节数组的形式读入,然后转化成Base64编码。

  1. public FileObj readFile(String path) throws IOException {
  2. //读文件
  3. File file = new File(path);
  4. FileObj fileObj = new FileObj();
  5. fileObj.setName(file.getName());
  6. fileObj.setType(file.getName().substring(file.getName().lastIndexOf(".") + 1));
  7. byte[] bytes = getContent(file);
  8. //将文件内容转化为base64编码
  9. String base64 = Base64.getEncoder().encodeToString(bytes);
  10. fileObj.setContent(base64);
  11. return fileObj;
  12. }

java.util.Base64已经提供了现成的函数Base64.getEncoder().encodeToString可以使用。
接下来就可以使用Elasticsearch的API将文件上传了。
上传需要使用IndexRequest对象,使用FastJson将fileObj转化为Json后,上传。需要使用indexRequest.setPipeline函数指定上文中定义的pipline。这样文件就会通过pipline进行预处理,然后进入fileindex索引中。

  1. public void upload(FileObj file) throws IOException {
  2. IndexRequest indexRequest = new IndexRequest("fileindex");
  3. //上传同时,使用attachment pipline进行提取文件
  4. indexRequest.source(JSON.toJSONString(file), XContentType.JSON);
  5. indexRequest.setPipeline("attatchment");
  6. IndexResponse indexResponse = client.index(indexRequest, RequestOptions.DEFAULT);
  7. System.out.println(indexResponse);
  8. }

文件查询

文件查询需要使用SearchRequest对象,首先要指定对关键字使用ik分词器的ik_smart模式分词

  1. SearchSourceBuilder srb = new SearchSourceBuilder();
  2. srb.query(QueryBuilders.matchQuery("attachment.content", keyword).analyzer("ik_smart"));
  3. searchRequest.source(srb);

之后就可以通过返回的Response对象获取每一个hits,之后获取返回的内容。

  1. Iterator<SearchHit> iterator = hits.iterator();
  2. int count = 0;
  3. while (iterator.hasNext()) {
  4. SearchHit hit = iterator.next();
  5. }

Elasticsearh一个非常强大的功能是文件的高亮(highlight)功能,所以可以设置一个highlighter,对查询到的文本进行高亮操作。

  1. HighlightBuilder highlightBuilder = new HighlightBuilder();
  2. HighlightBuilder.Field highlightContent = new HighlightBuilder.Field("attachment.content");
  3. highlightContent.highlighterType();
  4. highlightBuilder.field(highlightContent);
  5. highlightBuilder.preTags("<em>");
  6. highlightBuilder.postTags("</em>");
  7. srb.highlighter(highlightBuilder);

设置了前置<em></em>标签对对查询的结果进行包裹。这样查询到的结果中就会包含对应的结果。

多文件测试

简单的demo写好了,但是效果怎么样还需要使用多个文件进行测试。这是一个测试文件夹,里面下面放了各种类型的文件。用Elasticsearch实现Word、PDF,TXT文件的全文内容检索? - 图11
将这个文件夹里面的全部文件上传之后,使用elestacisearch``-head可视化界面查看导入的文件。
导入的文件
搜索代码:

  1. /**
  2. * 这部分会根据输入的关键字去查询数据库中的信息,然后返回对应的结果
  3. * @throws IOException
  4. */
  5. @Test
  6. public void fileSearchTest() throws IOException {
  7. ElasticOperation elo = eloFactory.generate();
  8. elo.search("数据库国务院计算机网络");
  9. }

运行demo,查询的结果如图所示。
搜索结果

还存在的一些问题

1、文件长度问题

通过测试发现,对于文本内容超过10万字的文件,elasticsearch只保留10w字,后面的就被截断了,这就需要进一步了解Elasticsearch对10w字以上的文本的支持。

2、编码上的一些问题

代码中,是将文件全部读入内存之后,在进行一系列的处理 ,毫无疑问,必定会带来问题,比如假如是一个超出内存的超大文件,或者是若干个大文件,在实际生产环境中,文件上传就会占用服务器的相当一大部分内存和带宽,这就要根据具体的需求,做进一步的优化。