From b2ffc501680069e41273f7acae19fb086ed9585c Mon Sep 17 00:00:00 2001 From: Tres Seaver Date: Sun, 17 May 2026 08:55:43 -0400 Subject: [PATCH 01/10] WIP: add '--search-type' option to CLI 'search' --- haiku_rag_slim/haiku/rag/app.py | 5 ++++- haiku_rag_slim/haiku/rag/cli.py | 12 +++++++++++- 2 files changed, 15 insertions(+), 2 deletions(-) diff --git a/haiku_rag_slim/haiku/rag/app.py b/haiku_rag_slim/haiku/rag/app.py index a6ecd55f..60c13b08 100644 --- a/haiku_rag_slim/haiku/rag/app.py +++ b/haiku_rag_slim/haiku/rag/app.py @@ -361,6 +361,7 @@ class HaikuRAGApp: # pragma: no cover query: str | None = None, limit: int | None = None, filter: str | None = None, + search_type: str = "hybrid", image: Path | None = None, ): if query is None and image is None: @@ -385,7 +386,9 @@ class HaikuRAGApp: # pragma: no cover read_only=self.read_only, before=self.before, ) as self.client: - results = await self.client.search(search_input, limit=limit, filter=filter) + results = await self.client.search( + search_input, limit=limit, filter=filter, search_type=search_type, + ) if not results: self.console.print("[yellow]No results found.[/yellow]") return diff --git a/haiku_rag_slim/haiku/rag/cli.py b/haiku_rag_slim/haiku/rag/cli.py index 42d8f433..38c7afa0 100644 --- a/haiku_rag_slim/haiku/rag/cli.py +++ b/haiku_rag_slim/haiku/rag/cli.py @@ -314,6 +314,12 @@ def search( # pragma: no cover "-f", help="SQL WHERE clause to filter documents (e.g., \"uri LIKE '%arxiv%'\")", ), + search_type: str | None = typer.Option( + None, + "--search-type", + "-s", + help="Type of search: one of 'hybrid' (default) / 'fts' / 'vector' ", + ), image: Path | None = typer.Option( None, "--image", @@ -326,7 +332,11 @@ def search( # pragma: no cover ), ): app = create_app(db) - asyncio.run(app.search(query=query, limit=limit, filter=filter, image=image)) + asyncio.run( + app.search( + query=query, limit=limit, filter=filter, search_type=search_type, image=image, + ) + ) @_cli.command("visualize", help="Show visual grounding for a chunk") From 7953de46b9b5029fbf1685f93168b720f6ebbc4f Mon Sep 17 00:00:00 2001 From: Tres Seaver Date: Mon, 18 May 2026 06:33:04 -0400 Subject: [PATCH 02/10] fix: declare 'SearchType' for 'search_type' arg fix: allow 'search_type' only for text searches --- haiku_rag_slim/haiku/rag/app.py | 12 ++++++++++-- haiku_rag_slim/haiku/rag/cli.py | 13 +++++++++---- haiku_rag_slim/haiku/rag/client/__init__.py | 3 ++- haiku_rag_slim/haiku/rag/client/models.py | 3 +++ haiku_rag_slim/haiku/rag/client/search.py | 3 ++- 5 files changed, 26 insertions(+), 8 deletions(-) create mode 100644 haiku_rag_slim/haiku/rag/client/models.py diff --git a/haiku_rag_slim/haiku/rag/app.py b/haiku_rag_slim/haiku/rag/app.py index 60c13b08..9efd3418 100644 --- a/haiku_rag_slim/haiku/rag/app.py +++ b/haiku_rag_slim/haiku/rag/app.py @@ -19,6 +19,7 @@ from rich.progress import ( from rich.syntax import Syntax from haiku.rag.client import HaikuRAG, RebuildMode +from haiku.rag.client.models import SearchType from haiku.rag.config import AppConfig, Config from haiku.rag.mcp import create_mcp_server from haiku.rag.monitor import FileWatcher, S3Watcher @@ -361,7 +362,7 @@ class HaikuRAGApp: # pragma: no cover query: str | None = None, limit: int | None = None, filter: str | None = None, - search_type: str = "hybrid", + search_type: SearchType | None = None, image: Path | None = None, ): if query is None and image is None: @@ -373,6 +374,10 @@ class HaikuRAGApp: # pragma: no cover self.console.print("[red]Pass either a query or --image, not both.[/red]") return + if query is None and search_type is not None: + self.console.print("[red]Pass --search-type only for text queries[/red]") + return + search_input: str | bytes if image is not None: search_input = image.read_bytes() @@ -387,7 +392,10 @@ class HaikuRAGApp: # pragma: no cover before=self.before, ) as self.client: results = await self.client.search( - search_input, limit=limit, filter=filter, search_type=search_type, + search_input, + limit=limit, + filter=filter, + search_type=search_type, ) if not results: self.console.print("[yellow]No results found.[/yellow]") diff --git a/haiku_rag_slim/haiku/rag/cli.py b/haiku_rag_slim/haiku/rag/cli.py index 38c7afa0..b32a54ae 100644 --- a/haiku_rag_slim/haiku/rag/cli.py +++ b/haiku_rag_slim/haiku/rag/cli.py @@ -15,6 +15,7 @@ from dotenv import find_dotenv, load_dotenv load_dotenv(find_dotenv(usecwd=True)) from haiku.rag.app import HaikuRAGApp # noqa: E402 +from haiku.rag.client.models import SearchType # noqa: E402 from haiku.rag.config import ( # noqa: E402 AppConfig, find_config_file, @@ -314,11 +315,11 @@ def search( # pragma: no cover "-f", help="SQL WHERE clause to filter documents (e.g., \"uri LIKE '%arxiv%'\")", ), - search_type: str | None = typer.Option( - None, + search_type: SearchType = typer.Option( + "hybrid", "--search-type", "-s", - help="Type of search: one of 'hybrid' (default) / 'fts' / 'vector' ", + help="Type of search to perform (text searches only)", ), image: Path | None = typer.Option( None, @@ -334,7 +335,11 @@ def search( # pragma: no cover app = create_app(db) asyncio.run( app.search( - query=query, limit=limit, filter=filter, search_type=search_type, image=image, + query=query, + limit=limit, + filter=filter, + search_type=search_type, + image=image, ) ) diff --git a/haiku_rag_slim/haiku/rag/client/__init__.py b/haiku_rag_slim/haiku/rag/client/__init__.py index 3a26a533..5282f57a 100644 --- a/haiku_rag_slim/haiku/rag/client/__init__.py +++ b/haiku_rag_slim/haiku/rag/client/__init__.py @@ -13,6 +13,7 @@ from urllib.parse import urlparse import httpx +from haiku.rag.client.models import SearchType from haiku.rag.config import AppConfig, Config from haiku.rag.converters import get_converter from haiku.rag.reranking import get_reranker @@ -349,7 +350,7 @@ class HaikuRAG: self, query: "str | bytes | PILImage.Image", limit: int | None = None, - search_type: str = "hybrid", + search_type: SearchType | None = None, filter: str | None = None, include_images: bool = True, ) -> list[SearchResult]: diff --git a/haiku_rag_slim/haiku/rag/client/models.py b/haiku_rag_slim/haiku/rag/client/models.py new file mode 100644 index 00000000..7737fd8a --- /dev/null +++ b/haiku_rag_slim/haiku/rag/client/models.py @@ -0,0 +1,3 @@ +from typing import Literal + +SearchType = Literal["vector", "fts", "hybrid"] diff --git a/haiku_rag_slim/haiku/rag/client/search.py b/haiku_rag_slim/haiku/rag/client/search.py index 6d19ef4f..5886c421 100644 --- a/haiku_rag_slim/haiku/rag/client/search.py +++ b/haiku_rag_slim/haiku/rag/client/search.py @@ -1,6 +1,7 @@ import base64 from typing import TYPE_CHECKING +from haiku.rag.client.models import SearchType from haiku.rag.reranking import get_reranker from haiku.rag.store.models.chunk import Chunk, SearchResult @@ -14,7 +15,7 @@ async def search( client: "HaikuRAG", query: "str | bytes | PILImage.Image", limit: int | None = None, - search_type: str = "hybrid", + search_type: SearchType = "hybrid", filter: str | None = None, include_images: bool = True, ) -> list[SearchResult]: From 70da24b6917138e56b4cc3ab889b32cda463888b Mon Sep 17 00:00:00 2001 From: Tres Seaver Date: Mon, 18 May 2026 06:40:41 -0400 Subject: [PATCH 03/10] docs: add '--search-type' examples to CLI docs --- docs/cli.md | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/docs/cli.md b/docs/cli.md index d580e288..86cfd69a 100644 --- a/docs/cli.md +++ b/docs/cli.md @@ -133,6 +133,18 @@ With options: haiku-rag search "python programming" --limit 10 # or -l 10 ``` +With search type: +```bash +# Hybrid search (the default) +haiku-rag search "python programming" --search-type hybrid # or -s hybrid + +# Full-text search only +haiku-rag search "python programming" --search-type fts # or -s fts + +# Vector search only +haiku-rag search "python programming" --search-type vector # or -s vector +``` + With filters (filter by document properties, use `--filter` or `-f`): ```bash # Filter by URI pattern From 5079205d3d5e956968078e47b150d8174ad32033 Mon Sep 17 00:00:00 2001 From: Tres Seaver Date: Mon, 18 May 2026 06:50:49 -0400 Subject: [PATCH 04/10] fix: use 'SearchType' for 'ChunkRepository.search' --- haiku_rag_slim/haiku/rag/store/repositories/chunk.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/haiku_rag_slim/haiku/rag/store/repositories/chunk.py b/haiku_rag_slim/haiku/rag/store/repositories/chunk.py index f32121c8..91b94206 100644 --- a/haiku_rag_slim/haiku/rag/store/repositories/chunk.py +++ b/haiku_rag_slim/haiku/rag/store/repositories/chunk.py @@ -10,6 +10,7 @@ if TYPE_CHECKING: from lancedb.index import FTS from lancedb.rerankers import RRFReranker +from haiku.rag.client.models import SearchType from haiku.rag.store.engine import Store, query_to_pydantic from haiku.rag.store.models.chunk import Chunk @@ -220,7 +221,7 @@ class ChunkRepository: self, query: str = "", limit: int = 5, - search_type: str = "hybrid", + search_type: SearchType = "hybrid", filter: str | None = None, query_vector: list[float] | None = None, ) -> list[tuple[Chunk, float]]: From b331dd0b76383fb246d6be196f34bc86bb7440f4 Mon Sep 17 00:00:00 2001 From: Tres Seaver Date: Mon, 18 May 2026 06:52:03 -0400 Subject: [PATCH 05/10] fix: default 'search_type' to 'None' in 'client.search.search' Apply 'hybrid' default only for text queries. --- haiku_rag_slim/haiku/rag/client/search.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/haiku_rag_slim/haiku/rag/client/search.py b/haiku_rag_slim/haiku/rag/client/search.py index 5886c421..7d5592d8 100644 --- a/haiku_rag_slim/haiku/rag/client/search.py +++ b/haiku_rag_slim/haiku/rag/client/search.py @@ -15,7 +15,7 @@ async def search( client: "HaikuRAG", query: "str | bytes | PILImage.Image", limit: int | None = None, - search_type: SearchType = "hybrid", + search_type: SearchType | None = None, filter: str | None = None, include_images: bool = True, ) -> list[SearchResult]: @@ -26,7 +26,8 @@ async def search( query: Text (``str``) or image (``bytes`` / ``PIL.Image.Image``). Image queries require a multimodal embedder and run vector-only. limit: Maximum number of results to return. Defaults to config.search.limit. - search_type: "vector", "fts", or "hybrid" (default). Text queries only. + search_type: "vector", "fts", or "hybrid". + Applicable onlly for text queries, where the default is "hybrid". filter: Optional SQL WHERE clause to filter documents before searching chunks. include_images: When True, populate ``SearchResult.image_data`` with base64 picture bytes for picture-labeled chunks. @@ -38,6 +39,10 @@ async def search( limit = client._config.search.limit if isinstance(query, str): + + if search_type is None: + search_type = "hybrid" + reranker = get_reranker(config=client._config) if reranker is None: From 4feac9683a75b47703b34aa20ee234b31e9d11b2 Mon Sep 17 00:00:00 2001 From: Tres Seaver Date: Mon, 18 May 2026 09:17:09 -0400 Subject: [PATCH 06/10] fix: default 'search_type' to None --- haiku_rag_slim/haiku/rag/cli.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/haiku_rag_slim/haiku/rag/cli.py b/haiku_rag_slim/haiku/rag/cli.py index b32a54ae..44c3750e 100644 --- a/haiku_rag_slim/haiku/rag/cli.py +++ b/haiku_rag_slim/haiku/rag/cli.py @@ -315,8 +315,8 @@ def search( # pragma: no cover "-f", help="SQL WHERE clause to filter documents (e.g., \"uri LIKE '%arxiv%'\")", ), - search_type: SearchType = typer.Option( - "hybrid", + search_type: SearchType | None = typer.Option( + None, "--search-type", "-s", help="Type of search to perform (text searches only)", From d7e96f2658c824406d7135c5eb2b1f0e734bb6b6 Mon Sep 17 00:00:00 2001 From: Tres Seaver Date: Mon, 18 May 2026 09:17:53 -0400 Subject: [PATCH 07/10] fix: typo --- haiku_rag_slim/haiku/rag/client/search.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/haiku_rag_slim/haiku/rag/client/search.py b/haiku_rag_slim/haiku/rag/client/search.py index 7d5592d8..c0e71228 100644 --- a/haiku_rag_slim/haiku/rag/client/search.py +++ b/haiku_rag_slim/haiku/rag/client/search.py @@ -27,7 +27,7 @@ async def search( Image queries require a multimodal embedder and run vector-only. limit: Maximum number of results to return. Defaults to config.search.limit. search_type: "vector", "fts", or "hybrid". - Applicable onlly for text queries, where the default is "hybrid". + Applicable only for text queries, where the default is "hybrid". filter: Optional SQL WHERE clause to filter documents before searching chunks. include_images: When True, populate ``SearchResult.image_data`` with base64 picture bytes for picture-labeled chunks. From f9ce3bc401767da5f331ad45bda9e1057c2e8fc0 Mon Sep 17 00:00:00 2001 From: Tres Seaver Date: Mon, 18 May 2026 09:27:34 -0400 Subject: [PATCH 08/10] refactor: move 'SearchType' to 'store.models.chunk' --- haiku_rag_slim/haiku/rag/app.py | 2 +- haiku_rag_slim/haiku/rag/cli.py | 2 +- haiku_rag_slim/haiku/rag/client/__init__.py | 3 +-- haiku_rag_slim/haiku/rag/client/models.py | 3 --- haiku_rag_slim/haiku/rag/client/search.py | 3 +-- haiku_rag_slim/haiku/rag/store/models/chunk.py | 5 ++++- haiku_rag_slim/haiku/rag/store/repositories/chunk.py | 3 +-- 7 files changed, 9 insertions(+), 12 deletions(-) delete mode 100644 haiku_rag_slim/haiku/rag/client/models.py diff --git a/haiku_rag_slim/haiku/rag/app.py b/haiku_rag_slim/haiku/rag/app.py index 9efd3418..072d1768 100644 --- a/haiku_rag_slim/haiku/rag/app.py +++ b/haiku_rag_slim/haiku/rag/app.py @@ -19,11 +19,11 @@ from rich.progress import ( from rich.syntax import Syntax from haiku.rag.client import HaikuRAG, RebuildMode -from haiku.rag.client.models import SearchType from haiku.rag.config import AppConfig, Config from haiku.rag.mcp import create_mcp_server from haiku.rag.monitor import FileWatcher, S3Watcher from haiku.rag.store.models.document import Document +from haiku.rag.store.models.chunk import SearchType if TYPE_CHECKING: from haiku.rag.store.models import SearchResult diff --git a/haiku_rag_slim/haiku/rag/cli.py b/haiku_rag_slim/haiku/rag/cli.py index 44c3750e..64a93ccd 100644 --- a/haiku_rag_slim/haiku/rag/cli.py +++ b/haiku_rag_slim/haiku/rag/cli.py @@ -15,7 +15,6 @@ from dotenv import find_dotenv, load_dotenv load_dotenv(find_dotenv(usecwd=True)) from haiku.rag.app import HaikuRAGApp # noqa: E402 -from haiku.rag.client.models import SearchType # noqa: E402 from haiku.rag.config import ( # noqa: E402 AppConfig, find_config_file, @@ -28,6 +27,7 @@ from haiku.rag.store.exceptions import ( # noqa: E402 MigrationRequiredError, ReadOnlyError, ) +from haiku.rag.store.models.chunk import SearchType # noqa: E402 from haiku.rag.utils import is_up_to_date # noqa: E402 _cli = typer.Typer( diff --git a/haiku_rag_slim/haiku/rag/client/__init__.py b/haiku_rag_slim/haiku/rag/client/__init__.py index 5282f57a..537c5e18 100644 --- a/haiku_rag_slim/haiku/rag/client/__init__.py +++ b/haiku_rag_slim/haiku/rag/client/__init__.py @@ -13,12 +13,11 @@ from urllib.parse import urlparse import httpx -from haiku.rag.client.models import SearchType from haiku.rag.config import AppConfig, Config from haiku.rag.converters import get_converter from haiku.rag.reranking import get_reranker from haiku.rag.store.engine import Store -from haiku.rag.store.models.chunk import Chunk, SearchResult +from haiku.rag.store.models.chunk import Chunk, SearchResult, SearchType from haiku.rag.store.models.document import Document from haiku.rag.store.models.document_item import extract_items from haiku.rag.store.repositories.chunk import ChunkRepository diff --git a/haiku_rag_slim/haiku/rag/client/models.py b/haiku_rag_slim/haiku/rag/client/models.py deleted file mode 100644 index 7737fd8a..00000000 --- a/haiku_rag_slim/haiku/rag/client/models.py +++ /dev/null @@ -1,3 +0,0 @@ -from typing import Literal - -SearchType = Literal["vector", "fts", "hybrid"] diff --git a/haiku_rag_slim/haiku/rag/client/search.py b/haiku_rag_slim/haiku/rag/client/search.py index c0e71228..d9d6c33a 100644 --- a/haiku_rag_slim/haiku/rag/client/search.py +++ b/haiku_rag_slim/haiku/rag/client/search.py @@ -1,9 +1,8 @@ import base64 from typing import TYPE_CHECKING -from haiku.rag.client.models import SearchType from haiku.rag.reranking import get_reranker -from haiku.rag.store.models.chunk import Chunk, SearchResult +from haiku.rag.store.models.chunk import Chunk, SearchResult, SearchType if TYPE_CHECKING: from PIL import Image as PILImage diff --git a/haiku_rag_slim/haiku/rag/store/models/chunk.py b/haiku_rag_slim/haiku/rag/store/models/chunk.py index 76e2a932..4d448742 100644 --- a/haiku_rag_slim/haiku/rag/store/models/chunk.py +++ b/haiku_rag_slim/haiku/rag/store/models/chunk.py @@ -1,4 +1,4 @@ -from typing import TYPE_CHECKING +from typing import Literal, TYPE_CHECKING from pydantic import BaseModel, PrivateAttr @@ -113,6 +113,9 @@ class Chunk(BaseModel): return ChunkMetadata.model_validate(self.metadata) +SearchType = Literal["vector", "fts", "hybrid"] + + class SearchResult(BaseModel): """Search result with optional provenance information for citations. diff --git a/haiku_rag_slim/haiku/rag/store/repositories/chunk.py b/haiku_rag_slim/haiku/rag/store/repositories/chunk.py index 91b94206..96a8bbf2 100644 --- a/haiku_rag_slim/haiku/rag/store/repositories/chunk.py +++ b/haiku_rag_slim/haiku/rag/store/repositories/chunk.py @@ -10,9 +10,8 @@ if TYPE_CHECKING: from lancedb.index import FTS from lancedb.rerankers import RRFReranker -from haiku.rag.client.models import SearchType from haiku.rag.store.engine import Store, query_to_pydantic -from haiku.rag.store.models.chunk import Chunk +from haiku.rag.store.models.chunk import Chunk, SearchType logger = logging.getLogger(__name__) From ad0161e6dab806fb638683c891b157ce598c443c Mon Sep 17 00:00:00 2001 From: Tres Seaver Date: Mon, 18 May 2026 09:31:12 -0400 Subject: [PATCH 09/10] chore: isort --- haiku_rag_slim/haiku/rag/app.py | 2 +- haiku_rag_slim/haiku/rag/store/models/chunk.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/haiku_rag_slim/haiku/rag/app.py b/haiku_rag_slim/haiku/rag/app.py index 072d1768..45aa4220 100644 --- a/haiku_rag_slim/haiku/rag/app.py +++ b/haiku_rag_slim/haiku/rag/app.py @@ -22,8 +22,8 @@ from haiku.rag.client import HaikuRAG, RebuildMode from haiku.rag.config import AppConfig, Config from haiku.rag.mcp import create_mcp_server from haiku.rag.monitor import FileWatcher, S3Watcher -from haiku.rag.store.models.document import Document from haiku.rag.store.models.chunk import SearchType +from haiku.rag.store.models.document import Document if TYPE_CHECKING: from haiku.rag.store.models import SearchResult diff --git a/haiku_rag_slim/haiku/rag/store/models/chunk.py b/haiku_rag_slim/haiku/rag/store/models/chunk.py index 4d448742..5e86ec80 100644 --- a/haiku_rag_slim/haiku/rag/store/models/chunk.py +++ b/haiku_rag_slim/haiku/rag/store/models/chunk.py @@ -1,4 +1,4 @@ -from typing import Literal, TYPE_CHECKING +from typing import TYPE_CHECKING, Literal from pydantic import BaseModel, PrivateAttr From 8026693450c459485ad53bf6239da873801c49f4 Mon Sep 17 00:00:00 2001 From: Tres Seaver Date: Mon, 18 May 2026 09:31:38 -0400 Subject: [PATCH 10/10] chore: ruff format --- haiku_rag_slim/haiku/rag/client/search.py | 1 - 1 file changed, 1 deletion(-) diff --git a/haiku_rag_slim/haiku/rag/client/search.py b/haiku_rag_slim/haiku/rag/client/search.py index d9d6c33a..0cfaa9b1 100644 --- a/haiku_rag_slim/haiku/rag/client/search.py +++ b/haiku_rag_slim/haiku/rag/client/search.py @@ -38,7 +38,6 @@ async def search( limit = client._config.search.limit if isinstance(query, str): - if search_type is None: search_type = "hybrid"