rag: cap chunk size at 6KB to fit nomic-embed-text 2048-tok context
The chunker emits any single paragraph as a stand-alone chunk regardless of size. One HVM page had a 14,858-char paragraph (a big config table) — nomic-embed-text 400'd the entire embed batch because the model's context is 2048 tokens. Added a hard-split fallback that splits any oversized chunk on line boundaries to MAX_CHARS=6000 (~1500 tokens, headroom). Also defaulted PRODUCT_NAME to "hvm" in rag/index.py to match server.py. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
+35
-11
@@ -31,6 +31,27 @@ from typing import Iterator
|
|||||||
CHARS_PER_TOKEN = 4
|
CHARS_PER_TOKEN = 4
|
||||||
TARGET_TOKENS = 500
|
TARGET_TOKENS = 500
|
||||||
TARGET_CHARS = TARGET_TOKENS * CHARS_PER_TOKEN
|
TARGET_CHARS = TARGET_TOKENS * CHARS_PER_TOKEN
|
||||||
|
# Hard cap: nomic-embed-text's context is 2048 tokens. Anything larger
|
||||||
|
# 400s the entire embed batch. 6000 chars ≈ 1500 tokens leaves headroom.
|
||||||
|
MAX_CHARS = 6000
|
||||||
|
|
||||||
|
|
||||||
|
def _hard_split(text: str) -> list[str]:
|
||||||
|
"""Split an oversized block on line boundaries into MAX_CHARS pieces."""
|
||||||
|
if len(text) <= MAX_CHARS:
|
||||||
|
return [text]
|
||||||
|
out: list[str] = []
|
||||||
|
buf: list[str] = []
|
||||||
|
buf_chars = 0
|
||||||
|
for line in text.splitlines(keepends=True):
|
||||||
|
if buf_chars + len(line) > MAX_CHARS and buf:
|
||||||
|
out.append("".join(buf).rstrip())
|
||||||
|
buf, buf_chars = [], 0
|
||||||
|
buf.append(line)
|
||||||
|
buf_chars += len(line)
|
||||||
|
if buf:
|
||||||
|
out.append("".join(buf).rstrip())
|
||||||
|
return out
|
||||||
|
|
||||||
|
|
||||||
def estimate_tokens(text: str) -> int:
|
def estimate_tokens(text: str) -> int:
|
||||||
@@ -104,23 +125,26 @@ def chunks_from_page(
|
|||||||
|
|
||||||
# ----- Body chunks: pack paragraphs up to TARGET_CHARS -------
|
# ----- Body chunks: pack paragraphs up to TARGET_CHARS -------
|
||||||
ordinal = 1
|
ordinal = 1
|
||||||
|
|
||||||
|
def emit(buf: list[str]) -> Iterator[dict]:
|
||||||
|
nonlocal ordinal
|
||||||
|
merged = "\n\n".join(buf)
|
||||||
|
for piece in _hard_split(merged):
|
||||||
|
yield {
|
||||||
|
"id": f"{metadata['bundle_id']}::{page_id}::{ordinal}",
|
||||||
|
"text": piece,
|
||||||
|
"metadata": {**metadata, "ordinal": ordinal},
|
||||||
|
}
|
||||||
|
ordinal += 1
|
||||||
|
|
||||||
buf: list[str] = []
|
buf: list[str] = []
|
||||||
buf_chars = 0
|
buf_chars = 0
|
||||||
for p in paragraphs:
|
for p in paragraphs:
|
||||||
if buf_chars + len(p) > TARGET_CHARS and buf:
|
if buf_chars + len(p) > TARGET_CHARS and buf:
|
||||||
yield {
|
yield from emit(buf)
|
||||||
"id": f"{metadata['bundle_id']}::{page_id}::{ordinal}",
|
|
||||||
"text": "\n\n".join(buf),
|
|
||||||
"metadata": {**metadata, "ordinal": ordinal},
|
|
||||||
}
|
|
||||||
ordinal += 1
|
|
||||||
buf = []
|
buf = []
|
||||||
buf_chars = 0
|
buf_chars = 0
|
||||||
buf.append(p)
|
buf.append(p)
|
||||||
buf_chars += len(p)
|
buf_chars += len(p)
|
||||||
if buf:
|
if buf:
|
||||||
yield {
|
yield from emit(buf)
|
||||||
"id": f"{metadata['bundle_id']}::{page_id}::{ordinal}",
|
|
||||||
"text": "\n\n".join(buf),
|
|
||||||
"metadata": {**metadata, "ordinal": ordinal},
|
|
||||||
}
|
|
||||||
|
|||||||
+1
-1
@@ -29,7 +29,7 @@ CHROMA_DIR = ROOT / "chroma"
|
|||||||
|
|
||||||
# Collection name — convention: <product>_docs. Override via env if needed.
|
# Collection name — convention: <product>_docs. Override via env if needed.
|
||||||
import os
|
import os
|
||||||
PRODUCT_NAME = os.environ.get("PRODUCT_NAME", "myproduct")
|
PRODUCT_NAME = os.environ.get("PRODUCT_NAME", "hvm")
|
||||||
COLLECTION = f"{PRODUCT_NAME}_docs"
|
COLLECTION = f"{PRODUCT_NAME}_docs"
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user