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
- `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

View file

@ -4,7 +4,7 @@ import sys
import warnings
from importlib.metadata import version
from pathlib import Path
from typing import Any
from typing import TYPE_CHECKING, Any
import typer
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
load_dotenv(find_dotenv(usecwd=True))
from haiku.rag.app import HaikuRAGApp # noqa: E402
from haiku.rag.config import ( # noqa: E402
AppConfig,
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.utils import is_up_to_date # noqa: E402
if TYPE_CHECKING:
from haiku.rag.app import HaikuRAGApp
_cli = typer.Typer(
context_settings={"help_option_names": ["-h", "--help"]},
no_args_is_help=True,
@ -48,7 +50,7 @@ def cli():
_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.
Args:
@ -57,6 +59,8 @@ def create_app(db: Path | None = None) -> HaikuRAGApp: # pragma: no cover
Returns:
HaikuRAGApp instance with proper config and db path.
"""
from haiku.rag.app import HaikuRAGApp
config = get_config()
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)
@ -402,6 +406,8 @@ def analyze( # pragma: no cover
@_cli.command("settings", help="Display current configuration settings")
def settings(): # pragma: no cover
from haiku.rag.app import HaikuRAGApp
config = get_config()
app = HaikuRAGApp(db_path=Path(), config=config, read_only=True)
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")
def download_models_cmd(): # pragma: no cover
from haiku.rag.app import HaikuRAGApp
app = HaikuRAGApp(db_path=Path(), config=get_config(), read_only=True)
try:
asyncio.run(app.download_models())

View file

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

View file

@ -1,5 +1,4 @@
from .engine import Store
from .exceptions import MigrationRequiredError, ReadOnlyError
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
IngesterApp / open_queue patched out so no real ingestion runs."""
import subprocess
import sys
from contextlib import contextmanager
from datetime import UTC, datetime
from unittest.mock import AsyncMock, MagicMock
@ -24,6 +26,22 @@ from haiku.rag.ingester.queue.models import JobOp
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 ---
@ -41,7 +59,7 @@ def _config_with_queue(queue: QueueConfig):
def _fake_app(report: BatchReport, monkeypatch) -> AsyncMock:
fake = AsyncMock()
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
@ -78,14 +96,14 @@ def _manifest() -> BatchManifest:
def _fake_dry_run_app(report: BatchDryRunReport, monkeypatch) -> AsyncMock:
fake = AsyncMock()
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
def _fake_manifest_app(report: BatchReport, monkeypatch) -> AsyncMock:
fake = AsyncMock()
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
@ -111,7 +129,7 @@ def test_run_batch_passes_progress_callback_when_enabled(monkeypatch):
return BatchReport(succeeded=1, dead=0)
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(
"haiku.rag.ingester.cli._batch_progress",
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)
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(
"haiku.rag.ingester.cli._batch_progress",
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)
fake = AsyncMock()
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)])
@ -322,7 +340,7 @@ def test_resolve_queue_config_keeps_dburi_when_path_override_present(tmp_path):
def test_serve_invokes_app(monkeypatch):
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"])
@ -338,7 +356,7 @@ def test_serve_passes_host_and_port(monkeypatch):
captured.update(kwargs)
return fake
monkeypatch.setattr("haiku.rag.ingester.cli.IngesterApp", _capture)
monkeypatch.setattr("haiku.rag.ingester.app.IngesterApp", _capture)
result = runner.invoke(
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)
return fake
monkeypatch.setattr("haiku.rag.ingester.cli.IngesterApp", _capture)
monkeypatch.setattr("haiku.rag.ingester.app.IngesterApp", _capture)
result = runner.invoke(
cli, ["serve", "--db", "x.lancedb", "--root-path", "/ingester/"]

View file

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

View file

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

View file

@ -3,7 +3,8 @@ from pathlib import Path
import pytest
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.repositories.chunk import ChunkRepository
from haiku.rag.store.repositories.document import DocumentRepository

View file

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

View file

@ -3,8 +3,8 @@ import asyncio
import pytest
from lancedb.table import AsyncTags
from haiku.rag.store import ReadOnlyError, Store
from haiku.rag.store.engine import REQUIRED_TABLES
from haiku.rag.store import ReadOnlyError
from haiku.rag.store.engine import REQUIRED_TABLES, Store
from haiku.rag.store.models import Document
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
from haiku.rag.store import Store
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

View file

@ -12,9 +12,8 @@ import json
import pyarrow as pa
import pytest
from haiku.rag.store import Store
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
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
import pytest
@ -12,6 +14,22 @@ from haiku.rag.store.exceptions import MigrationRequiredError
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:
def test_empty_input(self):
assert _parse_meta_options(None) == {}