search()函数,在同一个引擎内融合全文检索与 SQL 分析,实现一份数据同时支持搜索和分析,大幅简化架构、提升查询性能。
AI 时代日志爆增带来的难题
当下,日志成为 AI 时代最丰富的数据资源。每一次推理请求,从 prompt 输入到 token 输出,都会在请求路由、模型调度、GPU 显存分配、KV Cache 命中率、输出质量评估等十几个环节产生大量的日志。对于一个日处理千万级请求的推理服务而言, 日志规模可轻松达到数十 TB 。
这些日志不仅需要长时间存储,还需要进行有价值的分析:
-
排障时要定位追踪(搜索):比如定位一次 OOM 崩溃的请求上下文,追踪一条异常推理链路的调用栈。
-
运营决策时要分析:统计各模型的 P99 延迟分布,计算 token 成本分摊,对比 A/B 实验中不同 prompt 策略的效果。
当下比较常见做法: Elasticsearch 负责搜索,ClickHouse 或其他 AP 数据库负责分析。两套系统、两份数据,二者之间需要单独维护一条数据同步的链路。这不仅增加了架构的运维成本,也让实时性、一致性面临挑战。
而架构简化、统一,已成为新一代日志处理架构的必经之路。在这一背景下,
(基于 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()
内置
(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 在存储层还做了 ,不用把全量结果传到上层再排序。
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 版 一键启用 :