Revert ML search (#50) to restore a green next (build fix pending)
check / check (push) Successful in 18s
check / check (push) Successful in 18s
Reverts the #50 merge (224bd101): mlsearch.ts failed tsc in the CI build (make check does not run make build, so it slipped past review). next restored to green; #50 to be redone with make build in its gate.
Model: opus-4-8
This commit was merged in pull request #69.
This commit is contained in:
@@ -1,108 +0,0 @@
|
||||
/**
|
||||
* Tests for the content-similarity search surface over the CLIP index
|
||||
* (issue #50).
|
||||
*
|
||||
* The surface is `lib.mldata`: `forFile` reads the full stored payload from
|
||||
* disk, while `similar` and `searchByEmbedding` rank fileIDs by cosine
|
||||
* similarity over the in-RAM `Float32Array` index alone (no disk, no network).
|
||||
* The fixture uses axis-aligned vectors so the correct cosine ranking is
|
||||
* obvious by inspection; cosine ignores magnitude, so `[2, 0, 0]` ranks above
|
||||
* `[0.8, 0.6, 0]` for a `[1, 0, 0]` query.
|
||||
*/
|
||||
|
||||
import { describe, it, expect, beforeEach, afterEach } from "vitest";
|
||||
import { mkdtempSync, rmSync } from "node:fs";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
|
||||
import { MLDataStore } from "../../src/library/mldata.js";
|
||||
import { makeMLDataAPI, type MLDataAPI } from "../../src/library/mlsearch.js";
|
||||
import type { MLData } from "../../src/mldata-fetch.js";
|
||||
|
||||
// A payload shaped like Ente's: a CLIP embedding plus face data that only the
|
||||
// on-disk payload carries (never the RAM index).
|
||||
const payload = (embedding: number[]): MLData => ({
|
||||
face: { faces: [{ faceID: "f", detection: { box: { x: 0.5 } } }] },
|
||||
clip: { embedding },
|
||||
});
|
||||
|
||||
// A small fixture index. Directions are chosen so every cosine ranking below
|
||||
// is unambiguous.
|
||||
const fixture = (): Map<number, MLData> =>
|
||||
new Map([
|
||||
[10, payload([1, 0, 0])],
|
||||
[20, payload([0.8, 0.6, 0])],
|
||||
[30, payload([0, 1, 0])],
|
||||
[40, payload([-1, 0, 0])],
|
||||
[50, payload([2, 0, 0])],
|
||||
]);
|
||||
|
||||
describe("lib.mldata content-similarity search", () => {
|
||||
let dir: string;
|
||||
let store: MLDataStore;
|
||||
let api: MLDataAPI;
|
||||
|
||||
beforeEach(async () => {
|
||||
dir = mkdtempSync(join(tmpdir(), "quak-mlsearch-"));
|
||||
store = await MLDataStore.open(dir);
|
||||
const updation = new Map([...fixture().keys()].map((id) => [id, 1]));
|
||||
await store.storeFetched(fixture(), updation);
|
||||
api = makeMLDataAPI(() => store);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
rmSync(dir, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
it("forFile returns the whole stored payload, or undefined when uncached", async () => {
|
||||
const full = await api.forFile({ fileID: 20 });
|
||||
expect(full).toBeDefined();
|
||||
// Face data lives only in the payload, never in the RAM index.
|
||||
expect(full?.face).toBeDefined();
|
||||
expect(full?.clip).toEqual({ embedding: [0.8, 0.6, 0] });
|
||||
expect(await api.forFile({ fileID: 999 })).toBeUndefined();
|
||||
});
|
||||
|
||||
it("similar ranks other files by cosine and excludes the query itself", () => {
|
||||
// Query is file 10 = [1, 0, 0]. By cosine: 50 (1.0) > 20 (0.8) >
|
||||
// 30 (0) > 40 (-1); 10 itself is left out.
|
||||
const ranked = api.similar({ fileID: 10 });
|
||||
expect(ranked.map((r) => r.fileID)).toEqual([50, 20, 30, 40]);
|
||||
// Cosine ignores magnitude: [2,0,0] is a perfect match for [1,0,0].
|
||||
expect(ranked[0]).toMatchObject({ fileID: 50 });
|
||||
expect(ranked[0].score).toBeCloseTo(1, 5);
|
||||
});
|
||||
|
||||
it("similar honours limit and returns [] for an unindexed file", () => {
|
||||
expect(
|
||||
api.similar({ fileID: 10, limit: 2 }).map((r) => r.fileID),
|
||||
).toEqual([50, 20]);
|
||||
expect(api.similar({ fileID: 999 })).toEqual([]);
|
||||
});
|
||||
|
||||
it("searchByEmbedding ranks the index by cosine to the query vector", () => {
|
||||
// Query [0, 1, 0]: 30 (1.0) > 20 (0.6) > {10, 40, 50} all 0, broken by
|
||||
// ascending fileID.
|
||||
const ranked = api.searchByEmbedding({ embedding: [0, 1, 0] });
|
||||
expect(ranked.map((r) => r.fileID)).toEqual([30, 20, 10, 40, 50]);
|
||||
expect(ranked[0].score).toBeCloseTo(1, 5);
|
||||
|
||||
expect(
|
||||
api
|
||||
.searchByEmbedding({ embedding: [0, 1, 0], limit: 2 })
|
||||
.map((r) => r.fileID),
|
||||
).toEqual([30, 20]);
|
||||
});
|
||||
|
||||
it("searchByEmbedding returns [] for a wrong-length or zero query", () => {
|
||||
expect(api.searchByEmbedding({ embedding: [1, 0] })).toEqual([]);
|
||||
expect(api.searchByEmbedding({ embedding: [0, 0, 0] })).toEqual([]);
|
||||
});
|
||||
|
||||
it("degrades to empty results when no ML store is present", async () => {
|
||||
const none = makeMLDataAPI(() => undefined);
|
||||
expect(await none.forFile({ fileID: 10 })).toBeUndefined();
|
||||
expect(none.similar({ fileID: 10 })).toEqual([]);
|
||||
expect(none.searchByEmbedding({ embedding: [1, 0, 0] })).toEqual([]);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user