Project 2: BigQuery Part Deux

CS145: Stanford University, Fall 2026. Data Science Track.

Technical Memo: Build the Production Data Stack

Dear data scientists, Project 1 was the appetizer.

Real data work starts where Project 1 ended. Your team's BigQuery warehouse is real. The supplementary data isn't a clean CSV someone handed you, it lives somewhere else and needs a Spark job to land. The text columns aren't joinable by keyword alone, so you embed them. The model has to be tied to a decision a product manager will live with. Every layer is its own discipline, and in this project you touch all of them.

In Project 1 you queried BigQuery. In Project 2 you build the data stack a modern data team ships before lunch: Spark ELT, vector embeddings in BigQuery, a partitioned warehouse, and a model tied to a business question.

Mission: take your dataset and enrich it with the world's knowledge. Load and filter Wikipedia with Spark, embed the part that touches your domain in BigQuery, and combine semantic context with your existing analytics. By Nov 20 you have a portfolio piece that covers the full production stack from ingest to insight.

Project Scope

Estimated Work-Back Schedule

Proposal due Oct 30. Final due Nov 20. Five weeks of work, about 8 hours a week. Each week has a punchline; the rest is detail.

Week 1 · Land the production stack

Oct 17 to Oct 23 · ~8 hours · Phase 0

Spark loads a 1 GB Wikipedia slice into BigQuery next to your dataset. Filter to your domain, run one keyword join, parity-check against bigquery-public-data.samples.wikipedia. The proof of Phase 0 lands here.

Week 2 · Add a semantic layer

Oct 24 to Oct 30 · ~8 hours · Phase 1 + proposal

Embed your filtered slice in BigQuery with ML.GENERATE_EMBEDDING. Run one VECTOR_SEARCH query that finds neighbors your keyword join missed. Submit Phase 0 + Phase 1 output as your proposal by Oct 30.

Week 3 · Tell the analytic story

Oct 31 to Nov 6 · ~8 hours · Sections 1 to 3

Six non-simple queries, at least 2 using your Wiki layer. Three visualizations tied to specific queries. Train and evaluate your BQML model. Pull query plans and compute I/O costs at 10x and 100x scale.

Week 4 · Engineer for production

Nov 7 to Nov 13 · ~8 hours · Section 5 (tier 87+) + Section 6 (tier 93+)

Tier 87+: scale your fact layer to 5 GB+, design partition and cluster keys, build a materialized view, benchmark one query before and after. Tier 93+: embed 1 GB of Wikipedia text, build a real BQ vector index, run one hybrid keyword + semantic query, compare latency vs. brute-force.

Week 5 · Ship a capstone and a story

Nov 14 to Nov 20 · ~8 hours (Tier 100 capstone: +8) · Section 7 (tier 100) + Section 4 (all tiers)

Tier 100: build the capstone, a governed text-to-SQL agent on your warehouse, and measure how much your curation lifts a bare model. Everyone: write up the project, draft 2 or 3 AI anecdotes, record the 5-minute demo, submit to Gradescope.

Expected total: about 26 to 48 hours of work per person, depending on tier (about 8 hours a week solo). A team of 2 splits it: fewer hours each, or the same hours aimed a tier higher.

Tiers at a glance

Pick one row before you write the proposal. You can climb later, but the proposal asks which tier you are aiming for so partners (and we) know how to plan the work. Tier 87 is where the warehouse work starts paying off; Tier 93 is where the production retrieval layer becomes indexable; Tier 100 is the capstone.

