从两套系统到一条 SQL:SelectDB search() 搞定日志的搜索与分析

导读: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 此举的价值所在


请使用浏览器的分享功能分享到微信等