集成Elastic Search实现文档的全文搜索功能实战

技术选型

该领域已被Lucene独占,几乎无竞争对手。
但是直接使用Lucene非常复杂,因此出现了两个组件,一是solr,二是elastic search,elastic search流行度更高,但并非在所有应用场景占优,对于索引库已建立的情况下,如将某人的个人办公电脑所有文档进行全文搜索,这种情况下,solr的性能要明显优于es;但对于动态数据的不断插入索引库,如互联网应用,则es性能明显优于solr。

对于企业文档管理系统而言,文档处于动态变化中,但变化频率相对互联网应用频率较低,solr和es都可以使用,考虑到流行程度,最终选择的es

集成方式选择

在已经选择es的情况下,与SpringBoot整合,有几种技术方案:
1.transport-api:springboot版本不同,transport-api不同,不能适配不同的es版本;7.x不建议使用,8以后废弃

2.JestClient:非官方,更新慢;
3.RestTemplate:模拟发送Http请求,Es很多操作需要自己封装,麻烦;
4.HttpClient:同上;
5.ElasticSearch-Rest-Client:官方RestClient,封装了很多ES操作,
优点:API层次分明,上手简单;
缺点:

  • 很多地方需要拼接Json字符串,在java中拼接字符串有多恐怖你应该懂的
  • 需要自己把对象序列化为json存储
  • 查询到结果也需要自己反序列化为对象

6.Spring Data Elasticsearch

支持Spring的基于@Configuration的java配置方式,或者XML配置方式
提供了用于操作ES的便捷工具类ElasticsearchTemplate。包括实现文档到POJO之间的自动智能映射。
利用Spring的数据转换服务实现的功能丰富的对象映射
基于注解的元数据映射方式,而且可扩展以支持更多不同的数据格式
根据持久层接口自动生成对应实现方法,无需人工编写基本操作代码(类似mybatis,根据接口自动得到实现)。当然,也支持人工定制查询。

1-4种集成方式缺点很明显,不予考虑。第5种是es官方自带的类库,易用性较好,但仍存在一些缺点;第6种则是spring提供的服务,考虑到spring这个大家族,最终选择Spring Data Elasticsearch。

需求概述

需求上,需要实现全文搜索的对象是文档(搜索文件夹的名称意义有限,不考虑),进行全文搜索时,参与搜索的有2项信息,文档名称和文档内容,参与排序的是相关度、创建时间、更新时间。

文件内容读取

创建索引,需要读取文件内容。

文件类型

粗略考虑,能进行全文搜索的主要是office、文本、代码、pdf、markdown这几种常见格式,图片、视频、音频和压缩包不处理,需要在配置文件中明确定义,以便使用合适的读取器进行处理。
office:“docx”, “wps”, “doc”, “xls”, “xlsx”, “ppt”, “pptx”
图片:“jpg”, “jpeg”, “png”, “gif”, “bmp”, “ico”, “raw”
文本:txt,html,htm,asp,jsp,xml,json,properties,md,gitignore,log,java,py,c,cpp,sql,sh,bat,m,bas,prg,cmd
代码:“java”, “c”, “php”, “go”, “python”, “py”, “js”, “html”, “ftl”, “css”, “lua”, “sh”, “rb”, “yml”, “json”, “h”, “cpp”, “cs”, “aspx”, “jsp”
压缩包:“rar”, “zip”, “jar”, “7-zip”, “tar”, “gzip”, “7z”
多媒体:mp3,wav,mp4
markdown:md
xml:xml
pdf:pdf
flv:flv
cad:cad

文件编码

对于文本类的文件,文件编码可能会有多种,如UTF-8,UTF-16,GB2312,GBK,GB18030等,读取内容时需要保证编码方式正确,否则会产生乱码。

可以分为两类,一类带bom,即文件前几个字节代表编码方式,另外一类则不带bom,需要根据字节内容判断
以常见的windows记事本生成的txt文件为例,使用以下代码可正确读取(已验证)

/**
	 * 判断文件的编码格式
	 * @param fileName :file
	 * @return 文件编码格式
	 * @throws Exception
	 */
	public static String codeString(File fileName) throws Exception{
    
    
		BufferedInputStream bin = new BufferedInputStream(
		new FileInputStream(fileName));
		int p = (bin.read() << 8) + bin.read();
		String code = null;
		
		switch (p) {
    
    
			case 0xefbb:
				code = "UTF-8";
				break;
			case 0xfffe:
				code = "Unicode";
				break;
			case 0xfeff:
				code = "UTF-16BE";
				break;
			default:
				code = "GBK";
		}
		IOUtils.closeQuietly(bin);
		return code;
	}

这种判断方式和原理对于不带bom的文件无能为力。

看上去是个小事,但是真要自己写,工作量非常可观……
网上找了下第三方类库,有以下几个:
cpdetector:基于统计学原理的,不保证完全正确,api使用相当繁琐

public static String getFileEncode(String filePath) {
        String charsetName = null;
        try {
            File file = new File(filePath);
            CodepageDetectorProxy detector = CodepageDetectorProxy.getInstance();
            detector.add(new ParsingDetector(false));
            detector.add(JChardetFacade.getInstance());
            detector.add(ASCIIDetector.getInstance());
            detector.add(UnicodeDetector.getInstance());
            java.nio.charset.Charset charset = null;
            charset = detector.detectCodepage(file.toURI().toURL());
            if (charset != null) {
                charsetName = charset.name();
            } else {
                charsetName = "UTF-8";
            }
        } catch (Exception ex) {
            ex.printStackTrace();
            return null;
        }
        return charsetName;
    }

