Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3588deb844 |
+20
-3
@@ -258,9 +258,11 @@ export class MLDataStore {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Load the packed index if it is present and consistent with the payloads:
|
// Load the packed index if it is present and agrees with the payloads in
|
||||||
// its ids must all still be present and its vector file must be exactly the
|
// both directions: every id it names must still be present, its vector file
|
||||||
// size the id count and embedding length imply. Returns whether it loaded.
|
// must be exactly the size the id count and embedding length imply, and no
|
||||||
|
// embedding-bearing payload on disk may be missing from it. Returns whether
|
||||||
|
// it loaded.
|
||||||
private async tryLoadIndex(): Promise<boolean> {
|
private async tryLoadIndex(): Promise<boolean> {
|
||||||
let metaRaw: string;
|
let metaRaw: string;
|
||||||
try {
|
try {
|
||||||
@@ -281,6 +283,21 @@ export class MLDataStore {
|
|||||||
return false;
|
return false;
|
||||||
if (meta.fileIDs.some((id) => !this.present.has(id))) return false;
|
if (meta.fileIDs.some((id) => !this.present.has(id))) return false;
|
||||||
|
|
||||||
|
// The reverse must hold too. A payload carrying an embedding but absent
|
||||||
|
// from the index means the index is stale — realistically the process
|
||||||
|
// died after storeFetched renamed the payloads into place but before it
|
||||||
|
// rewrote clip.json/clip.f32. Loading such an index as "consistent"
|
||||||
|
// would drop those embeddings for good (neededFor sees the payloads
|
||||||
|
// present and never refetches), so treat it as a disagreement and
|
||||||
|
// rebuild. Only present ids the index omits are read; a payload
|
||||||
|
// legitimately without an embedding stays out and forces no rebuild.
|
||||||
|
const indexed = new Set(meta.fileIDs);
|
||||||
|
for (const id of this.present) {
|
||||||
|
if (indexed.has(id)) continue;
|
||||||
|
const payload = await this.readPayload(id);
|
||||||
|
if (payload && clipEmbedding(payload)) return false;
|
||||||
|
}
|
||||||
|
|
||||||
let bytes: Buffer;
|
let bytes: Buffer;
|
||||||
try {
|
try {
|
||||||
bytes = await readFile(join(this.dir, CLIP_VECTORS));
|
bytes = await readFile(join(this.dir, CLIP_VECTORS));
|
||||||
|
|||||||
@@ -122,6 +122,30 @@ describe("MLDataStore", () => {
|
|||||||
expect(reopened.getIndex().fileIDs).toEqual([100]);
|
expect(reopened.getIndex().fileIDs).toEqual([100]);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("rebuilds the index when a payload on disk is missing from it", async () => {
|
||||||
|
const store = await MLDataStore.open(dir);
|
||||||
|
await store.storeFetched(
|
||||||
|
new Map([[100, payload([0.5, 0.25, 0.75])]]),
|
||||||
|
new Map([[100, 10]]),
|
||||||
|
);
|
||||||
|
|
||||||
|
// A crash between storeFetched renaming a payload into place and
|
||||||
|
// rewriting the index leaves the payload complete on disk but absent
|
||||||
|
// from clip.json. Write a second payload directly to reproduce that
|
||||||
|
// torn state without touching the index.
|
||||||
|
writeFileSync(
|
||||||
|
join(dir, "200.json"),
|
||||||
|
JSON.stringify(payload([1, -2, 0.5])),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Reopening self-heals with no manual delete: the index is rebuilt from
|
||||||
|
// the payloads to include the orphaned embedding.
|
||||||
|
const reopened = await MLDataStore.open(dir);
|
||||||
|
const index = reopened.getIndex();
|
||||||
|
expect(index.fileIDs).toEqual([100, 200]);
|
||||||
|
expect([...index.embeddings]).toEqual([0.5, 0.25, 0.75, 1, -2, 0.5]);
|
||||||
|
});
|
||||||
|
|
||||||
it("appends new payloads and overwrites a refetched file in place", async () => {
|
it("appends new payloads and overwrites a refetched file in place", async () => {
|
||||||
const store = await MLDataStore.open(dir);
|
const store = await MLDataStore.open(dir);
|
||||||
await store.storeFetched(
|
await store.storeFetched(
|
||||||
|
|||||||
Reference in New Issue
Block a user