This document contains architecture notes, BigQuery schema guidance, pipeline design decisions, and operational information for contributors.
Understanding how this project fits into the larger open-data landscape:
Repo: https://github.com/hasadna/open-knesset-frontend-2.0
Next.js 15 frontend for Open Knesset. Consumes a FastAPI wrapper over the Knesset OData v2 API. Most actively maintained Knesset-related repo. Has comprehensive docs: PRD, architecture, design system, conventions.
Repo: https://github.com/hasadna/knesset-data-pipelines
ETL pipelines for Knesset data (committee protocols, votes, etc.) going back to 2005. Airflow-based, Docker-first. Includes Jupyter notebooks for exploration.
Repo: https://github.com/hasadna/knesset-data-python
Low-level Python client for the Knesset data service API. Installable: uv add knesset-data. Provides access to MK profiles, committees, votes. Note: Knesset blocks some requests — check for reblaze responses.
Site: http://kikar.org
API: http://kikar.org/api/v1/
Existing Hasadna project that collects and surfaces MK Facebook posts. The natural sister project. API supports filtering by content, date, MK, party.
Paper: https://arxiv.org/abs/2405.20269 · Data: https://huggingface.co/datasets/guymorlan/IsraParlTweet
Linked corpus from Hebrew University (Mor-Lan, Levi, Sheafer, Shenhav). Two halves:
- Knesset floor speeches 1992–2023 (~4.5M utterances) with speaker IDs, dates, topics — freely downloadable CSV (~22GB)
- MK tweets 2008–2023 with full text + engagement metrics — gated (contact guy.mor@mail.huji.ac.il)
- Includes MK tenure/party metadata and linguistic annotations (sentiment predictions directly relevant to our use case)
- License: CC-BY-4.0
Why we chose it: This corpus already crosses stated positions (tweets) with parliamentary activity (speeches), providing a strong historical foundation (2008–2023).
Limitation: Ends in 2023. We supplement with the official X API for ongoing updates (2024→present).
Repo: https://github.com/jschler/ILElectionData
Jonathan Schler. ~5M Facebook posts+comments from Israeli politicians (2019–2020). Openly downloadable with no gating. Narrower (elections-only, ends 2020) but useful as a model-training corpus.
Reality as of 2026:
- Full-archive search (back to 2006) is Enterprise-only (contract required, ~$42k/month). Not feasible.
- Pro tier (full-archive) is closed to new signups.
- Pay-per-use model (~$0.005/post, ~2M reads/mo cap) only covers the last 7 days.
Design decision: The project rejected third-party scraper APIs (twitterapi.io, data365, etc.) as not compliant with X's Developer Agreement. Instead, the pipeline uses IsraParlTweet's historical corpus for baseline data and the official X API for daily incremental updates.
Docs: http://oknesset-api.readthedocs.io/en/latest/
Main endpoint: http://main.knesset.gov.il/Activity/Info/Pages/Databases.aspx
Official public-record source for parliamentary activity (votes, bills, committees, MK profiles). Primary source for ground truth.
Google Sheet (external): https://docs.google.com/spreadsheets/d/1tUGtlYHUIWl3UUd98KM8QbkrAqYTBBgYI88v0HTXjRE/edit
Public reference document maintained externally. Use for initial discovery; actual MK identities resolved through the mk table in BigQuery.
The pipeline uses Google BigQuery as the canonical data warehouse. All processing (embeddings, clustering, issue classification) happens in BigQuery; results are cached locally for development and debugging.
- Database: Google BigQuery (deployed v4 schema)
- Dataset:
mk_tracking - Python environment: Python 3.11, uv package manager
- Backend: FastAPI
- Frontend: React 19, Vite, TypeScript
Never use CREATE OR REPLACE TABLE on shared tables. This wipes all metadata unless explicitly re-declared. Use ALTER TABLE for additive changes. If a full rebuild is needed, use the canonical db/schema.bq.sql (the source of truth for all metadata) and copy data back.
What gets silently lost when metadata is not preserved:
- PK/FK
NOT ENFORCEDconstraints — BigQuery refuses to create an FK referencing a table without a declared PK, and the query optimizer uses them for join elimination. - Column descriptions — BigQuery has no CHECKs or enum types, so allowed-value lists live in column descriptions (
platform,role_type,vote, ...). Bare recreations erase the value contract. - Partitioning & clustering —
social_postis day-partitioned (posted_at) and clustered (mk_id, platform). Recreate without it and per-MK queries silently become full-table scans. - Column defaults —
GENERATE_UUID()ids,CURRENT_TIMESTAMP(),FALSEflags. Lost defaults cause NULLs to be written where convention expects NOT NULL. NOT NULLmodes and dataset labels/descriptions (minor).
For detailed documentation, see:
| Resource | Location |
|---|---|
| Database design & rationale | docs/DATABASE_DESIGN.md |
| Schema reference (tables, columns, keys) | docs/DB_SCHEMA.md |
| Pipeline → DB ingestion contract | docs/INGESTION_CONTRACT.md |
| Ingestion MERGE script + staging schemas | db/merge_ingest.bq.sql, db/staging/*.schema.json |
| BigQuery DDL v4 (deployed) | db/schema.bq.sql |
| BigQuery seed data (deployed) | db/seed.bq.sql |
| Knesset OData API docs | http://oknesset-api.readthedocs.io/en/latest/ |
After deploying schema, initialize BigQuery tables:
bq query --use_legacy_sql=false < db/schema.bq.sql
bq query --use_legacy_sql=false < db/seed.bq.sqlInitialize issue anchor embeddings (one-time):
uv run big_query_to_data.py --initialize-anchorsThis embeds the seven live issues' hand-curated semantic anchors and inserts them into BigQuery. Subsequent runs omit this flag and assume anchors already exist.
- Use MERGE on natural keys — BigQuery has no unique constraints, so MERGE prevents duplicates in re-runnable jobs (versus plain INSERT).
- Never use streaming inserts (
insertAll) — they double-write on retries and rows are frozen for ~90 minutes. - Resolve MK IDs through the
mktable — never write handles directly as IDs. - Follow
docs/INGESTION_CONTRACT.mdfor the contract between pipeline stages and the database.
# List tables
bq ls mk_tracking
# Show schema with descriptions
bq show --schema --format=prettyjson mk_tracking.social_post
# Run a query
bq query --use_legacy_sql=false 'SELECT COUNT(*) FROM mk_tracking.mk'
# List jobs (useful for debugging)
bq ls -j --allAll tweets are embedded using gemini-embedding-2 (3,072-dimensional vectors). The embedding model is fixed globally in the configuration; custom dimensions can be specified per run via CLI flags.
Embeddings are computed incrementally:
- New tweets are embedded via the Gemini API
- Results are checkpointed after every completed row
- Rerunning resumes from a partial output without re-spending API quota
- Complete output files skip the embedding stage entirely
If data/processed/tweet_cluster_centroids.npz does not exist, the pipeline:
- Fits K-means with
K=30, seed42, one deterministic initialization - Selects the 100 tweets closest to each centroid
- Calls
gemini-2.5-flashto generate Hebrew titles and meanings - Asks Gemini to classify each cluster as garbage (not political/public messaging, or incoherent)
- Saves centroids, meanings, cluster sizes, sampled tweet IDs, garbage flags, and configuration
The generated file is the source of truth for garbage cluster IDs. If centroids already exist, clustering and summarization are skipped—but BigQuery read and incremental embedding still run. Delete only the centroid file to force recomputation.
For each tweet, the pipeline:
- Computes max cosine similarity across an issue's eight semantic anchors
- Applies softmax with temperature
0.07across all seven issues - Sets all probabilities to zero if:
- Text has fewer than 16 characters after trimming whitespace and URLs
- K-means assigns the tweet to a garbage cluster
Scores are versioned; rows from the current model are skipped while older versions are refreshed.
Every invocation appends a record to data/processed/big_query_to_data_run_history.jsonl, including:
- Number of new tweets embedded
- Whether centroids were updated
- Reported Gemini API token usage
- Gemini API cost in USD
- BigQuery bytes processed (but not monetary cost, since classroom credits, free tier, and billing settings cannot be inferred locally)
uvx ruff check . # lint
uvx ruff check . --fix # lint + autofix
uvx ruff format . # formatuv run pytest -qThe UI can run against a local JSON snapshot (for development without live BigQuery access):
# Validate the local evidence snapshot
uv run mkwork validate
# Start UI in offline mode
MK_WORK_DATA_BACKEND=json uv run mkworkThen open http://127.0.0.1:8000. For frontend HMR development:
cd ui && npm run devuv add <pkg> # add a runtime dep
uv add --dev <pkg> # add a dev dep
uv sync --frozen # install from lockfileThis repo contains three independent uv-managed Python projects sharing a root lock:
- Main: embeddings, clustering, FastAPI UI backend
- x_api_collect/ — X API data collection (collects MK tweets)
- bill_issues/ — Bill-to-issue classification using Gemini
The Dockerfile and run_daily_pipeline.sh orchestrate all three.
The Gemini API client retries quota and transient errors (429, 5xx) with exponential backoff. The CLI prints:
- Completed rows
- Percentage progress
- ETA
- Retry delays
No credentials or project IDs are ever written to the repository.