Tier What you ship Dataset and BQ cost Effort (per person)
Tier 80 The base project: Spark ELT for a 1 GB Wikipedia slice, BQ embeddings, 6 queries, BQML model, scale math, writeup.
"The notebook you'd send to a hiring manager."
1 GB Wikipedia + your dataset. ~$2 to $8 of your $50 BQ credit. 26 to 30 hours. You write ~150-310 SQL + ~60-120 Python; the ~190-line substrate is given.
Tier 87
warehouse layout
+ Section 5: scale fact layer to ≥ 5 GB, design partition and cluster keys, build a materialized view, before/after benchmark.
"The stack you'd ship before lunch."
5+ GB combined fact layer. + ~$1. 31 to 35 hours. + ~50-80 Python LOC.
Tier 93
production retrieval
+ Section 6: scale embed pass to 1 GB of text, build a real BQ vector index, run a hybrid keyword + semantic query, compare indexed vs. brute-force latency.
"The real-scale retrieval layer."
~5 GB of vector data. + ~$8 to $15 for the embed pass. 36 to 40 hours. + ~30-60 Python LOC.
Tier 100
capstone
+ Section 7: the capstone. Build a governed text-to-SQL agent on the warehouse you built (a definitions file + a reference doc + an eval set), then measure bare vs. grounded accuracy. Your dataset shapes the eval questions (forecasting, geospatial, extraction, or scale).
"The portfolio piece you'd open the interview with."
Your dataset. + ~$1 to $5 (mostly agent calls, no 22 GB scan). 44 to 48 hours (+8 for the capstone). + ~100-170 Python LOC.
+5 bonus Awarded at Tier 93 or above for quality of storytelling: analysis depth, engineering judgment, and how clearly the project's insights come through. n/a Not LOC; the polish you put on the writeup.

Final score = tier points + bonus. Each tier is additive (Phase 0 + Phase 1 + Sections 1-4 = 80; + Section 5 = 87; + Section 6 = 93; + Section 7 = 100). Typical total BQ spend $5 to $30 of your $50 credit; running out should not be the bottleneck, understanding should be. Watch the query estimator before scanning > 1 GB.

Choosing between the two Project 2 options? In this project tiers are additive: each one adds a section of work. The Systems project (NanoMem) grades tiers the other way, as a scale ceiling: the same code on bigger data.

Why This Project Matters

Grading

Per-tier ceilings, effort, and cost are in Tiers at a glance above. This section is the quality side: which metrics you must report at every tier, and the rubric that scores each section. A polished Tier 87 project earns 87. A sloppy Tier 100 project earns less than a tight Tier 87 one, because section quality is graded on its own merit.

Metrics you will report

Bytes scanned, $/query
Engineering cost. For each of your complex queries, what did it scan and what did it cost? Reported via the BigQuery query estimator.
R² / accuracy / PR-AUC
Model quality. Use the metric that matches the problem. R² for regression. Accuracy for balanced classification. Precision and recall (or PR-AUC) for imbalanced classification.
R@K (capstone b)
Retrieval quality. For the hybrid retrieval capstone: did the right answer appear in your top K? Averaged over eval queries. Higher is better, 1.0 is perfect.
Filter hit rate
Wikipedia filter precision. Sample ~100 articles from your filtered slice; what fraction are actually in your domain? Reported as a paragraph in your writeup.

Quality rubric (100 points)

Each section has its rubric, its expected output, and a "what good looks like" inline in the template. Read the section header before you start.

SectionTitlePoints
Tier 80: required for everyone
Phase 0Spark + 1 GB Wikipedia slice + keyword join + proposal (Oct 30). Banked on time; forfeit if late.10
Phase 1Embed slice in BigQuery (≤ 100 MB text) + one semantic join query 10
16 non-simple queries (2+ using Wiki layer) + 3 visualizations20
2BQML model + evaluation15
3Scale math + query plans (10x and 100x I/O)10
4Writeup + AI takeaways + 5-min demo15
Tier 80 total 80
Tier 87 unlock
5Warehouse layout: ≥ 5 GB fact layer + partition + cluster + MV + before/after bench7
Tier 87 total 87
Tier 93 unlock (requires Section 5)
6Production retrieval at scale: 1 GB embed + BQ vector index + hybrid keyword + semantic query + latency comparison6
Tier 93 total 93
Tier 100 unlock (requires Section 6)
7Capstone: a governed text-to-SQL agent on your warehouse + the bare-vs-grounded measurement7
Tier 100 total 100

The capstone: the Analyst's Agent

One capstone, worth 7 points, the Section 7 rung of the tiers table above (hours are counted there, per person). This is the payoff of the whole SQL arc: you built the stack by hand in Module 1B, and Anthropic runs it as company infrastructure (bare model 21%, grounded ~95%). Now you build it on the warehouse you shipped in Tiers 80 to 93, and measure the lift yourself. The generator is a commodity and is not graded; your curation and the measured lift are.

What "bare vs grounded" means. One question, the same model, two ways to answer it:

