使用LlamaIndex + DuckDB的SQL查询引擎
本指南展示了使用DuckDB的核心LlamaIndex SQL功能。
我们介绍一些核心的LlamaIndex数据结构,包括NLSQLTableQueryEngine和SQLTableRetrieverQueryEngine。
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-readers-wikipedia!pip install llama-index!pip install duckdb duckdb-engineimport loggingimport sys
logging.basicConfig(stream=sys.stdout, level=logging.INFO)logging.getLogger().addHandler(logging.StreamHandler(stream=sys.stdout))from llama_index.core import SQLDatabase, SimpleDirectoryReader, Documentfrom llama_index.readers.wikipedia import WikipediaReaderfrom llama_index.core.query_engine import NLSQLTableQueryEnginefrom llama_index.core.indices.struct_store import SQLTableRetrieverQueryEnginefrom IPython.display import Markdown, display使用我们的 NLSQLTableQueryEngine 实现基础文本转SQL
Section titled “Basic Text-to-SQL with our NLSQLTableQueryEngine”在这个初始示例中,我们将演示如何使用一些测试数据点填充SQL数据库,并通过我们的文本转SQL功能进行查询。
创建数据库架构 + 测试数据
Section titled “Create Database Schema + Test Data”我们使用流行的SQL数据库工具包sqlalchemy连接至DuckDB,并创建一个空的city_stats数据表。随后我们使用一些测试数据填充该表。
from sqlalchemy import ( create_engine, MetaData, Table, Column, String, Integer, select, column,)engine = create_engine("duckdb:///:memory:")# uncomment to make this work with MotherDuck# engine = create_engine("duckdb:///md:llama-index")metadata_obj = MetaData()# create city SQL tabletable_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)# print tablesmetadata_obj.tables.keys()dict_keys(['city_stats'])我们向 city_stats 表中引入一些测试数据
from sqlalchemy import insert
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": "Seoul", "population": 9776000, "country": "South Korea"},]for row in rows: stmt = insert(city_stats_table).values(**row) with engine.begin() as connection: cursor = connection.execute(stmt)with engine.connect() as connection: cursor = connection.exec_driver_sql("SELECT * FROM city_stats") print(cursor.fetchall())[('Toronto', 2930000, 'Canada'), ('Tokyo', 13960000, 'Japan'), ('Chicago', 2679000, 'United States'), ('Seoul', 9776000, 'South Korea')]创建 SQLDatabase 对象
Section titled “Create SQLDatabase Object”我们首先定义我们的SQLDatabase抽象(一个围绕SQLAlchemy的轻量级封装)。
from llama_index.core import SQLDatabasesql_database = SQLDatabase(engine, include_tables=["city_stats"])/Users/jerryliu/Programming/gpt_index/.venv/lib/python3.10/site-packages/duckdb_engine/__init__.py:162: DuckDBEngineWarning: duckdb-engine doesn't yet support reflection on indices warnings.warn(这里我们展示 NLSQLTableQueryEngine 的功能,它能够实现文本到SQL的转换。
- 我们构建一个
NLSQLTableQueryEngine并传入我们的 SQL 数据库对象。 - 我们对查询引擎运行查询。
query_engine = NLSQLTableQueryEngine(sql_database)response = query_engine.query("Which city has the highest population?")INFO:llama_index.indices.struct_store.sql_query:> Table desc str: Table 'city_stats' has columns: city_name (VARCHAR), population (INTEGER), country (VARCHAR) and foreign keys: .> Table desc str: Table 'city_stats' has columns: city_name (VARCHAR), population (INTEGER), country (VARCHAR) and foreign keys: .
/Users/jerryliu/Programming/gpt_index/.venv/lib/python3.10/site-packages/langchain/sql_database.py:238: UserWarning: This method is deprecated - please use `get_usable_table_names`. warnings.warn(
INFO:llama_index.token_counter.token_counter:> [query] Total LLM token usage: 332 tokens> [query] Total LLM token usage: 332 tokensINFO:llama_index.token_counter.token_counter:> [query] Total embedding token usage: 0 tokens> [query] Total embedding token usage: 0 tokensstr(response)' Tokyo has the highest population, with 13,960,000 people.'response.metadata{'result': [('Tokyo', 13960000)], 'sql_query': 'SELECT city_name, population \nFROM city_stats \nORDER BY population DESC \nLIMIT 1;'}使用我们的 SQLTableRetrieverQueryEngine 实现高级文本到SQL转换
Section titled “Advanced Text-to-SQL with our SQLTableRetrieverQueryEngine”在本指南中,我们处理这样一种场景:您的数据库中有大量表格,将所有表结构放入提示中可能会导致文本转SQL提示溢出。
我们首先使用我们的 ObjectIndex 对模式进行索引,然后在其基础上使用我们的 SQLTableRetrieverQueryEngine 抽象层。
engine = create_engine("duckdb:///:memory:")# uncomment to make this work with MotherDuck# engine = create_engine("duckdb:///md:llama-index")metadata_obj = MetaData()# create city SQL tabletable_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),)all_table_names = ["city_stats"]# create a ton of dummy tablesn = 100for i in range(n): tmp_table_name = f"tmp_table_{i}" tmp_table = Table( tmp_table_name, metadata_obj, Column(f"tmp_field_{i}_1", String(16), primary_key=True), Column(f"tmp_field_{i}_2", Integer), Column(f"tmp_field_{i}_3", String(16), nullable=False), ) all_table_names.append(f"tmp_table_{i}")
metadata_obj.create_all(engine)# insert dummy datafrom sqlalchemy import insert
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": "Seoul", "population": 9776000, "country": "South Korea"},]for row in rows: stmt = insert(city_stats_table).values(**row) with engine.begin() as connection: cursor = connection.execute(stmt)sql_database = SQLDatabase(engine, include_tables=["city_stats"])from llama_index.core.indices.struct_store import SQLTableRetrieverQueryEnginefrom llama_index.core.objects import ( SQLTableNodeMapping, ObjectIndex, SQLTableSchema,)from llama_index.core import VectorStoreIndextable_node_mapping = SQLTableNodeMapping(sql_database)
table_schema_objs = []for table_name in all_table_names: table_schema_objs.append(SQLTableSchema(table_name=table_name))
obj_index = ObjectIndex.from_objects( table_schema_objs, table_node_mapping, VectorStoreIndex,)INFO:llama_index.token_counter.token_counter:> [build_index_from_nodes] Total LLM token usage: 0 tokens> [build_index_from_nodes] Total LLM token usage: 0 tokensINFO:llama_index.token_counter.token_counter:> [build_index_from_nodes] Total embedding token usage: 6343 tokens> [build_index_from_nodes] Total embedding token usage: 6343 tokens使用 SQLTableRetrieverQueryEngine 查询索引
Section titled “Query Index with SQLTableRetrieverQueryEngine”query_engine = SQLTableRetrieverQueryEngine( sql_database, obj_index.as_retriever(similarity_top_k=1),)response = query_engine.query("Which city has the highest population?")INFO:llama_index.token_counter.token_counter:> [retrieve] Total LLM token usage: 0 tokens> [retrieve] Total LLM token usage: 0 tokensINFO:llama_index.token_counter.token_counter:> [retrieve] Total embedding token usage: 7 tokens> [retrieve] Total embedding token usage: 7 tokensINFO:llama_index.indices.struct_store.sql_query:> Table desc str: Table 'city_stats' has columns: city_name (VARCHAR), population (INTEGER), country (VARCHAR) and foreign keys: .> Table desc str: Table 'city_stats' has columns: city_name (VARCHAR), population (INTEGER), country (VARCHAR) and foreign keys: .INFO:llama_index.token_counter.token_counter:> [query] Total LLM token usage: 337 tokens> [query] Total LLM token usage: 337 tokensINFO:llama_index.token_counter.token_counter:> [query] Total embedding token usage: 0 tokens> [query] Total embedding token usage: 0 tokensresponseResponse(response=' The city with the highest population is Tokyo, with a population of 13,960,000.', source_nodes=[], metadata={'result': [('Tokyo', 13960000)], 'sql_query': 'SELECT city_name, population \nFROM city_stats \nORDER BY population DESC \nLIMIT 1;'})