将SpringAI与AI/LLM结合使用来查询关系数据库

智能真的很好说 2024-05-12 10:21:22
了解如何扩展 AIDocumentLibraryChat 项目以支持搜索关系数据库的问题。

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

数据集和元数据

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

要使 AI/LLM 能够找到所需的表和列,它需要知道它们的名称和描述。对于所有数据表(如博物馆表),元数据存储在 and 表中。他们的数据可以在以下文件中找到:column_metadata.csv 和 table_metadata.csv。它们包含表或列的唯一 ID、名称、说明等。该描述用于创建与问题嵌入进行比较的嵌入。描述的质量对结果有很大影响,因为嵌入更精确,描述更好。提供同义词是提高质量的一种选择。表元数据包含表的架构,用于仅将相关表架构添加到 AI/LLM 提示符。column_metadatatable_metadata

嵌入

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

Tabledescription嵌入Columndescription嵌入Rowcolumn嵌入

嵌入具有基于表描述的向量,嵌入具有表名、数据类型 = 表和元数据中的元数据 ID。Tabledescription

嵌入具有基于列描述的向量,嵌入具有表名、带有列名的 dataname、datatype = 列以及元数据中的元数据 id。Columndescription

嵌入具有基于内容行列值的向量。这用于艺术品的风格或主题,以便能够使用问题中的值。元数据的 datatype = row、列名为 dataname、tablename 和元数据 id。Rowcolumn

实现搜索

搜索有 3 个步骤:

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

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

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

为用户的查询提供相似性搜索。查询在嵌入中转换,并在标头值中使用 for the datatype,返回结果。VectorStoreFilterExpression

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

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

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

创建提示

提示符是在类中使用以下方法创建的:TableServicecreatePrompt

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

首先,滤除 的最小距离。然后,将创建按距离排序的文档列表行。rowDocuments

该方法创建包含一组列名和表名列表的记录。通过首先筛选距离最小的 3 个表来创建记录。然后,过滤掉这些表中距离最小的列。createTableColumnNames(...)tableColumnNamestableColumnNames

然后,通过使用 TableMetadataRepository 将表名映射到架构 DDL 字符串来创建 DDL 字符串。tableRecords

然后对排序后的行文档进行筛选,并设置值 、 和 。然后 用于创建一个并将其添加到 .MAX_ROW_DISTANCEjoinColumnjoinTablecolumnValueTableMetadataRepositoryTableNameSchematableRecords

现在,可以设置占位符和可选:systemPromptcolumnMatch

private final String systemPrompt = """ br...brInclude these columns in the query: {columns} \nbrOnly use the following tables: {schemas};\nbr%s \nbr""";brprivate final String columnMatch = """ brJoin this column: {joinColumn} of this table: {joinTable} where the column has this value: {columnValue}\nbr""";

该方法获取要替换占位符的列集。它可以将占位符替换为表的 DDL。如果行距低于阈值,则该属性将添加到字符串占位符处。然后替换占位符 、 和 。createMessages(...){columns}tableRecords{schemas}columnMatch%s{joinColumn}{joinTable}{columnValue}

有了有关所需列的信息、带有列的表的架构以及行匹配的可选联接信息,AI/LLM 能够创建合理的 SQL 查询。

执行查询并返回结果

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

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

首先,调用用于准备数据和创建 SQL 查询的方法,然后用于对数据库执行查询。返回。queryForRowSet(...)SqlRowSet

TableMapper 类使用该方法将结果转换为类:map(...)TableSearchDto

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

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

总结后端

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

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

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

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

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

AI/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}

该属性包含具有属性键和值的对象的 JavaScript 数组。为了能够在 Angular 材料表元件中显示列名和值,使用了以下属性:resultList

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

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

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

首先,创建结果集,并将属性设置为空数组。然后,创建并使用 进行迭代。对于 中的每个对象,将创建一个新对象。对于对象的每个属性,将创建一个新条目,其中属性名称为键,属性值为值。条目将在地图上设置,属性名称将添加到该集中。完成的映射被推送到数组上,并返回并设置为属性。columnDatamyListforEach(...)resultListMapcolumnDataresultcolumnDataresultcolumnNames

然后,集合中提供了一组列名,并且 .columnNamescolumnData

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

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

首先,检查 中是否存在 和 对象。然后,使用地图创建表。表头行设置为包含 .表的行和列是用 定义的。searchResultresultListdatasourcecolumnData<tr mat-header-row *matHeaderRowDef="columnNames"></tr>columnNames<tr mat-row *matRowDef="let row; columns: columnNames"></tr>

单元格是通过迭代如下方式创建的:.columnNames<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是数组元素的映射,映射值使用 检索。columnDataelement.get(disCol)总结前端

新的 Angular 语法使模板更具可读性。Angular Material 表格组件比预期的更灵活,并且很好地支持未知数量的列。

结论

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

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

在开始这样的项目之前,客户应该就哪种替代方案最适合要求做出明智的决定。

原文标题:Using Spring AI With AI/LLMs to Query Relational Databases

原文链接:https://dzone.com/articles/spring-ai-with-ai-llms-to-query-relational-databases

作者:Sven Loesekann

编译:LCR

0 阅读:6

智能真的很好说

简介:感谢大家的关注