使用Spring AI 和 LLM 实现数据库查询

AIDocumentLibraryChat 项目已扩展为支持提问来搜索关系数据库。用户可以输入一个问题,然后嵌入搜索相关的数据库表和列来回答问题。然后,LLM 获取相关表的数据库架构,并根据找到的表和列生成一个 SQL 查询,来展示结果回答问题。

数据集和元数据

使用的开源数据集有 6 个表,彼此之间有关系。它包含有关博物馆和艺术品的数据。为了获得有用的问题查询,必须为数据集提供元数据,并且必须在嵌入中转换元数据。

数据集和元数据

为了使 LLM 能够找到所需的表和列,它需要知道它们的名称和描述。对于像 museum 表这样的所有数据表,元数据都存储在 column_metadata 和 table_metadata 表中。它们的数据可以在以下文件中找到: column_metadata.csv 和 table_metadata.csv。它们包含表或列的唯一 ID、名称、描述等。该描述用于创建与问题嵌入进行比较的嵌入。描述的质量对结果有很大的影响,因为更好的描述会使嵌入更精确。提供同义词是提高质量的一种选择。表元数据包含表的模式,以便仅向 LLM 提示符添加相关的表模式。

嵌入

为了在 Postgresql 中存储嵌入,使用了向量扩展。可以使用 OpenAI 端点或 Spring AI 提供的 ONNX 库创建嵌入。创建了三种类型的嵌入:

  • Tabledescription嵌入
  • Columndescription嵌入
  • Rowcolumn嵌入

Tabledescription 嵌入有一个基于表描述的向量,嵌入有 tablename、datatype = table 和元数据中的元数据 id。
Columndescription 嵌入有一个基于列描述的向量,嵌入有表名、带列名的数据名、datatype = column 和元数据中的元数据 id。

Rowcolumn 嵌入有一个基于内容行列值的向量。用于美术作品的样式或主题,以便能够使用问题中的值。元数据具有datatype = row、作为 dataname 的列名、表名和元数据 id。

实现搜索

搜索有 3 个步骤:

  1. 检索嵌入
  2. 创建提示
  3. 执行查询并返回结果

检索嵌入

为了从具有向量扩展的 Postgresql 数据库中读取嵌入,Spring AI 使用 DocumentVSRepositoryBean 中的 VectorStore 类:

@Override
public List<Document> retrieve(String query, DataType dataType) {return this.vectorStore.similaritySearch(SearchRequest.query(query).withFilterExpression(new Filter.Expression(ExpressionType.EQ,new Key(MetaData.DATATYPE), new Value(dataType.toString()))));
}

VectorStore 为用户的查询提供相似性搜索。查询在嵌入中转换,并在头值中使用用于数据类型的FilterExpression 返回结果。

 TableService 类在 retrieveEmbeddings 方法中使用存储库:

private EmbeddingContainer retrieveEmbeddings(SearchDto searchDto) {var tableDocuments = this.documentVsRepository.retrieve(searchDto.getSearchString(), MetaData.DataType.TABLE, searchDto.getResultAmount());var columnDocuments = this.documentVsRepository.retrieve(searchDto.getSearchString(), MetaData.DataType.COLUMN,searchDto.getResultAmount());List<String> rowSearchStrs = new ArrayList<>();if(searchDto.getSearchString().split("[ -.;,]").length > 5) {var tokens = List.of(searchDto.getSearchString().split("[ -.;,]"));		for(int i = 0;i<tokens.size();i = i+3) {rowSearchStrs.add(tokens.size() <= i + 3 ? "" : tokens.subList(i, tokens.size() >= i +6 ? i+6 :      tokens.size()).stream().collect(Collectors.joining(" ")));}}var rowDocuments = rowSearchStrs.stream().filter(myStr -> !myStr.isBlank())  .flatMap(myStr -> this.documentVsRepository.retrieve(myStr, MetaData.DataType.ROW, searchDto.getResultAmount()).stream()).toList();return new EmbeddingContainer(tableDocuments, columnDocuments, rowDocuments);
}

