diff --git a/services/search/core.py b/services/search/core.py index 4a8d58a7e..871be886d 100644 --- a/services/search/core.py +++ b/services/search/core.py @@ -782,6 +782,10 @@ def searxng_search_results(query: str, count: int = 10, time_filter: str = None) if results: logger.info(f"{provider_name} search succeeded with {len(results)} results") break + # A completed empty/unrelated result set is not a transport + # failure. Advance to another provider rather than repeating + # the exact request and spending the tool deadline twice. + break except (NetworkError, ParseError, RateLimitError) as e: error_logger.error(f"{provider_name} search error (attempt {attempt + 1}): {e}") except Exception as e: @@ -915,6 +919,8 @@ def comprehensive_web_search( logger.info(f"Comprehensive search: {provider_name} returned {len(search_results)} results") break empty = True + last_err = None + break except Exception as e: last_err = e logger.warning(f"Comprehensive search: {provider_name} attempt {attempt + 1} failed: {e}") diff --git a/tests/test_search_empty_query_relaxation.py b/tests/test_search_empty_query_relaxation.py index 2b547db48..a6efceedf 100644 --- a/tests/test_search_empty_query_relaxation.py +++ b/tests/test_search_empty_query_relaxation.py @@ -2,6 +2,33 @@ from services.search import core import pytest +@pytest.mark.parametrize('comprehensive', [False, True]) +@pytest.mark.parametrize('transient_error', [False, True]) +def test_empty_results_advance_provider_but_transport_errors_get_one_retry(monkeypatch, tmp_path, comprehensive, transient_error): + calls = [] + def provider(name, query, count, time_filter=None): + calls.append(name) + if transient_error and name == 'primary' and calls.count(name) == 1: + raise ConnectionError('temporary failure') + return [] + monkeypatch.setattr(core, 'SEARCH_CACHE_DIR', tmp_path) + monkeypatch.setattr(core, 'search_cache_index', {}) + monkeypatch.setattr(core, '_get_search_settings', lambda: {'search_provider': 'primary'}) + monkeypatch.setattr(core, '_build_provider_chain', lambda primary: ['primary', 'fallback']) + monkeypatch.setattr(core, '_call_provider', provider) + monkeypatch.setattr(core, '_record_query', lambda *a, **k: None) + monkeypatch.setattr(core, 'cleanup_cache', lambda *a, **k: None) + query = 'reference site:example.org' + if comprehensive: + output, sources = core.comprehensive_web_search(query, return_sources=True) + assert sources == [] + if transient_error: + assert 'primary:empty' in output + else: + assert core.searxng_search_results(query) == [] + assert calls == (['primary', 'primary', 'fallback'] if transient_error else ['primary', 'fallback']) + + @pytest.mark.parametrize('url,accepted', [ ('https://python.org/downloads/', True), ('https://docs.python.org/3/', True),