Elasticsearch无索引查询分析介绍

本文涉及的产品
检索分析服务 Elasticsearch 版,2核4GB开发者规格 1个月
简介: 众所周知,Elasticsearch提供的高效而丰富的查询分析能力,是基于Lucene提供的字段存储、倒排索引以及doc values等特性。通过倒排索引,可以快速过滤出符合查询条件的文档集合;结合doc values,可以高效地获取文档特定列的值信息,以便进行排序以及各种聚合操作;而字段存储则允许获取文档的原始内容。不难看出,字段存储、倒排索引以及doc values之间,是存在一定数据冗余的(

众所周知,Elasticsearch提供的高效而丰富的查询分析能力,是基于Lucene提供的字段存储、倒排索引以及doc values等特性。通过倒排索引,可以快速过滤出符合查询条件的文档集合;结合doc values,可以高效地获取文档特定列的值信息,以便进行排序以及各种聚合操作;而字段存储则允许获取文档的原始内容。不难看出,字段存储、倒排索引以及doc values之间,是存在一定数据冗余的(即一份数据存放在多个不同的结构之中),Lucene之所以选择这样做,是为了加快分析和查询的效率(即以空间换时间),避免不同数据结构之前的转换(如基于倒排来构建类似doc values的列存)。

然而,有时为了节省成本(或者加快文档写入Lucene的速度),对于查询频率不高的字段,我们并不想构建倒排索引以及doc values,但我们仍然希望这些字段能够支持查询和分析。当然,这些字段的查询和分析性能差是我们能够接受的,因为他们本身并不经常使用。那么Elasticsearch(或者Lucene)是否支持这种方式的查询和分析呢?答案是肯定的。Elasticsearch提供的runtime字段,正可以完美地解决这种场景的需求(当然,runtime字段还有很多其他用途)。

Runtime字段

通过这种字段的名称不难理解,runtime字段是可以由ES在运行时阶段计算出来,即这类字段不必存在于source中,允许用户基于特定的信息(如source、自定义的params)动态创建任意字段,比如根据距离和时间动态计算出速度信息。另外,runtime类型的字段同样可以用于查询、聚合等请求中,但这类字段是不占磁盘空间的。同时,source中原有的字段也是可以定义为runtime类型通过runtime类型字段的引入,可以实现无索引字段的查询和聚合,只不过相对于索引字段而言,性能会差很多(需要进行全文档集扫描)。但针对不常用的查询字段,这个还是有很大价值的(节省存储成本、加快写入速度)。

让我们来看一个实例,这里我创建一个cars索引,其定义如下所示。通过mappings定义可以知道,Weight是runtime类型的字段,其他3个字段则是非runtime类型的字段。另外,mapping中定义的4个字段都是原始文档中存在的(见下面的批量索引操作)。

PUT /cars
{
  "settings": {
    "number_of_shards": "1",
    "number_of_replicas": "0"
  },
  "mappings": {
    "dynamic": "false",
    "runtime": {
      "Weight": {
        "type": "keyword"
      }
    },
    "properties": {
      "Type": {
         "type": "keyword"
      },
      "Distance": {
        "type": "long"
      },
      "Duration": {
        "type": "long"
      }
    }
  }
}
POST cars/_bulk?refresh=true
{"index":{}}
{"Type": "toyouta", "Distance": 80, "Duration": 15, "Weight": 1200}
{"index":{}}
{"Type": "volvo", "Distance": 200, "Duration": 25, "Weight": 2200}
{"index":{}}
{"Type": "benz", "Distance": 100, "Duration": 10, "Weight": 2500}
{"index":{}}
{"Type": "bwm", "Distance": 120, "Duration": 15, "Weight": 2000}
{"index":{}}
{"Type": "van", "Distance": 100, "Duration": 25, "Weight": 1000}

无索引字段查询

通过上面实例可以知道,文档中虽然包含了Weight字段,但因为其类型是runtime,不会为它创建倒排索引和doc values信息。我们可以通过_disk_usage接口进行确认,以下是kibana中执行POST /cars/_disk_usage?run_expensive_tasks=true时的输出。

可以看到,_disk_usage接口输出中不存在Weight字段相关的信息(注意,Weight字段的原始内容其实是承载于Lucene的_source字段中),这就证实了没有为Weight字段创建倒排索引以及doc values等。另外,通过Type字段对应的内容可以看到,它占用了40字节的倒排索引空间以及31字节的doc values空间,这也是符合我们预期的。

上文说过,虽然runtime字段不存在索引和doc values,但仍然可以使用它进行查询和分析操作,这里让我们验证下。

GET cars/_search 
{
  "query": {
    "range": {
      "Weight": {
        "gte": 1500
      }
    }
  }, 
  "sort": [
    {
      "Weight": {
        "order": "desc"
      }
    }
  ]
}
{
  "took" : 7,
  "timed_out" : false,
  "_shards" : {
    "total" : 1,
    "successful" : 1,
    "skipped" : 0,
    "failed" : 0
  },
  "hits" : {
    "total" : {
      "value" : 3,
      "relation" : "eq"
    },
    "max_score" : null,
    "hits" : [
      {
        "_index" : "cars",
        "_id" : "H5KoT4MBA7BZwWhNO13G",
        "_score" : null,
        "_source" : {
          "Type" : "benz",
          "Distance" : 100,
          "Duration" : 10,
          "Weight" : 2500
        },
        "sort" : [
          "2500"
        ]
      },
      {
        "_index" : "cars",
        "_id" : "HpKoT4MBA7BZwWhNO13G",
        "_score" : null,
        "_source" : {
          "Type" : "volvo",
          "Distance" : 200,
          "Duration" : 25,
          "Weight" : 2200
        },
        "sort" : [
          "2200"
        ]
      },
      {
        "_index" : "cars",
        "_id" : "IJKoT4MBA7BZwWhNO13G",
        "_score" : null,
        "_source" : {
          "Type" : "bwm",
          "Distance" : 120,
          "Duration" : 15,
          "Weight" : 2000
        },
        "sort" : [
          "2000"
        ]
      }
    ]
  }
}

通过上面的结果可以看到,通过Weight字段,我们过滤出了我们需要的文档集合并按照预期的顺序进行了输出。因此,这就验证了在没有索引和doc values的情况下,我们仍然使用runtime字段进行查询和分析操作。

合成字段查询

接下来让我们看一个runtime类型的合成字段。通过上面的索引操作指导,索引中仅仅记录了汽车的类型(Type)、行驶距离(Distance)以及行驶时间(Duration)信息,如果我们要按照汽车的速度进行排名,该怎么操作?原始索引中并没有速度相关信息的。通过runtime类型字段的定义,我们可以很容易实现这个需求。

定义runtime类型的字段时,我们可以选择在创建索引的时候在mappings中进行配置(如上面Weight字段的定义),也可以在查询请求中通过runtime_mappings进行定配置。前一种方式的好处是runtime字段对所有查询都是可见的(即便于复用),而后一种方式则是当前请求可见的,适用于临时runtime字段的查询。这里我们以后一种方式进行说明,具体请求和查询结果如下所示。

GET cars/_search 
{
  "runtime_mappings": {
    "Speed": {
      "type": "long",
      "script": {
        "source": "emit(doc['Distance'].value/doc['Duration'].value)"
      }
    }
  },
  "sort": [
    {
      "Speed": {
        "order": "desc"
      }
    }
  ]
}
{
  "took" : 0,
  "timed_out" : false,
  "_shards" : {
    "total" : 1,
    "successful" : 1,
    "skipped" : 0,
    "failed" : 0
  },
  "hits" : {
    "total" : {
      "value" : 5,
      "relation" : "eq"
    },
    "max_score" : null,
    "hits" : [
      {
        "_index" : "cars",
        "_id" : "H5KoT4MBA7BZwWhNO13G",
        "_score" : null,
        "_source" : {
          "Type" : "benz",
          "Distance" : 100,
          "Duration" : 10,
          "Weight" : 2500
        },
        "sort" : [
          10
        ]
      },
      {
        "_index" : "cars",
        "_id" : "HpKoT4MBA7BZwWhNO13G",
        "_score" : null,
        "_source" : {
          "Type" : "volvo",
          "Distance" : 200,
          "Duration" : 25,
          "Weight" : 2200
        },
        "sort" : [
          8
        ]
      },
      {
        "_index" : "cars",
        "_id" : "IJKoT4MBA7BZwWhNO13G",
        "_score" : null,
        "_source" : {
          "Type" : "bwm",
          "Distance" : 120,
          "Duration" : 15,
          "Weight" : 2000
        },
        "sort" : [
          8
        ]
      },
      {
        "_index" : "cars",
        "_id" : "HZKoT4MBA7BZwWhNO13G",
        "_score" : null,
        "_source" : {
          "Type" : "toyouta",
          "Distance" : 80,
          "Duration" : 15,
          "Weight" : 1200
        },
        "sort" : [
          5
        ]
      },
      {
        "_index" : "cars",
        "_id" : "IZKoT4MBA7BZwWhNO13G",
        "_score" : null,
        "_source" : {
          "Type" : "van",
          "Distance" : 100,
          "Duration" : 25,
          "Weight" : 1000
        },
        "sort" : [
          4
        ]
      }
    ]
  }
}

上述请求中,我们通过runtime_mappings属性额外定义了一个runtime类型的Speed字段,同时定义中我们指定了通过painless脚本来生成这个字段的值,即采用距离和时间计算速度。通过和之前Weight字段的定义对比可以发现,Weight字段的定义是没有指定script属性的,这种情况其实就是告诉Elasticsearch直接从_source中提取Weight属性的内容。

性能说明

对于runtime类型的字段,Elasticsearch不会为其构建索引,这就意味着其查询性能肯定是比较差的。在查询过程中,Elasticsearch需要遍历索引中的所有文档,然后根据runtime字段的定义来计算值并同查询条件进行匹配。因此,对runtime字段进行查询分析的时间,同遍历的文档数量、runtime字段值计算的复杂度有关,下面我们通过简单的实例加以说明。

测试准备

这里我创建了两个索引,其定义如下所示,其中oplogs-with-index的AliUid字段定义为写入时构建索引,oplogs-without-index的AliUid字段则定义为runtime类型。为了对比查询性能,我们需要保证往oplogs-with-index和oplogs-without-index写入相同的文档集。我的做法是先往oplogs-with-index写入一定数量(具体为6078060)的文档,然后通过_reindex接口(kibana操作为POST _reindex?wait_for_completion=false)将数据copy到oplogs-without-index。

PUT /oplogs-with-index
{
  "settings": {
    "number_of_shards": "4",
    "number_of_replicas": "0"
  },
  "mappings": {
    "dynamic": "false",
    "properties": {
      "AliUid": {
         "type": "keyword"
      }
    }
  }
}

PUT /oplogs-without-index
{
  "settings": {
    "number_of_shards": "4",
    "number_of_replicas": "0"
  },
  "mappings": {
    "dynamic": "false",
    "runtime": {
      "AliUid": {
         "type": "keyword"
      }
    }
  }
}

查询对比

对于oplogs-with-index而言,字段AliUid在写入过程中已经构建好了索引。因此,根据AliUid进行查询时,请求响应时间将会非常短。通过下面的操作可以看到,找到包含特定AliUid的所有文档数量花费的时间仅1毫秒。

接下来让我们对oplogs-without-index进行相同类型的操作,其结果如下所示。这次请求花费的时间为14秒多,比查询oplogs-with-index的时间多出好几个数量级。这其实是符合预期的,因为在开启倒排索引的情况下,可以快速定位包含查询字段的所有文档。而对于runtime类型的字段,则只能通过遍历所有的文档来进行查询过滤。

另外,如果我们限定track符合查询请求的文档数量的话,可以看到,将会有效降低runtime字段查询所需要的时间。很明显,之所以能达到这种效果,是因为遍历的文档数量显著减少了(查询到track要求的匹配数量后,可以提前退出)。

超时问题

通过上面的对比可以知道,runtime类型字段查询所耗费的时间同索引中的文档数量是线性相关的。当索引中文档数据量较多时,将很容易导致查询超时。针对runtime字段查询的场景,需要采用非同步类型的查询方式,允许客户端主动轮询查询结果是否完成。Elasticsearch提供的_async_search接口,在很大程度上,正是为了支持这种场景。如果在1秒内完成查询,则_async_search会立即返回查询结果,否则返回一个查询ID,允许客户端后续进行结果轮询。

异步查询操作如下:

轮询异步查询结果如下:

实现介绍

我们知道,Lucene查询涉及到几个核心的概念:Query,Weight以及Scorer。对于索引字段和runtime字段的查询,Elasticsearch都会封装成对应的Query、Weight和Scorer。对于runtime类型字段查询,其涉及到的核心基类如下所示:

public abstract class AbstractScriptFieldQuery<S extends AbstractFieldScript> extends Query {

    @Override
    public Weight createWeight(IndexSearcher searcher, ScoreMode scoreMode, float boost) throws IOException {
        return new ConstantScoreWeight(this, boost) {
            @Override
            public boolean isCacheable(LeafReaderContext ctx) {
                return false; // scripts aren't really cacheable at this point
            }

            @Override
            public Scorer scorer(LeafReaderContext ctx) {
                S scriptContext = scriptContextFunction.apply(ctx);
                DocIdSetIterator approximation = DocIdSetIterator.all(ctx.reader().maxDoc());
                TwoPhaseIterator twoPhase = new TwoPhaseIterator(approximation) {
                    @Override
                    public boolean matches() {
                        return AbstractScriptFieldQuery.this.matches(scriptContext, approximation.docID());
                    }

                    @Override
                    public float matchCost() {
                        return MATCH_COST;
                    }
                };
                return new ConstantScoreScorer(this, score(), scoreMode, twoPhase);
            }

        };
    }

    protected abstract boolean matches(S scriptContext, int docId);

}

通过AbstractScriptFieldQuery的createWeight方法可以知道,其创建Scorer内部采用了TwoPhaseIterator进行查询过滤,而TwoPhaseIterator内部引用的DocIdSetIterator对应的是Lucene segment的文档全集,即会遍历segment中所有的文档。另外,TwoPhaseIterator的matches方法允许对遍历的文档进行进一步过滤(这也是TwoPhaseIterator为啥叫two phase的原因)。对于runtime类型的字段而言,TwoPhaseIterator的matches方法实现交给AbstractScriptFieldQuery具体子类,比如上面cars索引实例中,Weight字段和Speed字段对应的实现逻辑是不一样的。

因此,通过AbstractScriptFieldQuery可以清楚地看到,对于runtime类型字段的查询,会遍历索引中的所有文档。这也就解释了,runtime类型字段的查询性能同索引文档集合的大小紧密相关的原因。

总结

本文主要是对Elasticsearch中的runtime字段的应用场景、特性及其实现做了简单介绍。runtime字段为查询和分析提供了更为灵活的手段,不要求查询的字段存在倒排索引、doc values等信息,甚至不要求存在于原始文档中。对于不常用的查询字段,非常适合定义为runtime类型,一方面这有助于降低保存索引数据的磁盘空间,另一方面可以加快文档写入Elasticsearch的速度。但需要注意的是,runtime类型的字段查询性能远远不如索引字段,而且查询时间是和索引中文档集的大小线性相关的。



相关实践学习
使用阿里云Elasticsearch体验信息检索加速
通过创建登录阿里云Elasticsearch集群,使用DataWorks将MySQL数据同步至Elasticsearch,体验多条件检索效果,简单展示数据同步和信息检索加速的过程和操作。
ElasticSearch 入门精讲
ElasticSearch是一个开源的、基于Lucene的、分布式、高扩展、高实时的搜索与数据分析引擎。根据DB-Engines的排名显示,Elasticsearch是最受欢迎的企业搜索引擎,其次是Apache Solr(也是基于Lucene)。 ElasticSearch的实现原理主要分为以下几个步骤: 用户将数据提交到Elastic Search 数据库中 通过分词控制器去将对应的语句分词,将其权重和分词结果一并存入数据 当用户搜索数据时候,再根据权重将结果排名、打分 将返回结果呈现给用户 Elasticsearch可以用于搜索各种文档。它提供可扩展的搜索,具有接近实时的搜索,并支持多租户。
相关文章
|
2月前
|
自然语言处理 大数据 应用服务中间件
大数据-172 Elasticsearch 索引操作 与 IK 分词器 自定义停用词 Nginx 服务
大数据-172 Elasticsearch 索引操作 与 IK 分词器 自定义停用词 Nginx 服务
79 5
|
2月前
|
存储 分布式计算 大数据
大数据-169 Elasticsearch 索引使用 与 架构概念 增删改查
大数据-169 Elasticsearch 索引使用 与 架构概念 增删改查
71 3
|
13天前
|
弹性计算 运维 Serverless
超值选择:阿里云Elasticsearch Serverless在企业数据检索与分析中的高性能与灵活性
本文介绍了阿里云Elasticsearch Serverless服务的高性价比与高度弹性灵活性。
|
1月前
|
存储 缓存 监控
优化Elasticsearch 索引设计
优化Elasticsearch 索引设计
26 5
|
1月前
|
存储 SQL 监控
|
1月前
|
存储 JSON 关系型数据库
Elasticsearch 索引
【11月更文挑战第3天】
43 4
|
1月前
|
运维 监控 安全
|
1月前
|
测试技术 API 开发工具
ElasticSearch7.6.x 模板及滚动索引创建及注意事项
ElasticSearch7.6.x 模板及滚动索引创建及注意事项
49 8
|
2月前
|
存储 JSON 监控
大数据-167 ELK Elasticsearch 详细介绍 特点 分片 查询
大数据-167 ELK Elasticsearch 详细介绍 特点 分片 查询
62 4
|
2月前
|
自然语言处理 搜索推荐 Java
SpringBoot 搜索引擎 海量数据 Elasticsearch-7 es上手指南 毫秒级查询 包括 版本选型、操作内容、结果截图(一)
SpringBoot 搜索引擎 海量数据 Elasticsearch-7 es上手指南 毫秒级查询 包括 版本选型、操作内容、结果截图
66 0