# Persistence Schema This document describes the first persistence slice for the RAG backend. The schema exists independently of the live FastAPI pipeline: ingestion, retrieval, answer generation, and citation generation are not writing to these tables yet. ## Database Target Intended production persistence uses a managed MySQL database supplied through the `DATABASE_URL` environment variable, for example with a `mysql+pymysql://...` SQLAlchemy URL. Managed MySQL requires TLS. Set `DB_CA_CERT` to the local CA certificate path so SQLAlchemy/PyMySQL can connect with certificate verification enabled. The CA certificate file is local deployment material and must not be committed. For Hugging Face Docker Space deployment, do not commit the CA certificate. Encode the CA certificate locally and store the base64 string as the `DB_CA_CERT_B64` Space secret. On startup, the app decodes that secret into a temporary CA file and uses it for SQLAlchemy SSL verification. Required Hugging Face Space secrets for the deployed app are: - `DATABASE_URL` - `DB_CA_CERT_B64` - `GROQ_API_KEY` - `API_TOKEN` Deployment to the Hugging Face Space is manual through the `hf` git remote; there is no GitHub auto-sync workflow in this repo. SQLite is only the local/development fallback for smoke testing and quick schema checks. If `DATABASE_URL` is not set, the persistence layer defaults to `sqlite:///./rag_persistence.db`. Set `DATABASE_URL` through environment variables in deployment. For local development it may be placed in `.env`, but `.env` must never be committed because it can contain database credentials. ## User Identity The schema includes `user_id` on every user-owned table before OAuth exists. For local development, `persistence.user_context.get_current_user_id()` returns the stable placeholder `local-dev-user`. Adding `user_id` now prevents a later migration where historical documents, chunks, queries, and citations would need to be backfilled or repartitioned by owner. When OAuth is added, it should replace only `get_current_user_id()` with a real authenticated identity lookup; the schema stays unchanged. ## Why Eval Tables Are Excluded Evaluation reports, benchmark questions, metrics, and experiment artifacts are not part of the application persistence model. They remain file-based under `eval/` and generated reports remain ignored under `eval/results/`. Keeping eval data out of this schema avoids mixing production user history with benchmark-only records and keeps this slice focused on app-level document and query history. ## Why Embeddings And FAISS Blobs Are Excluded This slice stores source text and retrieval metadata, not vector artifacts. Embeddings and FAISS indexes are intentionally excluded because: - they can be regenerated from chunks and the recorded embedding config, - they are large compared with the relational metadata, - their binary format and compatibility depend on model/index implementation, - cache invalidation is already tied to embedding model and input format. Future persistence can add a dedicated vector store or artifact cache if needed. ## Dedup Behavior Document deduplication uses `(user_id, source_hash)`, but `source_hash` is computed differently by ingestion path: - Upload ingestion uses the SHA-256 hash of the uploaded PDF bytes. - URL ingestion uses a hash of the existing document cache key, because the URL loader does not retain raw PDF bytes after chunking. Known limitation: the same PDF ingested once by upload and once by URL will not deduplicate across those paths because the two source hashes differ. This is an accepted tradeoff for now to avoid refactoring the URL loader and cache flow. ## Tables ### `users` Stores the application user identity. - `id` primary key - optional profile/auth fields: `email`, `display_name`, `auth_provider` - timestamps: `created_at`, `updated_at` ### `documents` Stores one ingested PDF per user and the retrieval configuration used for it. - `user_id` references `users.id` - input metadata: `source_type`, `filename`, `source_url`, `source_hash`, `cache_key` - lifecycle fields: `status`, `error_message` - retrieval config: `embedding_model`, `embedding_format`, `retrieval_mode`, `reranker_model`, `k_initial`, `k_final` - timestamps: `created_at`, `updated_at` Index: `documents(user_id, created_at)`. ### `chunks` Stores page-aware text chunks for a document. - `user_id` references `users.id` - `document_id` references `documents.id` - chunk identity: `chunk_id`, `chunk_index` - location/content: `page_number`, `text`, `text_hash`, `char_count` - timestamp: `created_at` Indexes: - `chunks(user_id, document_id)` - unique `chunks(document_id, chunk_id)` ### `queries` Stores one question/answer result against a persisted document. - `user_id` references `users.id` - `document_id` references `documents.id` - answer fields: `question`, `answer`, `status`, `is_abstained` - optional structured verification payload: `claim_verifications_json` - retrieval config: `embedding_model`, `retrieval_mode`, `reranker_model`, `k_initial`, `k_final` - optional timing: `latency_ms` - timestamp: `created_at` Index: `queries(user_id, document_id, created_at)`. ### `citations` Stores source chunks returned for a query. - `user_id` references `users.id` - `query_id` references `queries.id` - `document_id` references `documents.id` - optional chunk relation: `chunk_db_id` - response-facing source fields: `chunk_id`, `rank`, `page_number`, `excerpt` - optional scores: `retrieval_score`, `reranker_score` - timestamp: `created_at` Indexes: - `citations(user_id, query_id)` - `citations(query_id, rank)` ## Relationships - A user has many documents, chunks, queries, and citations. - A document has many chunks, queries, and citations. - A query has many citations. - A citation may point to a stored chunk through `chunk_db_id`; it also stores response-facing `chunk_id` and excerpt so citations remain readable even if a chunk relationship is unavailable.