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,是比较推荐的。

参考资料:

尽量避免返回全文档

默认情况下,Elasticsearch mapping中定义的字段只会索引数据,而不会存储原始数据,原始数据是存放在 _source 字段中的。在查询时,原始数据都会放在 _source 字段返回,每次查询,都会从磁盘上读取 _source 字段的内容,将它解析成json格式的数据,所以当text字段的数据量很大时,这个过程会给CPU(解析一个很大的Json结构的数据)、磁盘(读取很多数据)、网络(传输大量数据)带来很大压力,并且也会导致文件系统缓存没法高效利用。因此可以考虑只获取 _source 字段的部分字段,只返回有需要的数据,可以采取以下几种方法达到该目的:

1. 使用 fields 选项返回特定字段

在search时,可以使用fields选项指定特定的字段返回,比如一个文档有author, title, content三个字段,content是text类型,存储的数据量较大,在search时,不想返回该字段,则可以这样写:

1
2
3
4
5
6
7
8
9
10
GET my-index-000004/_search
{
"_source": false,
"query": {
"match": {
"content": "elasticsearch"
}
},
"fields": ["author", "title"]
}

这样,在返回值中,就只有author 和 title字段了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
"hits" : [
{
"_index" : "my-index-000004",
"_type" : "_doc",
"_id" : "1",
"_score" : 0.57344866,
"fields" : {
"title" : [
"fooooo"
],
"author" : [
"guangyu"
]
}
}
]

2. 使用 source filtering

可以使用 _source 选项指定 _source 字段中包含的字段,即只返回一个子集,比如:

1
2
3
4
5
6
7
8
9
GET my-index-000004/_search
{
"_source": ["author", "title"],
"query": {
"match": {
"content": "elasticsearch"
}
}
}

返回值:

1
2
3
4
5
6
7
8
9
GET my-index-000004/_search
{
"_source": ["author", "title"],
"query": {
"match": {
"content": "elasticsearch"
}
}
}

3. 将大文本字段单独存储,不放在 _source 中

在索引文档时,可以将text字段的数据单独存储,而不存储在 _source 字段中,这样默认在查询时,在 _source 字段中,就不会返回该大文本字段了,比如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
PUT my-index-000004/
{
"mappings": {
"_source": {
"excludes": ["content"]
},
"properties": {
"content": {
"type": "text",
"store": true
},
"author": {
"type": "keyword"
},
"title": {
"type": "keyword"
}
}
}
}

默认情况下,索引字段只索引数据,并不存储数据,将 content 字段从 _source 字段中排除,然后将 content 字段的 store 设置为 true,即将 content字段不仅索引数据还存储数据,这样就将 content 字段的数据跟 _source 字段分离开了,这样在进行search时, _source 字段中就不会包含 content 字段的原始数据了:

1
2
3
4
5
6
7
8
GET my-index-000004/_search
{
"query": {
"match": {
"content": "elasticsearch"
}
}
}

返回值:

1
2
3
4
5
6
7
8
9
10
11
12
"hits" : [
{
"_index" : "my-index-000004",
"_type" : "_doc",
"_id" : "1",
"_score" : 0.57344866,
"_source" : {
"author" : "guangyu",
"title" : "fooooo"
}
}
]

如果想要获取到 content 字段的原始内容,需要用到 stored_fields 参数:

1
2
3
4
5
6
7
8
9
GET my-index-000004/_search
{
"query": {
"match": {
"content": "elasticsearch"
}
},
"stored_fields": ["content"]
}

但是禁用 _source字段或者是禁用 _source 字段中的部分字段,都会带来副影响:

  • update, update_by_query, reindex 会受到影响
  • highlighting 会受到影响

所以,为了避免带来不必要的影响,非特殊情况不用方法3,建议考虑方法1和2。

参考资料:

使用 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。使用方式如下:

  1. 在text字段上添加上”term_vector”: “with_positions_offsets”选项,打开该字段的term vectors功能:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
PUT my-index-000006
{
"mappings" : {
"properties" : {
"author" : {
"type" : "keyword"
},
"content" : {
"type" : "text",
"term_vector": "with_positions_offsets"
},
"title" : {
"type" : "keyword"
}
}
}
}
  1. 查询时指定type为fvh
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
GET my-index-000006/_search
{
"query": {
"match": {
"content": "best"
}
},
"highlight": {
"fields": {
"content": {
"type": "fvh"
}
}
}
}

参考资料:

Elasticsearch大文本字段(large text field)优化方案

https://hackerain.me/2022/09/26/elasticsearch/elasticsearch_large_text_field.html

作者

hackerain

发布于

2022-09-26

更新于

2023-03-11

许可协议