Vector vs Parquet/SQL Decision

Not every question needs meaning. The wrong index wastes money and lies politely. Choose per question.

▶ Watch this reel

What you'll learn

  1. Structured questions
  2. Semantic questions
  3. Hybrid architecture
  4. Anti-patterns

Remember this

Structured questions → SQL

Semantic questions → vectors

Hybrid architecture

Anti-patterns

Code: The routing query layer — SQL leg, vector leg, one interface

from typing import Protocol

class QueryEngine(Protocol):
    def run(self, question: str, ctx: dict) -> Answer: ...

class SqlEngine:
    """Structured questions — DuckDB/SQL over the warehouse."""
    def run(self, question, ctx):
        sql = llm_to_sql(question, schema=WAREHOUSE_SCHEMA)   # text-to-SQL,
        rows = duckdb.sql(sql, params=ctx.get("params"))       # validated before exec
        return Answer(kind="table", data=rows.df())

class VectorEngine:
    """Semantic questions — top-k retrieval."""
    def run(self, question, ctx):
        hits = retriever.search(embed(question), k=20,
                                where=ctx.get("filters"))
        return Answer(kind="docs", data=[h.payload["text"] for h in hits])

ROUTER = [("structured", SqlEngine()), ("semantic", VectorEngine())]

def answer(question: str, ctx: dict) -> Answer:
    kind = classify(question)          # cheap classifier or LLM routing
    engine = dict(ROUTER)[kind]
    result = engine.run(question, ctx)
    if kind == "semantic" and ctx.get("must_filter"):
        # hybrid: vector candidates + exact SQL filtering
        return sql_filter(result, ctx["must_filter"])
    return result

# The agent version (Stage 6): the LLM picks sql_query vs vector_search
# as TOOLS — same two engines, routed by the model.