首先,documentVsRepository 用于根据用户的搜索字符串检索带有表/列嵌入的文档。然后,将搜索字符串分成6个单词的块,以搜索具有行嵌入的文档。行嵌入只是一个单词,为了获得低距离,查询字符串必须很短;否则,由于查询中的所有其他单词,距离会增加。然后使用块来检索带有嵌入的行文档。

创建提示词

提示词是通过 createPrompt 方法在 TablesService 类中创建的:

private Prompt createPrompt(SearchDto searchDto, EmbeddingContainer documentContainer) {final Float minRowDistance = documentContainer.rowDocuments().stream().map(myDoc -> (Float) myDoc.getMetadata().getOrDefault(MetaData.DISTANCE,  1.0f)).sorted().findFirst().orElse(1.0f);LOGGER.info("MinRowDistance: {}", minRowDistance);var sortedRowDocs = documentContainer.rowDocuments().stream().sorted(this.compareDistance()).toList();var tableColumnNames = this.createTableColumnNames(documentContainer);List<TableNameSchema> tableRecords = this.tableMetadataRepository.findByTableNameIn(tableColumnNames.tableNames()).stream().map(tableMetaData -> new TableNameSchema(tableMetaData.getTableName(), tableMetaData.getTableDdl())).collect(Collectors.toList());final AtomicReference<String> joinColumn = new AtomicReference<String>("");final AtomicReference<String> joinTable = new AtomicReference<String>("");final AtomicReference<String> columnValue = new AtomicReference<String>("");sortedRowDocs.stream().filter(myDoc -> minRowDistance <= MAX_ROW_DISTANCE).filter(myRowDoc -> tableRecords.stream().filter(myRecord ->  myRecord.name().equals(myRowDoc.getMetadata().get(MetaData.TABLE_NAME))).findFirst().isEmpty()).findFirst().ifPresent(myRowDoc -> {joinTable.set(((String) myRowDoc.getMetadata().get(MetaData.TABLE_NAME)));joinColumn.set(((String) myRowDoc.getMetadata().get(MetaData.DATANAME)));tableColumnNames.columnNames().add(((String) myRowDoc.getMetadata().get(MetaData.DATANAME)));columnValue.set(myRowDoc.getContent());this.tableMetadataRepository.findByTableNameIn(List.of(((String) myRowDoc.getMetadata().get(MetaData.TABLE_NAME)))).stream().map(myTableMetadata -> new TableNameSchema(myTableMetadata.getTableName(),myTableMetadata.getTableDdl())).findFirst().ifPresent(myRecord -> tableRecords.add(myRecord));});var messages = createMessages(searchDto, minRowDistance, tableColumnNames, tableRecords, joinColumn, joinTable, columnValue);Prompt prompt = new Prompt(messages);return prompt;
}

首先,过滤掉 rowDocuments 的最小距离。然后创建一个按距离排序的文档列表行。
方法 createTableColumnNames(…) 创建包含一组列名和一个表名列表的 tableColumnNames 记录。tableColumnNames 记录是通过首先筛选距离最小的 3 个表来创建的。然后过滤掉这些表中距离最小的列。

然后通过使用 TableMetadataRepository 将表名映射到模式 DDL 字符串来创建表记录。

然后对已排序的行文档进行 MAX_ROW_DISTANCE 过滤,并设置 joinColumn、joinTable 和columnValue 值。然后使用 TableMetadataRepository 创建 TableNameSchema 并将其添加到tableRecords 中。

现在可以设置 systemPrompt 中的占位符和可选的 columnMatch:

private final String systemPrompt = """ 
...
Include these columns in the query: {columns} \n
Only use the following tables: {schemas};\n
%s \n
""";
private final String columnMatch = """ 
Join this column: {joinColumn} of this table: {joinTable} where the column has this value: {columnValue}\n
""";

