import { LVS_Search_Mode } from '../_enums/lvs-search-mode.enum'; import { LVS_SearchResult } from '../_models/lvs-search-result.interface'; /** * Vektor pool kezelő utility class * Instance-based class, amely in-memory vektor pool-t kezel és vektor hasonlósági számításokat végez * Minden instance saját vektor pool-t tart fenn */ export class LVS_VectorPool_ControlService { /** * In-memory vektor pool tárolás * Key: vektor ID, Value: vektor értékek */ private vectorPool: Map = new Map(); /** * Normalizált vektor pool tárolás (L2 normalizált másolatok) * Key: vektor ID, Value: L2 normalizált vektor értékek */ private normalizedVectorPool: Map = new Map(); /** * `normalizedOnly` mód: ha `true`, a pool NEM tárolja a nyers vektorokat (`vectorPool`) — CSAK a * normalizált másolatot. A kizárólag-cosine fogyasztóknak (pl. RAG-keresés) ez ~50% memória-megtakarítás * (a két párhuzamos pool helyett egy). Ekkor az `l2Distance`-keresés és a `getAll()` nyers-érték-olvasása * N/A (a `size()` + a `cosineSimilarity`-keresés továbbra is helyes). Default `false` (visszafelé kompatibilis). */ private readonly normalizedOnly: boolean; constructor(set?: { normalizedOnly?: boolean }) { this.normalizedOnly = set?.normalizedOnly ?? false; } /** * Hozzáad egy vektort a pool-hoz * Opcionálisan L2 normalizált másolatot is tárol */ addVector(id: string, vector: number[]): void { if (!id) { throw new Error('Vector ID is required'); } if (!Array.isArray(vector) || vector.length === 0) { throw new Error('Vector must be a non-empty array'); } // Validáljuk, hogy a vektor számokból áll for (let i: number = 0; i < vector.length; i++) { if (typeof vector[i] !== 'number' || !isFinite(vector[i])) { throw new Error(`Vector must contain only finite numbers at index ${i}`); } } // Tároljuk az eredeti vektort — KIVÉVE normalizedOnly módban (akkor a nyers pool kihagyva, ~50% memória). if (!this.normalizedOnly) { this.vectorPool.set(id, vector); } // L2 normalizált másolatot is tárolunk a cosine similarity számításokhoz const normalized: number[] = LVS_VectorPool_ControlService.l2Normalize(vector); this.normalizedVectorPool.set(id, normalized); } /** * Eltávolít egy vektort a pool-ból */ removeVector(id: string): void { this.vectorPool.delete(id); this.normalizedVectorPool.delete(id); } /** * Frissít egy vektort a pool-ban */ updateVector(id: string, vector: number[]): void { // A `normalizedVectorPool` a forrás (mindkét módban populált; `normalizedOnly`-ban a `vectorPool` üres). if (!this.normalizedVectorPool.has(id)) { throw new Error(`Vector with ID "${id}" does not exist`); } this.addVector(id, vector); } /** * A pool elemszáma. Mindkét módban helyes (a `normalizedVectorPool` a forrás), így `normalizedOnly` * módban is — szemben a `getAll().size`-szal, ami ott üres (a nyers pool nincs tárolva). */ size(): number { return this.normalizedVectorPool.size; } /** * Törli az összes vektort a pool-ból */ clearPool(): void { this.vectorPool.clear(); this.normalizedVectorPool.clear(); } /** * Visszaadja az összes vektort a pool-ból * Debugging és export céljára */ getAll(): Map { return new Map(this.vectorPool); } /** * L2 normalizálás egy vektorra * A vektor hosszát 1-re normalizálja * Statikus metódus, mivel nincs szüksége instance state-re */ private static l2Normalize(vector: number[]): number[] { const magnitude: number = Math.sqrt( vector.reduce((sum: number, value: number) => sum + value * value, 0) ); if (magnitude === 0) { // Ha a vektor nullvektor, akkor nullvektort adunk vissza return new Array(vector.length).fill(0); } return vector.map((value: number) => value / magnitude); } /** * Cosine similarity számítás két vektor között * Normalizált vektorokat használ a számításhoz * Eredmény: 0 és 1 közötti érték, ahol 1 a legnagyobb hasonlóság * Statikus metódus, mivel nincs szüksége instance state-re */ static cosineSimilarity(a: number[], b: number[]): number { if (a.length !== b.length) { throw new Error( `Vectors must have the same dimension. Got ${a.length} and ${b.length}` ); } // Normalizáljuk a vektorokat const normalizedA: number[] = this.l2Normalize(a); const normalizedB: number[] = this.l2Normalize(b); // Dot product számítása let dotProduct: number = 0; for (let i: number = 0; i < normalizedA.length; i++) { dotProduct += normalizedA[i] * normalizedB[i]; } // Cosine similarity = dot product of normalized vectors // Mivel a vektorok normalizáltak, ez egyenlő a cosine similarity-vel return dotProduct; } /** * L2 distance (Euklideszi távolság) számítás két vektor között * Eredmény: 0 vagy pozitív érték, ahol 0 a legkisebb távolság * Statikus metódus, mivel nincs szüksége instance state-re */ static l2Distance(a: number[], b: number[]): number { if (a.length !== b.length) { throw new Error( `Vectors must have the same dimension. Got ${a.length} and ${b.length}` ); } // L2 distance = sqrt(sum((a[i] - b[i])^2)) let sumSquaredDiff: number = 0; for (let i: number = 0; i < a.length; i++) { const diff: number = a[i] - b[i]; sumSquaredDiff += diff * diff; } return Math.sqrt(sumSquaredDiff); } /** * Vektor keresés a pool-ban * Brute-force megközelítés (lineáris keresés) * * @param query A keresési query vektor * @param k A visszaadandó találatok száma (top-K) * @param mode A keresési mód (cosine similarity vagy L2 distance) * @param candidateIds Opcionális jelölt-ID szűrő: ha meg van adva, a keresés CSAK az ebben a * halmazban lévő ID-kra fut (a többit `Set.has` O(1)-gyel kihagyja). Így a hívónak NEM kell külön * szűkített pool-t építenie + a vektorokat újra-normalizálnia — a keresés a MÁR előre-normalizált * `normalizedVectorPool`-on iterál allokáció-mentesen. Üres halmaz → üres eredmény. * @returns A top-K találatok ID-val és score-rel, megfelelő sorrendben */ search( query: number[], k: number, mode: LVS_Search_Mode, candidateIds?: Set ): LVS_SearchResult[] { // Üres jelölt-halmaz → nincs mit keresni (a `undefined` viszont „nincs szűrés" = teljes pool). if (candidateIds && candidateIds.size === 0) { return []; } if (!Array.isArray(query) || query.length === 0) { throw new Error('Query vector must be a non-empty array'); } if (k <= 0) { throw new Error('k must be a positive number'); } // `normalizedOnly` módban nincs nyers pool → l2Distance nem számolható (csak cosine). if (this.normalizedOnly && mode === LVS_Search_Mode.l2Distance) { throw new Error('l2Distance search is not available in normalizedOnly mode (raw vectors are not stored)'); } // Üres pool → üres eredmény. A `normalizedVectorPool` a forrás (mindkét módban populált; a nyers // `vectorPool` `normalizedOnly`-ban üres, ezért NEM azon ellenőrzünk). if (this.normalizedVectorPool.size === 0) { return []; } // Validáljuk, hogy minden vektor ugyanolyan dimenziójú-e. A referencia-pool mód-függő: l2 a nyers, // cosine a normalizált (azonos dimenzió, de `normalizedOnly`-ban a nyers pool üres). const referencePool: Map = mode === LVS_Search_Mode.l2Distance ? this.vectorPool : this.normalizedVectorPool; const firstVector: number[] | undefined = Array.from(referencePool.values())[0]; if (firstVector && firstVector.length !== query.length) { throw new Error( `Query vector dimension (${query.length}) does not match pool vector ` + `dimension (${firstVector.length})` ); } // Számoljuk ki a hasonlóságot/távolságot minden vektorra const results: LVS_SearchResult[] = []; if (mode === LVS_Search_Mode.cosineSimilarity) { // PERF (BFR-AM-013): a query-t EGYSZER normalizáljuk, majd dot-product a MÁR ELŐRE eltárolt // `normalizedVectorPool` (l2-normalizált) vektorok ellen. Eredmény AZONOS a régi // `cosineSimilarity(query, vector)`-ral (= dot(norm(q), norm(v))), de: // (1) NEM normalizáljuk újra MINDEN vektort MINDEN keresésnél (a normalizálás a `addVector`-ban // egyszer megtörtént) → ~3× kevesebb FLOP; // (2) a per-összehasonlítás dot-product ALLOKÁCIÓ-MENTES (a régi `l2Normalize` hívásonként KÉT új // tömböt allokált → egy 24k-vektoros keresés ~48k tömb-allokáció + GC-spike-ot okozott). // A drágán eltárolt `normalizedVectorPool` korábban SOHA nem volt használva a keresésben. const queryNormalized: number[] = LVS_VectorPool_ControlService.l2Normalize(query); for (const [id, normalizedVector] of this.normalizedVectorPool.entries()) { if (candidateIds && !candidateIds.has(id)) { continue; } let dotProduct: number = 0; for (let i: number = 0; i < queryNormalized.length; i++) { dotProduct += queryNormalized[i] * normalizedVector[i]; } results.push({ id, score: dotProduct }); } // Cosine similarity: csökkenő sorrend (legnagyobb hasonlóság először) results.sort((a: LVS_SearchResult, b: LVS_SearchResult) => b.score - a.score); } else if (mode === LVS_Search_Mode.l2Distance) { for (const [id, vector] of this.vectorPool.entries()) { if (candidateIds && !candidateIds.has(id)) { continue; } results.push({ id, score: LVS_VectorPool_ControlService.l2Distance(query, vector) }); } // L2 distance: növekvő sorrend (legkisebb távolság először) results.sort((a: LVS_SearchResult, b: LVS_SearchResult) => a.score - b.score); } else { throw new Error(`Unknown search mode: ${mode}`); } // Visszaadjuk a top-K találatot return results.slice(0, k); } }