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