方法 createMessages(…) 获取用来替换 {columns} 占位符的列集。它获取 tableRecords,用表的 ddl 替换 {schemas} 占位符。如果行距离低于阈值,则在字符串占位符%s处添加属性columnMatch。然后替换占位符 {joinColumn}、{joinTable} 和 {columnValue}。

有了关于所需列的信息、包含这些列的表的模式和行匹配的可选连接的信息,LLM 就能够创建一个合理的 SQL 查询。

执行查询并返回结果

查询在以下方法 createQuery(...) 中执行:

public SqlRowSet searchTables(SearchDto searchDto) {EmbeddingContainer documentContainer = this.retrieveEmbeddings(searchDto);Prompt prompt = createPrompt(searchDto, documentContainer);String sqlQuery = createQuery(prompt);LOGGER.info("Sql query: {}", sqlQuery);SqlRowSet rowSet = this.jdbcTemplate.queryForRowSet(sqlQuery);return rowSet;
}

首先,调用准备数据和创建 SQL 查询的方法,然后使用 queryForRowSet(…) 在数据库上执行查询。返回 SqlRowSet。
TableMapper 类使用 map(…) 方法将结果转换为 TableSearchDto 类:

public TableSearchDto map(SqlRowSet rowSet, String question) {List<Map<String, String>> result = new ArrayList<>();while (rowSet.next()) {final AtomicInteger atomicIndex = new AtomicInteger(1);Map<String, String> myRow = List.of(rowSet.getMetaData().getColumnNames()).stream().map(myCol -> Map.entry(this.createPropertyName(myCol, rowSet, atomicIndex),Optional.ofNullable(rowSet.getObject(atomicIndex.get())).map(myOb -> myOb.toString()).orElse(""))).peek(x -> atomicIndex.set(atomicIndex.get() + 1)).collect(Collectors.toMap(myEntry -> myEntry.getKey(), myEntry -> myEntry.getValue()));result.add(myRow);}		return new TableSearchDto(question, result, 100);
}

首先,创建结果映射的结果列表。然后,对每行迭代 rowSet,以创建列名作为键、列值作为值的映射。这样可以灵活地返回列的数量及其结果。createPropertyName(…) 将索引整数添加到映射键中,以支持重复的键名。

展示

后端

Spring AI 非常支持创建具有灵活占位符数量的提示。创建嵌入和查询向量表也得到了很好的支持。

获取合理的查询结果需要必须为列和表提供的元数据。创建良好的元数据是一项随列和表的数量线性扩展的工作。为需要它们的列实现嵌入是一项额外的工作。

结果是,像 OpenAI 或 Ollama 这样具有“sqlcoder:70b-alpha-q6_K”模型的 LLM 可以回答以下问题:“显示艺术品名称和具有现实主义风格和肖像主题的博物馆名称。

LLM 可以在边界内回答与元数据有一定契合度的自然语言问题。对于一个免费的 OpenAI 帐户来说,所需的嵌入量太大了,而“sqlcoder:70b-alpha-q6_K”是最小的模型,结果合理。

LLM 提供了一种与关系数据库交互的新方法。在开始为数据库提供自然语言接口的项目之前,必须考虑工作量和预期结果。

LLM 可以帮助解决中小型复杂度的问题,用户应该对数据库有一定的了解。

前端

后端返回的结果是以键为列名和值为列值的映射列表。返回的映射条目的数量是未知的,因此显示结果的表必须支持灵活数量的列。示例 JSON 结果如下所示:

{"question":"...","resultList":[{"1_name":"Portrait of Margaret in Skating Costume","2_name":"Philadelphia Museum of Art"},{"1_name":"Portrait of Mary Adeline Williams","2_name":"Philadelphia Museum of Art"},{"1_name":"Portrait of a Little Girl","2_name":"Philadelphia Museum of Art"}],"resultAmount":100}

