SuperSonic主要流程
-
原始文本先经过Schema Mapper生成SchemaMapInfo;
-
经过Schema Parser生成SemanticQuery和SemanticParseInfo(包含S2SQL);其中规则/LLM解析均生成S2SQL;
-
S2SQL经过SemanticCorrector对S2SQL进行修正,生成修正S2SQL;
-
修正后的S2SQL再经过Semantic层转换为执行SQL,查询物理数据模型获取结果数据;
一、Mapper功能
1.作用
SchemaMapper用于从输入文本中,解析所有可能存在的SchemaElement;并将这些SchemaElement数据通过SchemaMapInfo将传递到下一步的Parser中,作为后续Parser解析识别使用;
2.分类
以访问次数为例:
基于Embedding的语义识别(EmbeddingMapper):
浏览量与访问次数语义近似
前后缀识别(HanlpDictMapper):
前缀
后缀
模糊识别(FuzzyNameMapper):
3.SchemaMapper使用的识别器
com.tencent.supersonic.chat.api.component.SchemaMapper=\
com.tencent.supersonic.chat.mapper.EmbeddingMapper,\
com.tencent.supersonic.chat.mapper.HanlpDictMapper,\
com.tencent.supersonic.chat.mapper.FuzzyNameMapper,\
com.tencent.supersonic.chat.mapper.QueryFilterMapper,\
com.tencent.supersonic.chat.mapper.EntityMapper
- EmbeddingMapper:支持语义层面映射,获取到输入文本中更多的语义相似的词匹配。
- HanlpDictMapper:借用Hanlp词典能力,从纯文本角度按前后缀方式识别文本中的相似词。
- FuzzyNameMapper:通过模糊搜索识别更多的词,以解决HanlpDictMapper只支持前后缀搜索的的不足。
- QueryFilterMapper:用于过滤查询条件,去掉无效或冗余的查询条件。
- EntityMapper:处理实体数据的映射,识别文本中的实体名称。
4.工作流程
输入数据:用户输入的查询文本
输出数据:SchemaElement数据
SchemaElement主要分为几个类型:model(模型)、metric(指标)、dimension(维度)、value(维度值)、entity(实体名)
①输入文本的接收与预处理
- 接收文本:接收用户输入的查询文本。
- 分词处理:使用HanLP工具进行分词处理,将输入文本分割为一个个词语。
②词典数据利用
- 词典数据格式:词典主要由word、nature、frequency,三类组成,其中nature构造如下;
同一个word,如果frequency不同,可以选取frequency较大的作为识别的内容;
- 实时词典构造:
- 数据更新:
- 定期(每1分钟)对比model、metric、dimension、value(仅在界面配置的维度值数据)、entity元数据,如果有变化则更新词典。
- 监听model、metric、dimension、value、entity元数据变更事件,实时更新词典。
- 具体实现:
- 数据更新:
com.tencent.supersonic.knowledge.listener.ApplicationStartedListener //初始化
com.tencent.supersonic.knowledge.dictionary.builder.ModelWordBuilder
com.tencent.supersonic.knowledge.dictionary.builder.MetricWordBuilder
com.tencent.supersonic.knowledge.dictionary.builder.DimensionWordBuilder
com.tencent.supersonic.knowledge.dictionary.builder.ValueWordBuilder
com.tencent.supersonic.knowledge.dictionary.builder.EntityWordBuilder
- 离线词典构造:
- 一般生产环境下,维度值数据量大且不像维度名、指标名等可以实时获取变更;因此,对于维度值的构造,采用离线方式;
- 定期或主动触发离线构造任务,通过查询语义层获取维度值,并按词典格式写入到文件中。
- 文件存储方式可支持本地文件、HDFS等。
IOAdapter=com.tencent.supersonic.knowledge.dictionary.HadoopFileIOAdapter
③探测过程
public List<T> detect(QueryContext queryContext, List<Term> terms, Set<Long> detectModelIds) {
// QueryContext queryContext:包含查询的上下文信息,例如用户的查询文本。
// List<Term> terms:要检测的术语列表。
// Set<Long> detectModelIds:用于检测的模型ID集合。
Map<Integer, Integer> regOffsetToLength = getRegOffsetToLength(terms);
// 该行代码通过调用 getRegOffsetToLength 方法获取术语的偏移量和长度的映射。
String text = queryContext.getRequest().getQueryText();
// 从查询上下文中提取用户的查询文本。
Set<T> results = new HashSet<>();
Set<String> detectSegments = new HashSet<>();
// 用于存储检测到的结果和检测段。
for (Integer startIndex = 0; startIndex <= text.length() - 1; ) { //遍历查询文本的每个起始索引。
for (Integer index = startIndex; index <= text.length(); ) { //从当前起始索引开始,逐步向后移动,检测文本段。
int offset = mapperHelper.getStepOffset(terms, startIndex);
index = mapperHelper.getStepIndex(regOffsetToLength, index);
// 通过 mapperHelper 获取当前的偏移量和更新索引。
if (index <= text.length()) {
String detectSegment = text.substring(startIndex, index);
detectSegments.add(detectSegment);
// 提取当前的检测段,并将其添加到 detectSegments 集合中。
detectByStep(queryContext, results, detectModelIds, startIndex, index, offset);
// 调用 detectByStep 方法进行具体的检测操作。
}
}
startIndex = mapperHelper.getStepIndex(regOffsetToLength, startIndex);
//更新起始索引,以便进行下一轮检测。
}
detectByBatch(queryContext, results, detectModelIds, detectSegments); //在所有段落检测完成后,进行批量检测。
return new ArrayList<>(results); //将结果集合转换为列表并返回。
}
语义识别(EmbeddingMapper)
- 分词结果:通过HanLP获取的分词结果。
- 向量查询:将所有分词结果导入向量数据库,进行批量查询,获取最相似的数据。
- 模型过滤:按model进行分类。
- 相似度过滤:按配置的相似度阈值进行过滤。
- 结果写入:将识别结果写入到QueryContext中的mapInfo中。
上下文识别 (HanlpDictMapper)
- 分词结果:通过HanLP获取的分词结果。
- Trie树查询:通过Trie树进行前后缀匹配。
- 编辑距离计算:计算分词结果与词典中词的编辑距离,按配置的相似度阈值进行过滤。
- 结果写入:将识别结果写入到QueryContext中的mapInfo中。
模糊识别(FuzzyNameMapper)
- 分词结果:通过HanLP获取的分词结果。
- 数据库模糊搜索:从数据库中进行模糊搜索。
- 编辑距离计算:计算分词结果与数据库中词的编辑距离,按配置的相似度阈值进行过滤。
- 结果写入:将识别结果写入到QueryContext中的mapInfo中。
④结果去重
- 结果汇总:各个Mapper识别到的SchemaElement按model进行分类,统一添加至QueryContext中的mapInfo中。
- 去重处理:在添加时进行对数据去重处理,避免重复识别。
⑤结果输出
- 最终生成的SchemaElement通过QueryContext传递给后续的Parser进行进一步解析和处理。
二、Parser功能
1.作用
SemanticParser接收用户输入文本和Mapper映射结果,并抽取语义信息SemanticParseInfo封装到SemanticQuery,用于后续SQL生成和前端信息展示。
2.分类
Parser主要分为SQL和Plugin两大类:
- SQL类型的Parser用于将用户输入的文本以及Mapper阶段探测识别生成的SchemaMapInfo转化为S2SQL。
- Plugin类型的Parser主要用来根据用户文本对Plugin做召回。
根据解析方式的不同,SQL类型的Parser又可分为规则和LLM两种类型
- 规则类型的Parser根据常见的数据查询场景内置了一些规则,并基于这些内置的规则和SchemaMapInfo来生成解析结果。
- LLM Parser则直接将用户输入的文本以及相关SchemaElement交给LLM来生成解析结果。
Plugin类型的Parser则主要通过Embedding或者FunctionCall的方式来召回一些插件,插件可以是一个网页,也可以是一个HTTP服务。
3.SqlParser
LLMSqlParser
输入参数:主要包含查询文本、识别的value取值linking、识别的modelName、以及相关的中文字段名;
输出参数:主要是大模型解析后的S2SQL;
LLMSqlParser通过把Mapper阶段识别到的相关SchemaElement提供给LLM作为先验知识的方式来生成S2SQL,主要实现原理如下:
-
首先进行前置判断,由于LLM Parser需要请求大模型,耗时较长,因此会首先通过SatisfactionChecker来判断是否已有得分较高且满足阈值(参考SemanticParseInfo中的score字段)的Query,若已有,则进行跳过。
-
其次进行数据模型的选择,由于Mapper阶段从文本中可能识别得到归属于多个数据模型的SchemaElement,因此在请求大模型之前,需要先根据规则(这个规则可以通过SPI的方式自行定义,如选择包含SchemaElement最多的数据模型)选择得到最有可能的模型。
-
开始组装模型请求结构体,根据步骤2得到的数据模型,获取这个数据模型三个部分的数据:
a. Mapper阶段识别到的指标和维度
b. 根据使用次数统计排序得到的TOP-N个指标和维度
c. Mapper阶段识别到的维度值(考虑到数据隐私,可通过系统设置中的开关来决定是否要把值传递给模型)
-
请求大模型,得到大模型解析到的S2SQL,若得到多条S2SQL,则进行去重并为每条S2SQL生成一个Query。
RuleSqlParser
RuleSqlParser通过规则的方式来生成S2SQL,通过这种方式来生成S2SQL的好处也显而易见,那就是快速而又成本低,主要实现原理如下:
1.如下图,系统中首先内置了指标和标签两种类型的共8种查询模式,以及规定了每种查询模式要求文本必须包含的元素。
2.当用户文本经过Mapper识别成SchemaMapInfo之后,RuleSqlParser则可以根据生成的SchemaMapInfo来判断是否满足某个查询模式的要求,若满足,则生成一个Query,放入候选Query列表。
其它辅助Parser
其它辅助Parser主要用于对RuleSqlParser做辅助增强。
ContextInheritParser是一个基于规则实现上下文的Parser,当RuleSqlParser所得到的SchemaMapInfo不足以构成上述任何一个查询模式的时候,ContextInheritParser就开始发挥作用了,它把当前会话里上一次成功问询的的SchemaMapInfo拿出来,并和本次的SchemaMapInfo做一次合并,然后再去挨个判断一下是否满足上述哪个查询模式,若满足,则同样生成一个Query,并放入候选Query列表。
TimeRangeParser主要用来解析文本中涉及到的时间,通过正则和xk-time这个开源时间解析工具来达到时间解析的目的。
AggregateTypeParser则是通过正则的方式来识别文本中涉及到的聚合算子意图词。
AgentCheckParser用于校验,判断当前生成的候选Query是否满足当前所选Agent的限定范围。
QueryTypeParser
QueryTypeParser主要做了两件事情。第一件事情是为每个规则类型的Query根据SemanticParseInfo中的结构体信息生成S2SQL,便于后续解析执行,第二件事情是根据S2SQL中的字段来为Query划分模式。划分规则如下:
ID模式: 主要用于ID查询实体的场景,如果S2SQL中含有对主键(在创建数据源的时候指定的主键并配置了实体别名)的过滤就为ID模式
标签模式: 主要用于标签圈选实体的场景,如果where子句中的字段都为标签,则为标签模式
指标模式: 主要用于指标计算场景,可根据维度来进行过滤和下钻,若不满足以上两种模式,且S2SQL中包含指标,则为指标模式
4.PluginParser
Plugin注册
用户在页面上录入一个Plugin之后,会通过监听机制,注册到向量数据库。由于向量库的文本ID不可重复,因此注册的时候需要根据系统内置的计算方式,为Plugin的每个预设问题分配一个唯一ID,从而方便在进行召回的时候,根据预设问题的ID反向计算得到Plugin的ID。
PluginParser
PluginParser是一个插件召回的抽象类,它定义了召回插件的基本流程和构造Query等一些公共方法。主要流程如下:
-
首先判断前置条件,Embedding和FunctionCall的前置条件各不相同,因此定义为抽象模板方法。
-
进行Plugin召回,Embedding和FunctionCall的召回方式也各不相同,因此也定义为抽象模板方法。
-
若有满足条件的Plugin成功被召回,则构造为一个Query
EmbeddingRecallParser
EmbeddingRecallParser是PluginParser的其中一种实现,主要定义了EmbeddingRecall所需要的前置条件和具体召回实现。
a. 前置条件:系统中当前助理下可被召回的Plugin不为空。
b. 具体召回实现:
-
把用户输入的文本去请求向量数据库。
-
从向量数据库召回多个(召回数量可配置)跟用户输入文本相似的Plugin预设问题,并按相似度进行排序。
-
按上面Plugin注册提到的ID计算方式,根据预设问题的ID反向计算得到插件的ID。
-
对召回的Plugin进行校验,由于每个Plugin在页面配置的时候设定了必要的参数,因此需要把SchemaMapInfo中识别到的Schema Element和Plugin的参数进行比对,以判断当前识别到的Element能否构成Plugin的参数。假如在Plugin定义中,name这个维度的值是这个召回所必要的参数,那么就需要判断SchemaMapInfo是否包含name这个维度的值,如Alice。
-
选中一个满足参数条件的且相似度最高的Plugin。
FunctionCallParser
FunctionCallParser是PluginParser的另一种实现,它和EmbeddingRecallParser相比略有不同,主要不同点在于FunctionCall的方式耗时较长,成本较高且无需注册,因此前置条件和召回方式都略有不同。
a. 前置条件: 系统中当前助理下可被召回的Plugin不为空且暂时还没有满足SatisfactionChecker条件的Query。
b. 具体召回实现:
-
获取到当前助理下所有可被用于召回的插件,并进行参数校验。
-
若校验之后只剩一个插件,则直接返回这个插件,否则把多个Plugin拼装FunctionCall结构体,请求FunctionCall。
-
得到返回结果,由于FunctionCall只会返回唯一一个结果,因此直接选中返回。
三、Corrector功能
1.作用
经过规则/LLM解析生成的S2SQL(尤其是LLM解析),常常出现不符合语义、信息展示不全、日期缺失等问题;Semantic Corrector主要功能是对S2SQL进行修正,使查询结果更符合用户要求。
2.分类
SuperSonic中的Corrector有以下几类,SchemaCorrector、SelectCorrector、WhereCorrector、GroupByCorrector、HavingCorrector;
3.SchemaCorrector
由于大模型生成的S2SQL,在字段名、维度值、以及聚合函数等上生产错误;该Corrector主要作用是修正在Schema相关侧的S2SQL的错误;
a.字段名修正
例如,S2SQL中,访问者 = 'alice' 和访问日期 >= '2023-11-04' ;传给大模型的filedNameList中,不存在字段名"访问者"、"访问日期";因此,需要将S2SQL中的字段"访问者"修正为"用户名",字段"访问日期"修正为"数据日期";
b.维度值修正
例如,访问者 = 'alice' 中的alice,模型有时候会生成错误的维度值如"Alice"等;S2QL的维度值,将导致无法查询到准确的数据;因此,需要将S2SQL中的维度值进行修正;
c.修正策略
目前的修正策略主要是利用hanlp来进行维度值修正,通过文本相似度来修正字段名;目前正在实现通过语义来修正字段名和维度值;
4.SelectCorrector
该corrector主要作用是将一些S2SQL Where条件中的字段一起展示出来,提升用户体验;如"按部门统计,访问次数大于10次部门有哪些",大模型生成的S2SQL:
SELECT 部门 FROM 超音数 GROUP BY 部门 HAVING SUM(访问次数) > 10
该查询只会将字段"部门"查出,通常用户需要知道超过10次的"部门"以及对应"访问次数"具体是多少;主要实现原理是通过jsqlparser框架将group by、order by涉及的字段,加入到select中;
5.WhereCorrector
①添加"数据日期"字段过滤:如前面的输入"按部门统计,访问次数大于10次部门有哪些",返回的S2SQL并没有加上"数据日期"过滤,默认会全表扫描;数据日期添加的逻辑如下:
- 判断此次查询是查询模式:指标模式or标签模式;
- 如果是指标模式,则获取问答设置中的指标模式时间范围;如果没有配置或配置的是负数,则不进行"数据日期"添加;
- 如果是标签模式,则获取问答设置中的标签模式时间范围,其他同b。
②解析相对时间:对于相对时间识别,大模型识别比较复杂;SuperSonic实现逻辑是:将相对时间currentDate加入到prompt中,并采用datediff来表达相对时间。
因此该Corrector的另外一个功能是,将约定的相对时间转换为数据库能执行的时间;
1、最近3天:datediff('day', 数据日期, '2022-11-06') <= 3
转换为:数据日期 <="2022-11-06" and 数据日期 >="2022-11-04"
2、最近12个月:datediff('month', 数据日期, '2022-11-06') <= 12
3、最近3年:datediff('year', 数据日期, '2022-11-06') <= 3
③添加QueryFilter限定条件
在QueryReq中有一个queryFilters参数,主要是在某些场景,需要将已知的限定条件带到后端;如:已经明确识别到了某些条件,需要将这些限定条件加入到查询中;因此,该功能主要利用jsqlparser将限定条件加入到S2SQL Where条件中;
6.GroupByCorrector
该corrector主要作用是对大模型生成的S2SQL的group by部分进行修正;以文本-"访问次数最高的部门"举例说明,大模型生成的S2SQL: SELECT 部门 FROM 超音数 WHERE 数据日期 = '2023-11-27' ORDER BY 访问次数 DESC LIMIT 1,不符合用户查询语言,没有对部门进行group by;
1、大模型生成的S2SQL
SELECT 部门 FROM 超音数 WHERE 数据日期 = '2023-11-27' ORDER BY 访问次数 DESC LIMIT 1
2、经过SchemaCorrector
SELECT 部门 FROM 超音数 WHERE 数据日期 = '2023-11-27' ORDER BY 访问次数 DESC LIMIT 1
3、经过SelectCorrector
SELECT 部门, 数据日期, 访问次数 FROM 超音数 WHERE 数据日期 = '2023-11-27' ORDER BY 访问次数 DESC LIMIT 1
4、经过WhereCorrector
SELECT 部门, 数据日期, 访问次数 FROM 超音数 WHERE 数据日期 = '2023-11-27' ORDER BY 访问次数 DESC LIMIT 1
5、经过GroupByCorrector
SELECT 部门, 数据日期, SUM(访问次数) FROM 超音数用户部门 WHERE 数据日期 = '2023-11-27' GROUP BY 部门, 数据日期 ORDER BY SUM(访问次数) DESC LIMIT 1
可以看到经过GroupByCorrector后,我们增加了对"部门"和"数据日期"的group by操作,并且是对指标进行SUM后排序;最终结果是符合预期;
7.HavingCorrector
该corrector主要作用是对大模型生成的S2SQL的Having部分进行修正;以文本-"访问次数大于10次的部门有哪些"举例说明,大模型生成的S2SQL:SELECT 部门 FROM 超音数 WHERE 访问次数 > 10;没有按照group by进行聚合,并且指标过滤也需要通过having才可行;
1、大模型生成的S2SQL
SELECT 部门 FROM 超音数 WHERE 访问次数 > 10
2、经过SchemaCorrector
SELECT 部门 FROM 超音数 WHERE 访问次数 > 10
3、经过SelectCorrector
SELECT 部门 FROM 超音数 WHERE 访问次数 > 10
4、经过WhereCorrector
SELECT 部门 FROM 超音数 WHERE (访问次数 > 10) AND 数据日期 = '2023-11-20'
5、经过GroupByCorrector
SELECT 部门 FROM 超音数用户部门 WHERE (SUM(访问次数) > 10) AND 数据日期 = '2023-11-20' GROUP BY 部门
6、经过HavingCorrector
SELECT 部门, SUM(访问次数) FROM 超音数用户部门 WHERE 数据日期 = '2023-11-20' GROUP BY 部门 HAVING SUM(访问次数) > 10
可以看到经过HavingCorrector后,在HAVING中增加了"SUM(访问次数) > 10 "过滤,满足了用户查询语义;