跳转到内容

文本转SQL指南(查询引擎 + 检索器)

这是LlamaIndex文本转SQL功能的基础指南。

  1. 我们首先展示如何在一个玩具数据集上执行文本到SQL转换:这将执行“检索”(对数据库的SQL查询)和“合成”。
  2. 然后我们展示如何在模式上构建一个TableIndex,以便在查询时动态检索相关表格。
  3. 接下来,我们将展示如何使用查询时行与列检索器来增强文本到SQL的上下文。
  4. 最后我们将向您展示如何单独定义一个文本到SQL检索器。

NOTE: Any Text-to-SQL application should be aware that executing arbitrary SQL queries can be a security risk. It is recommended to take precautions as needed, such as using restricted roles, read-only databases, sandboxing, etc.

如果您在 Colab 上打开这个笔记本,您可能需要安装 LlamaIndex 🦙。

%pip install llama-index-core llama-index-llms-openai llama-index-embeddings-openai
import os
import openai
os.environ["OPENAI_API_KEY"] = "sk-.."
# import logging
# import sys
# logging.basicConfig(stream=sys.stdout, level=logging.INFO)
# logging.getLogger().addHandler(logging.StreamHandler(stream=sys.stdout))
from IPython.display import Markdown, display

我们使用 sqlalchemy,一个流行的SQL数据库工具包,来创建一个空的 city_stats

from sqlalchemy import (
create_engine,
MetaData,
Table,
Column,
String,
Integer,
select,
)
engine = create_engine("sqlite:///:memory:")
metadata_obj = MetaData()
# create city SQL table
table_name = "city_stats"
city_stats_table = Table(
table_name,
metadata_obj,
Column("city_name", String(16), primary_key=True),
Column("population", Integer),
Column("country", String(16), nullable=False),
)
metadata_obj.create_all(engine)

我们首先定义我们的 SQLDatabase 抽象(一个围绕 SQLAlchemy 的轻量级封装)。

from llama_index.core import SQLDatabase
from llama_index.llms.openai import OpenAI
llm = OpenAI(temperature=0.1, model="gpt-4.1-mini")
sql_database = SQLDatabase(engine, include_tables=["city_stats"])

我们向SQL数据库添加一些测试数据。

from sqlalchemy import insert
sql_database = SQLDatabase(engine, include_tables=["city_stats"])
rows = [
{"city_name": "Toronto", "population": 2930000, "country": "Canada"},
{"city_name": "Tokyo", "population": 13960000, "country": "Japan"},
{
"city_name": "Chicago",
"population": 2679000,
"country": "United States",
},
{
"city_name": "New York",
"population": 8258000,
"country": "United States",
},
{"city_name": "Seoul", "population": 9776000, "country": "South Korea"},
{"city_name": "Busan", "population": 3334000, "country": "South Korea"},
]
for row in rows:
stmt = insert(city_stats_table).values(**row)
with engine.begin() as connection:
cursor = connection.execute(stmt)
# view current table
stmt = select(
city_stats_table.c.city_name,
city_stats_table.c.population,
city_stats_table.c.country,
).select_from(city_stats_table)
with engine.connect() as connection:
results = connection.execute(stmt).fetchall()
print(results)
[('Toronto', 2930000, 'Canada'), ('Tokyo', 13960000, 'Japan'), ('Chicago', 2679000, 'United States'), ('New York', 8258000, 'United States'), ('Seoul', 9776000, 'South Korea'), ('Busan', 3334000, 'South Korea')]

我们首先展示如何执行原始SQL查询,该查询直接在表上执行。

from sqlalchemy import text
with engine.connect() as con:
rows = con.execute(text("SELECT city_name from city_stats"))
for row in rows:
print(row)
('Busan',)
('Chicago',)
('New York',)
('Seoul',)
('Tokyo',)
('Toronto',)

第一部分:文本转SQL查询引擎

Section titled “Part 1: Text-to-SQL Query Engine”

一旦我们构建了SQL数据库,就可以使用NLSQLTableQueryEngine来构建自然语言查询,这些查询会被合成为SQL查询。

请注意,我们需要指定此查询引擎要使用的表。 如果不指定,查询引擎将拉取所有模式上下文,这可能会 超出LLM的上下文窗口限制。

from llama_index.core.query_engine import NLSQLTableQueryEngine
query_engine = NLSQLTableQueryEngine(
sql_database=sql_database, tables=["city_stats"], llm=llm
)
query_str = "Which city has the highest population?"
response = query_engine.query(query_str)
display(Markdown(f"<b>{response}</b>"))

