基数聚合

编辑

一个 单值 指标聚合,用于计算不同值的近似计数。

假设您正在索引商店销售数据,并且想要计算与查询匹配的已售出产品的唯一数量

resp = client.search(
    index="sales",
    size="0",
    aggs={
        "type_count": {
            "cardinality": {
                "field": "type"
            }
        }
    },
)
print(resp)
response = client.search(
  index: 'sales',
  size: 0,
  body: {
    aggregations: {
      type_count: {
        cardinality: {
          field: 'type'
        }
      }
    }
  }
)
puts response
const response = await client.search({
  index: "sales",
  size: 0,
  aggs: {
    type_count: {
      cardinality: {
        field: "type",
      },
    },
  },
});
console.log(response);
POST /sales/_search?size=0
{
  "aggs": {
    "type_count": {
      "cardinality": {
        "field": "type"
      }
    }
  }
}

响应

{
  ...
  "aggregations": {
    "type_count": {
      "value": 3
    }
  }
}

精度控制

编辑

此聚合还支持 precision_threshold 选项

resp = client.search(
    index="sales",
    size="0",
    aggs={
        "type_count": {
            "cardinality": {
                "field": "type",
                "precision_threshold": 100
            }
        }
    },
)
print(resp)
response = client.search(
  index: 'sales',
  size: 0,
  body: {
    aggregations: {
      type_count: {
        cardinality: {
          field: 'type',
          precision_threshold: 100
        }
      }
    }
  }
)
puts response
const response = await client.search({
  index: "sales",
  size: 0,
  aggs: {
    type_count: {
      cardinality: {
        field: "type",
        precision_threshold: 100,
      },
    },
  },
});
console.log(response);
POST /sales/_search?size=0
{
  "aggs": {
    "type_count": {
      "cardinality": {
        "field": "type",
        "precision_threshold": 100 
      }
    }
  }
}

precision_threshold 选项允许在内存和准确性之间进行权衡,并定义一个唯一计数,低于该计数时,预期计数将接近准确。高于此值,计数可能会变得稍微模糊。支持的最大值为 40000,高于此值的阈值将具有与 40000 阈值相同的效果。默认值为 3000

计数是近似的

编辑

计算精确计数需要将值加载到哈希集中并返回其大小。当处理高基数集和/或大值时,这无法扩展,因为所需的内存使用量以及在节点之间传递每个分片集的需要会利用集群的太多资源。

cardinality 聚合基于 HyperLogLog++ 算法,该算法基于值的哈希进行计数,并具有一些有趣的特性

  • 可配置的精度,决定如何在内存和准确性之间进行权衡,
  • 在低基数集上具有出色的准确性,
  • 固定的内存使用量:无论是否存在数十个还是数十亿个唯一值,内存使用量仅取决于配置的精度。

对于 c 的精度阈值,我们正在使用的实现需要大约 c * 8 个字节。

下图显示了误差在阈值之前和之后如何变化

cardinality error

对于所有 3 个阈值,计数在配置的阈值之前都是准确的。虽然不能保证,但这种情况很可能发生。实际的准确性取决于所讨论的数据集。一般来说,大多数数据集都显示出始终如一的良好准确性。另请注意,即使阈值低至 100,即使在计数数百万个项目时,误差仍然非常低(如上图所示的 1-6%)。

HyperLogLog++ 算法取决于哈希值的前导零,数据集中哈希的精确分布会影响基数的准确性。

预先计算的哈希

编辑

对于具有高基数的字符串字段,将字段值的哈希存储在索引中,然后在此字段上运行基数聚合可能会更快。这可以通过从客户端提供哈希值来完成,也可以通过使用 mapper-murmur3 插件让 Elasticsearch 为您计算哈希值来完成。

预先计算哈希通常仅在非常大和/或高基数字段上才有用,因为它节省了 CPU 和内存。但是,在数字字段上,哈希速度非常快,并且存储原始值所需的内存与存储哈希一样多或更少。对于低基数字符串字段也是如此,尤其是考虑到这些字段具有优化,以确保每个段的每个唯一值最多计算一次哈希。

脚本

编辑

如果您需要两个字段组合的基数,请创建一个组合它们的运行时字段并对其进行聚合。

resp = client.search(
    index="sales",
    size="0",
    runtime_mappings={
        "type_and_promoted": {
            "type": "keyword",
            "script": "emit(doc['type'].value + ' ' + doc['promoted'].value)"
        }
    },
    aggs={
        "type_promoted_count": {
            "cardinality": {
                "field": "type_and_promoted"
            }
        }
    },
)
print(resp)
response = client.search(
  index: 'sales',
  size: 0,
  body: {
    runtime_mappings: {
      type_and_promoted: {
        type: 'keyword',
        script: "emit(doc['type'].value + ' ' + doc['promoted'].value)"
      }
    },
    aggregations: {
      type_promoted_count: {
        cardinality: {
          field: 'type_and_promoted'
        }
      }
    }
  }
)
puts response
const response = await client.search({
  index: "sales",
  size: 0,
  runtime_mappings: {
    type_and_promoted: {
      type: "keyword",
      script: "emit(doc['type'].value + ' ' + doc['promoted'].value)",
    },
  },
  aggs: {
    type_promoted_count: {
      cardinality: {
        field: "type_and_promoted",
      },
    },
  },
});
console.log(response);
POST /sales/_search?size=0
{
  "runtime_mappings": {
    "type_and_promoted": {
      "type": "keyword",
      "script": "emit(doc['type'].value + ' ' + doc['promoted'].value)"
    }
  },
  "aggs": {
    "type_promoted_count": {
      "cardinality": {
        "field": "type_and_promoted"
      }
    }
  }
}

缺失值

编辑

missing 参数定义了应如何处理缺少值的文档。默认情况下,它们将被忽略,但也可以将它们视为具有值。

resp = client.search(
    index="sales",
    size="0",
    aggs={
        "tag_cardinality": {
            "cardinality": {
                "field": "tag",
                "missing": "N/A"
            }
        }
    },
)
print(resp)
response = client.search(
  index: 'sales',
  size: 0,
  body: {
    aggregations: {
      tag_cardinality: {
        cardinality: {
          field: 'tag',
          missing: 'N/A'
        }
      }
    }
  }
)
puts response
const response = await client.search({
  index: "sales",
  size: 0,
  aggs: {
    tag_cardinality: {
      cardinality: {
        field: "tag",
        missing: "N/A",
      },
    },
  },
});
console.log(response);
POST /sales/_search?size=0
{
  "aggs": {
    "tag_cardinality": {
      "cardinality": {
        "field": "tag",
        "missing": "N/A" 
      }
    }
  }
}

tag 字段中没有值的文档将与具有 N/A 值的文档落在同一个存储桶中。

执行提示

编辑

您可以使用不同的机制运行基数聚合

  • 直接使用字段值 (direct)
  • 通过使用字段的全局序号并在完成分片后解析这些值 (global_ordinals)
  • 通过使用段序号值并在每个段之后解析这些值 (segment_ordinals)

此外,还有两种“基于启发式”的模式。这些模式将导致 Elasticsearch 使用一些关于索引状态的数据来选择适当的执行方法。这两个启发式方法是

  • save_time_heuristic - 这是 Elasticsearch 8.4 及更高版本中的默认值。
  • save_memory_heuristic - 这是 Elasticsearch 8.3 及更早版本中的默认值

如果未指定,Elasticsearch 将应用启发式方法来选择适当的模式。另请注意,对于某些数据(非序号字段),direct 是唯一选项,在这种情况下,提示将被忽略。一般来说,没有必要设置此值。