Skip to content

Feat/chunking - #165

Merged
Ahmath-Gadji merged 6 commits into
devfrom
feat/chunking
Dec 17, 2025
Merged

Feat/chunking#165
Ahmath-Gadji merged 6 commits into
devfrom
feat/chunking

Conversation

@Ahmath-Gadji

@Ahmath-Gadji Ahmath-Gadji commented Dec 8, 2025

Copy link
Copy Markdown
Collaborator

Improved Chunking Strategy

This update introduces a smarter, more structured approach to document chunking. The parsed markdown is split into distinct element types: text, image_description, and tables.

Text

  • Text elements are concatenated and chunked intelligently.
  • Splitting is done recursively: first by newlines (paragraphs), then by punctuation if paragraphs exceed the target size.
  • Chunk size is controlled, with a defined overlap between consecutive chunks.

Images

  • Image descriptions are isolated into their own chunks.

Tables

  • Large tables are split into smaller, manageable table chunks.
  • Each table chunk includes the table header.
  • There is a one-row overlap between consecutive table chunks.

Retrieval

When retrieving documents, adjacent chunks can be included optionally and reranked. By this, we might find even more relevant chunks that were not retrieved.

Generation

Answer generation now enforces strict context-size control to prevent token overflow and ensure stable responses.

Summary by CodeRabbit

  • New Features

    • Surrounding-chunks retrieval to include adjacent context in search results.
    • Page-aware chunking and chunk contextualization for richer document segments.
  • Improvements

    • Language detection support and token-aware context formatting.
    • Reranker top_k increased (5 → 10) and streamlined chunking strategy.
  • Configuration

    • Added MAX_MODEL_LEN (default 8192) and WITH_SURROUNDING_CHUNKS option; Docker defaults aligned.
  • Documentation

    • Updated env var docs and prompt guidance.

✏️ Tip: You can customize this high-level summary in your review settings.

@Ahmath-Gadji Ahmath-Gadji linked an issue Dec 8, 2025 that may be closed by this pull request
@Ahmath-Gadji
Ahmath-Gadji force-pushed the feat/chunking branch 2 times, most recently from b249d15 to d890071 Compare December 9, 2025 09:23
@coderabbitai

coderabbitai Bot commented Dec 9, 2025

Copy link
Copy Markdown

Walkthrough

Refactors chunking to a single RecursiveSplitter with page-aware MDElement representations, adds chunk contextualization (language-aware), supports surrounding-chunk retrieval, introduces token-aware context formatting, increases embedder max_model_len to 8192, and updates configs, docs, and tests.

Changes