"How many people actively listen to Taylor Swift?"

Bare (no vocabulary)Grounded (your vocabulary)
How it answers Guesses what "actively" means (played once? this week? five times?) and picks one silently. Composes your pinned terms: active_since(30) ∩ listener_of('Taylor Swift').
The number Confident, and a different number on the next run. Your number, the same on every run.

That gap is the whole deliverable. On Anthropic's own data, grounding took a bare model from 21% to ~95% accurate. You saw this worked out on the Spotify schema in Module 1B; the capstone is you measuring the same lift on your own dataset. The numbers below (6 to 8 definitions, about 10 questions) are just how much of that vocabulary and eval you build.

What you buildThe bar
1. The knowledge model
curation
definitions.sql: 6 to 8 business terms, each a BigQuery table function (CREATE TABLE FUNCTION listener_of(artist STRING) AS (...)), parameterized and callable by name. reference.md: the schema plus a join policy (which joins are allowed, which are traps) and every NULL-prone column. Seed both from your Project 1 schema write-up and your Sections 1 to 4 verified queries.
2. The agent
execution
A text-to-SQL loop that composes your table functions by name. You hand the model a manifest (the function names, parameters, and one-line descriptions) plus the schema and join policy as context, and tell it to compose the vocabulary first and drop to raw SQL only when nothing fits. Generate with ML.GENERATE_TEXT (zero setup) or any model API; the model is swappable and not graded.
3. The eval set
coverage
About 10 English business questions with known-correct ground truth (a verified query you trace). Generate the questions from your domain description, not your definitions, so they surface gaps. Span your definitions and include ≥ 2 gotcha questions (NULLs, no-activity rows, dedup), e.g. "average rating per user," which a bare model quietly gets wrong by counting the users who never rated.
4. The measurement
the deliverable
Run the agent bare (no knowledge model) vs. grounded on that question set; report both accuracies. Grounded must beat bare, or your writeup explains the failure mode. Then close the gap: failures become required curation, re-measure.

Your dataset shapes the questions. The eval set is where the old career narratives live: a time-series dataset should include forecast-style questions, a geographic one ST_DISTANCE-style questions, a text-heavy one extraction and semantic questions, a large one scale questions. Same capstone, your flavor.

Graded on curation, not the model. Coverage (fraction of the independent question set answered correctly, target ≥ 85%), lift (grounded minus bare), breadth (about 10 questions spanning your definitions, ≥ 2 gotchas), and reference-doc completeness (every join path and NULL-prone column documented). A thin knowledge model produces a small lift and a low score; that is the point.

Optional extension. Add a per-query adversarial-review pass that challenges the agent's assumptions, and report the accuracy, token, and latency tradeoff (Anthropic saw +6% accuracy for +32% tokens and +72% latency). This is not the graded bonus (that is the +5 storytelling award); it is a way to push the project further.

Library Policy and Honor Code

Core rule: use the production tools (Spark, BigQuery, BQML, ML.GENERATE_EMBEDDING, VECTOR_SEARCH). The point of this project is to build with the real stack, not from scratch. What you write yourself is the SQL, the model spec, the analytic logic, the capstone work, and the writeup.

Use AI freely to learn the concepts and unblock yourself. Ask Claude to explain how ML.GENERATE_EMBEDDING works. Ask it to debug a Spark schema mismatch. Paste a query plan and have it walk you through what each stage is doing. The intuition layer is exactly what AI is good at, and you should use it.

Write the analytic logic yourself. Your 6 queries, your filter design, your model specification, your capstone work, your writeup. Not because typing matters, but because writing them yourself is what tells you whether you actually understood the intuition you just absorbed. The line, in one sentence: if you could not explain your queries and your model to a TA in plain English, with the chat window closed, you crossed it.

You CAN use
  • PySpark on Colab for the Phase 0 ELT.
  • BigQuery + BQML for storage, queries, embeddings, models.
  • ML.GENERATE_EMBEDDING / VECTOR_SEARCH / ML.GENERATE_TEXT as the production primitives they are.
  • matplotlib, seaborn, geopandas, plotly for visualization.
  • pandas / pyarrow for moving data between Spark, BQ, and Colab.
  • scikit-learn or sentence-transformers if your capstone genuinely needs them (e.g., a custom calibration in capstone c).
  • BigQuery public datasets (including bigquery-public-data.samples.wikipedia) as a free parity check for your Phase 0 work.
