fix(rag): chunking real por líneas + embedding de chunk completo
Build & Deploy ResearchOwl / build-and-push (push) Successful in 6s

simple_chunk:
- parte por \n+ (no solo \n\n): Wikipedia/trafilatura usan \n simple, lo
  que colapsaba cada fuente en un único chunk gigante
- subdivide párrafos que superan chunk_size
- el overlap arrastra un tail de N palabras en vez del párrafo completo
  (evita chunks inflados a ~2x cuando los párrafos son grandes)

processor: embedding sobre el chunk completo (antes truncaba a 1000 chars,
el vector solo representaba el principio del chunk → ranking RAG pobre)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
ChemaVX
2026-06-15 14:53:00 +00:00
co-authored by Claude Opus 4.8
parent 94dc0316f9
commit 972bd2f883
+31 -7
View File
@@ -70,9 +70,28 @@ class OllamaClient:
def simple_chunk(text: str, chunk_size: int = 800, overlap: int = 100) -> list[str]: def simple_chunk(text: str, chunk_size: int = 800, overlap: int = 100) -> list[str]:
""" """
Split text into overlapping chunks by approximate word count. Split text into overlapping chunks by approximate word count.
Respects paragraph boundaries when possible. Respects paragraph/line boundaries when possible.
Acepta párrafos separados por uno o más saltos de línea (Wikipedia y
trafilatura usan '\n' simple, lo que antes dejaba el documento entero como
un único 'párrafo' → un solo chunk gigante). Además subdivide por palabras
cualquier párrafo que por sí solo supere chunk_size.
""" """
paragraphs = [p.strip() for p in text.split("\n\n") if p.strip()] raw_paragraphs = [p.strip() for p in re.split(r"\n+", text) if p.strip()]
# Subdivide párrafos sobredimensionados en piezas de (chunk_size - overlap)
# palabras; así, al reinyectar 'overlap' palabras de solapamiento, ningún
# chunk resultante supera chunk_size.
piece_size = max(1, chunk_size - max(0, overlap))
paragraphs: list[str] = []
for para in raw_paragraphs:
words = para.split()
if len(words) <= chunk_size:
paragraphs.append(para)
else:
for i in range(0, len(words), piece_size):
paragraphs.append(" ".join(words[i:i + piece_size]))
chunks = [] chunks = []
current = [] current = []
current_words = 0 current_words = 0
@@ -81,10 +100,12 @@ def simple_chunk(text: str, chunk_size: int = 800, overlap: int = 100) -> list[s
para_words = len(para.split()) para_words = len(para.split())
if current_words + para_words > chunk_size and current: if current_words + para_words > chunk_size and current:
chunks.append("\n\n".join(current)) chunks.append("\n\n".join(current))
# overlap: keep last paragraph # overlap: arrastra un tail de 'overlap' palabras (no el párrafo
if overlap > 0 and current: # completo — eso duplicaba el tamaño cuando los párrafos eran grandes)
current = [current[-1]] if overlap > 0:
current_words = len(current[0].split()) tail = "\n\n".join(current).split()[-overlap:]
current = [" ".join(tail)]
current_words = len(tail)
else: else:
current = [] current = []
current_words = 0 current_words = 0
@@ -241,7 +262,10 @@ class ContentProcessor:
threshold=settings.quality_threshold, words=words) threshold=settings.quality_threshold, words=words)
continue continue
embedding = await self.ollama.embed(chunk[:1000]) # Embeber el chunk completo (ya acotado a ~chunk_size palabras).
# Antes truncaba a 1000 chars → el vector solo representaba el
# principio de cada chunk, degradando el ranking del RAG.
embedding = await self.ollama.embed(chunk)
await self.db.add_chunk( await self.db.add_chunk(
session_id=session_id, session_id=session_id,