Cohort / File(s) Summary
Configuration Consolidation
.hydra_config/config.yaml, .hydra_config/chunker/* (deleted), .hydra_config/rag/*
Removed semantic/markdown splitter configs; defaults use recursive_splitter. Added embedder.max_model_len (8192). Increased reranker.top_k to 10. Added base.yaml defaults for RAG files and retriever with_surrounding_chunks.
Chunker Architecture Refactor
openrag/components/indexer/chunker/chunker.py
Added ChunkContextualizer, BASE_CHUNK_FORMAT, CHUNK_FORMAT. Overhauled BaseChunker lifecycle and contextualization helpers; made RecursiveSplitter primary; removed SemanticsSplitter and MarkDownSplitter; updated factory registry.
Chunking Utilities & Page-Aware Processing
openrag/components/indexer/chunker/utils.py
Introduced MDElement class and PAGE_RE; added page-aware parsing (split_md_elements, get_page_number), table parsing/chunking (parse_markdown_table, chunk_table, clean_markdown_table_spacing); removed older token-based helpers.
Embedding Truncation Option
openrag/components/indexer/embeddings/openai.py
Added max_model_len attribute (default 8192) and pass extra_body={"truncate_prompt_tokens": self.max_model_len} to embedding API.
Vector DB: Surrounding Chunks & Metadata
openrag/components/indexer/vectordb/vectordb.py
Extended search APIs with with_surrounding_chunks; added _gen_chunk_order_metadata, updated parsing to produce Document objects, implemented get_surrounding_chunks, and expanded analyzer stop words for chunk markers.
Retriever & Propagation
openrag/components/retriever.py
Added with_surrounding_chunks to BaseRetriever and propagated it to vector DB calls. Renamed SingleRetreiverSingleRetriever and updated factory mapping and validation.
Context Formatting & Lang Detection
openrag/components/utils.py
Made format_context token-aware with max_context_tokens cap and trimming logic; added language detection setup and detect_language, plus module logger and globals.
Pipeline Changes
openrag/components/pipeline.py
Added RagPipeline.max_context_tokens (config-derived) and passed it into format_context calls.
Prompts
openrag/components/prompts/prompts.py, prompts/example1/*
load_prompt now returns str; renamed prompt var to CHUNK_CONTEXTUALIZER_PROMPT. Updated chunk contextualizer template and added "Do not cite sources or file names" rule.
Loaders: Image Description Formatting
openrag/components/indexer/loaders/base.py
get_image_description now wraps description with extra blank lines inside <image_description> tags.
Tests & Pytest Env
openrag/components/indexer/chunker/test_chunking.py, pytest.ini
Added comprehensive tests for md splitting/table chunking and updated pytest env vars (CONFIG_PATH, PROMPTS_DIR, LOG_DIR).
Docs & Smoke Test Env
docs/content/docs/documentation/env_vars.md, .github/workflows/smoke_test/.env
Doc updates: removed deprecated chunkers, added MAX_MODEL_LEN (8192) and WITH_SURROUNDING_CHUNKS. Added MAX_MODEL_LEN to smoke test env.
Docker & Dependencies
docker-compose.yaml, quick_start/docker-compose.yaml, pyproject.toml
Lowered vllm max-model-len defaults from 8194 → 8192. Replaced duplicate langdetect with fast-langdetect in dependencies.

Sequence Diagram

sequenceDiagram
    participant Client
    participant Pipeline as RAG Pipeline
    participant Chunker
    participant Ctx as ChunkContextualizer
    participant VectorDB
    participant Retriever

    Client->>Pipeline: submit document / request
    Pipeline->>Chunker: split_document(content)
    Chunker->>Chunker: _prepare_md_elements → MDElement list
    Chunker->>Chunker: split_text / chunk_table → chunks
    Chunker->>Ctx: detect_language(chunk_text)
    Ctx-->>Chunker: language
    Chunker->>Ctx: generate_context(chunk, lang)
    Ctx-->>Chunker: contextualized_chunk
    Chunker-->>Pipeline: list[Document] (with metadata)
    Pipeline->>VectorDB: async_add_documents(chunks)
    VectorDB->>VectorDB: attach order metadata & index
    Client->>Pipeline: retrieve(query)
    Pipeline->>Retriever: retrieve(query)
    Retriever->>VectorDB: async_search(query, with_surrounding_chunks=True)
    VectorDB->>VectorDB: search & get_surrounding_chunks(results)
    VectorDB-->>Retriever: list[Document] (including surrounding)
    Retriever-->>Pipeline: retrieved_docs
    Pipeline->>Pipeline: format_context(docs, max_context_tokens)
    Pipeline-->>Client: formatted_context
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

  • Focus review on: openrag/components/indexer/chunker/chunker.py, openrag/components/indexer/chunker/utils.py, openrag/components/indexer/vectordb/vectordb.py, openrag/components/utils.py, and configuration migration for hydra paths.

Poem

🐰
I nibble lines and stitch them neat,
Pages numbered, chunks complete,
I whisper language, wrap the text,
Surrounding bits found and indexed,
A hoppity RAG, all tidy and sweet.

Pre-merge checks and finishing touches

❌ Failed checks (1 warning, 1 inconclusive)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 49.21% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
Title check ❓ Inconclusive The title 'Feat/chunking' is vague and generic, lacking specificity about what chunking improvements were made. Use a more descriptive title that captures the main change, such as 'Refactor document chunking with recursive splitting and contextualization' or 'Implement improved markdown chunking with element-based parsing'.
✅ Passed checks (1 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feat/chunking

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

codescene-delta-analysis[bot]

This comment was marked as outdated.

codescene-delta-analysis[bot]

This comment was marked as outdated.

codescene-delta-analysis[bot]

This comment was marked as outdated.

@Ahmath-Gadji

Copy link
Copy Markdown
Collaborator Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Dec 10, 2025

Copy link
Copy Markdown
✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 8

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
openrag/components/indexer/loaders/base.py (1)

127-130: Align error message formatting with the updated image description format.

Lines 127 and 130 return error messages wrapped in <image_description> tags using single newlines, while line 153 now uses double newlines. This inconsistency may cause downstream parsing or chunking logic to handle error cases differently than successful descriptions.

Apply this diff to align the formatting:

-                            return """\n<image_description>\nInvalid image data format\n</image_description>\n"""
+                            return """<image_description>\n\nInvalid image data format\n\n</image_description>"""
-                        return """\n<image_description>\nUnsupported image data type\n</image_description>\n"""
+                        return """<image_description>\n\nUnsupported image data type\n\n</image_description>"""
openrag/components/indexer/chunker/chunker.py (1)

327-331: Dead code: semantic_splitter is not registered in CHUNKERS.

This code block is unreachable because semantic_splitter is not in the CHUNKERS dictionary. The factory raises ValueError at line 320-324 before reaching this block. Either remove this dead code or register the semantic splitter in CHUNKERS.

-        # Add embeddings if semantic splitter is selected
-        if name == "semantic_splitter":
-            embedder = EmbeddingFactory.get_embedder(
-                embeddings_config=dict(config.embedder)
-            )
-            chunker_params["embeddings"] = embedder
-
         chunker_params["llm_config"] = config.vlm
         return chunker_cls(**chunker_params)
🧹 Nitpick comments (11)
prompts/example1/chunk_contextualizer_tmpl.txt (1)

1-4: Tighten wording and align terminology in the chunk contextualizer prompt

You could make this a bit clearer and more consistent with the later “Previous Chunks”/“Current Chunk” labels:

  • Line 4: avoid “with respect to” and align with plural “Previous Chunks”, e.g.:
-Write a concise standalone sentence that summarizes the context of current chunk with respect to the previous chunk using the following context
+Write a concise standalone sentence that summarizes the context of the current chunk and how it relates to the previous chunks, using the following information:
  • Line 23: align with other bullets by adding a colon:
-- Current Chunk
+- Current Chunk:

Also applies to: 23-24

openrag/components/utils.py (1)

124-146: Instantiating ChatOpenAI on every call is inefficient.

ChatOpenAI(**config.llm) creates a new client instance each time format_context is called. This involves connection setup overhead. Consider caching the LLM instance at module level or using a lazy singleton pattern.

+# Module-level cached LLM for tokenization
+_tokenizer_llm = None
+
+def _get_tokenizer_llm():
+    global _tokenizer_llm
+    if _tokenizer_llm is None:
+        _tokenizer_llm = ChatOpenAI(**config.llm)
+    return _tokenizer_llm
+
+
 def format_context(docs: list[Document], max_context_tokens: int = 4096) -> str:
     if not docs:
         return "No document found from the database"

-    llm = ChatOpenAI(**config.llm)
+    llm = _get_tokenizer_llm()
     _length_function = llm.get_num_tokens
openrag/components/indexer/chunker/utils.py (3)

23-23: Use explicit Optional type hint for nullable parameter.

PEP 484 prohibits implicit Optional. Update the type hint for page_number.

     def __init__(
         self,
         type: Literal["text", "table", "image"],
         content: str,
-        page_number: int = None,
+        page_number: int | None = None,
     ):

223-225: Use explicit Optional type hint for length_function.

 def chunk_table(
-    table_element: MDElement, chunk_size: int = 512, length_function: callable = None
+    table_element: MDElement, chunk_size: int = 512, length_function: callable | None = None
 ) -> list[MDElement]:

94-94: Text segments lack page number assignment.

Text segments are appended as ("text", text_segment.strip()) without a page number, while tables and images include page_num. This inconsistency means text MDElement objects will have page_number=None while tables/images have actual page numbers. Consider assigning page numbers to text segments as well for consistency.

         if start > last:
             text_segment = md_text[last:start]
             if text_segment.strip():  # Only add non-empty text segments
-                parts.append(("text", text_segment.strip()))
+                text_page_num = get_page_number(last, page_markers)
+                parts.append(("text", text_segment.strip(), text_page_num))

Apply similar change for the remaining text at line 104:

         remaining_text = md_text[last:]
         if remaining_text.strip():  # Only add non-empty text segments
-            parts.append(("text", remaining_text.strip()))
+            remaining_page_num = get_page_number(last, page_markers)
+            parts.append(("text", remaining_text.strip(), remaining_page_num))
openrag/components/indexer/vectordb/vectordb.py (2)

387-388: Add strict=True to zip() to catch length mismatches.

While the lengths should match by construction, using strict=True provides a safety check and documents the expectation.

             order_metadata_l: list[dict] = _gen_chunk_order_metadata(n=len(chunks))
-            for chunk, vector, order_metadata in zip(chunks, vectors, order_metadata_l):
+            for chunk, vector, order_metadata in zip(chunks, vectors, order_metadata_l, strict=True):

556-556: Typo: existant_ids should be existing_ids.

-        existant_ids = set(doc.metadata.get("_id") for doc in docs)
+        existing_ids = set(doc.metadata.get("_id") for doc in docs)

And update subsequent usages on lines 581-582.

openrag/components/indexer/chunker/chunker.py (4)

43-44: Re-raise exception with context using from e.

This preserves the exception chain for debugging.

         except Exception as e:
-            raise ValueError(f"Error creating context generator: {e}")
+            raise ValueError(f"Error creating context generator: {e}") from e

118-118: BaseChunker inherits from ABC but has no abstract methods.

Either add @abstractmethod decorators to methods that subclasses must implement, or remove the ABC inheritance since all methods have concrete implementations.

-class BaseChunker(ABC):
+class BaseChunker:
     """Base class for document chunkers with built-in contextualization capability."""

70-72: Remove unused metadata parameter.

The metadata parameter is not used in this method.

     async def contextualize_chunks(
-        self, chunks: list[Document], metadata: dict = None
+        self, chunks: list[Document]
     ) -> list[Document]:

Also update the call site at line 152:

-        return await self.contextualizer.contextualize_chunks(chunks, metadata=metadata)
+        return await self.contextualizer.contextualize_chunks(chunks)

145-147: Use explicit Optional type hints.

     async def _apply_contextualization(
-        self, chunks: list[Document], metadata: dict = None
+        self, chunks: list[Document], metadata: dict | None = None
     ) -> list[Document]:

Similarly at line 196:

     def _get_chunks(
-        self, content: str, metadata: dict = None, log=None
+        self, content: str, metadata: dict | None = None, log=None
     ) -> list[Document]:
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 097c92e and 5e5b206.

📒 Files selected for processing (21)
  • .github/workflows/smoke_test/.env (1 hunks)
  • .hydra_config/chunker/semantic_splitter.yaml (0 hunks)
  • .hydra_config/chunker/token_splitter.yaml (0 hunks)
  • .hydra_config/config.yaml (3 hunks)
  • .hydra_config/rag/ChatBotRag.yaml (1 hunks)
  • .hydra_config/rag/SimpleRag.yaml (1 hunks)
  • .hydra_config/rag/base.yaml (1 hunks)
  • .hydra_config/retriever/base.yaml (1 hunks)
  • docker-compose.yaml (2 hunks)
  • docs/content/docs/documentation/env_vars.md (3 hunks)
  • openrag/components/indexer/chunker/chunker.py (2 hunks)
  • openrag/components/indexer/chunker/utils.py (2 hunks)
  • openrag/components/indexer/embeddings/openai.py (2 hunks)
  • openrag/components/indexer/loaders/base.py (1 hunks)
  • openrag/components/indexer/vectordb/vectordb.py (11 hunks)
  • openrag/components/pipeline.py (3 hunks)
  • openrag/components/prompts/prompts.py (1 hunks)
  • openrag/components/retriever.py (4 hunks)
  • openrag/components/utils.py (3 hunks)
  • prompts/example1/chunk_contextualizer_tmpl.txt (1 hunks)
  • quick_start/docker-compose.yaml (2 hunks)
💤 Files with no reviewable changes (2)
  • .hydra_config/chunker/semantic_splitter.yaml
  • .hydra_config/chunker/token_splitter.yaml
🧰 Additional context used
🧬 Code graph analysis (4)
openrag/components/pipeline.py (1)
openrag/components/utils.py (1)
  • format_context (124-146)
openrag/components/utils.py (2)
openrag/utils/logger.py (1)
  • get_logger (10-47)
openrag/config/config.py (1)
  • load_config (12-29)
openrag/components/indexer/chunker/chunker.py (1)
openrag/components/indexer/chunker/utils.py (5)
  • MDElement (16-30)
  • chunk_table (187-220)
  • chunk_table (223-279)
  • get_chunk_page_number (109-141)
  • split_md_elements (52-106)
openrag/components/indexer/vectordb/vectordb.py (1)
openrag/components/indexer/indexer.py (1)
  • chunk (53-57)
🪛 dotenv-linter (4.0.0)
.github/workflows/smoke_test/.env

[warning] 29-29: [ValueWithoutQuotes] This value needs to be surrounded in quotes

(ValueWithoutQuotes)

🪛 LanguageTool
docs/content/docs/documentation/env_vars.md

[grammar] ~123-~123: Ensure spelling is correct
Context: ...k exceeds this limit, the embedder will trucate it.| If you prefer to use an **externa...

(QB_NEW_EN_ORTHOGRAPHY_ERROR_IDS_1)


[grammar] ~123-~123: Ensure spelling is correct
Context: ...his limit, the embedder will trucate it.| If you prefer to use an **external embed...

(QB_NEW_EN_ORTHOGRAPHY_ERROR_IDS_1)

prompts/example1/chunk_contextualizer_tmpl.txt

[style] ~1-~1: Consider using “who” when you are referring to a person instead of an object.
Context: You are an AI assistant that creates brief contextual summaries to m...

(THAT_WHO)


[style] ~4-~4: ‘with respect to’ might be wordy. Consider a shorter alternative.
Context: ...summarizes the context of current chunk with respect to the previous chunk using the following ...

(EN_WORDINESS_PREMIUM_WITH_RESPECT_TO)

🪛 markdownlint-cli2 (0.18.1)
docs/content/docs/documentation/env_vars.md

218-218: Bare URL used

(MD034, no-bare-urls)

🪛 Ruff (0.14.8)
openrag/components/indexer/chunker/chunker.py

43-43: Do not catch blind exception: Exception

(BLE001)


44-44: Within an except clause, raise exceptions with raise ... from err or raise ... from None to distinguish them from errors in exception handling

(B904)


44-44: Avoid specifying long messages outside the exception class

(TRY003)


64-64: Do not catch blind exception: Exception

(BLE001)


71-71: Unused method argument: metadata

(ARG002)


71-71: PEP 484 prohibits implicit Optional

Convert to T | None

(RUF013)


110-110: zip() without an explicit strict= parameter

Add explicit value for parameter strict=

(B905)


113-113: Do not catch blind exception: Exception

(BLE001)


118-118: BaseChunker is an abstract base class, but it has no abstract methods or properties

(B024)


127-127: Unused method argument: kwargs

(ARG002)


146-146: PEP 484 prohibits implicit Optional

Convert to T | None

(RUF013)


196-196: PEP 484 prohibits implicit Optional

Convert to T | None

(RUF013)

openrag/components/indexer/chunker/utils.py

23-23: PEP 484 prohibits implicit Optional

Convert to T | None

(RUF013)


188-188: PEP 484 prohibits implicit Optional

Convert to T | None

(RUF013)


199-199: Loop control variable groups_ntoks overrides iterable it iterates

(B020)


199-199: zip() without an explicit strict= parameter

Add explicit value for parameter strict=

(B905)


223-223: Redefinition of unused chunk_table from line 187

(F811)


224-224: PEP 484 prohibits implicit Optional

Convert to T | None

(RUF013)


245-245: zip() without an explicit strict= parameter

Add explicit value for parameter strict=

(B905)

openrag/components/indexer/vectordb/vectordb.py

388-388: zip() without an explicit strict= parameter

Add explicit value for parameter strict=

(B905)


532-532: Consider moving this statement to an else block

(TRY300)

🔇 Additional comments (28)
openrag/components/indexer/embeddings/openai.py (1)

13-18: max_model_len plumbing looks good; confirm backend support for truncate_prompt_tokens

The new max_model_len field and its propagation via extra_body={"truncate_prompt_tokens": self.max_model_len} are consistent and align with the documented default of 8192. Just ensure your OpenAI‑compatible embedder actually honors truncate_prompt_tokens for embeddings; on some providers this field may be ignored or use a different name, which would defeat the intended truncation.

Also applies to: 37-41

.hydra_config/retriever/base.yaml (1)

3-4: Retriever config additions are consistent with env/docs

similarity_threshold and the new with_surrounding_chunks flag are wired in a standard Hydra/oc.env pattern and match the documented environment variables and behavior.

docs/content/docs/documentation/env_vars.md (1)

192-196: Retriever env var docs look coherent with config

The new entries for RETRIEVER_TYPE and WITH_SURROUNDING_CHUNKS clearly describe the available strategies and surrounding‑chunk behavior, and they match the retriever/base Hydra config and PR intent.

docker-compose.yaml (1)

47-47: Aligned --max-model-len defaults with documented 8192 limit

Both GPU and CPU vLLM commands now default --max-model-len to ${MAX_MODEL_LEN:-8192}, matching the new env var and documentation. This keeps docker defaults consistent with the embedder config.

Also applies to: 146-146

quick_start/docker-compose.yaml (1)

45-45: Quick-start compose now matches global max-model-len defaults

The quick-start VLLM templates use --max-model-len ${MAX_MODEL_LEN:-8192} for both GPU and CPU, keeping behavior aligned with the main docker-compose.yaml and the documented MAX_MODEL_LEN.

Also applies to: 142-142

.hydra_config/rag/SimpleRag.yaml (1)

1-3: Hydra defaults inheritance for SimpleRag looks correct

Adding:

defaults:
  - base
mode: SimpleRag

is a standard Hydra pattern to inherit common RAG settings from base while keeping a SimpleRag mode. This should simplify config reuse without changing behavior.

.hydra_config/rag/base.yaml (1)

1-4: LGTM!

Clean extraction of shared RAG configuration defaults. The values (chat_history_depth: 4, max_contextualized_query_len: 512) provide sensible defaults that child configurations can inherit and override.

.hydra_config/rag/ChatBotRag.yaml (1)

1-3: LGTM!

Good use of Hydra's defaults mechanism to inherit from base.yaml, reducing configuration duplication while maintaining explicit mode specification.

.hydra_config/config.yaml (3)

3-3: LGTM!

Consolidating to recursive_splitter as the default chunker aligns with the PR's chunking strategy improvements.


34-34: LGTM!

New max_model_len parameter with sensible default of 8192 tokens. Properly uses oc.decode for type conversion from environment variable.


53-53: LGTM!

Increasing top_k from 5 to 10 provides more documents for reranking, which complements the new surrounding chunks feature. The inline comment helpfully explains the rationale.

openrag/components/pipeline.py (3)

74-79: Calculation assumes uniform chunk sizes.

The formula top_k * chunk_size provides a reasonable upper-bound estimate, but note that with the new chunking strategy (tables, images), actual chunk sizes may vary significantly from the configured chunk_size. The format_context function will handle overflow by truncation, so this isn't a correctness issue, but context utilization could be suboptimal.

Consider whether this heuristic adequately represents the actual token budget when chunks have variable sizes. If chunks are typically smaller than chunk_size, you may be under-utilizing the context window.


152-152: LGTM!

Properly passes max_context_tokens to format_context, enabling token-aware context construction.


183-183: LGTM!

Consistent with the chat completion path, properly passing max_context_tokens for completions.

openrag/components/retriever.py (4)

35-41: LGTM!

Clean addition of with_surrounding_chunks parameter with sensible default. Properly stored as instance attribute for use in retrieval calls.


54-54: LGTM!

Correctly propagates the with_surrounding_chunks flag to the vector database search.


100-100: LGTM!

Consistent propagation of with_surrounding_chunks in MultiQueryRetriever.


141-141: LGTM!

Consistent propagation of with_surrounding_chunks in HyDeRetriever.

openrag/components/utils.py (2)

9-14: LGTM!

Appropriate imports and logger initialization for the new token-aware functionality.


163-164: Module-level semaphore initialization is intentional.

These calls ensure the distributed semaphore actors are created at import time. This is a valid pattern for eager initialization of Ray actors.

openrag/components/prompts/prompts.py (1)

33-33: All usages of the renamed variable have been properly updated. The old name CHUNK_CONTEXTUALIZER has no remaining references in the codebase, and the new name CHUNK_CONTEXTUALIZER_PMPT is correctly imported and used in openrag/components/indexer/chunker/chunker.py.

openrag/components/indexer/chunker/utils.py (2)

109-141: LGTM!

The page number detection logic correctly handles edge cases for chunks with page markers.


282-306: LGTM!

The table spacing normalization handles edge cases properly.

openrag/components/indexer/vectordb/vectordb.py (2)

523-532: LGTM!

The surrounding chunks retrieval and result extension logic is correct.


129-131: LGTM!

Stop words correctly exclude the new chunk formatting markers from being indexed.

openrag/components/indexer/chunker/chunker.py (3)

195-253: LGTM!

The chunk assembly logic correctly handles text splitting, table chunking, and page metadata assignment. The final sort by page ensures proper document order.


279-300: LGTM!

The RecursiveSplitter correctly initializes the text splitter with regex separators for paragraph and sentence-level splitting.


255-276: LGTM!

The document splitting flow is clean and well-logged.

Comment thread .github/workflows/smoke_test/.env Outdated
Comment thread docs/content/docs/documentation/env_vars.md Outdated
Comment thread docs/content/docs/documentation/env_vars.md Outdated
Comment thread openrag/components/indexer/chunker/utils.py Outdated
Comment thread openrag/components/indexer/chunker/utils.py Outdated
Comment thread openrag/components/indexer/loaders/base.py
Comment thread openrag/components/indexer/vectordb/vectordb.py Outdated
Comment thread openrag/components/indexer/vectordb/vectordb.py
@Ahmath-Gadji
Ahmath-Gadji marked this pull request as ready for review December 11, 2025 08:52
codescene-delta-analysis[bot]

This comment was marked as outdated.

codescene-delta-analysis[bot]

This comment was marked as outdated.

codescene-delta-analysis[bot]

This comment was marked as outdated.

codescene-delta-analysis[bot]

This comment was marked as outdated.

codescene-delta-analysis[bot]

This comment was marked as outdated.

codescene-delta-analysis[bot]

This comment was marked as outdated.

@Ahmath-Gadji

Copy link
Copy Markdown
Collaborator Author

@coderabbitai generate docstrings

coderabbitai Bot added a commit that referenced this pull request Dec 11, 2025
Docstrings generation was requested by @Ahmath-Gadji.

* #165 (comment)

The following files were modified:

* `openrag/components/indexer/chunker/chunker.py`
* `openrag/components/indexer/chunker/test_chunking.py`
* `openrag/components/indexer/chunker/utils.py`
* `openrag/components/indexer/embeddings/openai.py`
* `openrag/components/indexer/loaders/base.py`
* `openrag/components/indexer/vectordb/vectordb.py`
* `openrag/components/pipeline.py`
* `openrag/components/retriever.py`
* `openrag/components/utils.py`

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
openrag/components/prompts/prompts.py (1)

11-27: Type hint mismatch: function returns str, not tuple[str, str].

The function signature declares a return type of tuple[str, str], but line 27 returns only sys_msg (a single string).

 def load_prompt(
     prompt_name: str,
     prompts_dir: Path = prompts_dir,
     prompt_mapping: dict = prompt_mapping,
-) -> tuple[str, str]:
+) -> str:
openrag/components/retriever.py (1)

157-160: Bug: Incorrect variable checked in validation.

Line 159 checks retriever_type is None, but the intent is to validate whether the retriever class was found. This should check retriever_cls is None instead. As written, an unknown retriever type silently returns None from RETRIEVERS.get() and will raise an AttributeError later when instantiating.

         retriever_cls = RetrieverFactory.RETRIEVERS.get(retriever_type, None)
 
-        if retriever_type is None:
+        if retriever_cls is None:
             raise ValueError(f"Unknown retriever type: {retriever_type}")
♻️ Duplicate comments (3)
openrag/components/indexer/vectordb/vectordb.py (1)

555-600: Add guard for empty query responses to prevent IndexError.

As noted in the past review, if a query returns an empty response (e.g., chunk deleted or section_id doesn't exist), accessing response[0] on line 586 will raise an IndexError.

Apply this diff to add a guard:

         # Build output, skipping duplicates
         output_docs = []
         for response in responses:
+            if not response:
+                continue
             doc_id = response[0].get("_id")
             if doc_id not in existant_ids:

Based on past review comments.

openrag/components/utils.py (2)

125-147: Cache the tokenizer to avoid recreating ChatOpenAI on every call.

ChatOpenAI(**config.llm) is instantiated on every call to format_context. Since only get_num_tokens is needed, cache it at module scope to improve performance.

 config = load_config()
 logger = get_logger()
+_token_counter = None
+
+def _get_token_counter():
+    global _token_counter
+    if _token_counter is None:
+        _token_counter = ChatOpenAI(**config.llm).get_num_tokens
+    return _token_counter


 def format_context(docs: list[Document], max_context_tokens: int = 4096) -> str:
     if not docs:
         return "No document found from the database"

-    llm = ChatOpenAI(**config.llm)
-    _length_function = llm.get_num_tokens
+    _length_function = _get_token_counter()

     docs_with_tokens = list(map(lambda d: (_length_function(d.page_content), d), docs))

150-157: Fix import-time PermissionError: use configurable cache directory with lazy initialization.

The hardcoded path /app/model_weights/ at line 151 will cause a PermissionError during import in non-Docker environments (e.g., pytest runs). The directory creation happens at module load time, which breaks portability.

Use lazy initialization with a configurable cache directory:

-# Initialize language detector
-lang_detect_cache_dir = "/app/model_weights/"
-lang_detector_config = LangDetectConfig(
-    max_input_length=1024,  # chars
-    model="auto",
-    cache_dir=lang_detect_cache_dir,
-)
-lang_detector: LangDetector = LangDetector(config=lang_detector_config)
+# Lazy initialization of language detector
+_lang_detector: LangDetector = None
+
+def _get_lang_detector():
+    global _lang_detector
+    if _lang_detector is None:
+        import os
+        cache_dir = os.getenv("LANG_DETECT_CACHE_DIR", os.path.expanduser("~/.cache/openrag/lang_detect"))
+        os.makedirs(cache_dir, exist_ok=True)
+        _lang_detector = LangDetector(config=LangDetectConfig(
+            max_input_length=1024,
+            model="auto",
+            cache_dir=cache_dir,
+        ))
+    return _lang_detector

Then update detect_language:

 def detect_language(text: str):
-    outputs = lang_detector.detect(text, k=1)
+    outputs = _get_lang_detector().detect(text, k=1)
     return outputs[0].get("lang")
🧹 Nitpick comments (6)
openrag/components/indexer/vectordb/vectordb.py (1)

387-396: Add strict=True to zip for runtime safety.

As noted in the past review and static analysis, adding strict=True to the zip call ensures length mismatches are caught at runtime rather than silently dropping data.

Apply this diff:

-            for chunk, vector, order_metadata in zip(chunks, vectors, order_metadata_l):
+            for chunk, vector, order_metadata in zip(chunks, vectors, order_metadata_l, strict=True):

Based on past review and static analysis hints.

pyproject.toml (1)

47-49: Consolidate language detection to use fast_langdetect consistently.

Both langdetect>=1.0.9 and fast_langdetect>=1.0.0 are imported and used for the same purpose. While fast_langdetect is used in the centralized detect_language() function (utils.py, used by chunker.py), the legacy langdetect is used separately in media_loader.py's _detect_language() method. Refactor media_loader to use the centralized language detection or adopt fast_langdetect directly to avoid duplicate dependencies and align with the faster implementation.

openrag/components/retriever.py (1)

59-60: Typo in class name: SingleRetreiverSingleRetriever.

-class SingleRetreiver(BaseRetriever):
+class SingleRetriever(BaseRetriever):
     pass
openrag/components/indexer/chunker/utils.py (1)

17-31: Consider renaming type parameter to avoid shadowing built-in.

Using type as a parameter/attribute name shadows Python's built-in type() function. While functional, this can cause subtle issues if type() is needed within the class.

 class MDElement:
     """Class representing a segment of markdown content."""

     def __init__(
         self,
-        type: Literal["text", "table", "image"],
+        element_type: Literal["text", "table", "image"],
         content: str,
         page_number: Optional[int] = None,
     ):
-        self.type = type  # 'text', 'table', 'image'
+        self.type = element_type  # 'text', 'table', 'image'
         self.content = content
         self.page_number = page_number
openrag/components/indexer/chunker/chunker.py (2)

109-134: Consider adding an abstract method or removing ABC inheritance.

BaseChunker inherits from ABC but has no abstract methods, making the ABC inheritance unnecessary. Either add an abstract method (e.g., split_text) or remove the ABC inheritance.

-class BaseChunker(ABC):
+class BaseChunker:
     """Base class for document chunkers with built-in contextualization capability."""

63-67: Consider logging exception details for debugging.

The broad Exception catches are acceptable for resilience, but logging only the exception message may lose stack trace information useful for debugging. Consider using logger.warning(..., exc_info=True) or logger.exception(...) for better diagnostics.

Also applies to: 104-106

📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between d6adc9f and b7326d7.

⛔ Files ignored due to path filters (1)
  • uv.lock is excluded by !**/*.lock
