From 5ebe9ee67ab69daf5603a2e9482f169cf9625d25 Mon Sep 17 00:00:00 2001 From: mist Date: Tue, 2 Jun 2026 04:53:33 +0300 Subject: [PATCH] Fix invalidate_search_cache using a key that never matches stored entries (#852) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit invalidate_search_cache(query) built its cache key as generate_cache_key(f"{query}|10|None"), but the write path (searxng_search_results) replaces the caller's default count of 10 with the admin-configured _get_result_count() (default 5) before building the key. So a default search for "X" is cached under "X|5|None", while invalidation looked for "X|10|None" — they never match, and invalidate_search_cache silently failed to remove anything in the default configuration, violating its docstring ("invalidate ... just the given query"). Derive the count from _get_result_count() so invalidation matches the default-search entry the write path actually stores. The same bug (and fix) applies to both the src/search and services/search copies. Note: time-filtered variants (e.g. "X|5|day") still aren't reachable from a query-only signature, since cache keys are opaque SHA-256 hashes with no stored query; clearing those would need a broader cache-index redesign and is out of scope here. Adds tests/test_search_cache_invalidation.py covering the default-count case. --- services/search/core.py | 5 ++- src/search/core.py | 5 ++- tests/test_search_cache_invalidation.py | 45 +++++++++++++++++++++++++ 3 files changed, 53 insertions(+), 2 deletions(-) create mode 100644 tests/test_search_cache_invalidation.py diff --git a/services/search/core.py b/services/search/core.py index 946a0b4..7208ea2 100644 --- a/services/search/core.py +++ b/services/search/core.py @@ -203,7 +203,10 @@ def invalidate_search_cache(query: Optional[str] = None) -> None: search_cache_index.clear() logger.info("All search cache entries have been cleared.") else: - cache_key = generate_cache_key(f"{query}|10|None") + # Match the key the write path stores: searxng_search_results replaces + # the caller's default count with the configured _get_result_count() + # (default 5), so a hardcoded "|10|None" never matched a real entry. + cache_key = generate_cache_key(f"{query}|{_get_result_count()}|None") cache_file = SEARCH_CACHE_DIR / f"{cache_key}.cache" if cache_file.exists(): try: diff --git a/src/search/core.py b/src/search/core.py index f1a3453..850e026 100644 --- a/src/search/core.py +++ b/src/search/core.py @@ -207,7 +207,10 @@ def invalidate_search_cache(query: Optional[str] = None) -> None: search_cache_index.clear() logger.info("All search cache entries have been cleared.") else: - cache_key = generate_cache_key(f"{query}|10|None") + # Match the key the write path stores: searxng_search_results replaces + # the caller's default count with the configured _get_result_count() + # (default 5), so a hardcoded "|10|None" never matched a real entry. + cache_key = generate_cache_key(f"{query}|{_get_result_count()}|None") cache_file = SEARCH_CACHE_DIR / f"{cache_key}.cache" if cache_file.exists(): try: diff --git a/tests/test_search_cache_invalidation.py b/tests/test_search_cache_invalidation.py new file mode 100644 index 0000000..5ad245b --- /dev/null +++ b/tests/test_search_cache_invalidation.py @@ -0,0 +1,45 @@ +"""Regression test for invalidate_search_cache key construction. + +The write path (`searxng_search_results`) stores a cache entry under +``generate_cache_key(f"{query}|{count}|{time_filter}")`` where ``count`` is the +admin-configured result count (``_get_result_count()``, default **5**) — it +replaces the caller's default of 10 with the configured value before building +the key. + +The original ``invalidate_search_cache`` hardcoded ``f"{query}|10|None"``, so it +never matched the key the write path actually produced (``|5|None`` by default) +and silently failed to invalidate anything — a contract violation of its own +docstring ("invalidate ... just the given query"). The fix derives the count +from ``_get_result_count()`` so invalidation matches the stored default entry. +""" +import pytest + +from src.search import core +from src.search.cache import generate_cache_key + + +def test_invalidate_uses_configured_count_not_hardcoded_10(tmp_path, monkeypatch): + query = "python tutorial" + result_count = 5 # documented default of _get_result_count() + + # Pin the configured count and redirect the cache dir to keep the test hermetic. + monkeypatch.setattr(core, "_get_result_count", lambda: result_count) + monkeypatch.setattr(core, "SEARCH_CACHE_DIR", tmp_path) + + # Reproduce exactly what searxng_search_results writes for a default search: + # the caller's default count of 10 is replaced by result_count, time_filter=None. + write_key = generate_cache_key(f"{query}|{result_count}|None") + cache_file = tmp_path / f"{write_key}.cache" + cache_file.write_text("{}", encoding="utf-8") + core.search_cache_index[write_key] = None + + try: + core.invalidate_search_cache(query) + + assert not cache_file.exists(), ( + "invalidate_search_cache failed to remove the entry the write path " + "stored under the configured result count — it used a mismatched key." + ) + assert write_key not in core.search_cache_index + finally: + core.search_cache_index.pop(write_key, None)