icu4j:ibm出品的,看了下最新版本是2020年12月更新的,应该还算可靠

 <dependency> 
	 <groupId>com.ibm.icu</groupId> 
	 <artifactId>icu4j</artifactId> 
	 <version>59.2</version> 
 </dependency>
    public static String getFileEncoding(File file) {
    
    
        //默认设置为utf-8
        String encoding = "utf-8";

        try {
    
    
            Path path = Paths.get(file.getPath());
            byte[] data = Files.readAllBytes(path);
            CharsetDetector detector = new CharsetDetector();
            detector.setText(data);
            CharsetMatch match = detector.detect();
            if (match != null) {
    
    
                encoding = match.getName();
            }
        } catch (IOException exception) {
    
    
            //读取文件失败,不处理异常
        }
        return encoding;
    }

拿记事本试了下,另存为几种格式,发现类库输出的判断优于上面拿bom简易判断读取,最终选择使用该类库处理。

内容解析

文本内容只要保证编码格式,直接读取即可,需要做内容解析的主要是二进制格式的pdf及office系列文档。

采用apache poi类库来读取Word、Excel和PowerPoint这三类文件,注意2003版本及以下,和2007版本及以上需要分别处理

 <!--读取Word/Excel/PowerPoint文件内容-->
        <dependency>
            <groupId>org.apache.poi</groupId>
            <artifactId>poi-ooxml</artifactId>
            <version>3.17</version>
        </dependency>
        <dependency>
            <groupId>org.apache.poi</groupId>
            <artifactId>poi-scratchpad</artifactId>
            <version>3.17</version>
        </dependency>

采用apache pdf库来读取pdf内容

        <!--读取pdf文件内容-->
        <dependency>
            <groupId>org.apache.pdfbox</groupId>
            <artifactId>pdfbox</artifactId>
            <version>2.0.3</version>
        </dependency>

索引创建

读取文件内容,创建索引是常规操作,这块技术上没什么问题,不过从处理流程考虑,用户上传文件,系统进行存储和写库表是主流程,且必须完成,出错应给予提示,但创建索引并不属于主流程,该工作不应该占用主流程的处理时间,所以应该进行异步处理。

因此这里通过新线程来读取文件内容并创建索引。

权限控制

全文搜索组件Elasticsearch并不支持带数据权限搜索,因此只能从查询结果入手来解决,实现思路如下:
首先获取1页数据(例如,页面记录数为10),然后逐条判断当前用户是否有预览权限,如有,则加入结果列表;
判断结果列表是否已够10条,如不够,则继续读取下一页数据,当读取的记录数超出结果总数则停止。

前端默认只显示单页数据,同时不显示命中记录数,而是通过点击加载更多来加载下页数据。

同时,需考虑当前页数据之后是否还有数据,以便前端控制是否显示加载更多提示。

此处还有个棘手问题,带权限的数据拼接问题,首次查询没问题,但是后续查询,从哪里开始则是问题,相当于还需要将上次查询到哪一页记录下来,如果每次查询返回一个固定条数(如10条),则受数据权限过滤的影响,容易出现部分数据丢失的情况,例如单页数据大小是10,一共查询出28条数据,先查出10条,权限过滤后剩余了8条,再读取第2页数据,假设第11-20条数据经过权限过滤后有5条数据,则取2条补足,作为结果返回,用户此时点击了加载更多,这时候后端就面临1个麻烦,从哪个地方开始取数据,才能不重不漏,很明显上面例子产生了“半页数据”。

进一步查看es的api,发现这种类似搜索引擎的应用场景,es提供了scroll 来支持,查询结果会自动产生一个scrollId,下次查询传入该参数,则会从该记录继续往下搜索。

但是,如果每次加载相同数量的数据,还是面临半页数据的问题,因此调整实现思路,在查询结果足量情况下,每次至少返回10条数据,最多返回20条数据(主要受数据权限过滤的影响),这种实现方式,对于业务用户而言,并无多大影响。

已解决问题

  • 什么阶段创建索引库和映射?系统启动时,判断是否已存在索引库,若不存在,则创建?

实际并不需要自己创建索引,给实体类加上注解后,es引擎会自动创建索引库以及映射

  • 应该为文件夹和文档各建立1个索引库,还是共用1个索引库,在查询环节进行合并?

各建1个,es7.x版本已经废弃了类型,每个索引库只有1个默认的类型,即_doc,只存在一种数据结构的映射
调整需求,只考虑文档的全文搜索,因此该问题将不存在

  • 注解加到已有的实体对象,还是另行定义?

暂时没发现应另行定义的必要性,先按照复用已有的实体对象处理

  • 通过注解实现的IK分词未起作用?

SpringDataElasticSearch组件自身问题,根本就未读取ES组件的Field注解属性,在系统启动完成后,使用ElasticSearchRestTemplate接口,手动创建索引。

猜你喜欢

转载自blog.csdn.net/seawaving/article/details/128775211
今日推荐