📒 Files selected for processing (25)
  • .github/workflows/smoke_test/.env (1 hunks)
  • .hydra_config/chunker/semantic_splitter.yaml (0 hunks)
  • .hydra_config/chunker/token_splitter.yaml (0 hunks)
  • .hydra_config/config.yaml (3 hunks)
  • .hydra_config/rag/ChatBotRag.yaml (1 hunks)
  • .hydra_config/rag/SimpleRag.yaml (1 hunks)
  • .hydra_config/rag/base.yaml (1 hunks)
  • .hydra_config/retriever/base.yaml (1 hunks)
  • docker-compose.yaml (2 hunks)
  • docs/content/docs/documentation/env_vars.md (4 hunks)
  • openrag/components/indexer/chunker/chunker.py (2 hunks)
  • openrag/components/indexer/chunker/test_chunking.py (1 hunks)
  • openrag/components/indexer/chunker/utils.py (1 hunks)
  • openrag/components/indexer/embeddings/openai.py (2 hunks)
  • openrag/components/indexer/loaders/base.py (1 hunks)
  • openrag/components/indexer/vectordb/vectordb.py (11 hunks)
  • openrag/components/pipeline.py (3 hunks)
  • openrag/components/prompts/prompts.py (1 hunks)
  • openrag/components/retriever.py (4 hunks)
  • openrag/components/utils.py (3 hunks)
  • prompts/example1/chunk_contextualizer_tmpl.txt (1 hunks)
  • prompts/example1/sys_prompt_tmpl.txt (1 hunks)
  • pyproject.toml (1 hunks)
  • pytest.ini (1 hunks)
  • quick_start/docker-compose.yaml (2 hunks)
