+
VectorFin
Preview

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.

10 min
Setup time
768
Embedding dims
~500
S&P 500 (beta)
Weekly
Updates

Prerequisites

📋VectorFin Pro plan
🔑Analytics Hub share (linked dataset) from VectorFin
☁️BigQuery account

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.

sql
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.

sql
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;
BigQuery console showing vector-search results aggregated per earnings call
Each row is one earnings call. ticker and fiscal_period identify it; best_chunk_distance (~0.27 at the top) is the closest cosine distance found; relevant_chunk_indexes is the ranked array of matching chunk positions; chunks_in_top_k counts how many of that call's chunks landed in the top 100. Note this returns which chunks match, by index, not the text yet.

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.

sql
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;
BigQuery console showing the rehydrated top-5 transcript paragraphs with speaker labels
The top-5 closest chunks, now as readable text: each row carries the speaker and the paragraph_text of the actual transcript turn the embedding came from.

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.

fix

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.

sql
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'
fix

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.

fix

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.

info

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
sql
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
sql
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
signalsFlat composite quant signals (ticker, date, score, piotroski_*, altman_*, beneish_*, sloan_*, regime, effective_ts, knowledge_ts)
sql
SELECT ticker, date, score, altman_zone, regime FROM `[your_project].[your_dataset].signals` WHERE ticker = 'AAPL' ORDER BY date DESC

Start querying in 10 minutes

Sign up for VectorFin and get immediate API access.