导读:AI 时代日志量巨大,传统用 Elasticsearch 做搜索、ClickHouse 做分析的两套系统成本高且复杂。SelectDB(基于 Apache Doris 内核研发的商业化产品) 通过内置
search()函数,在同一个引擎内融合全文检索与 SQL 分析,实现一份数据同时支持搜索和分析,大幅简化架构、提升查询性能。
AI 时代日志爆增带来的难题
当下,日志成为 AI 时代最丰富的数据资源。每一次推理请求,从 prompt 输入到 token 输出,都会在请求路由、模型调度、GPU 显存分配、KV Cache 命中率、输出质量评估等十几个环节产生大量的日志。对于一个日处理千万级请求的推理服务而言,日志规模可轻松达到数十 TB。
这些日志不仅需要长时间存储,还需要进行有价值的分析:
- 排障时要定位追踪(搜索):比如定位一次 OOM 崩溃的请求上下文,追踪一条异常推理链路的调用栈。
- 运营决策时要分析:统计各模型的 P99 延迟分布,计算 token 成本分摊,对比 A/B 实验中不同 prompt 策略的效果。
当下比较常见做法: Elasticsearch 负责搜索,ClickHouse 或其他 AP 数据库负责分析。两套系统、两份数据,二者之间需要单独维护一条数据同步的链路。这不仅增加了架构的运维成本,也让实时性、一致性面临挑战。
而架构简化、统一,已成为新一代日志处理架构的必经之路。在这一背景下,SelectDB (基于 Apache Doris 内核研发的商业化产品) 这一以分析型见长的数据库,开始将搜索这一能力做到极致,并已见成效。其内置的 search() 正是其核心。
SelectDB(www.selectdb.com/) 作为 Apache Doris 的核心贡献者和商业化团队,在 Doris 开源内核基础上,提供了企业级特性、全托管运维服务及专业技术支持,帮助企业更便捷地将 Doris 能力应用于生产环境。
search():在 SQL 里做文本搜索
先看一个例子:
SELECT request_id, model_name, error_msg, latency_ms
FROM inference_logs
WHERE search('level:ERROR AND error_msg:"CUDA out of memory" AND model_name:gpt*')
AND log_time > NOW() - INTERVAL 1 HOUR
ORDER BY latency_ms DESC
LIMIT 100;
对于熟悉 Elasticsearch 的用户来说,上手 SelectDB 的 search() 函数几乎没有学习成本,其语法与 ES query_string 几乎一致。
search() 接受一个 DSL 字符串参数,兼容 ES query_string 语法。
search() 支持 Lucene 模式,可通过第二个参数传 JSON 配置,实现更复杂的布尔逻辑组合。
-- Lucene 模式:完整的 MUST/SHOULD/MUST_NOT 语义
WHERE search(
'level:ERROR AND msg:"timeout" OR msg:"connection refused"',
'{"mode":"lucene", "default_operator":"and"}'
)
-- 多字段搜索
WHERE search(
'CUDA error',
'{"fields":["error_msg","stack_trace","context"], "mode":"lucene"}'
)
Lucene 模式实现了 ES BooleanQuery 的 occur 语义:MUST(+)、SHOULD、MUST_NOT(-),也支持 minimum_should_match。已经在用 ES query_string 的团队,迁移时大部分查询仅换个函数名即可。
15 种算子,怎么用?
search() 内核中有 15 种查询算子,可以任意嵌套组合。挑几个典型场景来看:
1. 多条件组合定位故障
推理服务出了问题,工程师要同时限定错误级别、错误关键词、延迟范围,还要排除健康检查的噪音、限定特定机器。传统做法是写大量由 AND 拼接的 MATCH,或在 Elasticsearch 里构造嵌套 bool query。
而在 SelectDB 中,search() 一行即可搞定:
-- TERM + PHRASE + RANGE + NOT + LIST 五种算子组合,一次求值
WHERE search('
level:ERROR
AND error_msg:"connection refused"
AND latency:[500 TO *]
AND NOT module:healthcheck
AND host:IN(gpu-node-01 gpu-node-02 gpu-node-03)
')
五种算子编译成一棵查询树,一次性求值。 注:IN 和数值类型的 RANGE 算子支持会在后续版本迭代支持,当前版本尚未开放。
2. 正则和通配符
线上错误信息形式多样,关键词检索难以覆盖所有变体。search() 支持前缀通配(PREFIX)、通配符(WILDCARD)和正则(REGEXP),可提升检索命中与覆盖率:
-- 抓住所有 CUDA 相关错误,不管具体是什么
WHERE search('error_msg:/CUDA.*error/ AND level:ERROR')
-- 前缀匹配:所有以 timeout 开头的错误类型
WHERE search('error_type:timeout*')
3. BM25 打分
搜索结果可能有数千条,如何能快速找到最相关的?
search() 内置 BM25 打分(IDF 加权 + 文档长度归一化),并通过 score() 列直接暴露评分,便于按相关性排序与筛选:
-- 按相关性排序,最相关的错误日志排最前面
SELECT request_id, error_msg, score() AS score
FROM inference_logs
WHERE search('error_msg:"memory allocation failed" OR error_msg:"CUDA error"')
ORDER BY score DESC
LIMIT 20;
SelectDB 在存储层还做了 TopN 打分优化,不用把全量结果传到上层再排序。
4. 嵌套搜索
AI 应用的日志往往是嵌套的、非扁平结构。例如一条 Agent 调用日志中,可能包含多个工具的调用结果和返回信息:
{
"session_id": "sess_001",
"steps": [
{"tool": "web_search", "status": "ok", "latency": 200},
{"tool": "code_exec", "status": "error", "error_msg": "timeout"}
]
}
而 SelectDB 的 VARIANT 类型可以原生存储这类嵌套结构,配合 search() 的 NESTED 算子,可直接穿透数组进行内部检索:
-- 在 steps 数组内部搜索:哪些会话中有工具调用失败?
WHERE search('NESTED(steps, status:error AND tool:code_exec)')
无需将 JSON 拆成多张表,无需额外的 ETL 管道。
5. 多字段搜索
排障时,经常不确定错误信息在哪个字段。search() 支持跨字段搜索,有两种策略,可快速定位:
-- best_fields:关键词必须在同一个字段内匹配(更精确)
WHERE search('CUDA memory', '{"fields":["error_msg","context","stack_trace"]}')
-- cross_fields:关键词可以分散在不同字段(更宽泛)
WHERE search('CUDA memory', '{"fields":["error_msg","context"], "type":"cross_fields"}')
6. 和 SQL 分析能力混用
search() 返回布尔谓词,可直接嵌入到 JOIN、窗口函数、子查询中,便捷地实现在 SQL 层面深入关联与时序分析。
-- search + JOIN:搜索 OOM 错误,关联模型配置找出资源配置不足的模型
SELECT l.request_id, l.error_msg, m.gpu_memory_limit, m.max_batch_size
FROM (
SELECT *
FROM inference_logs
WHERE search('level:ERROR AND error_msg:"out of memory"')
AND log_time > NOW() - INTERVAL 1 HOUR
) l
JOIN model_configs m ON l.model_name = m.model_name;
-- search + 窗口函数:追踪错误趋势,发现是否在恶化
SELECT
model_name,
DATE_TRUNC('hour', log_time) AS hour,
COUNT(*) AS error_count,
LAG(COUNT(*)) OVER (
PARTITION BY model_name ORDER BY DATE_TRUNC('hour', log_time)
) AS prev_hour_errors
FROM inference_logs
WHERE search('level:ERROR')
AND log_time > NOW() - INTERVAL 24 HOUR
GROUP BY model_name, DATE_TRUNC('hour', log_time);
在 Elasticsearch 中要做同样的分析,一般需要编写复杂的聚合 DSL,或是将数据导入到其他系统。
为什么比多个 MATCH 快
SelectDB 早已提供 MATCH_ANY、MATCH_ALL、MATCH_PHRASE 等全文检索谓词。search() 的主要改进体现在多条件组合时的性能优势。
以一个典型的日志查询为例,同时过滤 4 个字段:
-- 写法 A:传统 MATCH(每个条件独立求值)
SELECT * FROM logs
WHERE level MATCH_ANY 'ERROR'
AND module MATCH_ANY 'inference'
AND error_msg MATCH_PHRASE 'CUDA out of memory'
AND context MATCH_ANY 'gpu'
-- 写法 B:search 函数(统一求值)
SELECT * FROM logs
WHERE search('level:ERROR AND module:inference AND error_msg:"CUDA out of memory" AND context:gpu')
从语法上看起来相似,但执行逻辑截然不同。接下来分别看看 MATCH 和 search() 的执行逻辑,便于对比。
1. MATCH:bitmap 物化 + 集合运算
每个 MATCH 在 Segment 层独立执行:
MATCH_ANY 'ERROR' → 打开 IndexReader → 搜索 → 生成 bitmap A
MATCH_ANY 'inference' → 打开 IndexReader → 搜索 → 生成 bitmap B
MATCH_PHRASE 'CUDA out of...' → 打开 IndexReader → 搜索 → 生成 bitmap C
MATCH_ANY 'gpu' → 打开 IndexReader → 搜索 → 生成 bitmap D
最终结果 = A ∩ B ∩ C ∩ D
每个条件都会生成一份完整的 bitmap;即便最终交集只有几十行,中间每个 bitmap 也可能包含上百万 bit。四个条件就意味着四次 IndexReader 的 open/search 操作,加上三次 bitmap 交集运算。
2. search():查询树 + 逐文档求值
search() 将所有条件编译成一棵查询树,参考 Lucene 的 Weight/Scorer 架构执行。与单一 MATCH 谓词执行相比,差异主要体现在三处:
A. 逐行推进,支持 AND 短路
并非先计算出每个条件的全量结果再进行交集,而是逐行推进:比如第一个条件匹配了行号#100,那么第二个条件就可以直接跳到 #100 进行检查,不匹配则跳过,无需对中间产生的完整 bitmap 进行物化。
当数据分布有倾斜时优势更大。比如 level:ERROR 只占日志的 0.1%,大量数据行在第一个条件就被快速跳过。
B. 共享 IndexReader,避免重复开销
多个字段共享已打开的 reader 实例,无需重复加载索引文件。而多个独立的 MATCH 谓词条件各自维护自己的 reader,这部分开销会显著叠加。
C. DSL 级别缓存,性能表现更佳
search() 以整个 DSL 表达式作为缓存 key,同一查询在不同 segment 上的结果可以复用。而 MATCH 采用单谓词粒度的缓存,命中率相对较低。在反复执行相似查询的交互式分析场景中,性能差距更为显著。
总结:MATCH 是各条件独立求值后再合并,而 search() 在一棵查询树内统一求值,条件越多、数据倾斜越明显,性能差异越大。
三个实战场景
以下用三个 AI 场景的 SQL,展示search()和聚合分析怎么配合使用。
1. 模型推理异常诊断
推理服务出现 GPU OOM 时,需要快速定位是哪些模型在报错、prompt 长度分布是否异常、延迟是否受影响。以下 SQL 在一条查询里完成过滤和聚合:
-- 搜索最近 1 小时所有 GPU OOM 错误,按模型聚合统计
SELECT
model_name,
COUNT(*) AS error_count,
AVG(prompt_tokens) AS avg_prompt_tokens,
MAX(prompt_tokens) AS max_prompt_tokens,
PERCENTILE_APPROX(latency_ms, 0.99) AS p99_latency
FROM inference_logs
WHERE search('level:ERROR AND error_msg:"CUDA out of memory"')
AND log_time > NOW() - INTERVAL 1 HOUR
GROUP BY model_name
ORDER BY error_count DESC;
倒排索引先从数十亿行日志里过滤出目标数据,MPP 引擎再进行聚合分析,非常连贯且便捷。反观传统的 Elasticsearch + OLAP 分治模式,在这中间多一次数据搬运动作,这就增加了延迟和复杂度。
2. 模型评估 A/B 分析
上线新版模型前,通常要对比新旧版本在不同 prompt 长度下的质量、延迟和成本。短 prompt 表现好不代表长 prompt 也同样好,需要分区间来看。以下 SQL 按 prompt 长度分桶,对比两个版本的核心指标:
-- 对比两个模型版本在不同 prompt 长度区间的表现
SELECT
model_version,
CASE
WHEN prompt_tokens < 100 THEN 'short'
WHEN prompt_tokens < 1000 THEN 'medium'
ELSE 'long'
END AS prompt_category,
COUNT(*) AS request_count,
AVG(quality_score) AS avg_quality,
AVG(latency_ms) AS avg_latency,
SUM(completion_tokens) * 0.00003 AS estimated_cost_usd
FROM eval_logs
WHERE search(
'task:evaluation AND status:completed AND model_version:IN(v2.1 v2.2)',
'{"mode":"lucene"}'
)
AND eval_time > NOW() - INTERVAL 7 DAY
GROUP BY model_version, prompt_category
ORDER BY model_version, prompt_category;
search() 使用 Lucene 语法进行多条件过滤,IN 操作可一次匹配多个版本。完成过滤后在 SQL 层直接做分桶聚合,质量、延迟与成本即可在同一表中展示,无需分别查询再合并。
3. AI Agent 调用链追踪
一次 Agent 执行可能触发十几次工具调用,任一环节的报错或超时都会影响最终结果。排查问题时,需要还原完整调用链,查看每一步调用了哪些工具、输入输出内容及耗时。下面的 SQL 按执行顺序还原一次异常会话的完整链路:
-- 追踪某个异常 agent 会话的完整调用链
SELECT
step_index,
tool_name,
input_summary,
output_summary,
latency_ms,
token_usage
FROM agent_trace_logs
WHERE search('session_id:sess_abc123 AND (status:ERROR OR status:TIMEOUT)')
ORDER BY step_index;
按 session_id 定位具体会话,同时过滤出报错和超时的步骤。对结果按 step_index 排序后即可得到一条完整的调用时间线,问题发生的环节一目了然。
从以上示例可以看出,三个场景实则采用同一模式:先搜索缩小范围,再在 SQL 里进行聚合或关联分析。
从 Elasticsearch 迁过来要花多少?
存储:成本节省 50%
Elasticsearch 的倒排索引 + 正排存储 + 副本,通常比源数据膨胀 2-3 倍。而 SelectDB 的倒排索引和列存数据分开存储,V3 存储格式支持 ZSTD 字典压缩(dict_compression = true),索引体积比 Elasticsearch 减少约 20%。如果在 TB 级日志场景下,整体存储成本能省 50% 以上。
运维:架构复杂度降低
迁移至 SelectDB 后,企业不再需要同时维护 ES 集群及其配套的 Kafka → Logstash → ES 同步链路。对于团队规模较小的 AI 公司而言,这意味着可以直接省去一个专职运维岗位。
迁移:兼容 ES query_string
search()兼容 ES query_string 语法,大部分原有查询仅需将 REST API 改成 SQL WHERE 即可。- 索引定义:ES mapping 的字段类型对应 SelectDB 列定义 +
USING INVERTED。
-- SelectDB 建表示例:日志表 + 倒排索引
CREATE TABLE inference_logs (
log_time DATETIME,
request_id VARCHAR(64),
model_name VARCHAR(32),
level VARCHAR(16),
error_msg TEXT,
context TEXT,
prompt_tokens INT,
completion_tokens INT,
latency_ms INT,
INDEX idx_level(level) USING INVERTED,
INDEX idx_error(error_msg) USING INVERTED PROPERTIES(
"parser" = "unicode", "support_phrase" = "true"
),
INDEX idx_context(context) USING INVERTED PROPERTIES(
"parser" = "unicode", "support_phrase" = "true"
),
INDEX idx_model(model_name) USING INVERTED
) ENGINE=OLAP
DUPLICATE KEY(log_time)
PROPERTIES (
"inverted_index_storage_format" = "V3"
);
另外,SelectDB 的倒排索引加减均不需要重写数据文件。可以先导入数据,再根据查询需求针对性添加索引,也可以随时删掉冗余的索引释放空间——整个过程无需停服、无需重新建表。
总结
传统的日志分析方案,往往是一条数据同步链路连接着两个世界:Elasticsearch 负责搜索,OLAP 引擎负责分析。两套系统各自独立部署,存储冗余、运维复杂、版本升级相互牵制,数据一致性存在隐患。而 SelectDB search() 的出现,让这一切变得简单起来。 同一份数据,倒排索引负责筛选,MPP 引擎负责计算,搜索与分析在同一个引擎内无缝融合。
search()集成了 15 种查询算子、BM25 相关性打分、嵌套数组搜索、多字段跨字段检索等原本需要搜索引擎才能提供的丰富功能。文本检索由此变成了一个普通的 WHERE 谓词,直接参与 JOIN、聚合、窗口函数、子查询,相当的便捷。
AI 场景下的日志,数据量更大、字段结构更复杂,既要能精确定位异常,还要进行聚合统计。能够将搜索和分析写在同一条 SQL 里,少维护一套系统,少一次数据搬运,查询延迟从分钟级提升至秒级,正是 SelectDB 此举的价值所在。
- 想立即体验? SelectDB Cloud 免费试用:www.selectdb.com/cloud
- 需要私有化部署? SelectDB Enterprise 下载试用:www.selectdb.com/enterprise
- 已是阿里云用户? 阿里云数据库 SelectDB 版 一键启用:http://www.aliyun.com/product/selectdb?utm_content=g_1000410296