💤 Files with no reviewable changes (2)
  • .hydra_config/chunker/token_splitter.yaml
  • .hydra_config/chunker/semantic_splitter.yaml
🚧 Files skipped from review as they are similar to previous changes (10)
  • openrag/components/indexer/embeddings/openai.py
  • .hydra_config/rag/base.yaml
  • openrag/components/indexer/chunker/test_chunking.py
  • .hydra_config/config.yaml
  • .hydra_config/rag/ChatBotRag.yaml
  • openrag/components/indexer/loaders/base.py
  • openrag/components/pipeline.py
  • docs/content/docs/documentation/env_vars.md
  • pytest.ini
  • .hydra_config/rag/SimpleRag.yaml
🧰 Additional context used
🧬 Code graph analysis (3)
openrag/components/utils.py (2)
openrag/utils/logger.py (1)
  • get_logger (10-47)
openrag/config/config.py (1)
  • load_config (12-29)
openrag/components/indexer/vectordb/vectordb.py (1)
openrag/components/indexer/indexer.py (1)
  • chunk (53-57)
openrag/components/indexer/chunker/chunker.py (6)
openrag/components/utils.py (2)
  • detect_language (160-162)
  • get_vlm_semaphore (172-176)
openrag/config/config.py (1)
  • load_config (12-29)