东京在所有城市中拥有最高的人口,人口数量为13,960,000。

该查询引擎适用于任何可以预先指定要查询的表的情况,或者所有表结构加上提示其余部分的总大小符合您的上下文窗口限制的情况。

第二部分:文本转SQL的查询时表格检索

Section titled “Part 2: Query-Time Retrieval of Tables for Text-to-SQL”

如果我们事先不知道要使用哪个表,并且表结构的总大小超出了上下文窗口的容量,我们应该将表结构存储在索引中,以便在查询时能够检索到正确的结构。

我们可以通过使用 SQLTableNodeMapping 对象来实现这一点,该对象接收一个 SQLDatabase 并为每个传入 ObjectIndex 构造函数的 SQLTableSchema 对象 生成一个 Node 对象。

from llama_index.core.indices.struct_store.sql_query import (
SQLTableRetrieverQueryEngine,
)
from llama_index.core.objects import (
SQLTableNodeMapping,
ObjectIndex,
SQLTableSchema,
)
from llama_index.core import VectorStoreIndex
from llama_index.core.embeddings.openai import OpenAIEmbedding
# set Logging to DEBUG for more detailed outputs
table_node_mapping = SQLTableNodeMapping(sql_database)
table_schema_objs = [
(SQLTableSchema(table_name="city_stats"))
] # add a SQLTableSchema for each table
obj_index = ObjectIndex.from_objects(
table_schema_objs,
table_node_mapping,
VectorStoreIndex,
embed_model=OpenAIEmbedding(model="text-embedding-3-small"),
)
query_engine = SQLTableRetrieverQueryEngine(
sql_database, obj_index.as_retriever(similarity_top_k=1)
)

现在我们可以使用我们的SQLTableRetrieverQueryEngine并查询它以获取响应。

response = query_engine.query("Which city has the highest population?")
display(Markdown(f"<b>{response}</b>"))

东京在所有城市中拥有最高的人口,人口数量为13,960,000。

# you can also fetch the raw result from SQLAlchemy!
response.metadata["result"]
[('Tokyo', 13960000)]

您还可以为定义的每个表模式添加额外的上下文信息。

# manually set context text
city_stats_text = (
"This table gives information regarding the population and country of a"
" given city.\nThe user will query with codewords, where 'foo' corresponds"
" to population and 'bar'corresponds to city."
)
table_node_mapping = SQLTableNodeMapping(sql_database)
table_schema_objs = [
(SQLTableSchema(table_name="city_stats", context_str=city_stats_text))
]

当提出类似“美国有多少个城市?”这样的问题时,会出现一个挑战。在这种情况下,生成的查询可能仅查找国家列为“US”的城市,可能会漏掉标记为“United States”的条目。为了解决这个问题,您可以应用查询时行检索、查询时列检索或两者的组合。

在查询时行检索中,我们对每个表的行进行嵌入,从而为每个表生成一个索引。

from llama_index.core.schema import TextNode
with engine.connect() as connection:
results = connection.execute(stmt).fetchall()
city_nodes = [TextNode(text=str(t)) for t in results]
city_rows_index = VectorStoreIndex(
city_nodes, embed_model=OpenAIEmbedding(model="text-embedding-3-small")
)
city_rows_retriever = city_rows_index.as_retriever(similarity_top_k=1)
city_rows_retriever.retrieve("US")
[NodeWithScore(node=TextNode(id_='8ae10176-afd8-40ee-a97b-b24f66235489', embedding=None, metadata={}, excluded_embed_metadata_keys=[], excluded_llm_metadata_keys=[], relationships={}, metadata_template='{key}: {value}', metadata_separator='\n', text="('Chicago', 2679000, 'United States')", mimetype='text/plain', start_char_idx=None, end_char_idx=None, metadata_seperator='\n', text_template='{metadata_str}\n\n{content}'), score=0.7843469586763699)]

然后,可以将每个表的行检索器提供给 SQLTableRetrieverQueryEngine。

rows_retrievers = {
"city_stats": city_rows_retriever,
}
query_engine = SQLTableRetrieverQueryEngine(
sql_database,
obj_index.as_retriever(similarity_top_k=1),
rows_retrievers=rows_retrievers,
)

在查询过程中,行检索器用于识别与输入查询语义最相似的行。这些检索到的行随后作为上下文被整合,以提升文本到SQL生成的性能。

response = query_engine.query("How many cities are in the US?")
display(Markdown(f"<b>{response}</b>"))

根据 city_stats 表中的数据,美国有 2 个城市。