resultList 属性包含一个带有属性键和值的 JavaScript 对象数组。为了能够在 Angular Material Table 组件中显示列名和值,使用了这些属性:

protected columnData: Map<string, string>[] = [];
protected columnNames = new Set<string>();

 table-search.component.ts 的 getColumnNames(…) 方法用于在属性中转换JSON结果:

private getColumnNames(tableSearch: TableSearch): Set<string> {const result = new Set<string>();this.columnData = [];const myList = !tableSearch?.resultList ? [] : tableSearch.resultList;myList.forEach((value) => {const myMap = new Map<string, string>();Object.entries(value).forEach((entry) => {result.add(entry[0]);myMap.set(entry[0], entry[1]);});this.columnData.push(myMap);});return result;
}

首先,创建结果集,并将 columnData 属性设置为空数组。然后,创建 myList 并使用 forEach(…)迭代。对于 resultList 中的每个对象,将创建一个新的 Map。对于对象的每个属性,将创建一个新条目,以属性名作为键,以属性值作为值。在columnData 映射上设置条目,并将属性名称添加到结果集中。将完成的映射推入 columnData 数组,返回结果并设置为 columnNames 属性。

然后在 columnNames 集中可以得到一组列名,在 columnData 中可以得到一个从列名到列值的映射。

模板 table-search.component.html 包含 material 表:

@if(searchResult && searchResult.resultList?.length) {
<table mat-table [dataSource]="columnData"><ng-container *ngFor="let disCol of columnNames" matColumnDef="{{ disCol }}"><th mat-header-cell *matHeaderCellDef>{{ disCol }}</th><td mat-cell *matCellDef="let element">{{ element.get(disCol) }}</td></ng-container><tr mat-header-row *matHeaderRowDef="columnNames"></tr><tr mat-row *matRowDef="let row; columns: columnNames"></tr>
</table>
}

首先,在 resultList中 检查 searchResult 是否存在和对象。然后,使用 columnData 映射的数据源创建表。表头行设置为 <tr mat-header-row *matHeaderRowDef="columnNames"></tr> 以包含columnNames。表的行和列是用 <tr mat-row *matRowDef="let row;列:columnNames " > < / tr >。

  • 单元格是通过迭代 columnname 来创建的: <ng-container *ngFor="let disCol of columnNames" matColumnDef="{{disCol}}">。
  • 标题单元格创建: <th mat-header-cell *matHeaderCellDef>{{disCol}}</th>。
  • 表格单元格是创建: <td mat-cell *matCellDef="let element">{{element.get(disCol)}}</td>。element 是 columnData 数组元素的映射,使用element.get(disCol)检索映射值。

总结

在 LLM 的帮助下质疑数据库需要对元数据进行一些努力,并且对数据库包含的内容有一个粗略的了解。AI/LLM 不适合创建查询,因为 SQL 查询需要正确性。需要一个相当大的模型来获得所需的查询正确性,并且需要 GPU 加速才能进行生产性使用。

设计良好的 UI,用户可以在其中拖放结果表中的表列,这可能是满足要求的不错选择。Angular Material Components 很好地支持拖放。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/bicheng/26438.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Beyond Compare 提示“缺少评估信息或损坏”,无法打开只要操作一行命令就可以了

在CMD 或者powershell下执行如下命令重新打开即可。 reg delete "HKEY_CURRENT_USER\Software\Scooter Software\Beyond Compare 4" /v CacheID /f重新打开&#xff0c;就ok 了

express入门03增删改查

目录 1 搭建服务器2 静态文件托管3 引入bootstrap4 引入jquery5 编写后端接口5.1 添加列表查询方法5.2 添加路由5.3 添加数据表格 总结 我们前两篇介绍了如何利用express搭建服务器&#xff0c;如何实现静态资源托管。那利用这两篇的知识点&#xff0c;我们就可以实现一个小功能…