openrag/utils/logger.py (1)
  • get_logger (10-47)
openrag/components/indexer/embeddings/base.py (1)
  • BaseEmbedding (5-20)
openrag/components/indexer/chunker/utils.py (3)
  • MDElement (17-31)
  • get_chunk_page_number (110-145)
  • split_md_elements (53-107)
openrag/components/indexer/indexer.py (1)
  • chunk (53-57)
🪛 Ruff (0.14.8)
openrag/components/indexer/vectordb/vectordb.py

388-388: zip() without an explicit strict= parameter

Add explicit value for parameter strict=

(B905)


532-532: Consider moving this statement to an else block

(TRY300)

openrag/components/indexer/chunker/chunker.py

62-62: Consider moving this statement to an else block

(TRY300)


63-63: Do not catch blind exception: Exception

(BLE001)


104-104: Do not catch blind exception: Exception

(BLE001)


109-109: BaseChunker is an abstract base class, but it has no abstract methods or properties

(B024)


118-118: Unused method argument: kwargs

(ARG002)

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: index-backup-restore
🔇 Additional comments (21)
quick_start/docker-compose.yaml (1)

45-45: LGTM: Max model length standardized to 8192.

The default max-model-len value has been consistently updated to 8192 across both the x-vllm template and vllm-cpu service, aligning with the embedder model capabilities documented elsewhere in the PR.

