探索大语言模型与OLAP数据库的融合,为复杂数据查询提供智能化能力。
六个月前,我写过一篇文章说明我们为什么用 Apache Doris 替代了 ClickHouse 作为数据管理系统的 OLAP 引擎。那时候,我们在自动生成 SQL 语句方面还在苦苦摸索。随着时间推移,我们已经取得了足够重大的进展,值得分享给大家,所以我又来了。
我们已经采纳了大语言模型(LLM)来赋能我们的 Doris 驱动的 OLAP 服务。
我们这样做的初衷是让内部员工免于陡峭的 SQL 学习曲线。因此我们用 LLM 作为中间层,它把自然语言问题转换成 SQL 语句,然后发送到 OLAP 引擎执行。
就像所有 AI 相关的项目一样,我们也遇到了不少障碍:
LLM 不理解数据术语,比如"字段""行""列"和"表"。反而,它能完美地翻译业务术语,比如"企业收入"和"DAU",这些本质上就是对字段/行/列的描述。这意味着只有当分析师在提问时用了完全准确的词汇来指代他们需要的指标时,系统才能良好运作。
我们使用的 LLM 推理速度较慢。响应需要超过 10 秒。由于按 token 计费,成本效率成为了一个问题。
虽然 LLM 是在庞大的公开数据集上训练的,但对于小众领域的知识了解不足。在我们的情况下,LLM 对独立音乐非常陌生,所以即使这些歌曲存在于我们的数据库中,LLM 也无法正确识别它们。
有时候我们的输入问题需要最新且充分的法律、政治、财务和监管信息,这些很难包含在训练数据集或知识库中。我们需要把 LLM 连接到更广泛的信息库,才能执行更多样化的任务。
我们逐个解决了这些问题。
针对问题 1,我们在 LLM 和 OLAP 引擎之间引入了一个语义层。这一层把业务术语转换为对应的数据字段。它能从各种自然语言措辞中识别数据过滤条件,关联到涉及的指标,然后生成 SQL 语句。
除此之外,语义层还可以优化计算逻辑。当分析师输入的问题涉及复杂查询时,比如说多表联接,语义层可以把它拆分成多个单表查询,以减少语义失真。
为了提高 LLM 使用的成本效益,我们评估了所有场景的计算复杂度,比如指标计算、详细记录检索和用户分群。然后我们制定了规则,只让 LLM 解析步骤专注于复杂任务。这意味着对于简单的计算任务,它会跳过解析。
例如,当分析师输入"告诉我主流音乐平台的收入",LLM 识别出这个问题只涉及几个指标或维度,所以它不会进一步解析,而是直接发送给 SQL 生成和执行。这可以大大缩短查询响应时间,减少 API 费用。
为了让 LLM 掌握小众领域知识,我们在 LLM 之前增加了一个 Schema Mapper。Schema Mapper 把输入问题映射到一个外部知识库,然后 LLM 再进行解析。
我们不断测试和优化 Schema Mapper。我们对外部知识库中的内容进行分类和评级,执行各种层级的映射(全文映射和模糊映射)来实现更好的语义解析。
我们用插件把 LLM 连接到更多信息领域,对于不同类型的插件有不同的集成方式:
嵌入本地文件:当我们需要"教"LLM 最新的监管政策时特别有用,这些政策通常是文本文件。首先系统对本地文本文件进行向量化,执行语义搜索在本地文件中找到匹配或相似的术语,提取相关内容并放入 LLM 解析窗口来生成输出。
第三方插件:市场上有很多第三方插件专为各行各业设计。有了它们,LLM 就能处理范围广泛的话题。每个插件都有自己的提示词和调用函数。一旦输入问题触发了某个提示词,相关插件就会被调用。
在完成上述四项优化后,SuperSonic 框架应运而生。
现在让我带你了解一下这个框架:
分析师输入一个问题。
Schema Mapper 把问题映射到外部知识库。
如果外部知识库中有匹配的字段,问题就不会被 LLM 解析。相反,会触发一个指标计算公式让 OLAP 引擎开始查询。如果没有匹配字段,问题进入 LLM。
根据预定义的规则,LLM 评定问题的复杂程度。如果是简单查询,它直接进入 OLAP 引擎;如果是复杂查询,它会被语义解析并转换为 DSL 语句。
在语义层,DSL 语句会根据其查询场景进行拆分。例如,如果是多表联接查询,这一层会生成多个单表查询 SQL 语句。
如果问题涉及外部知识,LLM 会调用第三方插件。
要回答某首歌是否可以在综艺节目中表演,系统从 OLAP 数据仓库检索关于这首歌的详细信息,并结合商用查询第三方插件的结果来呈现。
至于这个框架的 OLAP 部分,经过多轮架构演进后,这是我们当前的 OLAP 管道:
原始数据被分类为标签和指标,由分析师自定义。标签和指标在统一管理下,以避免定义不一致。然后它们被组合成各种标签集和指标集用于不同的查询。
我们从架构优化经验中总结出了两个主要收获。
在采纳 Apache Doris 之前,我们曾使用 ClickHouse 来加速标签和指标的计算,使用 Elasticsearch 来处理维度数据。这是两个分析引擎,需要我们把查询语句适配到两者。维护成本很高。
因此我们用 Apache Doris 替换了 ClickHouse,并利用 Elasticsearch Catalog 功能把 Elasticsearch 数据连接到 Doris。这样我们就有了一个统一的查询网关。
在我们 OLAP 架构的早期版本中,我们习惯把数据放入扁平表,这造成了不少麻烦。一方面,扁平表吸收了上游所有的写入延迟,累积起来导致数据实时性的严重丧失。另一方面,扁平表中 50% 的数据是维度数据,这些数据很少更新。每创建一个新的扁平表,就要附带一堆庞大的维度数据,消耗大量存储空间。
因此我们把扁平表拆分成指标表和维度表。由于它们的更新频率不同,我们把它们放在不同的数据模型中。
指标表:我们在 Apache Doris 的聚合键(Aggregate Key)模型中安排指标数据,这意味着新数据会通过 SUM、MAX、MIN 等方式与旧数据合并。
维度表:这些表采用 Apache Doris 的唯一键(Unique Key)模型,这意味着新记录会替换旧记录。这可以大大提升我们的查询场景性能。
你可能会问,这是否会给查询带来麻烦,因为大多数查询需要两种类型表的数据?别担心,我们用 Doris 的 Rollup 功能来解决这个问题。在基础表的基础上,我们可以选择需要的维度来创建 Rollup 视图,它会自动执行 GROUP BY。这使我们不需要为每个 Rollup 视图定义标签,并大大加快了查询速度。
在使用 Apache Doris 的过程中,我们也发现了其他一些便利的功能,我这里也为你列举一下:
物化视图:物化视图是一个预计算的数据集。当你经常需要访问特定维度的数据时,它是加速查询的一种方式。在这些场景中,我们基于原有指标定义派生指标和指标。例如,我们通过组合指标 1、指标 2 和指标 3 创建派生指标:sum(m1+m2+m3)。然后我们可以为它创建物化视图。根据 Doris 的发布计划,版本 2.1 将支持多表物化视图,我们期待这一功能。
Flink-Doris-Connector:这是为了在数据摄取中保证恰好一次(Exactly-Once)。Flink-Doris-Connector 实现了检查点机制和两阶段提交,允许从关系数据库到 Doris 的自动数据同步。
当聚合任务数或数据量对 Flink 来说变得过大时,数据压缩中可能出现巨大延迟。我们用垂直压缩(Vertical Compaction)和段压缩(Segment Compaction)来解决这个问题。垂直压缩支持仅加载部分列,所以在压缩扁平表时可以减少存储消耗。段压缩可以避免在数据写入过程中生成太多段,并允许在写入时同时进行压缩。
为了降低成本并提高服务可用性,我们计划测试新发布的 Doris 存储计算分离(Storage-Compute Separation)和跨集群复制(Cross-Cluster Replication)功能,我们欢迎关于 SuperSonic 框架和 Apache Doris 项目的任何想法和建议。