VectorFin in BigQuery
Run BigQuery-native vector search over the shared VectorFin transcript embeddings: create a Vertex embedding model, VECTOR_SEARCH the corpus, then rehydrate the matched text. Framed as preview, this is the shape it takes once we share the data with you via Analytics Hub or a GCS bucket.
Prerequisites
Vector search walkthrough
Imagine we share the `transcripts` embeddings (and the raw transcript text) with you as a BigQuery Analytics Hub source or a GCS bucket. Here is how you query them with BigQuery-native vector search, with no separate vector database and no copies. The embeddings are 768-dim Google gemini-embedding-2-preview vectors, the same model you reproduce below for the query side.
Create the embedding model connection
Register a remote model over the Vertex AI Gemini embedding endpoint so BigQuery can embed your query text in-database. Use the same model and dimensionality the corpus was embedded with.
CREATE OR REPLACE MODEL `[your_project].[your_dataset].gemini2_embed`
REMOTE WITH CONNECTION DEFAULT
OPTIONS (ENDPOINT = 'gemini-embedding-2-preview');Run vector search
Embed the question with ML.GENERATE_EMBEDDING (RETRIEVAL_QUERY, 768 dims), then VECTOR_SEARCH the shared transcript embeddings by cosine distance. Aggregating per (ticker, fiscal_period) tells you which earnings calls are most relevant and which chunks inside them matched.
SELECT
base.ticker AS ticker,
base.fiscal_period AS fiscal_period,
MIN(distance) AS best_chunk_distance,
ARRAY_AGG(base.chunk_idx ORDER BY distance LIMIT 10) AS relevant_chunk_indexes,
COUNT(*) AS chunks_in_top_k
FROM VECTOR_SEARCH(
TABLE `[your_project].[your_dataset].transcripts`,
'embedding',
(
SELECT ml_generate_embedding_result AS embedding
FROM ML.GENERATE_EMBEDDING(
MODEL `[your_project].[your_dataset].gemini2_embed`,
(SELECT 'How are operating margins trending and what is driving the change?' AS content),
STRUCT(TRUE AS flatten_json_output,
'RETRIEVAL_QUERY' AS task_type,
768 AS output_dimensionality)
)
),
top_k => 100,
distance_type => 'COSINE'
)
GROUP BY ticker, fiscal_period
ORDER BY best_chunk_distance
LIMIT 20;
Rehydrate the matched text
The search returns chunk indexes, not prose. Join the ranked indexes back to the raw transcript text (shared alongside the embeddings) to read the actual paragraphs. UNNEST the index array WITH OFFSET to keep rank order, then keep the closest few.
WITH vector_matches AS (
SELECT
base.ticker AS ticker,
base.fiscal_period AS fiscal_period,
ARRAY_AGG(base.chunk_idx ORDER BY distance LIMIT 100) AS relevant_chunk_indexes
FROM VECTOR_SEARCH(
TABLE `[your_project].[your_dataset].transcripts`,
'embedding',
(
SELECT ml_generate_embedding_result AS embedding
FROM ML.GENERATE_EMBEDDING(
MODEL `[your_project].[your_dataset].gemini2_embed`,
(SELECT 'How are operating margins trending and what is driving the change?' AS content),
STRUCT(TRUE AS flatten_json_output,
'RETRIEVAL_QUERY' AS task_type,
768 AS output_dimensionality)
)
),
top_k => 100,
distance_type => 'COSINE'
)
WHERE base.ticker = 'AAPL' AND base.fiscal_period = '2026-Q2'
GROUP BY ticker, fiscal_period
)
SELECT
t.speaker,
t.paragraph_number,
t.paragraph_text
FROM vector_matches v
CROSS JOIN UNNEST(v.relevant_chunk_indexes) AS chunk_idx WITH OFFSET AS rank
JOIN `[your_project].whystock_actuals.stock_earning_call_transcripts` t
ON t.ticker = v.ticker
AND t.fiscal_period = v.fiscal_period
AND t.paragraph_number = chunk_idx
WHERE rank < 5
ORDER BY rank;
Why row 3 is an Operator intro: semantic similarity is not factual answering
It is completely irrelevant to a human, but to an embedding model it is a mathematically logical result. This is a classic example of the difference between semantic similarity and factual answering. Here is why our vector search pulled this specific chunk, and how to engineer your pipeline to handle it.
Why the model thinks it is relevant
The analyst vector association: gemini-embedding-2-preview was trained on mountains of financial text. It has learned that an analyst like Amit Daryanani at Evercore ISI frequently covers Apple and routinely asks highly specific questions about revenue growth and margins, so in high-dimensional vector space the concept of "Evercore analyst" sits extremely close to "financial metrics" and "margin drivers". Conversational proximity: in the structure of an earnings call the operator's introduction is the immediate precursor to the actual question, and because our embeddings are chunked strictly at the paragraph level the model scores this intro highly, its semantic neighborhood is identical to the target topic even though it lacks the factual payload. Distance context: a cosine distance of 0.29 is decent, but it is not a dead ringer (which would sit closer to 0.05 to 0.15). The model is essentially saying it is about 70% confident this chunk belongs to a conversation about revenue.
How to fix this in a trading pipeline
If you are passing raw array text directly into a quantitative trading algorithm, noise like this will break your signal. You need to layer in defensive filtering.
1. The SQL quick fix: exclude the Operator
The simplest immediate fix is to leverage the structured metadata we ship. The Operator in an earnings call never provides material financial data or forward guidance, so you can immediately prune this noise by adding a speaker filter to your rehydration query.
JOIN `[your_project].whystock_actuals.stock_earning_call_transcripts` t
ON t.ticker = v.ticker
AND t.fiscal_period = v.fiscal_period
AND t.paragraph_number = v.chunk_idx
AND t.speaker != 'Operator'2. The agentic filter (the robust solution)
This is exactly why raw vector retrieval should never trigger a direct trade execution. When your orchestration agent picks up this text, its prompt should be designed to act as a secondary filter. If you ask the LLM to output a strict JSON schema containing primary_driver and margin_sentiment_score, it will look at "Our next question is from Amit..." and appropriately return empty or neutral values, preventing a false-positive signal.
3. Context-aware chunking (the root-cause solution)
Chunking strictly by individual paragraph isolates statements too much. A better approach for earnings calls is semantic chunking or sliding windows. Instead of embedding the Operator, the analyst, and the CEO as three separate vectors, our pipeline should concatenate the Q&A block (intro, question, and answer) into a single unified chunk. This ensures the embedding captures the complete context of the exchange.
Enterprise: native BigQuery on the source buckets
For Enterprise customers VectorFin shares the source GCS buckets directly (the Iceberg tables plus the raw earnings-call transcripts, financial statements, and SEC filing data), so all of it can be queried natively in BigQuery as external Iceberg tables with ML.GENERATE_EMBEDDING / VECTOR_SEARCH, with no ETL and no copies. The walkthrough above is exactly what you run, pointed at your linked dataset. Want this wired into your billing project? Use the "Contact us to discuss" link below.
Available Tables
VectorFin data tables: bitemporal (effective_ts + knowledge_ts), append-only, weekly updates.
transcripts (embeddings)Earnings call chunk embeddings (768-dim, gemini-embedding-2-preview), the VECTOR_SEARCH target▼
VECTOR_SEARCH(TABLE `[your_project].[your_dataset].transcripts`, 'embedding', (/* query embedding */), top_k => 100, distance_type => 'COSINE')whystock_actuals.stock_earning_call_transcriptsRaw transcript paragraphs (speaker, paragraph_number, paragraph_text); join here to rehydrate matched chunks▼
JOIN `[your_project].whystock_actuals.stock_earning_call_transcripts` t ON t.ticker = v.ticker AND t.fiscal_period = v.fiscal_period AND t.paragraph_number = chunk_idxsignalsFlat composite quant signals (ticker, date, score, piotroski_*, altman_*, beneish_*, sloan_*, regime, effective_ts, knowledge_ts)▼
SELECT ticker, date, score, altman_zone, regime FROM `[your_project].[your_dataset].signals` WHERE ticker = 'AAPL' ORDER BY date DESCRelated Integrations
Start querying in 10 minutes
Sign up for VectorFin and get immediate API access.