feat: RAG 本地知识库 - 向量存储、文档分块、语义检索、知识库管理面板
This commit is contained in:
@@ -0,0 +1,164 @@
|
||||
/**
|
||||
* VectorStore - 向量存储与相似度检索
|
||||
*
|
||||
* 基于 IndexedDB 持久化,支持余弦相似度搜索
|
||||
*/
|
||||
|
||||
export class VectorStore {
|
||||
constructor(dbName = 'metona-ollama-vectors') {
|
||||
this.dbName = dbName;
|
||||
this.db = null;
|
||||
}
|
||||
|
||||
async init() {
|
||||
return new Promise((resolve, reject) => {
|
||||
const req = indexedDB.open(this.dbName, 1);
|
||||
req.onerror = () => reject(req.error);
|
||||
req.onsuccess = () => { this.db = req.result; resolve(); };
|
||||
req.onupgradeneeded = (e) => {
|
||||
const db = e.target.result;
|
||||
if (!db.objectStoreNames.contains('vectors')) {
|
||||
const store = db.createObjectStore('vectors', { keyPath: 'id' });
|
||||
store.createIndex('collectionId', 'collectionId', { unique: false });
|
||||
}
|
||||
if (!db.objectStoreNames.contains('collections')) {
|
||||
db.createObjectStore('collections', { keyPath: 'id' });
|
||||
}
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
_tx(store, mode = 'readonly') {
|
||||
return this.db.transaction(store, mode).objectStore(store);
|
||||
}
|
||||
|
||||
// ── 集合管理 ──
|
||||
|
||||
async createCollection(name, embeddingModel = '') {
|
||||
const col = {
|
||||
id: `kb_${Date.now()}_${Math.random().toString(36).slice(2, 7)}`,
|
||||
name,
|
||||
embeddingModel,
|
||||
docCount: 0,
|
||||
chunkCount: 0,
|
||||
createdAt: Date.now(),
|
||||
updatedAt: Date.now()
|
||||
};
|
||||
return new Promise((resolve, reject) => {
|
||||
const req = this._tx('collections', 'readwrite').put(col);
|
||||
req.onsuccess = () => resolve(col);
|
||||
req.onerror = () => reject(req.error);
|
||||
});
|
||||
}
|
||||
|
||||
async getCollections() {
|
||||
return new Promise((resolve, reject) => {
|
||||
const req = this._tx('collections').getAll();
|
||||
req.onsuccess = () => resolve(req.result || []);
|
||||
req.onerror = () => reject(req.error);
|
||||
});
|
||||
}
|
||||
|
||||
async getCollection(id) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const req = this._tx('collections').get(id);
|
||||
req.onsuccess = () => resolve(req.result || null);
|
||||
req.onerror = () => reject(req.error);
|
||||
});
|
||||
}
|
||||
|
||||
async updateCollection(col) {
|
||||
col.updatedAt = Date.now();
|
||||
return new Promise((resolve, reject) => {
|
||||
const req = this._tx('collections', 'readwrite').put(col);
|
||||
req.onsuccess = () => resolve();
|
||||
req.onerror = () => reject(req.error);
|
||||
});
|
||||
}
|
||||
|
||||
async deleteCollection(colId) {
|
||||
// 删除集合下所有向量
|
||||
const vectors = await this.getVectorsByCollection(colId);
|
||||
const store = this._tx('vectors', 'readwrite');
|
||||
for (const v of vectors) store.delete(v.id);
|
||||
return new Promise((resolve, reject) => {
|
||||
const req = this._tx('collections', 'readwrite').delete(colId);
|
||||
req.onsuccess = () => resolve();
|
||||
req.onerror = () => reject(req.error);
|
||||
});
|
||||
}
|
||||
|
||||
// ── 向量操作 ──
|
||||
|
||||
async addVectors(items) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const tx = this.db.transaction('vectors', 'readwrite');
|
||||
const store = tx.objectStore('vectors');
|
||||
for (const item of items) store.put(item);
|
||||
tx.oncomplete = () => resolve();
|
||||
tx.onerror = () => reject(tx.error);
|
||||
});
|
||||
}
|
||||
|
||||
async getVectorsByCollection(colId) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const idx = this._tx('vectors').index('collectionId');
|
||||
const req = idx.getAll(colId);
|
||||
req.onsuccess = () => resolve(req.result || []);
|
||||
req.onerror = () => reject(req.error);
|
||||
});
|
||||
}
|
||||
|
||||
async deleteVectorsByCollection(colId) {
|
||||
const vectors = await this.getVectorsByCollection(colId);
|
||||
return new Promise((resolve, reject) => {
|
||||
const tx = this.db.transaction('vectors', 'readwrite');
|
||||
const store = tx.objectStore('vectors');
|
||||
for (const v of vectors) store.delete(v.id);
|
||||
tx.oncomplete = () => resolve();
|
||||
tx.onerror = () => reject(tx.error);
|
||||
});
|
||||
}
|
||||
|
||||
async deleteVectorsByDocument(colId, docId) {
|
||||
const vectors = await this.getVectorsByCollection(colId);
|
||||
const docVectors = vectors.filter(v => v.docId === docId);
|
||||
return new Promise((resolve, reject) => {
|
||||
const tx = this.db.transaction('vectors', 'readwrite');
|
||||
const store = tx.objectStore('vectors');
|
||||
for (const v of docVectors) store.delete(v.id);
|
||||
tx.oncomplete = () => resolve();
|
||||
tx.onerror = () => reject(tx.error);
|
||||
});
|
||||
}
|
||||
|
||||
// ── 相似度检索 ──
|
||||
|
||||
async search(colId, queryEmbedding, topK = 5) {
|
||||
const vectors = await this.getVectorsByCollection(colId);
|
||||
if (vectors.length === 0) return [];
|
||||
|
||||
return vectors
|
||||
.map(v => ({
|
||||
...v,
|
||||
score: VectorStore.cosineSimilarity(queryEmbedding, v.embedding)
|
||||
}))
|
||||
.sort((a, b) => b.score - a.score)
|
||||
.slice(0, topK);
|
||||
}
|
||||
|
||||
/**
|
||||
* 余弦相似度
|
||||
*/
|
||||
static cosineSimilarity(a, b) {
|
||||
if (!a || !b || a.length !== b.length) return 0;
|
||||
let dot = 0, normA = 0, normB = 0;
|
||||
for (let i = 0; i < a.length; i++) {
|
||||
dot += a[i] * b[i];
|
||||
normA += a[i] * a[i];
|
||||
normB += b[i] * b[i];
|
||||
}
|
||||
const denom = Math.sqrt(normA) * Math.sqrt(normB);
|
||||
return denom === 0 ? 0 : dot / denom;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user