Librarian: simple result cache for repeated queries
CI / compile (pull_request) Successful in 8s
CI / unit (pull_request) Successful in 26s
CI / integration (pull_request) Successful in 26s
build / build (push) Successful in 25s
CI / compile (push) Successful in 9s
CI / unit (push) Successful in 27s
CI / integration (push) Successful in 27s
CI / compile (pull_request) Successful in 8s
CI / unit (pull_request) Successful in 26s
CI / integration (pull_request) Successful in 26s
build / build (push) Successful in 25s
CI / compile (push) Successful in 9s
CI / unit (push) Successful in 27s
CI / integration (push) Successful in 27s
A repeat of the same query (whitespace/case-normalised, scoped by deep-vs-shallow) returns the stored hits and skips the whole Crossref call and DB scan. Disk-backed (survives restart), TTL'd (CONJURER_LIBRARIAN_CACHE_TTL, default 7d; 0 disables) and size-bounded (CONJURER_LIBRARIAN_CACHE_MAX, default 500). Reuses DiskQueue, so it's a handful of lines. Nothing fancy - exact (normalised) match, not fuzzy. Checked before Crossref only on a fresh search (a resume from checkpoint still continues its scan), and stored after a completed search. Tests: hit/miss, normalisation, deep/shallow separation, expiry, disable, prune. Suite: 58 unit + 59 integration green. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit was merged in pull request #19.
This commit is contained in:
@@ -0,0 +1,70 @@
|
||||
"""Integration: the librarian's simple result cache.
|
||||
|
||||
A repeat of the same query (normalised) returns stored hits and skips the whole
|
||||
Crossref + DB scan. Nothing fancy: whitespace/case-insensitive exact match,
|
||||
disk-backed, TTL'd, size-bounded, deep/shallow kept separate.
|
||||
"""
|
||||
import sys
|
||||
import types
|
||||
|
||||
import pytest
|
||||
|
||||
if "habanero" not in sys.modules:
|
||||
_habanero = types.ModuleType("habanero")
|
||||
_habanero.Crossref = object
|
||||
sys.modules["habanero"] = _habanero
|
||||
|
||||
import conjurer_librarian as lib # noqa: E402
|
||||
from durable_queue import DiskQueue # noqa: E402
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def cache(tmp_path, monkeypatch):
|
||||
box = DiskQueue(str(tmp_path / "cache"))
|
||||
monkeypatch.setattr(lib, "_cache", box)
|
||||
monkeypatch.setattr(lib, "CACHE_TTL_SECONDS", 3600)
|
||||
monkeypatch.setattr(lib, "CACHE_MAX_ENTRIES", 500)
|
||||
return box
|
||||
|
||||
|
||||
_HITS = {"10.1000/x": {"Title": ["A Paper"], "type": "journal-article"}}
|
||||
|
||||
|
||||
def test_put_then_get_is_a_hit(cache):
|
||||
lib._cache_put("kwas foliowy", False, _HITS)
|
||||
assert lib._cache_get("kwas foliowy", False) == _HITS
|
||||
|
||||
|
||||
def test_different_query_misses(cache):
|
||||
lib._cache_put("kwas foliowy", False, _HITS)
|
||||
assert lib._cache_get("witamina c", False) is None
|
||||
|
||||
|
||||
def test_normalised_case_and_whitespace_hit_same_entry(cache):
|
||||
lib._cache_put(" Kwas Foliowy ", False, _HITS)
|
||||
assert lib._cache_get("kwas foliowy", False) == _HITS
|
||||
|
||||
|
||||
def test_deep_and_shallow_are_cached_separately(cache):
|
||||
lib._cache_put("q", False, _HITS)
|
||||
assert lib._cache_get("q", True) is None # a deep search is a different key
|
||||
assert lib._cache_get("q", False) == _HITS
|
||||
|
||||
|
||||
def test_expired_entry_is_a_miss(cache):
|
||||
cache.put(lib._cache_key("q", False), {"query": "q", "final_result": _HITS, "expires": 0})
|
||||
assert lib._cache_get("q", False) is None
|
||||
|
||||
|
||||
def test_ttl_zero_disables_the_cache(cache, monkeypatch):
|
||||
monkeypatch.setattr(lib, "CACHE_TTL_SECONDS", 0)
|
||||
lib._cache_put("q", False, _HITS) # no-op when disabled
|
||||
assert len(cache) == 0
|
||||
assert lib._cache_get("q", False) is None
|
||||
|
||||
|
||||
def test_prune_bounds_the_cache(cache, monkeypatch):
|
||||
monkeypatch.setattr(lib, "CACHE_MAX_ENTRIES", 3)
|
||||
for i in range(6):
|
||||
lib._cache_put(f"query-{i}", False, _HITS)
|
||||
assert len(cache) <= 3
|
||||
Reference in New Issue
Block a user