Classic top-k retrieval cannot answer aggregate questions over structured data
(“average claim amount by region”, “how many claims were denied in 2024”).
The roadmap adds an aggregate query strategy that turns a question into a
query over the user’s tabular data.
The query text comes from model output. Model output is untrusted: it can be wrong, and it can be steered by prompt injection hidden in the documents or the question. The obvious shortcut, asking the model for pandas/Python code and running it, gives that untrusted output arbitrary code execution in the user’s process (file system, network, credentials in the environment).
Aggregation runs only as read-only SQL in an embedded DuckDB engine. ragsearch never executes model-generated Python, pandas expressions, or shell commands.
Guardrails, all enforced in library code rather than in the prompt:
read_only=True; DuckDB refuses to open the same file read-only while a
read-write connection to it is still open in the same process.enable_external_access = false, set memory_limit and threads
(guardrail 5), and last set lock_configuration = true so the query cannot
change any of them.
This blocks read_csv('/etc/...'), COPY ... TO, ATTACH, and HTTP/S3 reads.connection.extract_statements(sql);
reject anything that is not exactly one statement of type SELECT
(rejects SET, ATTACH, COPY, INSTALL, LOAD, CALL, DML/DDL).
DuckDB reports PRAGMA as SELECT, so after stripping SQL comments also
reject a leading PRAGMA keyword. DESCRIBE, SHOW, SUMMARIZE and
FROM x also report as SELECT and are allowed on purpose (read-only
schema inspection). This check is defence in depth; guardrails 1–2 are the
primary controls.connection.sql(query).limit(n) (default 1,000 rows), and report when the
limit was hit. Unlike wrapping the text as SELECT * FROM (<query>) LIMIT n,
this tolerates a trailing ; or -- comment in model output.connection.interrupt() from a timer
(default 10 s), plus memory_limit and threads settings on the query
connection.DuckDB becomes an optional dependency (e.g. ragsearch[structured]); without
it the aggregate strategy is unavailable and the planner does not route to it.
mode=ro, set_authorizer, attach
limits, extensions off by default), but it has no native Parquet reading
and weaker analytical SQL. Rejected on capability, not safety.Positive:
Trade-offs:
excel extension is
downloaded on first use), which conflicts with guardrail 2. Load Excel
through pandas at ingestion, as setup() does today.Before aggregate ships, tests must show that each of these is rejected or
contained: multiple statements, non-SELECT statements (COPY, ATTACH,
INSTALL, SET, PRAGMA), file-reading table functions (read_csv,
read_text), re-enabling configuration, runaway queries (timeout), and
oversized results (row limit).
Prototype check (DuckDB 1.4.5, guardrails 1, 2, 4, 5 applied): an aggregate
SELECT succeeded; read_csv, read_text, COPY ... TO, ATTACH,
INSERT, INSTALL, and SET enable_external_access=true were all refused;
interrupt() stopped a long query at the timeout; .limit(n) capped
results, including for queries ending in ; or a -- comment. extract_statements classified PRAGMA version as SELECT, hence
the extra keyword check in guardrail 3.