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

news2025/6/27 19:21:41

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.coloradmin.cn/o/1822574.html

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

相关文章

JAVA动态表达式:Antlr4 表达式树解析

接上面 JAVA动态表达式&#xff1a;Antlr4 G4 模板 读取字符串表达式结构树-CSDN博客 目前已经实现了常量及分组常规表达式的解析。 String formula "啦啦啦1 and 11 and 23 and 1123 contains 1 and 23455 notcontains 5"; String formula "啦啦啦1 and (…

基于JSP技术的电子商城系统

开头语&#xff1a; 你好&#xff0c;我是计算机学长码农猫哥。如果你对电子商城系统感兴趣或有相关开发需求&#xff0c;欢迎联系我。 开发语言&#xff1a;Java 数据库&#xff1a;MySQL 技术&#xff1a;JSP技术 工具&#xff1a;Eclipse、Tomcat 系统展示 首页 管理…

算法007:三数之和

. - 力扣&#xff08;LeetCode&#xff09;. - 备战技术面试&#xff1f;力扣提供海量技术面试资源&#xff0c;帮助你高效提升编程技能,轻松拿下世界 IT 名企 Dream Offer。https://leetcode.cn/problems/3sum/ 这个题相较于前几个题来说比较难&#xff0c;思想是前面一个题目…

【MySQL】mysql中常见的内置函数(日期、字符串、数学函数)

文章目录 案例表日期函数字符串函数数学函数其他函数 案例表 emp students 表 exam_result 表 日期函数 注意current_time和now的区别 案例一&#xff1a; 创建一张表用来记录生日&#xff0c;表结构如下 添加日期&#xff1a; insert tmp (birthday) values (2003-01-3…

Matrix->Matrix工具类获取Matrix的平移、缩放、错切数值

// 传入矩阵&#xff0c;获取矩阵数值 class MatrixValues(matrix: Matrix) {val scaleX: Floatval scaleY: Floatval transX: Floatval transY: Floatval skewX : Float val skewY : Floatinit {val fromValues FloatArray(9)matrix.getValues(fromValues)// 缩放数值scaleX …

1、MFC应用程序框架

MFC简介 MFCMFC样式 MFC应用程序框架单文档应用程序框架工程文件的组成结构 MFC应用程序框架分析SDK应用程序和MFC应用程序运行过程对比MFC应用程序框架主要类之间的关系 MFC消息映射机制概述消息消息映射机制Windows消息分类消息映射表 添加消息处理函数各种Windows消息的消息…

Linux 系统删除乱码文件

项目场景&#xff1a; 通过rm -rf 删除乱码文件&#xff0c;删除不了 问题描述 这时直接使用命令rm -rf 是删除不了的。只能通过删除 inode方法处理。 原因分析&#xff1a; 在Linux上传文件或文件夹时&#xff0c;由于出现连接中断&#xff0c;出现了大量的乱码文件&#…

顶顶通呼叫中心中间件(mod_cti基于FreeSWITCH)-通话时长限制

文章目录 前言联系我们场景运用机器人场景普通通话场景 前言 顶顶通呼叫中心中间件限制通话时长有两种写法&#xff0c;分别作用于机器人场景与普通通话场景。 普通场景可分为分机互打、分机外呼手机等。 联系我们 有意向了解呼叫中心中间件的用户&#xff0c;可以点击该链接…

Sping源码(九)—— Bean的初始化(非懒加载)— lookupMethod标签

序言 在继续深入Spring的对象创建流程之前&#xff0c;这篇文章先简单介绍一下lookupMethod标签的用法及作用。 准备的xml 自定义名为methodOverride.xml的配置文件。 <?xml version"1.0" encoding"UTF-8"?> <beans xmlns"http://www.s…

旅行者1号有什么秘密?飞行240多亿公里,为什么没发生碰撞?

旅行者1号有什么秘密&#xff1f;飞行240多亿公里&#xff0c;为什么没发生碰撞&#xff1f; 自古以来&#xff0c;人类就对浩瀚无垠的宇宙充满了好奇与向往。从最初的仰望星空&#xff0c;到如今的深空探测&#xff0c;人类探测宇宙的历史发展可谓是一部波澜壮阔的史诗。 在…

