分布式搜索和分析引擎——Elasticsearch(高级查询语法)
本文将深入探讨Elasticsearch中的查询机制及其进阶用法,包括基础查询方式、span相关的搜索语法、子字段分词以及滚动搜索等。下文的查询示例基于真实的APK安全分析索引
apk_v6。
示例索引(apk_v6)
本篇所有示例基于以下索引。这是一个APK样本数据库的完整mapping,别名为apk,90分片,ES 7.15版本。
1 | { |
字段类型速查
| 分类 | 代表字段 | 类型 | 说明 |
|---|---|---|---|
| 哈希 | md5、sha1、sha256、dex_md5、cert_sha1 | keyword | 精确匹配,不分词 |
| 名称 | name、virus_name、mf_app_name、cert_name | keyword | APK包名、病毒名等 |
| 分类标签 | source、wd_level、wd_apk_type、targets | keyword | 来源、等级、类型 |
| 状态标识 | status、sign、ave_sign、verify | byte/short | 小数值枚举 |
| 数值 | dex_size、spread_cnt、version_code、safe | long | 可做range/聚合 |
| 计数 | app_key_count、mf_permission_count、mf_service_count | integer | 各类组件数量 |
| 时间 | createtime、updatetime | date | 格式yyyy-MM-dd HH:mm:ss |
| 嵌套 | app_key{app_key,sdk_name} | nested | SDK信息 |
| 嵌套 | mf_providers{name,authorities} | nested | Provider组件 |
| 嵌套 | mf_meta_datas{name,value} | nested | 元数据键值对 |
注意:apk_v6的所有字符串字段都是keyword类型(不分词),因此聚合和排序时直接使用字段名即可,不需要加.keyword后缀。下文中match(分词搜索)、match_phrase、Span查询、match_phrase_prefix、match_bool_prefix、高亮、子字段映射等依赖text分词的章节,使用通用字段(title/content)演示——实际使用时需要为目标字段配置text类型和分词器。
高级查询
在Elasticsearch的查询中,通常使用
query作为顶层元素,顶层元素下的子句可分为查询子句(Query Clauses)和复合查询子句(Compound Query Clauses)。前者直接匹配文档内容,后者用于组合多个查询或过滤条件。
核心查询子句
match
全文搜索,适用于text类型字段,支持分词和模糊匹配。会对查询文本进行分词处理(如中文按词、英文按空格),并计算文档相关性评分(_score)。apk_v6无text字段,此处用通用字段演示。
1 | { |
term
精确匹配单个值,适用于
keyword、numeric、date等结构化字段。不分析查询文本,直接匹配倒排索引中的精确值。
1 | { |
terms
匹配多个值中的任意一个,等价于SQL中的IN操作。
1 | { |
range
范围查询,支持数值、日期等类型。
按时间范围查询最近30天创建的APK:
1 | { |
按数值范围查询DEX文件大小:
1 | { |
match_phrase
短语匹配,要求所有词条按顺序连续出现。适用于text类型字段,apk_v6无text字段,此处用通用字段演示。
1 | { |
exists
检查字段是否存在(即文档中是否包含该字段且值非null)。
查找有病毒名的APK样本:
1 | { |
复合查询子句
bool
组合多个查询条件,支持四种逻辑关系
- must:必须匹配,贡献评分。
- filter:必须匹配,不贡献评分(结果会被缓存)。
- must_not:必须不匹配。
- should:选择性匹配,满足任意条件即可。
查找2025年创建的、非白名单、标记为恶意的APK:
1 | { |
dis_max
取多个查询中的最高评分作为最终评分,避免不同字段的评分相互影响。
tie_breaker控制其他查询的评分权重(0~1),0表示只取最高分,1表示所有查询评分求和。
1 | { |
function_score
自定义评分逻辑,结合查询结果和自定义函数调整文档评分。
gauss是衰减函数,让越接近origin的文档得分越高。boost_mode控制函数结果与原始评分的合并方式:multiply(相乘)、sum(相加)、replace(替换)等。
按传播量加权、近期样本优先:
1 | { |
constant_score
将查询包装为过滤条件,返回固定评分(_score=1)。
boost设定固定评分值,默认为1.0。
1 | { |
其他特殊查询子句
multi_match
在多个字段上执行相同的查询,支持多种匹配模式。apk_v6无text字段,此处用通用字段演示。
fields中^2表示该字段权重加倍。type支持多种模式:best_fields(取最高分字段,默认)、most_fields(累加所有字段分数)、cross_fields(跨字段当一个字段查)、phrase(短语匹配)、phrase_prefix(短语前缀)。
1 | { |
nested
查询嵌套文档(处理nested类型的字段)。
查找包含特定SDK的APK:
1 | { |
查找包含特定meta-data的APK:
1 | { |
wildcard
通配符查询,支持 *(任意字符)和 ?(单个字符)。
查找腾讯系包名的APK:
1 | { |
注意:在keyword字段上做wildcard代价较高,尤其是前缀通配(*xxx),数据量大时慎用。
regexp
正则表达式查询。
1 | { |
Span 查询
Span查询用于精确控制词条之间的位置关系和距离,比match_phrase更灵活。适合需要”两个词之间最多隔几个词”或”某个词不能出现在另一个词附近”这类场景。Span查询只能用于text分词字段,apk_v6无text字段,以下用通用字段演示。
span_term
最基础的span查询,匹配单个精确词条,是其他span查询的构建块。
1 | { |
span_near
要求多个span查询在指定距离内按顺序(或不按顺序)出现。
slop指定允许的最大间隔词数,in_order控制是否要求按顺序。
1 | { |
上面的查询要求”分布式”和”搜索”按顺序出现且中间最多隔2个词。
span_or
匹配多个span查询中的任意一个。
1 | { |
span_not
排除:匹配
include但不匹配exclude所覆盖的位置范围。
1 | { |
span_containing
要求一个大范围的span包含一个小范围的span。常用于”A和B靠近且附近还有C”这类组合条件。
1 | { |
Span查询对比
| 查询 | 作用 | 关键参数 |
|---|---|---|
span_term |
单词条匹配 | — |
span_near |
多词条按距离和顺序匹配 | slop、in_order |
span_or |
多个span取并集 | — |
span_not |
排除特定位置范围 | include、exclude |
span_containing |
大范围包含小范围 | big、little |
模糊与前缀查询
fuzzy
基于编辑距离(Levenshtein Distance)的模糊匹配,容忍拼写错误。keyword字段也支持fuzzy查询。
fuzziness可以是具体数值(0、1、2)或AUTO(根据词长自动选择)。
1 | { |
prefix
前缀匹配,查询以指定字符串开头的词条。
1 | { |
match_phrase_prefix
短语匹配 + 最后一个词做前缀补全,适合搜索框的自动补全场景。适用于text字段,apk_v6无text字段,此处用通用字段演示。
max_expansions限制最后一个词前缀展开的最大词条数,防止性能爆炸。
1 | { |
match_bool_prefix
把查询文本分词后,前面的词用
term匹配,最后一个词用prefix匹配。适合实时搜索联想。适用于text字段,apk_v6无text字段,此处用通用字段演示。
和match_phrase_prefix的区别:不要求词条按顺序连续出现。
1 | { |
ids
按文档
_id精确查找。
1 | { |
子字段与多字段映射(fields)
同一个原始字段可以用不同的分词器建多个子字段,查询时按需选择。一个字段既能做全文搜索(text + 分词),又能做精确排序和聚合(keyword + 不分词)。apk_v6的字段均为keyword,未使用多字段映射,以下用通用字段演示。
映射定义:
1 | { |
查询时的使用:
| 字段引用 | 类型 | 用途 |
|---|---|---|
title |
text(ik分词) | 中文全文搜索 |
title.keyword |
keyword | 精确匹配、排序、聚合 |
title.english |
text(english分词) | 英文搜索(词干提取等) |
1 | { |
注意:term查询不能直接用在text字段上(text字段存的是分词后的词条,和原始值不一致),要精确匹配必须走.keyword子字段。而像apk_v6这样字段本身就是keyword类型的,直接用字段名即可,不需要加.keyword后缀。
结果控制
_source 过滤
控制返回文档中包含哪些字段,减少网络传输。
1 | { |
也可以用includes/excludes做更细粒度的控制:
1 | { |
高亮(highlight)
在搜索结果中把匹配的关键词用标签包裹,方便前端展示。高亮主要用于text分词字段,apk_v6无text字段,此处用通用字段演示。
1 | { |
返回结果里会多一个highlight字段,内容是带标签的文本片段。
常用参数:
| 参数 | 作用 |
|---|---|
pre_tags / post_tags |
高亮标签 |
fragment_size |
每个片段的字符数(默认100) |
number_of_fragments |
返回几个片段(默认5) |
type |
高亮器类型:unified(默认)、plain、fvh |
排序(sort)
默认按_score降序排列。可以指定一个或多个字段排序,一旦指定了sort就不再计算_score(除非显式加上_score)。
按创建时间倒序、再按传播量倒序:
1 | { |
排序字段必须是keyword、数值、日期等可排序类型,text字段不能直接排序(要用.keyword子字段)。apk_v6的字段都是keyword或数值,直接用即可。
分页
from / size(浅分页)
最基本的分页方式,from是偏移量,size是每页条数。
1 | { |
from + size不能超过index.max_result_window(默认10000)。超过这个限制用下面两种深度分页方式。
scroll(滚动搜索)
适合一次性遍历大量数据(如数据导出、批量处理),不适合实时搜索。原理是创建一个快照,通过scroll_id逐批获取。
1 | POST /apk/_search?scroll=5m |
scroll=5m表示快照保持5分钟。sort: ["_doc"]按索引顺序遍历,效率最高。拿到第一批结果和_scroll_id后,后续请求:
1 | POST /_search/scroll |
反复调用直到返回hits.hits为空。用完必须清理:
1 | DELETE /_search/scroll |
search_after(实时深度分页)
适合实时翻页到很深的位置,用上一页最后一条的排序值作为起点。比scroll更轻量,不占服务端资源。
1 | { |
search_after的值是上一页最后一条文档的sort值数组。要求sort里必须有一个唯一字段(如_id)保证顺序确定。
三种分页方式对比
| 方式 | 适用场景 | 能否跳页 | 深度限制 | 资源占用 |
|---|---|---|---|---|
from/size |
浅分页(前几页) | 能 | 默认10000条 | 低 |
scroll |
批量遍历/导出 | 不能 | 无 | 高(服务端保持快照) |
search_after |
实时深度翻页 | 不能 | 无 | 低 |
聚合(aggregations)
聚合是ES的分析能力核心,相当于SQL里的GROUP BY + 聚合函数。聚合和查询可以同时使用——query筛选文档范围,aggs在结果上做统计。
聚合类型
| 类型 | 说明 | 常用 |
|---|---|---|
| 桶聚合(Bucket) | 按条件分组,类似GROUP BY | terms、range、date_histogram、histogram、filter |
| 指标聚合(Metric) | 计算数值指标 | avg、sum、min、max、cardinality、value_count、stats |
| 管道聚合(Pipeline) | 在其他聚合的结果上二次计算 | avg_bucket、max_bucket、cumulative_sum |
常用桶聚合
terms
按字段值分组统计,类似GROUP BY。
统计各威胁等级的APK数量:
1 | { |
size: 0表示不返回文档,只返回聚合结果。aggs里的size是返回的桶数量。注意这里直接用wd_level而不是wd_level.keyword——因为它本身就是keyword类型。
range
按数值范围分桶。
按DEX文件大小分档统计:
1 | { |
date_histogram
按时间间隔分桶。
按月统计APK入库趋势:
1 | { |
calendar_interval用于自然周期(day、week、month、year),fixed_interval用于固定时长(1h、30m、7d)。
常用指标聚合
1 | { |
stats一次返回count、min、max、avg、sum五个指标。cardinality是去重计数(近似值)。
嵌套聚合
桶聚合内部可以再嵌套指标聚合或子桶聚合,实现多维分析。
按威胁等级分组,每组内算平均DEX大小,并再按status细分:
1 | { |
nested类型聚合
对nested字段做聚合时,必须先用nested聚合进入嵌套文档上下文,才能对嵌套字段做terms等聚合。
统计各SDK出现的APK数量:
1 | { |
post_filter
post_filter在聚合计算之后再过滤文档,只影响返回的hits,不影响聚合结果。典型场景:先聚合算出所有分类的统计数据,再只返回某个分类的文档。
聚合统计所有威胁等级的数量分布,但只返回black级别的文档:
1 | { |
聚合all_levels统计的是2022年以来所有等级的分布,但返回的hits只有black级别的文档。