You CANNOT use
  • Pre-built end-to-end pipelines. No "I just ran AutoML and it picked the model for me." You make the modeling choices.
  • AI-written analytic logic without comprehension. Your 6 queries are yours. If you cannot explain why each one is in your notebook, it does not count.
  • Pre-computed Cohere/HF Wikipedia embeddings. Compute your own slice embeddings in BQ so you understand the cost and shape.
  • Tableau, Looker, or other external BI tools as your primary visualization. Stay in the notebook so everything is reproducible.

Honor Code

You must follow the Stanford Honor Code:

Frequently Asked Questions

Getting Started

Q: What exactly carries over from Project 1?

You can reuse: your project overview, table descriptions, and setup code. Your Project 1 fact tables stay in their BigQuery dataset and become one half of every join in Project 2.

Must be new: all 6 queries, all 3 visualizations, your Wikipedia layer, your embeddings, and the BQML model. If you partner, choose one of the two Project 1 notebooks as your starting point; you do not merge both. If you start fresh, your new dataset still needs 3+ tables and at least 500 MB.

Q: What does the Oct 30 proposal look like?

The proposal IS Phase 0, worth 10 points. Submit by Oct 30 with Phase 0 actually working and the 10 points are yours; miss the deadline and Phase 0 forfeits. One markdown cell at the top of your notebook (with Phase 0 code cells run above it), covering:

  • Proof of Phase 0. Paste your Spark filter recipe (the keyword set or category prefix), the output of your one keyword join query, and one viz.
  • Filter precision. Sample 30-100 articles from your slice. What fraction are actually in your domain? One short paragraph.
  • What surprised you. One paragraph on what the join surfaced.
  • Plans. Target tier (80 / 87 / 100). If 100, which capstone (a-f). If partnering: who owns which section.

Q: Where are the rubric details?

Inline in the template Colab notebook. Each section has its rubric, its expected output, and a "what good looks like" cell. The summary on this page is the macro picture; the per-section details live in the template.

Phase 0: Spark + Wikipedia (all tiers)

Q: How do I run Spark on Colab?

The template's first cell installs a pinned PySpark and starts a local SparkSession. Colab's free CPU runtime is enough for the 1 GB filtered slice. Do not try to set up a Dataproc cluster, you do not need one and it would cost you credits you don't have.

Q: How do I filter Wikipedia to my domain?

Three recipes the template ships, pick one and adapt:

  • Title-regex / keyword set. List 20 to 50 keywords for your domain. Spark filters articles whose title or first paragraph matches. Cheap and obvious.
  • Category prefix. Wikipedia articles ship with a categories array. Filter on prefixes like Category:Medicine, Category:Companies_of_*. Higher precision than keyword.
  • Link-graph hop. Start from a seed set of articles you know are in-domain, follow their outbound links one hop. Highest recall, fiddliest to debug.

The template includes four worked recipes (FHIR, NYC taxi, finance, music) you can copy and edit. Report your filter's hit rate (sampled) in 1-3 sentences.

Q: What does the Phase 0 keyword join look like for my domain?

Three examples to anchor on. Same shape, different domain:

  • FHIR / healthcare: for each of your top-20 patient conditions, count Wikipedia articles whose title or first paragraph mentions the condition name. LEFT JOIN wiki_domain w ON LOWER(w.title) LIKE CONCAT('%', LOWER(c.condition_name), '%'). The chart is condition coverage from highest to lowest.
  • NYC taxi / urban: for each NYC neighborhood you see in your trip data, join to Wikipedia articles about that neighborhood. Plot trip pickup count against article length as a proxy for cultural prominence.
  • Finance / public companies: for each ticker in your dataset, look up the article whose title matches the company name. Pull the infobox industry field. Aggregate analytics by inferred industry.

Q: Colab dies in the middle of my Wikipedia download. Help.

Two mitigations. First, checkpoint to Drive: write the filtered Parquet to a mounted Drive folder before doing the BQ load. If the session dies, you don't restart the whole pipeline. Second, filter early: apply your keyword filter inside the Spark read, not after. The full 22 GB doesn't need to materialize in RAM, Spark can stream-filter it.

