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_URLDB_CA_CERT_B64GROQ_API_KEYAPI_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.
idprimary 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_idreferencesusers.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_idreferencesusers.iddocument_idreferencesdocuments.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_idreferencesusers.iddocument_idreferencesdocuments.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_idreferencesusers.idquery_idreferencesqueries.iddocument_idreferencesdocuments.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-facingchunk_idand excerpt so citations remain readable even if a chunk relationship is unavailable.