Elasticsearch大文本字段(large text field)优化方案
背景
在全文检索的场景下,经常会有text类型的字段存储的数据量比较大,比如一个pdf文档或者是一本书的内容,可能会有几兆,几十兆,上百兆的大小,单个字段的内容过大,会对集群性能以及稳定性造成比较大的影响,因此本文档针对该场景给出优化建议。
优化点
尽量避免大文本字段
首先应该尽量避免大文本字段,大文本字段会给磁盘、网络、CPU带来较大的压力,并且不能够高效利用文件系统缓存,会对查询以及高亮的性能带来较大影响。通常的建议是在应用侧能够将大文本拆分成小文本,比如要索引一本书的内容,不要把整本书的内容全放到一个doc的text字段中,而是可以按照书的章节或者是段落进行划分,每个doc存放书的一部分,然后在每个doc中添加一个字段,标志该doc属于哪本书即可。这样虽然增加了应用层的逻辑,但是会给ES的性能以及稳定性带来较大的提升。
Elasticsearch在text类型上是没有强制大小限制的,但是在http请求上,默认是有100M的大小限制的,由参数 http.max_content_length 控制,超过该限制,则索引会失败,但即使该参数可调无限大,在Lucene层面也有2GB的大小限制。
根据经验,将单文档大小,或者说text字段的大小,维持在1-2MB,是比较推荐的。
参考资料:
- https://www.elastic.co/guide/en/elasticsearch/reference/current/general-recommendations.html
- https://discuss.elastic.co/t/how-big-a-field-can-be/158810
尽量避免返回全文档
默认情况下,Elasticsearch mapping中定义的字段只会索引数据,而不会存储原始数据,原始数据是存放在 _source 字段中的。在查询时,原始数据都会放在 _source 字段返回,每次查询,都会从磁盘上读取 _source 字段的内容,将它解析成json格式的数据,所以当text字段的数据量很大时,这个过程会给CPU(解析一个很大的Json结构的数据)、磁盘(读取很多数据)、网络(传输大量数据)带来很大压力,并且也会导致文件系统缓存没法高效利用。因此可以考虑只获取 _source 字段的部分字段,只返回有需要的数据,可以采取以下几种方法达到该目的:
1. 使用 fields 选项返回特定字段
在search时,可以使用fields选项指定特定的字段返回,比如一个文档有author, title, content三个字段,content是text类型,存储的数据量较大,在search时,不想返回该字段,则可以这样写:
1 | GET my-index-000004/_search |
这样,在返回值中,就只有author 和 title字段了:
1 | "hits" : [ |
2. 使用 source filtering
可以使用 _source 选项指定 _source 字段中包含的字段,即只返回一个子集,比如:
1 | GET my-index-000004/_search |
返回值:
1 | GET my-index-000004/_search |
3. 将大文本字段单独存储,不放在 _source 中
在索引文档时,可以将text字段的数据单独存储,而不存储在 _source 字段中,这样默认在查询时,在 _source 字段中,就不会返回该大文本字段了,比如:
1 | PUT my-index-000004/ |
默认情况下,索引字段只索引数据,并不存储数据,将 content 字段从 _source 字段中排除,然后将 content 字段的 store 设置为 true,即将 content字段不仅索引数据还存储数据,这样就将 content 字段的数据跟 _source 字段分离开了,这样在进行search时, _source 字段中就不会包含 content 字段的原始数据了:
1 | GET my-index-000004/_search |
返回值:
1 | "hits" : [ |
如果想要获取到 content 字段的原始内容,需要用到 stored_fields 参数:
1 | GET my-index-000004/_search |
但是禁用 _source字段或者是禁用 _source 字段中的部分字段,都会带来副影响:
- update, update_by_query, reindex 会受到影响
- highlighting 会受到影响
所以,为了避免带来不必要的影响,非特殊情况不用方法3,建议考虑方法1和2。
参考资料:
- https://www.elastic.co/guide/en/elasticsearch/reference/current/search-fields.html
- https://www.elastic.co/guide/en/elasticsearch/reference/current/mapping-source-field.html
- https://www.elastic.co/guide/en/elasticsearch/reference/current/mapping-store.html
- https://www.agilytic.be/blog/tech-talk-elasticsearch-stored-fields
使用 fvh highlighter
一般全文检索,都需要用到高亮 highlighting,将搜索到的匹配内容用一些tag标记出来,highlight的大致原理就是将text按照分词切分成fragments,然后对这些fragments进行打分,在找到的匹配的fragments中标记出来对应的查询term。
在highlight过程中,需要用到分词的一些词频、位置以及在文本中的偏移等信息,获取这些信息主要有3种途径:
- the postings list,即在mapping中给某个字段的index_options选项配置成offsets,这样它就会将词频、文档数、位置、偏移等信息存储到倒排索引结构中,但是默认情况下,不会存储偏移登信息到倒排索引中,具体参考文档:https://www.elastic.co/guide/en/elasticsearch/reference/current/index-options.html
- term vectors,是启用额外的数据结构来存储词频、位置、偏移等信息,每个文档都会有对应的term vector结构,在mapping中给某个字段加上 “term_vector”: “with_positions_offsets” 就可以启用该功能,但是打开term vectors的话,会占用额外的磁盘空间,因为使用额外的数据结构存放了更多的数据,具体参考文档:https://www.elastic.co/guide/en/elasticsearch/reference/current/docs-termvectors.html
- plain highlighting,是实时计算这些信息的,会在内存中创建一个小索引,来重新运行query获得这些信息,该方式耗时且耗资源。
highlight的算法目前也有3种方式:
- Unified highlighter,在有term vectors或者postings list信息可用时,会优先使用他们中提前存储好的信息来切分fragments以及进行评分,评分是使用的BM25算法,该方式比较通用,是默认的highlighter;
- Plain highlighter,只能使用plain highlighting实时的方式获取分词位置偏移等信息,打分也是比较简单的通过统计查询的分词个数进行打分,该方式适用于在单字段上进行简单的查询,跨字段复杂查询建议使用其他两种;
- Fast vector highlighter,只能使用term vectors的方式获取分词位置偏移等信息,打分采用tf–idf算法,类似于Plain highlighter,但是考虑了更多因素,该方式在大文本字段(大于1M)的场景下,有比较好的性能;
因此在大文本字段的场景下,由于fvh的方式使用提前存储到term vectors中的分词信息,不需要实时计算,性能表现更佳,因此推荐使用fvh highlighter。使用方式如下:
- 在text字段上添加上”term_vector”: “with_positions_offsets”选项,打开该字段的term vectors功能:
1 | PUT my-index-000006 |
- 查询时指定type为fvh
1 | GET my-index-000006/_search |
参考资料:
Elasticsearch大文本字段(large text field)优化方案
https://hackerain.me/2022/09/26/elasticsearch/elasticsearch_large_text_field.html