Q: How do I parity-check my Phase 0 join?

Run the same title-keyword query against bigquery-public-data.samples.wikipedia (a public 314M-row Wikipedia revision sample with a title column, no upload required). Counts should agree within sampling tolerance once you align on title case and keyword set. If they disagree wildly, your filter is wrong before you spend a week on analysis. The template ships this parity helper.

Phase 1: Embeddings in BigQuery (all tiers)

Q: How much does Phase 1 actually cost?

It depends on how much text you embed. ML.GENERATE_EMBEDDING at text-embedding-005 charges roughly $0.025 per 1k characters. Honest numbers for a typical filtered slice:

  • 30K articles × ~500 chars (first paragraph): ~$0.40
  • 100K articles × ~500 chars: ~$1.25
  • 100K articles × full text (~3k chars): ~$7.50

Default rule: embed first paragraphs only, cap at ~100 MB of text. Total typical cost: $1 to $3. If you want to embed more, the FAQ math is right there; you make the call.

Q: What's the minimum SQL to embed and query?

Four statements. Template ships them; this is the shape:

-- 1. Register the remote embedding model
CREATE OR REPLACE MODEL `myproj.cs145_p2.embed_model`
REMOTE WITH CONNECTION `myproj.us.vertex_ai_connection`
OPTIONS(ENDPOINT = 'text-embedding-005');

-- 2. Embed your filtered slice (input table must have a column named `content`)
CREATE OR REPLACE TABLE `myproj.cs145_p2.wiki_domain_embedded` AS
SELECT
  article_id, title, url, first_paragraph,
  ml_generate_embedding_result AS embedding
FROM ML.GENERATE_EMBEDDING(
  MODEL `myproj.cs145_p2.embed_model`,
  (SELECT article_id, title, url, first_paragraph,
          first_paragraph AS content
   FROM `myproj.cs145_p2.wiki_domain`)
)
WHERE ml_generate_embedding_status = '';

-- 3. Index it (optional for slices < 5 GB; brute-force search works at small scale)
CREATE OR REPLACE VECTOR INDEX wiki_idx
ON `myproj.cs145_p2.wiki_domain_embedded`(embedding)
OPTIONS(distance_type = 'COSINE', index_type = 'IVF');

-- 4. Query it semantically. VECTOR_SEARCH returns query.*, base.*, distance.
SELECT query.query_text, base.title, base.url, distance
FROM VECTOR_SEARCH(
  TABLE `myproj.cs145_p2.wiki_domain_embedded`, 'embedding',
  (SELECT
     'my query text' AS query_text,
     ml_generate_embedding_result AS embedding
   FROM ML.GENERATE_EMBEDDING(
     MODEL `myproj.cs145_p2.embed_model`,
     (SELECT 'my query text' AS content))),
  top_k => 5, distance_type => 'COSINE');

Q: What does the Phase 1 semantic join look like for my domain?

The shape is always: embed your query strings, run VECTOR_SEARCH against the embedded Wiki slice, join the top-K hits back to your dataset. Three concrete forms:

  • FHIR / healthcare: for each top-20 condition in your cohort, return the 3 Wikipedia articles closest in embedding space. Common surprise: the keyword join hits "diabetes mellitus" but the semantic join also surfaces "insulin resistance" and "metabolic syndrome," because the embedding model knows they cluster.
  • NYC taxi / urban: for each high-traffic neighborhood, return the 3 articles most semantically similar to its zone description. The semantic join catches articles that don't say the neighborhood name but describe the same vibe (transit hub, nightlife district, business corridor).
  • Finance / public companies: for each ticker's industry description, return the 3 closest Wikipedia articles. Useful for sector enrichment when the infobox-industry field is missing or stale.

Document what your semantic join found that the Phase 0 keyword join missed. That delta is the lesson.

Q: My semantic join returns nonsense. What's wrong?

Three things to check, in order. First: did your filter produce articles actually in your domain? Run the filter precision sample again. Garbage in, garbage out. Second: are you embedding the same field you're searching? If you embedded first_paragraph but your query text is a single keyword, cosine similarity won't reward you. Third: does your query text match the corpus register? Short keyword queries against long descriptive paragraphs is the classic mismatch.

