Problem
POST /documents accepts a document of unbounded size and indexes it one round trip at a time.
DocIn in app/main.py puts no limit on text:
class DocIn(BaseModel):
id: str
title: str
source: str
text: str
Compare AskRequest.question, which is carefully bounded: Field(min_length=1, max_length=2000). The bound was clearly considered on one side of the API and not the other.
Then ingest in app/ingest.py does:
vectors = embedder.embed([c.text for c in chunks]) # every chunk in one call
...
for c, vec in zip(chunks, vectors):
await conn.execute("INSERT INTO chunks ...", (...)) # one round trip per chunk
A 20 MB document becomes roughly 17,000 chunks. Those are embedded in a single SentenceTransformer.encode call, so the full result set is materialized in memory at once, and then inserted with 17,000 sequential round trips inside a single transaction that holds locks on the document's rows the whole time. doc_acl inserts have the same one-at-a-time pattern in a loop.
Since the endpoint currently has no authentication (#5), this is reachable by anyone who can reach the service.
Suggested approach
- Bound the input. Add
max_length to DocIn.text and title, and a maximum chunk count in ingest, returning a 413 with a clear message rather than trying and dying. Pick numbers deliberately and put them in app/config.py next to the other settings.
- Batch the embedding. Feed
embedder.embed in fixed-size batches instead of one call per document, so memory is bounded by batch size rather than by document size.
- Batch the inserts. psycopg 3 has
cursor.executemany with pipeline support, and COPY for the larger case. Either turns thousands of round trips into a handful.
- Same treatment for the
doc_acl insert loop.
- Add a test that ingesting a document near the limit stays within a reasonable number of database round trips. Counting round trips is a fair proxy and is stable in CI, unlike timing.
Done when
- Oversized documents are rejected with a clear error rather than accepted.
- Chunk and ACL inserts do not scale as one round trip per row.
- Embedding memory is bounded by a batch size constant, not by document size.
- The limits are configurable and documented in
.env.example and the README.
If you want to take this on, comment on the issue to claim it and it will be assigned. Please keep to a maximum of 2 open claims per person at a time so other contributors get a chance.
Problem
POST /documentsaccepts a document of unbounded size and indexes it one round trip at a time.DocIninapp/main.pyputs no limit ontext:Compare
AskRequest.question, which is carefully bounded:Field(min_length=1, max_length=2000). The bound was clearly considered on one side of the API and not the other.Then
ingestinapp/ingest.pydoes:A 20 MB document becomes roughly 17,000 chunks. Those are embedded in a single
SentenceTransformer.encodecall, so the full result set is materialized in memory at once, and then inserted with 17,000 sequential round trips inside a single transaction that holds locks on the document's rows the whole time.doc_aclinserts have the same one-at-a-time pattern in a loop.Since the endpoint currently has no authentication (#5), this is reachable by anyone who can reach the service.
Suggested approach
max_lengthtoDocIn.textandtitle, and a maximum chunk count iningest, returning a 413 with a clear message rather than trying and dying. Pick numbers deliberately and put them inapp/config.pynext to the other settings.embedder.embedin fixed-size batches instead of one call per document, so memory is bounded by batch size rather than by document size.cursor.executemanywith pipeline support, andCOPYfor the larger case. Either turns thousands of round trips into a handful.doc_aclinsert loop.Done when
.env.exampleand the README.If you want to take this on, comment on the issue to claim it and it will be assigned. Please keep to a maximum of 2 open claims per person at a time so other contributors get a chance.