Merge pull request #537 from lawrenceakka/help-speedup

Stop eagerly importing Store from haiku.rag.store
This commit is contained in:
Yiorgis Gozadinos 2026-08-13 11:34:42 +03:00 committed by GitHub
commit 614be8b2a7
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
13 changed files with 79 additions and 33 deletions

View file

@ -4,6 +4,8 @@
### Fixed ### Fixed
- `cross-encoder` reranking no longer ties the scores of strongly-relevant candidates, which left their order to the sort. Scores remain 0-1. - `cross-encoder` reranking no longer ties the scores of strongly-relevant candidates, which left their order to the sort. Scores remain 0-1.
- haiku-rag CLI startup no longer imports `lancedb`, `pyarrow` and `pydantic_ai`.
- `haiku.rag.store` no longer re-exports `Store`; import it from `haiku.rag.store.engine`.
## [0.73.0] - 2026-08-06 ## [0.73.0] - 2026-08-06

View file

@ -4,7 +4,7 @@ import sys
import warnings import warnings
from importlib.metadata import version from importlib.metadata import version
from pathlib import Path from pathlib import Path
from typing import Any from typing import TYPE_CHECKING, Any
import typer import typer
from dotenv import find_dotenv, load_dotenv from dotenv import find_dotenv, load_dotenv
@ -13,7 +13,6 @@ from dotenv import find_dotenv, load_dotenv
# Env loading needs to be before config import; usecwd=True searches from cwd, not this .py file's location # Env loading needs to be before config import; usecwd=True searches from cwd, not this .py file's location
load_dotenv(find_dotenv(usecwd=True)) load_dotenv(find_dotenv(usecwd=True))
from haiku.rag.app import HaikuRAGApp # noqa: E402
from haiku.rag.config import ( # noqa: E402 from haiku.rag.config import ( # noqa: E402
AppConfig, AppConfig,
find_config_file, find_config_file,
@ -29,6 +28,9 @@ from haiku.rag.store.exceptions import ( # noqa: E402
from haiku.rag.store.models.chunk import SearchType # noqa: E402 from haiku.rag.store.models.chunk import SearchType # noqa: E402
from haiku.rag.utils import is_up_to_date # noqa: E402 from haiku.rag.utils import is_up_to_date # noqa: E402
if TYPE_CHECKING:
from haiku.rag.app import HaikuRAGApp
_cli = typer.Typer( _cli = typer.Typer(
context_settings={"help_option_names": ["-h", "--help"]}, context_settings={"help_option_names": ["-h", "--help"]},
no_args_is_help=True, no_args_is_help=True,
@ -48,7 +50,7 @@ def cli():
_read_only: bool = False _read_only: bool = False
def create_app(db: Path | None = None) -> HaikuRAGApp: # pragma: no cover def create_app(db: Path | None = None) -> "HaikuRAGApp": # pragma: no cover
"""Create HaikuRAGApp with loaded config and resolved database path. """Create HaikuRAGApp with loaded config and resolved database path.
Args: Args:
@ -57,6 +59,8 @@ def create_app(db: Path | None = None) -> HaikuRAGApp: # pragma: no cover
Returns: Returns:
HaikuRAGApp instance with proper config and db path. HaikuRAGApp instance with proper config and db path.
""" """
from haiku.rag.app import HaikuRAGApp
config = get_config() config = get_config()
db_path = db if db else config.storage.data_dir / "haiku.rag.lancedb" db_path = db if db else config.storage.data_dir / "haiku.rag.lancedb"
return HaikuRAGApp(db_path=db_path, config=config, read_only=_read_only) return HaikuRAGApp(db_path=db_path, config=config, read_only=_read_only)
@ -402,6 +406,8 @@ def analyze( # pragma: no cover
@_cli.command("settings", help="Display current configuration settings") @_cli.command("settings", help="Display current configuration settings")
def settings(): # pragma: no cover def settings(): # pragma: no cover
from haiku.rag.app import HaikuRAGApp
config = get_config() config = get_config()
app = HaikuRAGApp(db_path=Path(), config=config, read_only=True) app = HaikuRAGApp(db_path=Path(), config=config, read_only=True)
app.show_settings() app.show_settings()
@ -719,6 +725,8 @@ def tag_restore( # pragma: no cover
@_cli.command("download-models", help="Download Docling and Ollama models per config") @_cli.command("download-models", help="Download Docling and Ollama models per config")
def download_models_cmd(): # pragma: no cover def download_models_cmd(): # pragma: no cover
from haiku.rag.app import HaikuRAGApp
app = HaikuRAGApp(db_path=Path(), config=get_config(), read_only=True) app = HaikuRAGApp(db_path=Path(), config=get_config(), read_only=True)
try: try:
asyncio.run(app.download_models()) asyncio.run(app.download_models())

View file

@ -4,6 +4,7 @@ from collections.abc import Iterator
from contextlib import contextmanager from contextlib import contextmanager
from datetime import UTC, datetime from datetime import UTC, datetime
from pathlib import Path from pathlib import Path
from typing import TYPE_CHECKING
import typer import typer
import yaml import yaml
@ -28,11 +29,6 @@ from haiku.rag.config import ( # noqa: E402
load_yaml_config, load_yaml_config,
set_config, set_config,
) )
from haiku.rag.ingester.app import ( # noqa: E402
BatchProgress,
BatchProgressCallback,
IngesterApp,
)
from haiku.rag.ingester.batch import BatchManifest # noqa: E402 from haiku.rag.ingester.batch import BatchManifest # noqa: E402
from haiku.rag.ingester.queue.migrations import open_queue # noqa: E402 from haiku.rag.ingester.queue.migrations import open_queue # noqa: E402
from haiku.rag.logging import configure_cli_logging # noqa: E402 from haiku.rag.logging import configure_cli_logging # noqa: E402
@ -41,6 +37,9 @@ from haiku.rag.store.exceptions import ( # noqa: E402
ReadOnlyError, ReadOnlyError,
) )
if TYPE_CHECKING:
from haiku.rag.ingester.app import BatchProgress, BatchProgressCallback
_cli = typer.Typer( _cli = typer.Typer(
name="haiku-ingester", name="haiku-ingester",
no_args_is_help=True, no_args_is_help=True,
@ -167,7 +166,7 @@ def _write_manifest(manifest: BatchManifest, path: Path) -> None:
@contextmanager @contextmanager
def _batch_progress( def _batch_progress(
description: str, description: str,
) -> Iterator[BatchProgressCallback | None]: # pragma: no cover ) -> "Iterator[BatchProgressCallback | None]": # pragma: no cover
console = Console(file=sys.stdout) console = Console(file=sys.stdout)
if not console.is_terminal: if not console.is_terminal:
yield None yield None
@ -184,7 +183,7 @@ def _batch_progress(
) )
task_id = None task_id = None
def _update(snapshot: BatchProgress) -> None: def _update(snapshot: "BatchProgress") -> None:
nonlocal task_id nonlocal task_id
task_description = ( task_description = (
f"{description} ({snapshot.succeeded} ok, {snapshot.dead} dead)" f"{description} ({snapshot.succeeded} ok, {snapshot.dead} dead)"
@ -244,6 +243,8 @@ def serve(
) -> None: ) -> None:
"""Run the production ingester: pollers + workers (and the HTTP API """Run the production ingester: pollers + workers (and the HTTP API
unless --no-api is set). Blocks until SIGINT/SIGTERM.""" unless --no-api is set). Blocks until SIGINT/SIGTERM."""
from haiku.rag.ingester.app import IngesterApp
app_config = get_config() app_config = get_config()
if host is not None: if host is not None:
app_config.ingester.api.host = host app_config.ingester.api.host = host
@ -309,6 +310,8 @@ async def _run_batch(
output: Path | None = None, output: Path | None = None,
manifest_path: Path | None = None, manifest_path: Path | None = None,
) -> None: ) -> None:
from haiku.rag.ingester.app import IngesterApp
db = _resolve_db_path(app_config, db_path) db = _resolve_db_path(app_config, db_path)
app = IngesterApp(config=app_config, db_path=db) app = IngesterApp(config=app_config, db_path=db)
if dry_run: if dry_run:

View file

@ -1,5 +1,4 @@
from .engine import Store
from .exceptions import MigrationRequiredError, ReadOnlyError from .exceptions import MigrationRequiredError, ReadOnlyError
from .models import Chunk, Document from .models import Chunk, Document
__all__ = ["Store", "Chunk", "Document", "MigrationRequiredError", "ReadOnlyError"] __all__ = ["Chunk", "Document", "MigrationRequiredError", "ReadOnlyError"]

View file

@ -1,6 +1,8 @@
"""haiku-ingester CLI: exercises every subcommand via CliRunner with """haiku-ingester CLI: exercises every subcommand via CliRunner with
IngesterApp / open_queue patched out so no real ingestion runs.""" IngesterApp / open_queue patched out so no real ingestion runs."""
import subprocess
import sys
from contextlib import contextmanager from contextlib import contextmanager
from datetime import UTC, datetime from datetime import UTC, datetime
from unittest.mock import AsyncMock, MagicMock from unittest.mock import AsyncMock, MagicMock
@ -24,6 +26,22 @@ from haiku.rag.ingester.queue.models import JobOp
runner = CliRunner() runner = CliRunner()
def test_importing_ingester_cli_does_not_load_lancedb():
"""Test that lancedb is not imported automatically by the cli. Doing so is
expensive. Must be run in a subprocess because lancedb might be imported by
other tests in the same session."""
result = subprocess.run(
[
sys.executable,
"-c",
"import haiku.rag.ingester.cli, sys; assert 'lancedb' not in sys.modules",
],
capture_output=True,
text=True,
)
assert result.returncode == 0, result.stderr
# --- helpers --- # --- helpers ---
@ -41,7 +59,7 @@ def _config_with_queue(queue: QueueConfig):
def _fake_app(report: BatchReport, monkeypatch) -> AsyncMock: def _fake_app(report: BatchReport, monkeypatch) -> AsyncMock:
fake = AsyncMock() fake = AsyncMock()
fake.run_batch.return_value = report fake.run_batch.return_value = report
monkeypatch.setattr("haiku.rag.ingester.cli.IngesterApp", lambda **_: fake) monkeypatch.setattr("haiku.rag.ingester.app.IngesterApp", lambda **_: fake)
return fake return fake
@ -78,14 +96,14 @@ def _manifest() -> BatchManifest:
def _fake_dry_run_app(report: BatchDryRunReport, monkeypatch) -> AsyncMock: def _fake_dry_run_app(report: BatchDryRunReport, monkeypatch) -> AsyncMock:
fake = AsyncMock() fake = AsyncMock()
fake.run_batch_dry_run.return_value = report fake.run_batch_dry_run.return_value = report
monkeypatch.setattr("haiku.rag.ingester.cli.IngesterApp", lambda **_: fake) monkeypatch.setattr("haiku.rag.ingester.app.IngesterApp", lambda **_: fake)
return fake return fake
def _fake_manifest_app(report: BatchReport, monkeypatch) -> AsyncMock: def _fake_manifest_app(report: BatchReport, monkeypatch) -> AsyncMock:
fake = AsyncMock() fake = AsyncMock()
fake.run_batch_from_manifest.return_value = report fake.run_batch_from_manifest.return_value = report
monkeypatch.setattr("haiku.rag.ingester.cli.IngesterApp", lambda **_: fake) monkeypatch.setattr("haiku.rag.ingester.app.IngesterApp", lambda **_: fake)
return fake return fake
@ -111,7 +129,7 @@ def test_run_batch_passes_progress_callback_when_enabled(monkeypatch):
return BatchReport(succeeded=1, dead=0) return BatchReport(succeeded=1, dead=0)
fake.run_batch.side_effect = _run_batch fake.run_batch.side_effect = _run_batch
monkeypatch.setattr("haiku.rag.ingester.cli.IngesterApp", lambda **_: fake) monkeypatch.setattr("haiku.rag.ingester.app.IngesterApp", lambda **_: fake)
monkeypatch.setattr( monkeypatch.setattr(
"haiku.rag.ingester.cli._batch_progress", "haiku.rag.ingester.cli._batch_progress",
lambda _: _progress_context(lambda snapshot: None), lambda _: _progress_context(lambda snapshot: None),
@ -227,7 +245,7 @@ def test_run_batch_manifest_passes_progress_callback(monkeypatch, tmp_path):
return BatchReport(succeeded=1, dead=0) return BatchReport(succeeded=1, dead=0)
fake.run_batch_from_manifest.side_effect = _run_manifest fake.run_batch_from_manifest.side_effect = _run_manifest
monkeypatch.setattr("haiku.rag.ingester.cli.IngesterApp", lambda **_: fake) monkeypatch.setattr("haiku.rag.ingester.app.IngesterApp", lambda **_: fake)
monkeypatch.setattr( monkeypatch.setattr(
"haiku.rag.ingester.cli._batch_progress", "haiku.rag.ingester.cli._batch_progress",
lambda _: _progress_context(lambda snapshot: None), lambda _: _progress_context(lambda snapshot: None),
@ -256,7 +274,7 @@ def test_run_batch_manifest_reports_validation_error(monkeypatch, tmp_path):
_write_manifest(manifest_path) _write_manifest(manifest_path)
fake = AsyncMock() fake = AsyncMock()
fake.run_batch_from_manifest.side_effect = ValueError("bad manifest") fake.run_batch_from_manifest.side_effect = ValueError("bad manifest")
monkeypatch.setattr("haiku.rag.ingester.cli.IngesterApp", lambda **_: fake) monkeypatch.setattr("haiku.rag.ingester.app.IngesterApp", lambda **_: fake)
result = runner.invoke(cli, ["run-batch", "--manifest", str(manifest_path)]) result = runner.invoke(cli, ["run-batch", "--manifest", str(manifest_path)])
@ -322,7 +340,7 @@ def test_resolve_queue_config_keeps_dburi_when_path_override_present(tmp_path):
def test_serve_invokes_app(monkeypatch): def test_serve_invokes_app(monkeypatch):
fake = AsyncMock() fake = AsyncMock()
monkeypatch.setattr("haiku.rag.ingester.cli.IngesterApp", lambda **_: fake) monkeypatch.setattr("haiku.rag.ingester.app.IngesterApp", lambda **_: fake)
result = runner.invoke(cli, ["serve", "--db", "x.lancedb", "--no-api"]) result = runner.invoke(cli, ["serve", "--db", "x.lancedb", "--no-api"])
@ -338,7 +356,7 @@ def test_serve_passes_host_and_port(monkeypatch):
captured.update(kwargs) captured.update(kwargs)
return fake return fake
monkeypatch.setattr("haiku.rag.ingester.cli.IngesterApp", _capture) monkeypatch.setattr("haiku.rag.ingester.app.IngesterApp", _capture)
result = runner.invoke( result = runner.invoke(
cli, ["serve", "--db", "x.lancedb", "--host", "0.0.0.0", "--port", "9999"] cli, ["serve", "--db", "x.lancedb", "--host", "0.0.0.0", "--port", "9999"]
@ -357,7 +375,7 @@ def test_serve_passes_root_path(monkeypatch):
captured.update(kwargs) captured.update(kwargs)
return fake return fake
monkeypatch.setattr("haiku.rag.ingester.cli.IngesterApp", _capture) monkeypatch.setattr("haiku.rag.ingester.app.IngesterApp", _capture)
result = runner.invoke( result = runner.invoke(
cli, ["serve", "--db", "x.lancedb", "--root-path", "/ingester/"] cli, ["serve", "--db", "x.lancedb", "--root-path", "/ingester/"]

View file

@ -1,7 +1,6 @@
import pytest import pytest
from haiku.rag.store import Store from haiku.rag.store.engine import Store, get_database_stats
from haiku.rag.store.engine import get_database_stats
class TestGetDatabaseStats: class TestGetDatabaseStats:

View file

@ -2,7 +2,7 @@ from importlib import metadata
import pytest import pytest
from haiku.rag.store import Store from haiku.rag.store.engine import Store
from haiku.rag.store.exceptions import MigrationRequiredError from haiku.rag.store.exceptions import MigrationRequiredError

View file

@ -3,7 +3,8 @@ from pathlib import Path
import pytest import pytest
from haiku.rag.client import HaikuRAG from haiku.rag.client import HaikuRAG
from haiku.rag.store import ReadOnlyError, Store from haiku.rag.store import ReadOnlyError
from haiku.rag.store.engine import Store
from haiku.rag.store.models import Chunk, Document from haiku.rag.store.models import Chunk, Document
from haiku.rag.store.repositories.chunk import ChunkRepository from haiku.rag.store.repositories.chunk import ChunkRepository
from haiku.rag.store.repositories.document import DocumentRepository from haiku.rag.store.repositories.document import DocumentRepository

View file

@ -3,8 +3,8 @@ import re
import pytest import pytest
from lancedb.table import AsyncTable, AsyncTags from lancedb.table import AsyncTable, AsyncTags
from haiku.rag.store import ReadOnlyError, Store from haiku.rag.store import ReadOnlyError
from haiku.rag.store.engine import RESTORE_TABLE_ORDER from haiku.rag.store.engine import RESTORE_TABLE_ORDER, Store
from haiku.rag.store.models import Document from haiku.rag.store.models import Document
from haiku.rag.store.repositories.document import DocumentRepository from haiku.rag.store.repositories.document import DocumentRepository

View file

@ -3,8 +3,8 @@ import asyncio
import pytest import pytest
from lancedb.table import AsyncTags from lancedb.table import AsyncTags
from haiku.rag.store import ReadOnlyError, Store from haiku.rag.store import ReadOnlyError
from haiku.rag.store.engine import REQUIRED_TABLES from haiku.rag.store.engine import REQUIRED_TABLES, Store
from haiku.rag.store.models import Document from haiku.rag.store.models import Document
from haiku.rag.store.repositories.document import DocumentRepository from haiku.rag.store.repositories.document import DocumentRepository

View file

@ -8,9 +8,8 @@ introduced by later migrations (``picture_data`` in v0.45.0,
import pytest import pytest
from haiku.rag.store import Store
from haiku.rag.store.compression import compress_docling_split from haiku.rag.store.compression import compress_docling_split
from haiku.rag.store.engine import DocumentRecord from haiku.rag.store.engine import DocumentRecord, Store
from haiku.rag.store.upgrades.v0_40_0 import _apply_populate_document_items from haiku.rag.store.upgrades.v0_40_0 import _apply_populate_document_items

View file

@ -12,9 +12,8 @@ import json
import pyarrow as pa import pyarrow as pa
import pytest import pytest
from haiku.rag.store import Store
from haiku.rag.store.compression import compress_json, decompress_json from haiku.rag.store.compression import compress_json, decompress_json
from haiku.rag.store.engine import DocumentItemRecord, DocumentRecord from haiku.rag.store.engine import DocumentItemRecord, DocumentRecord, Store
from haiku.rag.store.upgrades.v0_45_0 import _apply_extract_picture_bytes from haiku.rag.store.upgrades.v0_45_0 import _apply_extract_picture_bytes
PNG_BYTES = b"\x89PNG\r\n\x1a\nfake-picture-bytes" PNG_BYTES = b"\x89PNG\r\n\x1a\nfake-picture-bytes"

View file

@ -1,3 +1,5 @@
import subprocess
import sys
from unittest.mock import patch from unittest.mock import patch
import pytest import pytest
@ -12,6 +14,22 @@ from haiku.rag.store.exceptions import MigrationRequiredError
runner = CliRunner() runner = CliRunner()
def test_importing_cli_does_not_load_lancedb():
"""Test that lancedb is not imported automatically by the cli. Doing so is
expensive. Must be run in a subprocess because lancedb might be imported by
other tests in the same session."""
result = subprocess.run(
[
sys.executable,
"-c",
"import haiku.rag.cli, sys; assert 'lancedb' not in sys.modules",
],
capture_output=True,
text=True,
)
assert result.returncode == 0, result.stderr
class TestParseMetaOptions: class TestParseMetaOptions:
def test_empty_input(self): def test_empty_input(self):
assert _parse_meta_options(None) == {} assert _parse_meta_options(None) == {}