LeafDocLookup 是 Elasticsearch 中用于在脚本(script)执行期间访问当前文档字段值的一个关键类。它实现了 Map<String, ScriptDocValues<?>> 接口,使得脚本可以通过类似 doc['field_name'] 的方式来读取字段的 Doc Values(列式存储的字段值),主要用于排序、聚合或脚本评分等场景。
📌 核心作用
- 提供对当前文档字段值的快速访问 :在 Lucene 的叶子段(leaf segment)上下文中,通过
docId定位到具体文档,并获取其字段的 Doc Values。 - 支持脚本中使用
doc['field']语法:这是 Elasticsearch 脚本(如 Painless 脚本)中访问字段的标准方式。 - 缓存字段数据加载结果:避免重复加载同一个字段的 FieldData,提升性能。
- 安全地加载 FieldData :通过
AccessController.doPrivileged绕过某些安全限制(如内存估算相关的权限问题)。
🔍 关键成员与方法说明
| 成员/方法 | 说明 |
|---|---|
fieldTypeLookup |
根据字段名查找其 MappedFieldType(即字段的映射类型)。 |
fieldDataLookup |
根据字段类型获取对应的 IndexFieldData(用于加载 Doc Values)。 |
reader |
当前处理的 Lucene 段(LeafReaderContext),代表一个索引分片中的一个 segment。 |
docId |
当前正在处理的文档 ID(在该 segment 内的相对 ID)。 |
localCacheScriptFieldData |
缓存已加载的 DocValuesField<?>,避免重复加载同一字段。 |
setDocument(int docId) |
设置当前要查询的文档 ID,通常由搜索过程调用。 |
getScriptField(String fieldName) |
获取指定字段的 DocValuesField,并设置当前 docId。 |
get(Object key) |
实现 Map.get(),返回 ScriptDocValues<?>,供脚本使用(如 doc['price'].value)。 |
🧠 使用示例(在 Painless 脚本中)
// 脚本中访问字段
if (doc['price'].size() > 0) {
return doc['price'].value * 1.1;
}
背后就是通过 LeafDocLookup.get("price") 获取 ScriptDocValues<Double>,再调用 .value 或 .size() 等方法。
⚠️ 注意事项
- 仅适用于已加载 Doc Values 的字段 :通常要求字段设置了
"doc_values": true(默认对非 analyzed 字段开启)。 - 不支持
_source字段 :doc['...']只能访问倒排索引或 Doc Values 中的字段,不能访问原始 JSON(那是params['_source']的用途)。 - 不可变 Map :除了
get()和containsKey(),其他Map方法(如put,clear)都抛出UnsupportedOperationException,因为它是只读视图。
✅ 总结
LeafDocLookup 是 Elasticsearch 脚本引擎与底层 Lucene FieldData 之间的桥梁,让脚本能高效、安全地读取当前文档的字段值(基于 Doc Values) ,是实现 doc['field'] 语法的核心支撑类。
LeafDocLookup 是针对一个文档(document)在一次调用中进行处理的 ,但它本身并不"遍历"多个文档------它是一个面向单个文档的上下文工具 ,由 Elasticsearch 搜索执行框架在处理每个匹配文档时逐个设置当前文档 ID,然后供脚本使用。
📌 更准确地说:
-
LeafDocLookup属于某个 Lucene 段(segment) (通过LeafReaderContext reader绑定)。 -
它内部维护一个
docId字段,表示当前正在处理的文档在该 segment 中的文档 ID。 -
每当 Elasticsearch 需要对一个新文档执行脚本(比如在 script score、script field 或聚合脚本中),就会调用:
leafDocLookup.setDocument(docId);然后脚本通过
doc['field']访问该文档的字段值。
🔁 处理流程示例(简化版)
假设你有一个查询 + 脚本评分:
{
"query": {
"function_score": {
"script_score": {
"script": "doc['price'].value * params.factor",
"params": { "factor": 1.2 }
}
}
}
}
Elasticsearch 内部大致会这样处理:
- 找到命中的文档(分布在不同 segment 中)。
- 对每个 segment,创建一个
LeafDocLookup实例(每个 segment 一个)。 - 对该 segment 中的每个命中文档:
- 调用
leafDocLookup.setDocument(luceneDocId) - 执行 Painless 脚本,脚本中
doc['price']会调用leafDocLookup.get("price") LeafDocLookup根据当前docId返回该文档的 price 值
- 调用
- 重复直到所有命中文档处理完毕。
✅ 所以结论是:
是的,
LeafDocLookup是"一个文档一个文档"地处理的,但它本身不控制循环------它只是为"当前文档"提供字段值访问能力。真正的文档遍历由上层搜索/聚合执行器控制。
这种设计既高效(缓存字段数据、复用 lookup 对象),又线程安全(每个 segment 的处理通常是单线程的)。