fix: require cached gallery rows to keep the enrolled order

A cache key proves the store, not the rows inside the file. The old check
asked only that every cached row appear somewhere in the enrolled list, so a
row moved across SKUs still passed: the matrix that travelled with it would
then be filed under the wrong product, and a valid key would rename a SKU.

Rows now walk the enrolled list forward and never backwards, so a moved,
repeated or foreign row has nowhere left to match. Views that could not be
decoded stay absent without disturbing the order of the ones after them,
which is what keeps a shorter cache a hit rather than a re-embed.
This commit is contained in:
kiyo9w 2026-09-28 17:22:46 +09:00
parent 13a4e0f475
commit 02e69acac7
2 changed files with 105 additions and 33 deletions

View file

@ -236,6 +236,31 @@ class FeatureCacheTests(unittest.TestCase):
self.assertTrue(gallery.load())
return gallery, backbone
def cache_payload(self, gallery):
"""The rows and matrix a loaded gallery left on disk."""
with np.load(gallery._cache_path(), allow_pickle=False) as payload:
return (
[str(value) for value in payload["skus"].tolist()],
[str(value) for value in payload["views"].tolist()],
payload["matrix"],
)
def rewrite_cache(self, gallery, skus, views, matrix):
"""The same valid key with different contents: only the rows can catch it."""
path = gallery._cache_path()
with np.load(path, allow_pickle=False) as payload:
key = str(payload["key"].item())
np.savez(path, key=key, skus=np.array(skus), views=np.array(views), matrix=matrix)
def re_embeds(self, first, why):
"""The one right answer to a rewritten cache: build it from the pixels."""
backbone = Backbone()
gallery = a_stored_gallery(self.directory.name, ("sku_a", "sku_b"), backbone)
self.assertTrue(gallery.load(), why)
self.assertEqual(backbone.calls, 1, f"{why} must not be served from cache")
np.testing.assert_array_equal(gallery._matrix, first._matrix)
self.assertEqual([entry.sku_id for entry in gallery._entries], ["sku_a", "sku_a", "sku_b"])
def test_a_second_start_answers_the_same_from_the_cache(self):
first, backbone = self.loaded()
self.assertEqual(backbone.calls, 1)
@ -282,34 +307,73 @@ class FeatureCacheTests(unittest.TestCase):
self.assertEqual(backbone.calls, 1)
np.testing.assert_array_equal(second._matrix, first._matrix)
def test_a_tampered_cache_row_is_not_served(self):
# A current key says nothing about the rows inside the file. Swap them
# and the gallery must re-embed, never name a SKU by a moved row.
first, _ = self.loaded()
path = first._cache_path()
with np.load(path, allow_pickle=False) as payload:
key = str(payload["key"].item())
skus = [str(value) for value in payload["skus"].tolist()]
views = [str(value) for value in payload["views"].tolist()]
matrix = payload["matrix"]
# The two sku_a views are adjacent, so a label swap has to cross SKUs to
# mean anything - and the matrix that stays put is the dangerous one:
def test_swapped_cache_rows_are_not_served(self):
# A current key says nothing about the rows inside the file. The two
# sku_a views are adjacent, so a swap has to cross SKUs to mean
# anything - and the matrix that moves with them is the dangerous part:
# a vector that would then be filed under the wrong product.
first, _ = self.loaded()
skus, views, matrix = self.cache_payload(first)
self.assertEqual(list(zip(skus, views)),
[("sku_a", "sku_a.jpeg"), ("sku_a", "sku_a_1.jpeg"), ("sku_b", "sku_b.jpeg")])
tampered = {
"reordered rows": (skus[::-1], views[::-1], matrix[::-1]),
"swapped labels": ([skus[2], skus[1], skus[0]], views, matrix),
"another store's row": (skus[1:] + ["other_store"], views + ["other_store.jpeg"], matrix),
self.rewrite_cache(first, skus[::-1], views[::-1], matrix[::-1])
self.re_embeds(first, "rows swapped with their vectors")
def test_mislabelled_cache_rows_are_not_served(self):
# The views and the matrix stay where they are, so every row names a
# product it was not embedded for.
first, _ = self.loaded()
skus, views, matrix = self.cache_payload(first)
self.rewrite_cache(first, [skus[2], skus[1], skus[0]], views, matrix)
self.re_embeds(first, "labels swapped around the views")
def test_a_duplicated_cache_row_is_not_served(self):
# The first view written twice: one image would answer for two products.
first, _ = self.loaded()
skus, views, matrix = self.cache_payload(first)
self.rewrite_cache(first, skus[:1] * 2 + skus[2:], views[:1] * 2 + views[2:], matrix)
self.re_embeds(first, "a row repeated")
def test_a_foreign_sku_row_is_not_served(self):
# A row from another store's catalog: a SKU this gallery never enrolled.
first, _ = self.loaded()
skus, views, matrix = self.cache_payload(first)
self.rewrite_cache(first, skus[1:] + ["other_store"], views[1:] + ["other_store.jpeg"], matrix)
self.re_embeds(first, "another store's row")
def test_a_cache_may_omit_a_view_it_could_not_decode(self):
# The writer drops a view it could not read and keeps everyone else's
# order, so such a cache is still this store's - and still worth serving
# rather than re-embedding the whole catalog on every start.
first, _ = self.loaded()
skus, views, matrix = self.cache_payload(first)
self.rewrite_cache(first, skus[:1] + skus[2:], views[:1] + views[2:], matrix[[0, 2]])
backbone = Backbone()
gallery = a_stored_gallery(self.directory.name, ("sku_a", "sku_b"), backbone)
self.assertTrue(gallery.load())
self.assertEqual(backbone.calls, 0, "a readable shorter cache is still a hit")
self.assertEqual([entry.sku_id for entry in gallery._entries], ["sku_a", "sku_b"])
np.testing.assert_array_equal(gallery._matrix, first._matrix[[0, 2]])
def test_a_nonnumeric_cache_matrix_is_not_served(self):
# Only the float matrix _cache_write produces is a cache. A complex
# matrix is finite and casts without complaint, so it answers with a
# different matrix than the one on disk; integer, boolean and text
# matrices are not embeddings at all.
first, _ = self.loaded()
skus, views, matrix = self.cache_payload(first)
others = {
"complex": matrix.astype(np.complex128),
"timedelta": np.zeros_like(matrix, dtype="timedelta64[s]"),
"integer": (matrix * 8).astype(np.int32),
"boolean": np.ones_like(matrix, dtype=bool),
"text": np.zeros(matrix.shape, dtype="<U4"),
}
for label, (bad_skus, bad_views, bad_matrix) in tampered.items():
np.savez(path, key=key, skus=np.array(bad_skus), views=np.array(bad_views), matrix=bad_matrix)
backbone = Backbone()
gallery = a_stored_gallery(self.directory.name, ("sku_a", "sku_b"), backbone)
self.assertTrue(gallery.load(), label)
self.assertEqual(backbone.calls, 1, f"{label} must not be served from cache")
np.testing.assert_array_equal(gallery._matrix, first._matrix)
self.assertEqual([entry.sku_id for entry in gallery._entries], ["sku_a", "sku_a", "sku_b"])
for name, other in others.items():
with self.subTest(matrix=name):
self.rewrite_cache(first, skus, views, other)
self.re_embeds(first, f"a {name} matrix")
def test_a_loaded_gallery_reloads_when_its_source_moves(self):
gallery, backbone = self.loaded()