Q: Should I build a CREATE VECTOR INDEX at Phase 1?

No. BQ vector indexes require ~5 GB of vector data minimum, which a 100 MB-text Phase 1 slice is well short of. Brute-force VECTOR_SEARCH is the right call at this scale. Tier 93 (Section 6) is where you scale the embed pass up enough to build a real index.

Section 1: SQL + visualizations (all tiers)

Q: What counts as a "non-simple" query?

Combines 2+ of {JOINs, CTEs, window functions, subqueries, complex CASE, VECTOR_SEARCH}. A VECTOR_SEARCH joined back to your dataset is non-simple by definition. Six queries total; at least 2 must use the Wiki layer (keyword join, semantic join, or both).

Q: What makes a good join query and a good visualization?

Joins must demonstrate a new insight, not just technical ability. Each chart should directly illustrate findings from a specific query. Include a caption like "This chart shows results from Query #3, revealing that..."

Good visualization
  • Clear trend or pattern
  • Directly answers your question
  • Labeled axes, title, legend
Bad visualization
  • Random scatter with no pattern
  • Unlabeled axes
  • Too cluttered or unrelated to the analysis

Section 2: BQML (all tiers)

Q: How do I create and evaluate a BigQuery ML model?

-- Train
CREATE OR REPLACE MODEL `your_dataset.model_name`
OPTIONS(
  model_type='linear_reg',
  input_label_cols=['target_column']
) AS
SELECT feature1, feature2, feature3, target_column
FROM `your_dataset.training_table`;

-- Evaluate
SELECT * FROM ML.EVALUATE(
  MODEL `your_dataset.model_name`,
  (SELECT feature1, feature2, target_column
   FROM `your_dataset.test_table`));

Model types: linear_reg (continuous), logistic_reg (binary classification), boosted_tree_regressor / boosted_tree_classifier (advanced), arima_plus (forecasting, capstone d).

Q: Can I use the embeddings as features in my BQML model?

Yes, and it's a great move. Use ML.GENERATE_EMBEDDING on the relevant text column of your training rows, then feed the embedding into a boosted_tree_* model. Saves your bullet point: "I used semantic features in my model."

Q: What makes a good ML prediction target?

Something product-relevant that you can't trivially calculate. "Predict tip amount from trip features" is good; "Predict trip_id" is not.

Red flag: if your model hits 99.9% accuracy, you probably have data leakage.

Section 3: Scale math (all tiers)

Q: How do I compute I/O costs at 10x and 100x scale?

The formulas (BigSort, BNLJ, SMJ, HPJ) are covered in the Big Sort lesson and Module 3 join lessons; use the equation sheet. The recipe: pick the operator that shows up in your query plan, substitute your table page counts and buffer size, then compute the same expression with P(R) replaced by 10 × P(R) and 100 × P(R). Report both numbers.

Tier 87: Warehouse layout (Section 5)

Q: My fact layer is 1 GB. Does that qualify for Section 5?

No. Section 5 requires the fact layer to be ≥ 5 GB. Below that, partitioning teaches almost nothing: the optimizer's small-table tricks dominate, $/query is rounding error, and your before/after benchmark won't show measurable improvement. Scale your Spark ELT to ≥ 5 GB first, then Section 5 starts paying out.

Q: How do I pick partition and cluster keys?

Partition on a date / time column most of your queries filter on. BigQuery physically separates rows into partitions, so a WHERE date BETWEEN ... skips most of the scan. Cluster on a categorical column most of your queries group or filter on (1 to 4 columns). Clustering re-sorts rows within a partition for faster range reads.

Run your chosen query before and after applying the layout. Report bytes scanned and $/query in both. If the layout didn't help, your keys don't match your queries; pick different ones.

Q: What does a materialized view buy me?

Precomputed, incrementally maintained query results. For an ML feature pipeline, CREATE MATERIALIZED VIEW feature_v AS SELECT ... FROM fact ... means your training query reads the MV instead of recomputing the aggregation. Cheaper, faster, and if your fact layer keeps growing, the MV updates incrementally instead of from scratch.

Tier 93: Production retrieval at scale (Section 6)

