Spaces:
Running
Running
File size: 4,257 Bytes
e3aec01 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 | /*********************************************************************
* A simple vector store using IndexedDB for persistence.
* Coded by Jason Mayes 2026.
*--------------------------------------------------------------------
* Connect with me on social if aquestions or comments:
*
* LinkedIn: https://www.linkedin.com/in/webai/
* Twitter / X: https://x.com/jason_mayes
* Github: https://github.com/jasonmayes
* CodePen: https://codepen.io/jasonmayes
*********************************************************************/
export class VectorStore {
/**
* @param {string} dbName Name of the IndexedDB database.
*/
constructor(dbName = 'unnamed') {
this.dbName = dbName;
this.db = null;
}
/**
* Sets the database name to use for storage and retrieval.
* @param {string} name The name of the database to use.
*/
setDb(name) {
if (this.dbName !== name) {
this.dbName = name;
if (this.db) {
this.db.close();
this.db = null;
}
}
}
/**
* Initializes the IndexedDB database.
* @return {!Promise<!IDBDatabase>}
* @private
*/
async initDb() {
if (this.db) return this.db;
return new Promise((resolve, reject) => {
// Increment version to 2 to trigger upgrade if you already had version 1
const request = indexedDB.open(this.dbName, 1);
request.onupgradeneeded = (event) => {
const db = event.target.result;
// Single store for both text and vectors
if (!db.objectStoreNames.contains('embeddings')) {
db.createObjectStore('embeddings', { keyPath: 'id', autoIncrement: true });
}
};
request.onsuccess = (event) => {
this.db = event.target.result;
resolve(this.db);
};
request.onerror = (e) => reject(e.target.error);
});
}
/**
* Stores an embedding and its associated text.
* @param {!Array<number>} embedding The vector embedding.
* @param {string} text The original text data.
* @return {!Promise<void>}
*/
async storeBatch(items) {
const db = await this.initDb();
const transaction = db.transaction(['embeddings'], 'readwrite');
const store = transaction.objectStore('embeddings');
return new Promise((resolve, reject) => {
transaction.oncomplete = () => resolve();
transaction.onerror = (e) => reject(e.target.error);
for (const item of items) {
// item.embedding should be a Float32Array for max speed
store.add({
text: item.text,
embedding: item.embedding
});
}
});
}
/**
* Fetches all vectors from the database.
* @return {!Promise<!Array<{id: number, embedding: !Array<number>}>>}
*/
async getAllVectors() {
const db = await this.initDb();
return new Promise((resolve, reject) => {
const transaction = db.transaction(['embeddings'], 'readonly');
const store = transaction.objectStore('embeddings');
const vectors = [];
// A cursor lets us step through records without loading
// the whole store into memory at once.
const request = store.openCursor();
request.onsuccess = (event) => {
const cursor = event.target.result;
if (cursor) {
// We only grab the ID and the embedding
vectors.push({
id: cursor.value.id,
embedding: cursor.value.embedding // Float32Array
});
cursor.continue();
} else {
// No more results
resolve(vectors);
}
};
request.onerror = (e) => reject(e.target.error);
});
}
/**
* Fetches the text for a given ID.
* @param {number} id The ID of the text to fetch.
* @return {!Promise<string>}
*/
async getTextByID(id) {
const db = await this.initDb();
return new Promise((resolve, reject) => {
const transaction = db.transaction(['embeddings'], 'readonly');
const store = transaction.objectStore('embeddings');
const request = store.get(id);
request.onsuccess = () => {
// Just return the text property
resolve(request.result ? request.result.text : null);
};
request.onerror = (e) => reject(e.target.error);
});
}
}
|