c++中main(int argc, char* argv[])参数详解

目录 一、main函数形式 1.无参数&#xff1a; 2.带有两个参数&#xff1a; 二、参数详解 1.int argc 2.char* argv[] 三、示例演示 一、main函数形式 在C中&#xff0c;main 函数可以有两种常见的参数形式&#xff1a; 1.无参数&#xff1a; 代码如下&#xff1a; i…

私域运营技术干货 | 基于精准用户分群的个性化智能外呼策略实践

智能外呼产品经过了近几年的发展&#xff0c;作为一种用户触达的手段&#xff0c;普及率越来越高。但是智能外呼产品本身的劣势就是客户黏性差&#xff0c;迁移成本低&#xff0c;导致市场竞争非常激烈&#xff0c;各家都是拼价格拼线路资源&#xff0c;同质化严重。如何建立云…

游戏报错steam_api.dll丢失怎么解决?steam_api.dll缺失的7种靠谱解决方法

steam_api.dll 是一个由 Valve Corporation 开发的动态链接库文件&#xff0c;专门用于其 Steam 游戏平台。这个文件是 Windows 操作系统下的一个重要组件&#xff0c;它确保了通过 Steam 平台发布的游戏能够正常运行&#xff0c;并且能够使用 Steamworks API 提供的各种功能。…

TIA博途Wincc_如何实现开机画面等待几秒后,自动跳转到主画面?

TIA博途Wincc_如何实现开机画面等待几秒后,自动跳转到主画面? 想要实现的功能: 上电开机后,在开机画面等待几秒后,自动跳转到主画面, 如下图所示,新建一个项目后,添加一个开机画面和主画面 如下图所示,在HMI变量中添加一个int型变量BitTime, 如下图所示,设置该变量…

怎么把pdf格式文件其中几页单独弄出来

在现代办公和学习环境中&#xff0c;pdf格式的文件因其跨平台兼容性和良好的保持原样特性而备受欢迎。然而&#xff0c;有时我们可能只需要pdf文件中的某几页&#xff0c;而不是整个文件。这时&#xff0c;将PDF文件中的特定页面单独提取出来就显得尤为重要。 搜索一下&#xf…

IDEA 高效插件工具

文章目录 LombokMaven Helper 依赖冲突any-rule(正则表达式插件)快速生成javadocGsonFormat (Aits) 将json解析成类Diagrams使用 类图SequenceDiagram时序图GenerateAllSetter&#xff08;AltEnter&#xff09;大小写转写String ManipulationGitToolBox 代码提交人activate-pow…

Flutter- AutomaticKeepAliveClientMixin 实现Widget保持活跃状态

前言 在 Flutter 中&#xff0c;AutomaticKeepAliveClientMixin 是一个 mixin&#xff0c;用于给 State 类添加能力&#xff0c;使得当它的内容滚动出屏幕时仍能保持其状态&#xff0c;这对于 TabBarView 或者滚动列表中使用 PageView 时非常有用&#xff0c;因为这些情况下你…

诊所管理系统如何重塑患者就医流程

随着信息技术的快速发展&#xff0c;诊所管理系统的应用正在为医疗服务带来革命性的变化。这一系统不仅仅是一种管理工具&#xff0c;更是一种全方位的健康管理解决方案&#xff0c;从诊前、诊中到诊后&#xff0c;为患者提供了一系列便捷、高效的服务&#xff0c;让患者的就医…

信息收集---网站目录和CMS指纹识别

一. 网站目录收集 1. 常见网站敏感文件 网站的备份文件/数据库备份文件 wwwroot.zip Db.zip 后台登陆的目录 manage login 安装包&#xff08;源码&#xff09; 上传的目录uploads mysql的管理界面 phpmyadmin 程序的安装路径 2. Dirb 工具 工具介绍 dirb 是一款用…