Q: What does Section 6 actually require?

  1. Scale up the embed pass. Lift the Phase 1 cap from 100 MB of text to ~1 GB (a typical filter, embedding first paragraphs of ~1-2 M articles or full text of ~300 K articles). Expect $8 to $15 of BQ credit.
  2. Build a real vector index. CREATE VECTOR INDEX on the scaled embedded table; BQ's index minimum is ~5 GB of vector data, which a 1 GB text embed pass clears.
  3. Run one hybrid query. Combine a keyword score (BM25-style: title / first-paragraph keyword match) with vector similarity, in a single SQL statement.
  4. Compare latency and bytes scanned between indexed VECTOR_SEARCH and brute-force VECTOR_SEARCH (force brute force with options => '{"use_brute_force": true}'). Report both.

Q: How do I write a hybrid query?

The standard shape: run vector search and a keyword filter as two CTEs, fuse the scores in a final SELECT. Reciprocal-rank fusion is the simplest defensible fuser; a weighted-sum on normalized scores is fine too. Document the fusion you chose in a comment.

WITH vec AS (
  SELECT base.article_id, base.title, distance,
         ROW_NUMBER() OVER (ORDER BY distance) AS vec_rank
  FROM VECTOR_SEARCH(
    TABLE wiki_domain_embedded, 'embedding',
    (SELECT ML.GENERATE_EMBEDDING(...)),
    top_k => 50)
), kw AS (
  SELECT article_id, title,
         ROW_NUMBER() OVER (ORDER BY LENGTH(title)) AS kw_rank
  FROM wiki_domain
  WHERE LOWER(title) LIKE '%diabetes%'
  LIMIT 50
)
SELECT COALESCE(vec.article_id, kw.article_id) AS id,
       1.0/(60+vec.vec_rank) + 1.0/(60+kw.kw_rank) AS rrf_score
FROM vec FULL JOIN kw USING (article_id)
ORDER BY rrf_score DESC LIMIT 10;

Q: My vector index is slower than brute force at 1 GB. Did I do something wrong?

Probably not. BQ's vector index has a fixed build cost and a query overhead; below a few million vectors, brute force can beat the index on small queries. That is itself the finding: document where the crossover is and why. Real-scale retrieval teams pick indexed vs. brute force based on exactly this kind of empirical measurement.

Q: $8 to $15 is a lot. Can I skip the scale-up and just use my Phase 1 slice?

No; the BQ vector index will not build below its data minimum, so the "production retrieval" story doesn't work at Phase 1 scale. If $15 is a budget problem, embed first-paragraphs of a broader filter (cheaper per article) rather than full text of a narrow filter. The cost table shows typical totals stay under your $50 credit.

Tier 100: The capstone (Section 7)

Q: What is the capstone?

Build a governed text-to-SQL agent on the warehouse you shipped in Tiers 80 to 93, then measure how much your curation lifts a bare model. It is the same three-file recipe you learned in Module 1B, on your own dataset: a definitions.sql (6 to 8 business terms, each pinned to one SQL query), a reference.md (tables, join paths, every NULL-prone column), and an eval set (about 10 questions with known answers). The agent must consult the definitions first, generate SQL, run it, and answer.

Q: How do I define the vocabulary in BigQuery, concretely?

BigQuery supports exactly this: a CREATE TABLE FUNCTION is a persistent, parameterized, table-valued function, the same idea as the Postgres example in Module 1B. You define each term once and call it by name:

CREATE TABLE FUNCTION myds.listener_of(artist STRING) AS (
  SELECT DISTINCT l.user_id
  FROM myds.Listens l JOIN myds.Songs s USING (song_id)
  WHERE s.artist = artist
);
-- compose by name, no re-derivation:
SELECT user_id FROM myds.Users
EXCEPT DISTINCT
SELECT user_id FROM myds.listener_of('Taylor Swift');

Then the agent does not discover these on its own. You supply a manifest in the model's context: the function names, their parameters, and a one-line description each, alongside the schema and join policy, with the instruction to compose the vocabulary first and write raw SQL only when nothing fits. The table functions are where the vocabulary lives and runs; the manifest is how the model learns it exists. (Prefer not to create database objects? The alternative is a manifest-only approach: keep the definitions as a YAML or markdown of name to SQL template and have the agent inline them as CTEs. That is the dbt / Cube style, and it is fine too.)

Q: What does the text-to-SQL agent actually do? Am I building a model?