【Shopee】计算虾皮订单的各项支出和订单收入计算方法

虾皮订单成交截图 基础条件&#xff1a; 商品金额&#xff1a;11.92 [4x2.98] 商品原价&#xff1a;7.5 商品折后价&#xff1a;2.98 商品数量&#xff1a;4 优惠券与回扣&#xff1a; 店铺优惠券&#xff08;减10%&#xff09;&#xff1a;1.2 [11.92x10% 四舍五入了] 订单实…

基于软件在环的飞控机建模仿真

安全关键系统&#xff08;Safety-Critical System&#xff0c;SCS&#xff09;是指由于某些行为或组合行为能够引发整体系统失效&#xff0c;继而导致财物损失、人员受伤等严重影响的系统&#xff0c;诸多安全关键领域如航空航天、核电系统、医疗设备、交通运输等领域的系统都属…

redis 笔记2之哨兵

文章目录 一、哨兵1.1 简介1.2 实操1.2.1 sentinel.conf1.2.2 问题1.2.3 哨兵执行流程和选举原理1.2.4 使用建议 一、哨兵 1.1 简介 上篇说了复制&#xff0c;有个缺点就是主机宕机之后&#xff0c;从机只会原地待命&#xff0c;并不能升级为主机&#xff0c;这就不能保证对外…

Redis之线程IO模型

引言 Redis是个单线程程序&#xff01;这点必须铭记。除了Redis之外&#xff0c;Node.js也是单线程&#xff0c;Nginx也是单线程&#xff0c;但是他们都是服务器高性能的典范。 Redis单线程为什么能够这么快&#xff01; 因为他所有的数据都在内存中&#xff0c;所有的运算都…

❤ npm运行打包报错归纳

❤ 前端运行打包报错归纳 &#xff08;安装依赖&#xff09;Cannot read property ‘pickAlgorithm’ of null" npm uninstall //删除项目下的node_modules文件夹 npm cache clear --force //清除缓存后 npm install //重新安装 备用安装方式 npm install with --for…

英格索兰IC12D3A1AWS-A控制器过热维修

在现代工业生产中&#xff0c;拧紧控制器作为一种自动控制工具&#xff0c;被广泛应用于汽车、航空、电子等领域。然而&#xff0c;在使用过程中&#xff0c;可能会出现IngsollRang拧紧控制器过热故障&#xff0c;影响生产效率和产品质量。 【拧紧设备维修】【英格索兰IngsollR…

初始化三板斧 - centos7

1、关闭防火墙、关闭SELinux ① 立即关闭防火墙 systemctl stop firewalld ② 设置开机关闭防火墙 systemctl disable firewalld ③ 立即关闭SELinxu setenforce 0 ④ 设置开机关闭SELinux 将SELINUXenforcing 修改替换为 SELINUXdisabled vim /etc/selinux/config se…

M41T11M6F串行实时时钟-国产兼容RS4C411

RS4C411是一款低功耗串行实时时钟&#xff08;RTC&#xff09;&#xff0c;具有56字节的NVRAM。内置32.768 kHz振荡器&#xff08;外部晶体控制&#xff09;和RAM的前8字节用于时钟/日历功能&#xff0c;并以二进制编码十进制&#xff08;BCD&#xff09;格式配置。地址和数据通…

汽车金属管检测新方法,分度盘高速视觉检测机检测效果如何?

汽车金属管是指在汽车制造和维修中广泛使用的金属管道&#xff0c;用于传输流体、气体或其他介质。汽车金属管在汽车中扮演着重要的角色&#xff0c;用于传输液体&#xff08;如燃油、冷却液、润滑油&#xff09;、气体&#xff08;如空气、排气&#xff09;、制动系统、液压系…

利用three-csg-ts对做物体交互式挖洞

默认物体均为居中&#xff0c;如果指定位置没有发生偏移&#xff0c;可能是因为在执行布尔操作之前没有正确设置变换。确保在进行布尔运算之前应用所有必要的变换。以下是经过修正的完整代码示例&#xff0c;它会确保圆柱正确旋转并与盒子进行 CSG 操作。 安装依赖 首先&…