Coverage for src/local_deep_research/advanced_search_system/parallel_search.py: 100%
16 statements
« prev ^ index » next coverage.py v7.16.0, created at 2026-09-06 15:42 +0000
« prev ^ index » next coverage.py v7.16.0, created at 2026-09-06 15:42 +0000
1"""Parallel search execution helper.
3Provides :func:`run_parallel_searches`, a small helper that runs a sequence
4of search queries concurrently using :class:`~concurrent.futures.ThreadPoolExecutor`
5while preserving the research context expected by worker threads.
7The helper is intentionally generic:
9* ``search_fn`` is a callable accepting the query string and returning
10 whatever per-query payload the caller needs (a list of results, a dict
11 with metadata, etc.). Callers wrap it with context-preserving
12 decorators (e.g. :func:`preserve_research_context`) before passing it
13 in; this helper stays focused on the concurrency concern.
15Returns a list of ``(query, payload)`` tuples in completion order.
16Callers that need a question-keyed dict build it from this list; callers
17that only need a flat list of results flatten it directly.
18"""
20from __future__ import annotations
22import concurrent.futures
23from typing import Callable, List, Optional, Tuple, TypeVar
25from loguru import logger
27T = TypeVar("T")
30def run_parallel_searches(
31 queries: List[str],
32 search_fn: Callable[[str], T],
33 max_workers: Optional[int] = None,
34) -> List[Tuple[str, T]]:
35 """Run ``search_fn`` for each query in parallel.
37 Args:
38 queries: Queries to search. If empty, returns an empty list
39 immediately (and logs a warning).
40 search_fn: Callable invoked as ``search_fn(query)`` inside a worker
41 thread. Callers are responsible for wrapping it with any
42 context-preserving decorators (e.g.
43 :func:`preserve_research_context`) before passing it in, and
44 for their own error handling (the callable should never raise
45 — return an empty payload on failure instead, matching the
46 pre-existing contract of the strategies this was extracted
47 from).
48 max_workers: Size of the thread pool. Defaults to ``len(queries)``
49 when ``None``, matching the historical behavior of the
50 source-based, focused-iteration, and progressive strategies.
52 Returns:
53 A list of ``(query, payload)`` tuples in completion order. Each
54 ``payload`` is whatever ``search_fn`` returned for that query.
55 """
56 if not queries:
57 logger.warning("No queries provided for parallel search")
58 return []
60 if max_workers is None:
61 max_workers = len(queries)
63 def _worker(query: str) -> Tuple[str, T]:
64 return (query, search_fn(query))
66 with concurrent.futures.ThreadPoolExecutor(
67 max_workers=max_workers
68 ) as executor:
69 futures = [executor.submit(_worker, q) for q in queries]
70 return [
71 future.result()
72 for future in concurrent.futures.as_completed(futures)
73 ]