No, you are not training anything. The agent is a small loop (about 100 to 170 lines) around a commodity model. Per question it: (1) assembles context, loading your manifest (the vocabulary names and descriptions), the schema, and the join policy into the prompt; (2) asks the model to write BigQuery SQL for the question, composing your table functions where they fit; (3) runs that SQL on BigQuery; (4) returns the answer, and for an eval, compares it to the confirmed answer. The model's job is the open-ended part: map fuzzy English to the right composition of your vocabulary and parameters, and write raw SQL for anything the vocabulary does not cover. Your job is the harness plus the grounding, the manifest and policy that make that mapping reliable. For a small domain you paste the whole manifest into the prompt; with hundreds of terms you would retrieve just the relevant ones, which is the same idea at scale.

Q: What is the deliverable, exactly?

The bare-vs-grounded measurement. Run your agent with no knowledge model, then with it, on the same question set, and report both accuracies. Grounded must beat bare, or your writeup explains the failure mode (a valid outcome; it is how Anthropic learned that raw query history was worthless). Then run the gap-closing loop: every failed question becomes required curation, and you re-measure.

Q: How is the eval set built so it is not gameable?

Generate the questions from your domain description, not your definitions, so they surface gaps you did not plan for (this is how Anthropic builds "long-tail" evals). Author about 10, spanning your definitions and including at least 2 gotcha questions (NULLs, no-activity rows, dedup). Ground truth for each is a verified SQL query you trace by hand, the Module 1 debug-table skill. You are graded on coverage (fraction answered correctly, target ≥ 85%), lift, breadth, and reference-doc completeness, not on the model you called.

Q: Which model do I use? Do I need my own API key?

The generator is a commodity and is not graded, so use whatever is easiest. The zero-setup default is BigQuery ML ML.GENERATE_TEXT (or AI.GENERATE_TABLE), which calls Gemini from inside BigQuery with no external key and bills against your $50 credit; the template ships the remote-model connection set up for you. If you already have a Claude, OpenAI, or Gemini API key, use it. Running an open model in Colab is allowed but not recommended: a small open model is weaker at text-to-SQL and muddies the lift you are trying to measure.

Q: My dataset is time-series / geographic / text-heavy. Does the capstone still fit?

Yes, and its shape lives in your eval questions. A time-series dataset should include forecast-style questions, a geographic one ST_DISTANCE-style questions, a text-heavy one extraction and semantic questions, a large one scale questions. Same capstone, your flavor. Your agent may even call a BQML model, and a prediction can be one of your definitions.

AI Takeaways (all tiers)

Q: How much AI is too much?

Use AI freely. The real bar is whether you can talk through your filter design, your queries, your model, and your capstone in plain English. That skill is what data jobs actually lean on, even more than typing the SQL yourself. See the AI Policy for the full rule.

Q: What should the AI Takeaways section look like?

Short and informal. Half a page, not a report. 2 to 3 anecdotes (a few sentences each) plus one plain-English walkthrough of one piece of your pipeline. Good anecdotes capture a moment:

  • "AI suggested a Spark schema that silently dropped null titles. I caught it when my filter hit rate plummeted between dev and prod."
  • "AI's first cut at my VECTOR_SEARCH query embedded the same model for docs and query, but compared cosine across the wrong axis."
  • "AI suggested logistic regression for a continuous target. I caught it when ML.EVALUATE returned accuracy instead of R²."

Q: What should I double-check on AI-generated code?

  • Model type matches your target (regression vs classification).
  • Features make logical sense. No data leakage from features that "know" the target.
  • Embeddings and search use compatible models (same embedder for docs and queries).
  • Filter precision is sanity-checked, not assumed.
  • Query cost estimates match what you expected before you run anything > 1 GB scan.

Submission

Proposal due Oct 30. Final notebook + demo recording due Nov 20. Both submitted to Gradescope.

Watch this first

Project overview (~3 min)

What you build, the five-week climb, the four tiers, where the capstone fits.

Template walkthrough (~5 min)

A file-by-file tour of the template notebook: what's given, what you write at each tier.

Spark & Distributed SQL colab (~3 min, background)

Optional refresher for the Spark side of Phase 0: the Spark and Distributed SQL colab walkthrough from Module 5.