虽然查询时行检索增强了文本到SQL的生成能力,但它会对每一行单独进行嵌入处理,即使许多行包含重复值(例如分类数据中的值)也是如此。这可能导致令牌使用效率低下和不必要的开销。此外,在具有大量列的表中,检索器可能仅呈现部分相关值,可能会遗漏其他对准确生成查询至关重要的值。

为解决此问题,可以使用查询时列检索方法。该方法对选定列中的每个不同值建立索引,为表中的每一列创建独立的索引。

city_cols_retrievers = {}
for column_name in ["city_name", "country"]:
stmt = select(city_stats_table.c[column_name]).distinct()
with engine.connect() as connection:
values = connection.execute(stmt).fetchall()
nodes = [TextNode(text=t[0]) for t in values]
column_index = VectorStoreIndex(
nodes, embed_model=OpenAIEmbedding(model="text-embedding-3-small")
)
column_retriever = column_index.as_retriever(similarity_top_k=1)
city_cols_retrievers[column_name] = column_retriever

然后,可以将每个表的列检索器提供给 SQLTableRetrieverQueryEngine。

cols_retrievers = {
"city_stats": city_cols_retrievers,
}
query_engine = SQLTableRetrieverQueryEngine(
sql_database,
obj_index.as_retriever(similarity_top_k=1),
rows_retrievers=rows_retrievers,
cols_retrievers=cols_retrievers,
llm=llm,
)

在查询过程中,列检索器用于识别与输入查询语义最相似的列值。这些检索到的值随后作为上下文融入,以提升文本到SQL生成的性能。

response = query_engine.query("How many cities are in the US?")
display(Markdown(f"<b>{response}</b>"))

美国有2个城市。

第四部分:文本转SQL检索器

Section titled “Part 4: Text-to-SQL Retriever”

目前我们的文本转SQL功能封装在一个查询引擎中,包含检索和合成两部分。

你可以单独使用SQL检索器。我们向你展示一些可以尝试的不同参数,并演示如何将其接入我们的RetrieverQueryEngine来获得大致相同的结果。

from llama_index.core.retrievers import NLSQLRetriever
# default retrieval (return_raw=True)
nl_sql_retriever = NLSQLRetriever(
sql_database, tables=["city_stats"], llm=llm, return_raw=True
)
results = nl_sql_retriever.retrieve(
"Return the top 5 cities (along with their populations) with the highest population."
)
from llama_index.core.response.notebook_utils import display_source_node
for n in results:
display_source_node(n)

节点ID: f640a54f-7413-4dc0-9135-cd63c7ca8f45
相似度:
文本: [(‘东京’, 13960000), (‘首尔’, 9776000), (‘纽约’, 8258000), (‘釜山’, 3334000), (‘多伦多’, …

# default retrieval (return_raw=False)
nl_sql_retriever = NLSQLRetriever(
sql_database, tables=["city_stats"], return_raw=False
)
results = nl_sql_retriever.retrieve(
"Return the top 5 cities (along with their populations) with the highest population."
)
# NOTE: all the content is in the metadata
for n in results:
display_source_node(n, show_source_metadata=True)

节点ID: 05c61a90-598e-4c29-a6b4-b27f2579819e
相似度:
文本:
元数据: {‘city_name’: ‘Tokyo’, ‘population’: 13960000}

节点ID: c7f5fc4c-9754-4946-92c6-54a0d2b40fd9
相似度:
文本:
元数据: {‘city_name’: ‘Seoul’, ‘population’: 9776000}

节点ID: 3a00e201-f3b5-430e-af0e-aa4c34a71131
相似度:
文本:
元数据: {‘city_name’: ‘New York’, ‘population’: 8258000}

节点ID: ee911f7f-8aae-4bad-a52d-c0bdfab63942
相似度:
文本:
元数据: {‘city_name’: ‘Busan’, ‘population’: 3334000}

节点ID: dca6b482-52e4-41e0-992f-a58109e6f3f6
相似度:
文本:
元数据: {‘city_name’: ‘Toronto’, ‘population’: 2930000}

我们将SQL检索器与标准的RetrieverQueryEngine组合以合成响应。结果大致类似于我们封装的Text-to-SQL查询引擎。

from llama_index.core.query_engine import RetrieverQueryEngine
query_engine = RetrieverQueryEngine.from_args(nl_sql_retriever, llm=llm)
response = query_engine.query(
"Return the top 5 cities (along with their populations) with the highest population."
)
print(str(response))
The top 5 cities with the highest populations are:
1. Tokyo - 13,960,000
2. Seoul - 9,776,000
3. New York - 8,258,000
4. Busan - 3,334,000
5. Toronto - 2,930,000