Also applies to: 142-142

.hydra_config/retriever/base.yaml (1)

4-4: LGTM: Surrounding chunks configuration added.

The new with_surrounding_chunks parameter (default: true) integrates cleanly with the retriever and vector DB changes, enabling optional context expansion during retrieval.

docker-compose.yaml (1)

47-47: LGTM: Max model length standardized to 8192.

Consistent with quick_start/docker-compose.yaml, the max-model-len is now set to 8192 across both vllm services.

Also applies to: 146-146

.github/workflows/smoke_test/.env (1)

29-30: LGTM: MAX_MODEL_LEN properly configured.

The inline comment has been correctly separated from the value assignment, resolving the previous parsing concern. The value is now cleanly set to 8192.

prompts/example1/chunk_contextualizer_tmpl.txt (1)

1-22: LGTM: Contextualization prompt is clear and well-structured.

The restructured template provides explicit guidance with clear field definitions, core principles, output format requirements, and practical examples. The emphasis on concise (1-2 sentences), language-aligned context is appropriate for chunking workflows.

prompts/example1/sys_prompt_tmpl.txt (1)

11-11: LGTM: Source citation guideline added.

The new rule to avoid citing sources or file names is clear and aligns with the broader prompt restructuring to produce cleaner, more focused responses.

openrag/components/indexer/vectordb/vectordb.py (5)

2-2: LGTM: Standard library import added.

The time import is appropriately used for generating deterministic chunk IDs in _gen_chunk_order_metadata.


77-77: LGTM: Surrounding chunks parameter added to abstract interface.

The with_surrounding_chunks parameter is consistently added to both search method signatures with backward-compatible defaults.

Also applies to: 89-89


129-131: LGTM: Chunk markers added to stop words.

The addition of chunk delimiter markers to the stop words list prevents them from interfering with search indexing.


1020-1035: LGTM: Unique chunk IDs now guaranteed.

The base_ts + i pattern ensures each section_id is unique, addressing the previous time.time_ns() duplication concern.


523-532: LGTM: Conditional surrounding chunks fetching with logging.

The logic cleanly extends retrieved documents with surrounding chunks when enabled, with appropriate debug logging.

openrag/components/prompts/prompts.py (1)

33-33: LGTM!

The rename from CHUNK_CONTEXTUALIZER to CHUNK_CONTEXTUALIZER_PROMPT aligns with the naming convention of other prompt constants in this file (QUERY_CONTEXTUALIZER_PROMPT, HYDE_PROMPT, MULTI_QUERY_PROMPT, etc.).

openrag/components/retriever.py (2)

35-41: LGTM!

The with_surrounding_chunks parameter is properly added to BaseRetriever and correctly propagated through **kwargs to child classes. This enables consistent control of surrounding chunk retrieval across all retriever types.


54-54: Parameter propagation is correct.

The with_surrounding_chunks parameter is consistently passed to the database search methods across all retriever implementations.

Also applies to: 100-100, 141-141

openrag/components/indexer/chunker/utils.py (3)

38-50: LGTM!

The page number calculation correctly handles the [PAGE_N] marker semantics where content after the marker belongs to page N+1.


110-145: LGTM!

The get_chunk_page_number function correctly determines start and end pages for chunks containing page separators, handling edge cases like chunks starting/ending exactly at separators.


191-249: LGTM!

The table chunking implementation correctly handles overlap by including the previous last row in new subtables, ensuring context continuity across chunks.

openrag/components/indexer/chunker/chunker.py (4)

19-22: LGTM!

The chunk format constants provide a consistent structure for contextualized and non-contextualized chunks, including filename metadata for traceability.


55-67: LGTM on semaphore usage.

Using get_vlm_semaphore() for context generation aligns with the use of config.vlm for the LLM config, ensuring consistent rate limiting.


284-305: LGTM!

RecursiveSplitter properly initializes the text splitter with paragraph-first, then sentence-level splitting via regex separators. The strict=True in zip (line 101) is a good practice for catching length mismatches.


331-331: The use of config.vlm for chunker's LLM config is intentional and correct.

The chunker processes multi-modal documents containing images, tables, and text. It imports get_vlm_semaphore() from utils.py, uses it in the context generation method, and explicitly handles image and table element types during document parsing. The loaders similarly use config.vlm for document processing. In contrast, components like format_context in utils.py, which use config.llm, only perform text-only operations like token counting. The distinction is appropriate: VLM config for multi-modal content processing, LLM config for text-only operations.

Likely an incorrect or invalid review comment.

Comment thread openrag/components/indexer/chunker/chunker.py
Comment thread openrag/components/indexer/chunker/utils.py

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between b7326d7 and 56dc3bc.

