Vector vs Parquet/SQL Decision
Not every question needs meaning. The wrong index wastes money and lies politely. Choose per question.
▶ Watch this reelWhat you'll learn
- Structured questions
- Semantic questions
- Hybrid architecture
- Anti-patterns
Remember this
- SQL owns exactness: aggregates, filters, joins — computed answers that must be complete
- Vectors own meaning: similarity, intent, retrieval — ranked answers where 'close enough' is correct
- Hybrid architecture with a router (or agent tool-calling); never vectorize exact data
Structured questions → SQL
- Aggregates, exact filters, joins, time-series/reporting.
- Answers must be COMPLETE and EXACT — similarity is a category error.
- Tools: Postgres / SQL Server / DuckDB over Parquet (Stage 3).
Semantic questions → vectors
- Find-similar, dedupe, intent/concept search, RAG retrieval.
- Answers are ranked, approximate, top-k — 'close enough' is correct.
- Tools: pgvector / Qdrant / etc. (GA-09–GA-11).
Hybrid architecture
- Router (classifier or agent tool-calling) → SQL leg and/or vector leg → fuse.
- Commonest real pattern: vectors fetch candidates → SQL applies exact filters.
Anti-patterns
- ❌ Vectorizing exact data (SKUs, ledgers) — neighbors are wrong answers.
- ❌ Paying embedding+index costs for questions SQL answers free.
- ✅ The one-question test: is an approximate ranked answer acceptable? No → SQL. Yes → vectors. Both → hybrid.
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.