diff --git a/CHANGELOG.md b/CHANGELOG.md index f188f85f..e7ca5cab 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,11 @@ - LLM tests (QA, embeddings, research graph) replay from cassettes without real API calls - docling-serve tests run without Docker container in CI - Uses pytest-recording with custom JSON body serializer +- **Contextualized FTS Search**: Full-text search now includes section headings + - New `content_fts` column stores contextualized content (headings + body text) + - FTS index now searches `content_fts` for better keyword matching on section context + - Original `content` column preserved for display and context expansion + - Migration automatically populates `content_fts` for existing databases ### Removed diff --git a/evaluations/pyproject.toml b/evaluations/pyproject.toml index 630974db..c1b0e202 100644 --- a/evaluations/pyproject.toml +++ b/evaluations/pyproject.toml @@ -2,7 +2,7 @@ name = "haiku.rag-evals" description = "Benchmarking and evaluation scripts for haiku.rag" -version = "0.23.0" +version = "0.23.1" authors = [{ name = "Yiorgis Gozadinos", email = "ggozadinos@gmail.com" }] license = { text = "MIT" } requires-python = ">=3.12" diff --git a/haiku_rag_slim/haiku/rag/chunkers/docling_local.py b/haiku_rag_slim/haiku/rag/chunkers/docling_local.py index 1bebbe07..2ca7a2f4 100644 --- a/haiku_rag_slim/haiku/rag/chunkers/docling_local.py +++ b/haiku_rag_slim/haiku/rag/chunkers/docling_local.py @@ -117,8 +117,6 @@ class DoclingLocalChunker(DocumentChunker): result: list[Chunk] = [] for chunk in raw_chunks: - # Use raw chunk text - headings are stored separately in metadata - # and prepended at embedding time for better semantic search text = chunk.text # Extract metadata from DocChunk.meta (cast to DocMeta for type safety) diff --git a/haiku_rag_slim/haiku/rag/client.py b/haiku_rag_slim/haiku/rag/client.py index 338182f9..f7a7b32f 100644 --- a/haiku_rag_slim/haiku/rag/client.py +++ b/haiku_rag_slim/haiku/rag/client.py @@ -1410,12 +1410,13 @@ class HaikuRAG: embeddings = await self.chunk_repository.embedder.embed_documents(texts) # Build updated records - for chunk, embedding in zip(chunks, embeddings): + for chunk, content_fts, embedding in zip(chunks, texts, embeddings): pending_records.append( self.store.ChunkRecord( id=chunk.id, # type: ignore[arg-type] document_id=chunk.document_id, # type: ignore[arg-type] content=chunk.content, + content_fts=content_fts, metadata=json.dumps(chunk.metadata), order=chunk.order, vector=embedding, diff --git a/haiku_rag_slim/haiku/rag/embeddings/__init__.py b/haiku_rag_slim/haiku/rag/embeddings/__init__.py index 7894ea69..f17416e1 100644 --- a/haiku_rag_slim/haiku/rag/embeddings/__init__.py +++ b/haiku_rag_slim/haiku/rag/embeddings/__init__.py @@ -32,16 +32,15 @@ class EmbedderWrapper: def contextualize(chunks: list["Chunk"]) -> list[str]: - """Prepare chunk content for embedding by adding context. + """Prepare chunk content for embedding/FTS by adding context. Prepends section headings to chunk content for better semantic search. - The embeddings will capture section context while stored content stays raw. Args: chunks: List of chunks to contextualize. Returns: - List of contextualized text strings for embedding. + List of contextualized text strings. """ texts = [] for chunk in chunks: diff --git a/haiku_rag_slim/haiku/rag/store/engine.py b/haiku_rag_slim/haiku/rag/store/engine.py index ed1b3dbb..6e2b9ace 100644 --- a/haiku_rag_slim/haiku/rag/store/engine.py +++ b/haiku_rag_slim/haiku/rag/store/engine.py @@ -40,6 +40,7 @@ def create_chunk_model(vector_dim: int): id: str = Field(default_factory=lambda: str(uuid4())) document_id: str content: str + content_fts: str = Field(default="") metadata: str = Field(default="{}") order: int = Field(default=0) vector: Vector(vector_dim) = Field(default_factory=lambda: [0.0] * vector_dim) # type: ignore @@ -288,9 +289,9 @@ class Store: self.chunks_table = self.db.open_table("chunks") else: self.chunks_table = self.db.create_table("chunks", schema=self.ChunkRecord) - # Create FTS index on the new table with phrase query support + # Create FTS index on content_fts (contextualized content) for better search self.chunks_table.create_fts_index( - "content", replace=True, with_position=True, remove_stop_words=False + "content_fts", replace=True, with_position=True, remove_stop_words=False ) # Create or get settings table @@ -392,9 +393,9 @@ class Store: self.ChunkRecord = create_chunk_model(self.embedder._vector_dim) self.chunks_table = self.db.create_table("chunks", schema=self.ChunkRecord) - # Create FTS index on the new table with phrase query support + # Create FTS index on content_fts (contextualized content) for better search self.chunks_table.create_fts_index( - "content", replace=True, with_position=True, remove_stop_words=False + "content_fts", replace=True, with_position=True, remove_stop_words=False ) def close(self): diff --git a/haiku_rag_slim/haiku/rag/store/repositories/chunk.py b/haiku_rag_slim/haiku/rag/store/repositories/chunk.py index b5e7799e..ed47af5f 100644 --- a/haiku_rag_slim/haiku/rag/store/repositories/chunk.py +++ b/haiku_rag_slim/haiku/rag/store/repositories/chunk.py @@ -27,15 +27,22 @@ class ChunkRepository: self.embedder = store.embedder def _ensure_fts_index(self) -> None: - """Ensure FTS index exists on the content column.""" + """Ensure FTS index exists on the content_fts column.""" try: self.store.chunks_table.create_fts_index( - "content", replace=True, with_position=True, remove_stop_words=False + "content_fts", replace=True, with_position=True, remove_stop_words=False ) except Exception as e: # Log the error but don't fail - FTS might already exist logger.debug(f"FTS index creation skipped: {e}") + def _contextualize_content(self, chunk: Chunk) -> str: + """Generate contextualized content for FTS by prepending headings.""" + meta = chunk.get_chunk_metadata() + if meta.headings: + return "\n".join(meta.headings) + "\n" + chunk.content + return chunk.content + async def create(self, entity: Chunk | list[Chunk]) -> Chunk | list[Chunk]: """Create one or more chunks in the database. @@ -54,6 +61,7 @@ class ChunkRepository: id=chunk_id, document_id=entity.document_id, content=entity.content, + content_fts=self._contextualize_content(entity), metadata=json.dumps( {k: v for k, v in entity.metadata.items() if k != "order"} ), @@ -86,6 +94,7 @@ class ChunkRepository: id=chunk_id, document_id=chunk.document_id, content=chunk.content, + content_fts=self._contextualize_content(chunk), metadata=json.dumps( {k: v for k, v in chunk.metadata.items() if k != "order"} ), @@ -136,6 +145,7 @@ class ChunkRepository: values={ "document_id": entity.document_id, "content": entity.content, + "content_fts": self._contextualize_content(entity), "metadata": json.dumps( {k: v for k, v in entity.metadata.items() if k != "order"} ), @@ -190,9 +200,9 @@ class ChunkRepository: self.store.chunks_table = self.store.db.create_table( "chunks", schema=self.store.ChunkRecord ) - # Create FTS index on the new table with phrase query support + # Create FTS index on content_fts (contextualized content) for better search self.store.chunks_table.create_fts_index( - "content", replace=True, with_position=True, remove_stop_words=False + "content_fts", replace=True, with_position=True, remove_stop_words=False ) async def delete_by_document_id(self, document_id: str) -> bool: @@ -409,6 +419,7 @@ class ChunkRepository: id=str(row["id"]), document_id=str(row["document_id"]), content=str(row["content"]), + content_fts=str(row.get("content_fts", "")), metadata=str(row["metadata"]), order=int(row["order"]) if "order" in row else 0, ) diff --git a/haiku_rag_slim/haiku/rag/store/upgrades/__init__.py b/haiku_rag_slim/haiku/rag/store/upgrades/__init__.py index ef8661fb..43a93dc4 100644 --- a/haiku_rag_slim/haiku/rag/store/upgrades/__init__.py +++ b/haiku_rag_slim/haiku/rag/store/upgrades/__init__.py @@ -66,9 +66,13 @@ from haiku.rag.store.upgrades.v0_19_6 import ( from haiku.rag.store.upgrades.v0_20_0 import ( upgrade_add_docling_document as upgrade_0_20_0_docling, ) +from haiku.rag.store.upgrades.v0_23_1 import ( + upgrade_contextualize_chunks as upgrade_0_23_1_contextualize, +) upgrades.append(upgrade_0_9_3_order) upgrades.append(upgrade_0_9_3_fts) upgrades.append(upgrade_0_10_1_add_title) upgrades.append(upgrade_0_19_6_embeddings) upgrades.append(upgrade_0_20_0_docling) +upgrades.append(upgrade_0_23_1_contextualize) diff --git a/haiku_rag_slim/haiku/rag/store/upgrades/v0_23_1.py b/haiku_rag_slim/haiku/rag/store/upgrades/v0_23_1.py new file mode 100644 index 00000000..5797ee7e --- /dev/null +++ b/haiku_rag_slim/haiku/rag/store/upgrades/v0_23_1.py @@ -0,0 +1,100 @@ +import json + +from lancedb.pydantic import LanceModel, Vector +from pydantic import Field + +from haiku.rag.store.engine import Store +from haiku.rag.store.upgrades import Upgrade + + +def _apply_add_content_fts(store: Store) -> None: # pragma: no cover + """Add content_fts column with contextualized content for better FTS.""" + # Read existing chunks + try: + chunks_arrow = store.chunks_table.search().to_arrow() + rows = chunks_arrow.to_pylist() + except Exception: + return + + if not rows: + return + + # Infer vector dimensions from first row + vec = rows[0].get("vector") + if not isinstance(vec, list) or not vec: + return + vector_dim = len(vec) + + class ChunkRecord(LanceModel): + id: str + document_id: str + content: str + content_fts: str = Field(default="") + metadata: str = Field(default="{}") + order: int = Field(default=0) + vector: Vector(vector_dim) = Field( # type: ignore + default_factory=lambda: [0.0] * vector_dim + ) + + # Drop and recreate table with new schema + try: + store.db.drop_table("chunks") + except Exception: + pass + + store.chunks_table = store.db.create_table("chunks", schema=ChunkRecord) + + # Populate content_fts with contextualized content + new_records: list[ChunkRecord] = [] + for row in rows: + metadata_raw = row.get("metadata") or "{}" + try: + metadata = ( + json.loads(metadata_raw) + if isinstance(metadata_raw, str) + else metadata_raw + ) + except Exception: + metadata = {} + + headings = metadata.get("headings") if isinstance(metadata, dict) else None + content = row.get("content", "") + + # Build contextualized content for FTS + if headings: + content_fts = "\n".join(headings) + "\n" + content + else: + content_fts = content + + new_records.append( + ChunkRecord( + id=row.get("id"), + document_id=row.get("document_id"), + content=content, + content_fts=content_fts, + metadata=metadata_raw, + order=row.get("order", 0), + vector=row.get("vector") or [0.0] * vector_dim, + ) + ) + + if new_records: + store.chunks_table.add(new_records) + + # Drop old FTS index on content column if it exists + try: + store.chunks_table.drop_index("content_idx") + except Exception: + pass + + # Create FTS index on content_fts + store.chunks_table.create_fts_index( + "content_fts", replace=True, with_position=True, remove_stop_words=False + ) + + +upgrade_contextualize_chunks = Upgrade( + version="0.23.1", + apply=_apply_add_content_fts, + description="Add content_fts column for contextualized FTS search", +) diff --git a/haiku_rag_slim/pyproject.toml b/haiku_rag_slim/pyproject.toml index e7e65abf..e1b922c1 100644 --- a/haiku_rag_slim/pyproject.toml +++ b/haiku_rag_slim/pyproject.toml @@ -2,7 +2,7 @@ name = "haiku.rag-slim" description = "Opinionated agentic RAG powered by LanceDB, Pydantic AI, and Docling - Minimal dependencies" -version = "0.23.0" +version = "0.23.1" authors = [{ name = "Yiorgis Gozadinos", email = "ggozadinos@gmail.com" }] license = { text = "MIT" } readme = { file = "README.md", content-type = "text/markdown" } diff --git a/pyproject.toml b/pyproject.toml index 770556a1..d5db3079 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -2,7 +2,7 @@ name = "haiku.rag" description = "Opinionated agentic RAG powered by LanceDB, Pydantic AI, and Docling" -version = "0.23.0" +version = "0.23.1" authors = [{ name = "Yiorgis Gozadinos", email = "ggozadinos@gmail.com" }] license = { text = "MIT" } readme = { file = "README.md", content-type = "text/markdown" } diff --git a/tests/cassettes/test_chunk/test_chunk_content_fts_populated.yaml b/tests/cassettes/test_chunk/test_chunk_content_fts_populated.yaml new file mode 100644 index 00000000..b7741c18 --- /dev/null +++ b/tests/cassettes/test_chunk/test_chunk_content_fts_populated.yaml @@ -0,0 +1,42 @@ +interactions: +- request: + headers: + accept: + - application/json + accept-encoding: + - gzip, deflate, zstd + connection: + - keep-alive + content-length: + - '100' + content-type: + - application/json + host: + - localhost:11434 + method: POST + parsed_body: + encoding_format: base64 + input: + - This is the raw chunk content. + model: qwen3-embedding:4b + uri: http://localhost:11434/v1/embeddings + response: + headers: + content-type: + - application/json + transfer-encoding: + - chunked + parsed_body: + data: + - embedding: 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 + index: 0 + object: embedding + model: qwen3-embedding:4b + object: list + usage: + prompt_tokens: 8 + total_tokens: 8 + status: + code: 200 + message: OK +version: 1 diff --git a/tests/cassettes/test_chunk/test_chunk_content_fts_without_headings.yaml b/tests/cassettes/test_chunk/test_chunk_content_fts_without_headings.yaml new file mode 100644 index 00000000..d671f5ac --- /dev/null +++ b/tests/cassettes/test_chunk/test_chunk_content_fts_without_headings.yaml @@ -0,0 +1,42 @@ +interactions: +- request: + headers: + accept: + - application/json + accept-encoding: + - gzip, deflate, zstd + connection: + - keep-alive + content-length: + - '101' + content-type: + - application/json + host: + - localhost:11434 + method: POST + parsed_body: + encoding_format: base64 + input: + - Plain content without headings. + model: qwen3-embedding:4b + uri: http://localhost:11434/v1/embeddings + response: + headers: + content-type: + - application/json + transfer-encoding: + - chunked + parsed_body: + data: + - embedding: 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 + index: 0 + object: embedding + model: qwen3-embedding:4b + object: list + usage: + prompt_tokens: 6 + total_tokens: 6 + status: + code: 200 + message: OK +version: 1 diff --git a/tests/test_chunk.py b/tests/test_chunk.py index 2738b383..94bdaf75 100644 --- a/tests/test_chunk.py +++ b/tests/test_chunk.py @@ -342,3 +342,86 @@ def test_search_result_get_primary_label(): # Empty labels result = SearchResult(content="x", score=0.5, labels=[]) assert result._get_primary_label() is None + + +@pytest.mark.vcr() +async def test_chunk_content_fts_populated(temp_db_path): + """Test that content_fts column is populated with contextualized content.""" + from haiku.rag.embeddings import get_embedder + + async with HaikuRAG(db_path=temp_db_path, config=Config, create=True) as client: + # Create a chunk with headings + chunk = Chunk( + document_id="test-doc", + content="This is the raw chunk content.", + metadata={"headings": ["Chapter 1", "Section 1.1"]}, + order=0, + ) + + # Generate embedding + embedder = get_embedder(Config) + embedding = (await embedder.embed_documents([chunk.content]))[0] + chunk.embedding = embedding + + # Store the chunk + await client.chunk_repository.create(chunk) + + # Read the raw record from the database + records = list( + client.store.chunks_table.search() + .where(f"id = '{chunk.id}'") + .limit(1) + .to_arrow() + .to_pylist() + ) + + assert len(records) == 1 + record = records[0] + + # Verify content is raw (no headings) + assert record["content"] == "This is the raw chunk content." + + # Verify content_fts is contextualized (headings + content) + assert ( + record["content_fts"] + == "Chapter 1\nSection 1.1\nThis is the raw chunk content." + ) + + +@pytest.mark.vcr() +async def test_chunk_content_fts_without_headings(temp_db_path): + """Test that content_fts equals content when no headings present.""" + from haiku.rag.embeddings import get_embedder + + async with HaikuRAG(db_path=temp_db_path, config=Config, create=True) as client: + # Create a chunk without headings + chunk = Chunk( + document_id="test-doc", + content="Plain content without headings.", + metadata={}, + order=0, + ) + + # Generate embedding + embedder = get_embedder(Config) + embedding = (await embedder.embed_documents([chunk.content]))[0] + chunk.embedding = embedding + + # Store the chunk + await client.chunk_repository.create(chunk) + + # Read the raw record from the database + records = list( + client.store.chunks_table.search() + .where(f"id = '{chunk.id}'") + .limit(1) + .to_arrow() + .to_pylist() + ) + + assert len(records) == 1 + record = records[0] + + # Both should be the same when no headings + assert record["content"] == "Plain content without headings." + assert record["content_fts"] == "Plain content without headings." diff --git a/uv.lock b/uv.lock index 2889f0cd..534821a0 100644 --- a/uv.lock +++ b/uv.lock @@ -1248,7 +1248,7 @@ wheels = [ [[package]] name = "haiku-rag" -version = "0.23.0" +version = "0.23.1" source = { editable = "." } dependencies = [ { name = "haiku-rag-slim", extra = ["cohere", "docling", "inspector", "mxbai", "voyageai", "zeroentropy"] }, @@ -1303,7 +1303,7 @@ dev = [ [[package]] name = "haiku-rag-evals" -version = "0.23.0" +version = "0.23.1" source = { editable = "evaluations" } dependencies = [ { name = "datasets" }, @@ -1324,7 +1324,7 @@ requires-dist = [ [[package]] name = "haiku-rag-slim" -version = "0.23.0" +version = "0.23.1" source = { editable = "haiku_rag_slim" } dependencies = [ { name = "docling-core" },