ICC2:如何获取get_xx -filter后可用的属性有哪些?

我正在「拾陆楼」和朋友们讨论有趣的话题&#xff0c;你⼀起来吧&#xff1f; 拾陆楼知识星球入口 report_attribute -app -class cell $instname 这种直接告诉你指定cell有哪些属性&#xff0c;以及对应的值是什么 或者直接用list_attribute也可以 list_attribute -help可以…

积累和消耗,人生本质的两件事

人生的本质其实就两件事&#xff0c;消耗和积累。 纵观你身边所有的人&#xff0c;他们做的所有的事&#xff0c;基本都可以分为两类。 一、积累 二、消耗 比如说感情&#xff0c;在我们每一个人的青春回忆里&#xff0c;都或多或少有一段刻骨铭心的感情&#xff0c;有些人的感…

Linux进程间通信---使用【共享内存+信号量+消息队列】的组合来实现服务器进程与客户进程间的通信

IPC结合实现进程间通信实例 下面将使用【共享内存信号量消息队列】的组合来实现服务器进程与客户进程间的通信。 共享内存用来传递数据&#xff1b;信号量用来同步&#xff1b;消息队列用来 在客户端修改了共享内存后通知服务器读取。 server.c&#xff1a;服务端接收信息 …

如何解除内存卡的写保护并格式化为exFAT文件系统

最近有客户提问内存卡提示写保护&#xff0c;且无法格式化为exFAT格式的问题&#xff0c;可能是由于多种原因引起的。以下是一些可能的解决方法&#xff1a; 1. 检查物理写保护开关 一些SD卡和MicroSD卡适配器上有一个小的物理开关&#xff0c;可以启用或禁用写保护。确保这个…

C# WPF 读写CAN数据

C# WPF 读写CAN数据 CAN 分析仪 分析仪资料下载 官方地址&#xff1a;https://www.zhcxgd.com/1.html CSDN&#xff1a; 项目配置 复制Dll库文件 文件在上面的资料里面 设置不安全代码 CAN C#工具类 CAN_Tool.cs using Microsoft.VisualBasic; using System; using Sys…

MySQL 触发器(实验报告)

一、实验名称&#xff1a; 触发器 二、实验日期&#xff1a; 2024 年 6月 8日 三、实验目的&#xff1a; 掌握MySQL触发器的创建及调用&#xff1b; 四、实验用的仪器和材料&#xff1a; 硬件&#xff1a;PC电脑一台&#xff1b; 配置&#xff1a;内存&#xff0c;…

学习笔记丨嵌入式BI分析的12个关键功能

编者注&#xff1a;以下内容节选编译自嵌入式分析厂商Qrvey发表的《What is Embedded Analytics?》&#xff08;什么是嵌入式分析&#xff09;一文&#xff0c;作者为Qrvey产品市场主管Brian Dreyer。 什么是嵌入式分析&#xff1f; 嵌入式分析是指能够将数据分析的特性和功…

用ChatGPT 4o画漂亮的燃尽图代码

把代码给ChatGPT&#xff0c;然后他就会帮我生成出来了。 而且图是动态的&#xff0c;可以调整颜色文字之类的内容 # Given data for Sprint 5 Progress data_sprint_5 {User Story: [BEAN-40, BEAN-42, BEAN-41, BEAN-22, BEAN-33, BEAN-44, BEAN-10, BEAN-26, BEAN-37, BEA…

【SQL边干边学系列】07高级问题-3

文章目录 前言回顾高级问题41.逾期订单42.逾期订单-哪些员工&#xff1f;43.逾期订单与总订单相比44.逾期订单与总订单相比 - 丢失的员工45.逾期订单与总订单相比 - 修复null46.逾期订单与总订单之间的百分比47.逾期订单与总订单相比 - 修正decimal 答案41.逾期订单42.逾期订单…