Skip to content
This repository was archived by the owner on May 27, 2026. It is now read-only.
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 32 additions & 9 deletions projects/pgai/pgai/vectorizer/parsing.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import asyncio
import os
from abc import ABC, abstractmethod
from concurrent.futures import ThreadPoolExecutor
from pathlib import Path
from typing import Any, Literal

Expand All @@ -8,11 +10,14 @@

from pgai.vectorizer.loading import LoadedDocument

# Thread pool for CPU-intensive parsing operations
_PARSING_EXECUTOR = ThreadPoolExecutor(max_workers=4, thread_name_prefix="parsing")

Copilot AI Jun 11, 2025

Copy link

Choose a reason for hiding this comment

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

[nitpick] Hardcoding max_workers=4 may not scale across environments—consider making this configurable or using os.cpu_count() to align with available cores.

Suggested change
_PARSING_EXECUTOR = ThreadPoolExecutor(max_workers=4, thread_name_prefix="parsing")
max_workers = int(os.getenv("PARSING_MAX_WORKERS", os.cpu_count() or 4))
_PARSING_EXECUTOR = ThreadPoolExecutor(max_workers=max_workers, thread_name_prefix="parsing")

Copilot uses AI. Check for mistakes.

@smoya smoya Jun 11, 2025

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

That's actually a good suggestion. Good bot.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

The os.cpu_count doesn't make a lot of sense since python is single threaded. This just configures how many documents can be parsed in parallel. But I'll make it configurable.

@alejandrodnm alejandrodnm Jun 18, 2025

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

If the parsing is CPU bound why not use a ProcessPoolExecutor instead?

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

NIT: python is not single threaded, it uses a lock to keep threads from executing in parallel. If you have IO bound tasks (reading files, network requests), the GIL is release so it gives concurrency benefits.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Okay so more precisely python wont utilize more than 1 core to execute that parsing, I think the argument still stands. The cloud lambda also runs only with one core so multiprocessing instead of threading is just additional overhead. I think.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

The cloud lambda is configured to have 2560MiB of RAM, which according to this S/O post should correspond to two cores.



class ParsingNone(BaseModel):
implementation: Literal["none"]

def parse(self, _1: dict[str, Any], payload: str | LoadedDocument) -> str: # noqa: ARG002
async def parse(self, _1: dict[str, Any], payload: str | LoadedDocument) -> str: # noqa: ARG002
if isinstance(payload, LoadedDocument):
raise ValueError(
"Cannot chunk Document with parsing_none, "
Expand All @@ -24,13 +29,15 @@ def parse(self, _1: dict[str, Any], payload: str | LoadedDocument) -> str: # no
class ParsingAuto(BaseModel):
implementation: Literal["auto"]

def parse(self, row: dict[str, Any], payload: str | LoadedDocument) -> str:
async def parse(self, row: dict[str, Any], payload: str | LoadedDocument) -> str:
if isinstance(payload, LoadedDocument):
if payload.file_type == "epub":
# epub is not supported by docling, but by pymupdf
return ParsingPyMuPDF(implementation="pymupdf").parse(row, payload)
return await ParsingPyMuPDF(implementation="pymupdf").parse(
row, payload
)

return ParsingDocling(implementation="docling").parse(row, payload)
return await ParsingDocling(implementation="docling").parse(row, payload)
else:
return payload

Expand All @@ -40,7 +47,7 @@ class BaseDocumentParsing(BaseModel, ABC):

implementation: str

def parse(self, row: dict[str, Any], payload: LoadedDocument | str) -> str:
async def parse(self, row: dict[str, Any], payload: LoadedDocument | str) -> str:
"""
Parse a document payload into a string representation.

Expand All @@ -66,10 +73,10 @@ def parse(self, row: dict[str, Any], payload: LoadedDocument | str) -> str:
if payload.file_type in ["txt", "md"]:
return payload.content.getvalue().decode("utf-8")

return self.parse_doc(row, payload)
return await self.parse_doc(row, payload)

@abstractmethod
def parse_doc(self, row: dict[str, Any], payload: LoadedDocument) -> str:
async def parse_doc(self, row: dict[str, Any], payload: LoadedDocument) -> str:
"""
Parse a binary document into a string representation, Markdown preferable.
Must be implemented by subclasses.
Expand All @@ -82,7 +89,15 @@ class ParsingPyMuPDF(BaseDocumentParsing):
implementation: Literal["pymupdf"] # type: ignore[reportIncompatibleVariableOverride]

@override
def parse_doc(self, row: dict[str, Any], payload: LoadedDocument) -> str: # noqa: ARG002
async def parse_doc(self, row: dict[str, Any], payload: LoadedDocument) -> str: # noqa: ARG002
# Run blocking parsing operation in thread pool
loop = asyncio.get_event_loop()
Comment thread
Askir marked this conversation as resolved.
Outdated
return await loop.run_in_executor(
_PARSING_EXECUTOR, self._parse_with_pymupdf, payload
)

def _parse_with_pymupdf(self, payload: LoadedDocument) -> str:
"""Synchronous pymupdf parsing to run in thread pool."""
# Note: deferred import to avoid import overhead
import pymupdf # type: ignore
import pymupdf4llm # type: ignore
Expand All @@ -105,7 +120,15 @@ class ParsingDocling(BaseDocumentParsing):
cache_dir: Path | str = DOCLING_CACHE_DIR

@override
def parse_doc(self, row: dict[str, Any], payload: LoadedDocument) -> str: # noqa: ARG002
async def parse_doc(self, row: dict[str, Any], payload: LoadedDocument) -> str: # noqa: ARG002
# Run blocking parsing operation in thread pool
loop = asyncio.get_event_loop()
return await loop.run_in_executor(
_PARSING_EXECUTOR, self._parse_with_docling, payload
)

def _parse_with_docling(self, payload: LoadedDocument) -> str:
"""Synchronous docling parsing to run in thread pool."""
# Note: deferred import to avoid import overhead
from docling.datamodel.base_models import (
DocumentStream, # type: ignore
Expand Down
2 changes: 1 addition & 1 deletion projects/pgai/pgai/vectorizer/vectorizer.py
Original file line number Diff line number Diff line change
Expand Up @@ -1188,7 +1188,7 @@ async def _generate_embeddings(
loading_errors.append((item, (LoadingError(e=e))))
continue

payload = self.vectorizer.config.parsing.parse(item, payload)
payload = await self.vectorizer.config.parsing.parse(item, payload)
chunks = self.vectorizer.config.chunking.into_chunks(item, payload)
for chunk_id, chunk in enumerate(chunks, 0):
formatted = self.vectorizer.config.formatting.format(chunk, item)
Expand Down