GrooveSeek

Semantic search over a Markdown knowledge base, served over MCP.

View the Project on GitHub alphabet-h/grooveseek

26. Make the index size caps configurable, spell “no limit” out, and keep the read caps

Context and problem

Until this record, one constant, MAX_RAW_BINARY_BYTES (50 MiB, grooveseek/src/parser/mod.rs), decided six things at once: the file size at which the indexer skips a binary file before reading it, the same check on the handle it reads from, the decompression budget of the .xlsx preflight, the cumulative budget of .docx / .pptx part reads, the text budget of one PDF, and the binary cap of get_document / resources/read. MAX_RAW_TEXT_BYTES did the first two for Markdown and plain text.

Workbooks of 300 MB exist in ordinary office use, and they were skipped with a warning. The cap cannot simply be raised for everyone: rebuild_index is an MCP tool a client can call at any time, the file is held in memory whole while it is parsed, calamine also loads the whole shared-strings table of an .xlsx into memory, and an allocation failure aborts the process. Neither catch_unwind nor running the parse on another thread stops an abort.

The question is how to let an operator admit such a file knowingly, without changing anything for a configuration that does not ask for it.

Decision drivers

Options considered

  1. How “no limit” is spelled.
    • 0. Rejected: products disagree on what it means. Recoll reads 0 as no limit for filtermaxseconds and as “reject everything” for compressedfilemaxkbs; Zoekt reads it as “use the default”; Open WebUI treats an unset or 0 RAG_FILE_MAX_SIZE as no limit for uploads and as 100 MB for archive extraction, within one product. This repository already refuses [embedding].max_input_chars = 0.
    • -1. The most common choice (Elasticsearch indexed_chars, Apache Tika, Solr). Rejected: a negative byte count in TOML does not read as an intent, and it invites “and -2?”.
    • Leaving the key out means no limit. Rejected: every server-type product surveyed (Onyx, Dify, Meilisearch, Qdrant, Weaviate, Nextcloud) treats an unset limit as a finite default.
    • The string "unlimited". Taken. 0 and negative values are errors whose message points at it.
  2. The decompression budget.
    • Tie it to the raw-byte cap. Rejected: the XML inside a workbook inflates to several times the file, so max_binary_file_size = "300 MiB" would still skip a 300 MB workbook at decompression, and the only way through would be "unlimited", which removes the zip-bomb check from every file.
    • Tie it only when the raw cap is "unlimited". Rejected: “no limit” would silently remove a second defence.
    • Bound it by ratio, as Apache POI (minimum inflate ratio 1%) and Tika (output to input 100) do. Rejected for now: the existing preflight counts absolute bytes in two layers (declared, then actually inflated), and changing the axis is a separate decision.
    • A key of its own, max_decompressed_size, 50 MiB by default and independent of the raw cap. Taken.
  3. What a read returns.
    • Let get_document / resources/read follow the index caps. Rejected: one MCP request would then hold a 300 MB file in memory.
    • Keep the read caps where they are: 50 MiB for binary formats, 1 MiB for text. Taken. A document indexed past them stays searchable and carries no uri, through the size recorded in the index (ADR-0005).
  4. Where the keys live. A new [limits] section was rejected; [index] already holds the settings of groove index and rebuild_index, and the caps are a property of indexing.

  5. One key for binary and text. Rejected: text has no chunk-size bound (Markdown is cut only at headings), so raising a shared cap would let one heading-less .txt become one enormous chunk.

Decision

Consequences

References