📒 Files selected for processing (7)
  • openrag/components/indexer/chunker/chunker.py (2 hunks)
  • openrag/components/indexer/chunker/test_chunking.py (1 hunks)
  • openrag/components/indexer/chunker/utils.py (1 hunks)
  • openrag/components/indexer/vectordb/vectordb.py (11 hunks)
  • openrag/components/prompts/prompts.py (2 hunks)
  • openrag/components/retriever.py (5 hunks)
  • pytest.ini (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (2)
  • openrag/components/indexer/chunker/test_chunking.py
  • pytest.ini
🧰 Additional context used
🧬 Code graph analysis (2)
openrag/components/indexer/vectordb/vectordb.py (1)
openrag/components/indexer/indexer.py (1)
  • chunk (53-57)
openrag/components/indexer/chunker/chunker.py (6)
openrag/components/utils.py (2)
  • detect_language (160-162)
  • get_vlm_semaphore (172-176)
openrag/config/config.py (1)
  • load_config (12-29)
openrag/utils/logger.py (1)
  • get_logger (10-47)
openrag/components/indexer/embeddings/base.py (1)
  • BaseEmbedding (5-20)
openrag/components/indexer/chunker/utils.py (4)
  • MDElement (17-31)
  • chunk_table (191-253)
  • get_chunk_page_number (110-145)
  • split_md_elements (53-107)
openrag/components/indexer/indexer.py (1)
  • chunk (53-57)
🪛 Ruff (0.14.8)
openrag/components/indexer/vectordb/vectordb.py

388-388: zip() without an explicit strict= parameter

Add explicit value for parameter strict=

(B905)


532-532: Consider moving this statement to an else block

(TRY300)

openrag/components/indexer/chunker/chunker.py

61-61: Consider moving this statement to an else block

(TRY300)


62-62: Do not catch blind exception: Exception

(BLE001)


103-103: Do not catch blind exception: Exception

(BLE001)


117-117: Unused method argument: kwargs

(ARG002)

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: index-backup-restore
🔇 Additional comments (31)
openrag/components/prompts/prompts.py (2)

11-27: LGTM!

The return type change from tuple[str, str] to str correctly reflects the actual implementation. The function reads and returns a single string from the prompt file.


33-33: LGTM!

The constant rename fixes the typo and aligns with the naming convention used by other prompt constants in this file.

openrag/components/retriever.py (6)

35-41: LGTM!

The with_surrounding_chunks parameter is properly added to BaseRetriever with a sensible default of True. This enables retrieval of adjacent chunks to surface additional context, as described in the PR objectives.


43-56: LGTM!

The with_surrounding_chunks parameter is correctly propagated to the vector DB search call, maintaining consistency with the updated async_search signature in vectordb.py.


59-60: LGTM!

The typo fix from SingleRetreiver to SingleRetriever corrects the class name spelling and is reflected in the factory mapping at line 147.


86-102: LGTM!

The with_surrounding_chunks parameter is correctly propagated through the multi-query search flow, inheriting from BaseRetriever and passing it to the vector DB call.


129-142: LGTM!

The HyDeRetriever correctly propagates the with_surrounding_chunks parameter through the hypothetical document search flow.


159-160: LGTM!

Changing the validation from retriever_type is None to retriever_cls is None is more appropriate, as it checks the actual resolved class rather than the string identifier.

openrag/components/indexer/vectordb/vectordb.py (8)

70-79: LGTM!

The with_surrounding_chunks parameter is properly added to both search method signatures with backward-compatible defaults. This extends the retrieval API to optionally include adjacent chunks.

Also applies to: 82-91


118-135: LGTM!

The addition of [CHUNK_START], [CHUNK_END], and [CONTEXT] to the stop words list correctly prevents these formatting markers from influencing BM25 search results. These markers are introduced by the chunk formatting constants in chunker.py.


385-396: LGTM!

The order metadata generation and attachment flow is well-implemented. Each chunk receives unique section_id, prev_section_id, and next_section_id fields that enable bidirectional linking for surrounding chunk retrieval. The use of strict=True in the zip ensures all sequences have matching lengths.


427-455: LGTM!

The multi-query search correctly propagates with_surrounding_chunks to each individual search and deduplicates results by _id. This prevents duplicate chunks when multiple queries retrieve the same sections.


457-533: LGTM!

The search implementation correctly:

  1. Performs vector/hybrid search with configured parameters
  2. Parses the search results into Document objects
  3. Optionally fetches and appends surrounding chunks when with_surrounding_chunks=True
  4. Returns the combined result set

The flow is clean and the conditional surrounding chunk retrieval is properly integrated.


555-602: LGTM!

The get_surrounding_chunks implementation is well-designed:

  • Efficiently collects prev/next section IDs from retrieved documents
  • Queries all related sections in parallel using asyncio.gather
  • Properly handles empty responses (line 586-587) to avoid IndexError
  • Deduplicates by _id to prevent returning chunks already in the result set
  • Constructs clean Document objects with appropriate metadata

1022-1037: LGTM!

The _gen_chunk_order_metadata function correctly generates unique section IDs by using base_ts + i, ensuring no duplicates even in rapid succession (addressing the previous review concern). The prev/next linking logic properly handles edge cases (first and last chunks).


1040-1055: LGTM!

The _parse_documents_from_search_results helper cleanly converts search results into Document objects, properly separating page_content from metadata and excluding internal fields like vector.

openrag/components/indexer/chunker/utils.py (8)

1-14: LGTM!

The import additions and PAGE_RE pattern are correct. The regex properly matches page markers in the format [PAGE_N] where N is a digit, enabling page-aware chunking throughout the module.


17-31: LGTM!

The MDElement class provides a clean, type-safe representation of markdown segments. The use of Literal for the type field ensures only valid element types are used, and the optional page_number accommodates elements without page information.


34-50: LGTM!

Both helper functions are correctly implemented:

  • span_inside properly checks span containment
  • get_page_number correctly interprets page markers, with the important semantic that content after [PAGE_N] belongs to page N+1

53-107: LGTM!

The split_md_elements function is well-implemented:

  • Correctly identifies page markers and assigns page numbers to elements
  • Properly handles image/table precedence (skips tables inside image descriptions)
  • Fills text segments between special elements
  • Returns a clean list of MDElement objects

110-145: LGTM!

The get_chunk_page_number function correctly determines the start and end pages for text chunks:

  • Handles chunks with no page markers (entire chunk on previous page)
  • Correctly interprets whether chunk starts/ends with a separator
  • Returns a clear dict with start_page and end_page

148-188: LGTM!

The parse_markdown_table function correctly parses markdown tables and groups rows by the Domain column (first column). The logic handles malformed rows gracefully and maintains group boundaries.


191-253: LGTM!

The chunk_table function implements intelligent table chunking:

  • Preserves the table header in each subtable
  • Implements one-row overlap (line 223-224) to maintain context across chunks
  • Uses strict=True in zip for safety (line 215)
  • Properly handles token size calculations to respect the chunk_size limit

The overlap strategy ensures users can understand continuity between consecutive table chunks.


256-280: LGTM!

The clean_markdown_table_spacing function properly normalizes table spacing by trimming cells and rebuilding rows with consistent spacing. This improves the consistency of table representations.

openrag/components/indexer/chunker/chunker.py (7)

18-21: LGTM!

The chunk format constants clearly separate contextualized and non-contextualized formats. The markers ([CHUNK_START], [CHUNK_END], [CONTEXT]) are properly excluded from BM25 indexing via the stop words list in vectordb.py.


24-66: LGTM!

The ChunkContextualizer class is well-designed:

  • _generate_context constructs a rich prompt with document context (filename, first chunks, previous chunks)
  • Uses a semaphore to control concurrent VLM requests
  • Gracefully handles errors by logging and returning empty context
  • The broad exception catch (line 62) is appropriate here for resilience

68-105: LGTM!

The contextualize_chunks method efficiently processes chunks in parallel:

  • Creates contextualization tasks for all chunks with appropriate first/previous chunk context
  • Uses tqdm.gather for progress feedback
  • Applies the CHUNK_FORMAT template with generated context
  • Falls back to original chunks on error, ensuring robustness

108-134: LGTM!

The BaseChunker initialization is clean and efficient:

  • Properly calculates chunk overlap from the overlap rate
  • Uses the LLM's token counter for accurate length measurement
  • Conditionally initializes the contextualizer only when needed
  • The unused kwargs parameter (line 117) allows subclass flexibility

135-192: LGTM!

The helper methods are well-designed:

  • _apply_contextualization applies the appropriate format based on whether contextualization is enabled
  • _prepare_md_elements intelligently separates large tables/images while keeping small ones with text
  • split_text provides lazy initialization with a sensible fallback

194-254: LGTM!

The _get_chunks method implements the core chunking logic effectively:

  • Separates MD elements into tables/images and text
  • Chunks large tables with the specialized chunk_table function
  • Assigns page numbers to text chunks using get_chunk_page_number
  • Sorts final chunks by page for consistent ordering

283-304: LGTM!

The RecursiveSplitter correctly implements the recursive splitting strategy described in the PR objectives:

  • First splits by newlines (paragraphs)
  • Then by punctuation when paragraphs exceed chunk size
  • The positive lookbehind regex (?<=[\.\?\!]) properly splits after sentence-ending punctuation

Comment on lines +256 to +280
async def split_document(
self, doc: Document, task_id: Optional[str] = None
) -> list[Document]:
"""Split document into chunks with optional contextualization."""
metadata = doc.metadata
log = logger.bind(
file_id=metadata.get("file_id"),
partition=metadata.get("partition"),
task_id=task_id,
)
log.info("Starting document chunking")
source = metadata["source"]

# Split the document into chunks of text, tables, and images
all_content = doc.page_content.strip()
splits = split_md_elements(all_content)
splits = combine_md_elements(
splits, llm=self.llm, chunk_max_size=self.chunk_size
)

# Add overlap image and table chunks
splits = add_overlap(
chunks=splits,
target_chunk_types=["table", "image"],
add_before=True,
add_after=True,
chunk_overlap=self.chunk_overlap,
)
detected_lang = detect_language(text=doc.page_content)

# only split text elements into chunks
chunks = []
for chunk_type, content in splits:
if chunk_type == "text":
chunks.extend(self.split_text(content))
else:
chunks.append(content)
# Process document through pipeline
chunks = self._get_chunks(doc.page_content.strip(), metadata, log=log)

# regrouping chunks based on token length
chunks = combine_chunks(
chunks=chunks, llm=self.llm, chunk_max_size=self.chunk_size
# Apply contextualization if enabled
log.info(
"Contextualizing chunks", apply_contextualization=self.contextual_retrieval
)
chunks = await self._apply_contextualization(chunks, lang=detected_lang)

chunks_w_context = chunks # Default to original chunks if no contextualization
if self.contextual_retrieval:
log.info("Contextualizing chunks")
chunks_w_context = await self._contextualize_chunks(chunks, source=source)

filtered_chunks = []
prev_page_num = 1
for chunk, chunk_w_context in zip(chunks, chunks_w_context):
if not chunk.strip(): # skip empty chunks
continue

page_info = self._get_chunk_page_info(
chunk_str=chunk, previous_page=prev_page_num
)
start_page = page_info["start_page"]
end_page = page_info["end_page"]
prev_page_num = end_page
filtered_chunks.append(
Document(
page_content=chunk_w_context,
metadata={**metadata, "page": start_page},
)
)
log.info("Document chunking completed")
return filtered_chunks
return chunks

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Look for imports and the detect_language function usage in chunker.py
head -100 openrag/components/indexer/chunker/chunker.py | grep -E "import|from"

Repository: linagora/openrag

Length of output: 608


🏁 Script executed:

#!/bin/bash
# Search for _apply_contextualization signature and detected_lang usage
rg -A 10 "async def _apply_contextualization" openrag/components/indexer/chunker/ --type=py

Repository: linagora/openrag

Length of output: 1079


🏁 Script executed:

#!/bin/bash
# Search for detect_language function definition and implementation
rg -B 2 -A 5 "def detect_language" openrag/ --type=py

Repository: linagora/openrag

Length of output: 448


🏁 Script executed:

#!/bin/bash
# Find lang_detector initialization and any validation/fallback logic
rg -B 10 "def detect_language" openrag/components/utils.py -A 5 | head -50

Repository: linagora/openrag

Length of output: 507


🏁 Script executed:

#!/bin/bash
# Search for lang_detector initialization
rg "lang_detector" openrag/components/ --type=py -B 3 -A 3

Repository: linagora/openrag

Length of output: 907


🏁 Script executed:

#!/bin/bash
# Check imports at the top of utils.py
head -50 openrag/components/utils.py

Repository: linagora/openrag

Length of output: 1593


🏁 Script executed:

#!/bin/bash
# Get the full implementation of _apply_contextualization
rg -A 50 "async def _apply_contextualization" openrag/components/indexer/chunker/chunker.py --type=py

Repository: linagora/openrag

Length of output: 2008


🏁 Script executed:

#!/bin/bash
# Find contextualizer initialization in chunker
rg "self.contextualizer" openrag/components/indexer/chunker/chunker.py -B 2 -A 2

Repository: linagora/openrag

Length of output: 361


🏁 Script executed:

#!/bin/bash
# Search for contextualize_chunks implementation
rg -B 2 "def contextualize_chunks" openrag/ --type=py -A 20

Repository: linagora/openrag

Length of output: 1938


🏁 Script executed:

#!/bin/bash
# Find _generate_context implementation
rg -B 2 "async def _generate_context" openrag/components/indexer/chunker/chunker.py -A 30

Repository: linagora/openrag

Length of output: 1218


Add language validation to handle unsupported language codes gracefully.

The detect_language function can return any of ~176 language codes from fasttext-langdetect, but _apply_contextualization and related methods expect Literal["en", "fr"]. Unsupported language codes are passed directly to the LLM prompt without validation, causing unpredictable behavior. Add explicit validation or mapping to fallback to a supported language (e.g., "en") when an unsupported code is detected.

🤖 Prompt for AI Agents
In openrag/components/indexer/chunker/chunker.py around lines 256 to 280, the
detected_lang from detect_language may be any fasttext code but downstream
contextualization expects only supported codes (Literal["en","fr"]), so validate
and coerce the detected language before calling _apply_contextualization: check
detected_lang against a small allowed set (e.g., {"en","fr"}), map known
close/variant codes if desired (e.g., "en-US" -> "en", "fr-CA" -> "fr"), and
otherwise set a sensible fallback like "en"; log a warning when coercing/using
the fallback so it's visible for debugging. Ensure the validated/coerced
language is passed into _apply_contextualization instead of the raw detected
value.

@Ahmath-Gadji
Ahmath-Gadji merged commit ff47830 into dev Dec 17, 2025
4 checks passed
@Ahmath-Gadji
Ahmath-Gadji deleted the feat/chunking branch December 17, 2025 09:47
@paultranvan paultranvan added the feat Add a new feature label Dec 17, 2025
@coderabbitai coderabbitai Bot mentioned this pull request Jan 28, 2026
This was referenced Feb 5, 2026
@coderabbitai coderabbitai Bot mentioned this pull request Feb 13, 2026
@coderabbitai coderabbitai Bot mentioned this pull request Mar 24, 2026
6 tasks
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

feat Add a new feature

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Chunking anomaly: Some Chunks are too small for no reason

2 participants