diff --git a/packages/orama/.tshy/browser.json b/packages/orama/.tshy/browser.json index 144e2968..84dceb96 100644 --- a/packages/orama/.tshy/browser.json +++ b/packages/orama/.tshy/browser.json @@ -1,14 +1,7 @@ { "extends": "./build.json", - "include": [ - "../src/**/*.ts", - "../src/**/*.mts", - "../src/**/*.tsx", - "../src/**/*.json" - ], - "exclude": [ - "../src/package.json" - ], + "include": ["../src/**/*.ts", "../src/**/*.mts", "../src/**/*.tsx", "../src/**/*.json"], + "exclude": ["../src/package.json"], "compilerOptions": { "outDir": "../.tshy-build/browser" } diff --git a/packages/orama/.tshy/commonjs.json b/packages/orama/.tshy/commonjs.json index 7c9db50b..c11b0192 100644 --- a/packages/orama/.tshy/commonjs.json +++ b/packages/orama/.tshy/commonjs.json @@ -1,15 +1,7 @@ { "extends": "./build.json", - "include": [ - "../src/**/*.ts", - "../src/**/*.cts", - "../src/**/*.tsx", - "../src/**/*.json" - ], - "exclude": [ - "../src/**/*.mts", - "../src/package.json" - ], + "include": ["../src/**/*.ts", "../src/**/*.cts", "../src/**/*.tsx", "../src/**/*.json"], + "exclude": ["../src/**/*.mts", "../src/package.json"], "compilerOptions": { "outDir": "../.tshy-build/commonjs" } diff --git a/packages/orama/.tshy/deno.json b/packages/orama/.tshy/deno.json index c929546d..cf94b877 100644 --- a/packages/orama/.tshy/deno.json +++ b/packages/orama/.tshy/deno.json @@ -1,14 +1,7 @@ { "extends": "./build.json", - "include": [ - "../src/**/*.ts", - "../src/**/*.mts", - "../src/**/*.tsx", - "../src/**/*.json" - ], - "exclude": [ - "../src/package.json" - ], + "include": ["../src/**/*.ts", "../src/**/*.mts", "../src/**/*.tsx", "../src/**/*.json"], + "exclude": ["../src/package.json"], "compilerOptions": { "outDir": "../.tshy-build/deno" } diff --git a/packages/orama/.tshy/esm.json b/packages/orama/.tshy/esm.json index 959294a8..2929f454 100644 --- a/packages/orama/.tshy/esm.json +++ b/packages/orama/.tshy/esm.json @@ -1,14 +1,7 @@ { "extends": "./build.json", - "include": [ - "../src/**/*.ts", - "../src/**/*.mts", - "../src/**/*.tsx", - "../src/**/*.json" - ], - "exclude": [ - "../src/package.json" - ], + "include": ["../src/**/*.ts", "../src/**/*.mts", "../src/**/*.tsx", "../src/**/*.json"], + "exclude": ["../src/package.json"], "compilerOptions": { "outDir": "../.tshy-build/esm" } diff --git a/packages/orama/package.json b/packages/orama/package.json index 943ba0fa..6e988362 100644 --- a/packages/orama/package.json +++ b/packages/orama/package.json @@ -81,9 +81,7 @@ } }, "types": "./dist/commonjs/index.d.ts", - "files": [ - "dist" - ], + "files": ["dist"], "repository": { "type": "git", "url": "https://github.com/askorama/orama" @@ -170,10 +168,7 @@ "./components": "./src/components.ts", "./trees": "./src/trees.ts" }, - "esmDialects": [ - "deno", - "browser" - ] + "esmDialects": ["deno", "browser"] }, "module": "./dist/esm/index.js" } diff --git a/packages/orama/src/trees/avl.ts b/packages/orama/src/trees/avl.ts index 17f3204b..488d42c2 100644 --- a/packages/orama/src/trees/avl.ts +++ b/packages/orama/src/trees/avl.ts @@ -1,3 +1,4 @@ +/* eslint-disable no-extra-semi */ /* eslint-disable @typescript-eslint/no-this-alias */ import { Nullable } from '../types.js' @@ -105,27 +106,69 @@ export class AVLTree { return new AVLNode(key, value) } - if (key < node.k) { - node.l = this.insertNode(node.l, key, value, rebalanceThreshold) - } else if (key > node.k) { - node.r = this.insertNode(node.r, key, value, rebalanceThreshold) - } else { - if (Array.isArray(node.v)) { - if (Array.isArray(value)) { - ;(node.v as any[]).push(...(value as V[])) + const path: Array<{ parent: Nullable>; node: AVLNode }> = [] + let current = node + let parent: Nullable> = null + + while (current !== null) { + path.push({ parent, node: current }) + + if (key < current.k) { + if (current.l === null) { + current.l = new AVLNode(key, value) + path.push({ parent: current, node: current.l }) + break } else { - ;(node.v as any[]).push(value) + parent = current + current = current.l + } + } else if (key > current.k) { + if (current.r === null) { + current.r = new AVLNode(key, value) + path.push({ parent: current, node: current.r }) + break + } else { + parent = current + current = current.r } } else { - node.v = value + // Key already exists + if (Array.isArray(current.v)) { + if (Array.isArray(value)) { + ;(current.v as any[]).push(...(value as V[])) + } else { + ;(current.v as any[]).push(value) + } + } else { + current.v = value + } + return node } - return node } - node.updateHeight() - + // Update heights and rebalance if necessary + let needRebalance = false if (this.insertCount++ % rebalanceThreshold === 0) { - return this.rebalanceNode(node) + needRebalance = true + } + + for (let i = path.length - 1; i >= 0; i--) { + const { parent, node: currentNode } = path[i] + currentNode.updateHeight() + + if (needRebalance) { + const rebalancedNode = this.rebalanceNode(currentNode) + if (parent) { + if (parent.l === currentNode) { + parent.l = rebalancedNode + } else if (parent.r === currentNode) { + parent.r = rebalancedNode + } + } else { + // This is the root node + node = rebalancedNode + } + } } return node @@ -171,25 +214,40 @@ export class AVLTree { } public getSize(): number { - const countNodes = (node: Nullable>): number => { - if (!node) return 0 - return 1 + countNodes(node.l) + countNodes(node.r) + let count = 0 + const stack: Array>> = [] + let current = this.root + + while (current || stack.length > 0) { + while (current) { + stack.push(current) + current = current.l + } + current = stack.pop()! + count++ + current = current.r } - return countNodes(this.root) + + return count } public isBalanced(): boolean { - const checkBalanced = (node: Nullable>): boolean => { - if (!node) return true + if (!this.root) return true + + const stack: Array> = [this.root] + while (stack.length > 0) { + const node = stack.pop()! const balanceFactor = node.getBalanceFactor() if (Math.abs(balanceFactor) > 1) { return false } - return checkBalanced(node.l) && checkBalanced(node.r) + if (node.l) stack.push(node.l) + if (node.r) stack.push(node.r) } - return checkBalanced(this.root) + + return true } public remove(key: K): void { @@ -204,9 +262,10 @@ export class AVLTree { } if ((node.v as unknown as Set).size === 1) { - this.removeNode(node, key) + this.root = this.removeNode(this.root, key) + } else { + ;(node.v as unknown as Set) = new Set([...(node.v as unknown as Set).values()].filter((v) => v !== id)) } - ;(node.v as unknown as Set) = new Set([...(node.v as unknown as Set).values()].filter((v) => v !== id)) } private findNodeByKey(key: K): Nullable> { @@ -224,91 +283,162 @@ export class AVLTree { } private removeNode(node: Nullable>, key: K): Nullable> { - if (!node) return null + if (node === null) return null - if (key < node.k) { - node.l = this.removeNode(node.l, key) - } else if (key > node.k) { - node.r = this.removeNode(node.r, key) - } else { - if (!node.l || !node.r) { - node = node.l || node.r + const path: Array> = [] + let current = node + + while (current !== null && current.k !== key) { + path.push(current) + if (key < current.k) { + current = current.l! } else { - const minLargerNode = this.findMinNode(node.r) - node.k = minLargerNode.k - node.v = minLargerNode.v - node.r = this.removeNode(node.r, minLargerNode.k) + current = current.r! } } - if (!node) return null + if (current === null) { + // Key not found + return node + } - node.updateHeight() - return this.rebalanceNode(node) - } + // Node with only one child or no child + if (current.l === null || current.r === null) { + const child = current.l ? current.l : current.r - private findMinNode(node: AVLNode): AVLNode { - while (node.l) { - node = node.l + if (path.length === 0) { + // Node to be deleted is root + node = child + } else { + const parent = path[path.length - 1] + if (parent.l === current) { + parent.l = child + } else { + parent.r = child + } + } + } else { + // Node with two children: Get the inorder successor + let successorParent = current + let successor = current.r + + while (successor.l !== null) { + successorParent = successor + successor = successor.l + } + + // Copy the successor's content to current node + current.k = successor.k + current.v = successor.v + + // Delete the successor + if (successorParent.l === successor) { + successorParent.l = successor.r + } else { + successorParent.r = successor.r + } + + current = successorParent + } + + // Update heights and rebalance + path.push(current) + for (let i = path.length - 1; i >= 0; i--) { + const currentNode = path[i] + currentNode.updateHeight() + const rebalancedNode = this.rebalanceNode(currentNode) + if (i > 0) { + const parent = path[i - 1] + if (parent.l === currentNode) { + parent.l = rebalancedNode + } else if (parent.r === currentNode) { + parent.r = rebalancedNode + } + } else { + // Root node + node = rebalancedNode + } } + return node } public rangeSearch(min: K, max: K): V[] { const result: V[] = [] - const traverse = (node: Nullable>) => { - if (!node) return - if (min < node.k) traverse(node.l) - if (node.k >= min && node.k <= max) { - if (Array.isArray(node.v)) { - result.push(...node.v) + const stack: Array> = [] + let current = this.root + + while (current || stack.length > 0) { + while (current) { + stack.push(current) + current = current.l + } + current = stack.pop()! + if (current.k >= min && current.k <= max) { + if (Array.isArray(current.v)) { + result.push(...current.v) } else { - result.push(node.v) + result.push(current.v) } } - if (max > node.k) traverse(node.r) + if (current.k > max) { + break + } + current = current.r } - traverse(this.root) + return result } public greaterThan(key: K, inclusive = false): V[] { const result: V[] = [] - const traverse = (node: Nullable>) => { - if (!node) return - if ((inclusive && node.k >= key) || (!inclusive && node.k > key)) { - if (Array.isArray(node.v)) { - result.push(...node.v) + const stack: Array> = [] + let current = this.root + + while (current || stack.length > 0) { + while (current) { + stack.push(current) + current = current.r // Traverse right subtree first + } + current = stack.pop()! + if ((inclusive && current.k >= key) || (!inclusive && current.k > key)) { + if (Array.isArray(current.v)) { + result.push(...current.v) } else { - result.push(node.v) + result.push(current.v) } - traverse(node.l) - traverse(node.r) - } else { - traverse(node.r) + } else if (current.k <= key) { + break // Since we're traversing in descending order, we can break early } + current = current.l } - traverse(this.root) + return result } public lessThan(key: K, inclusive = false): V[] { const result: V[] = [] - const traverse = (node: Nullable>) => { - if (!node) return - if ((inclusive && node.k <= key) || (!inclusive && node.k < key)) { - if (Array.isArray(node.v)) { - result.push(...node.v) + const stack: Array> = [] + let current = this.root + + while (current || stack.length > 0) { + while (current) { + stack.push(current) + current = current.l + } + current = stack.pop()! + if ((inclusive && current.k <= key) || (!inclusive && current.k < key)) { + if (Array.isArray(current.v)) { + result.push(...current.v) } else { - result.push(node.v) + result.push(current.v) } - traverse(node.l) - traverse(node.r) - } else { - traverse(node.l) + } else if (current.k > key) { + break // Since we're traversing in ascending order, we can break early } + current = current.r } - traverse(this.root) + return result } } diff --git a/packages/orama/src/trees/radix.ts b/packages/orama/src/trees/radix.ts index 56317b7f..04ae48ac 100644 --- a/packages/orama/src/trees/radix.ts +++ b/packages/orama/src/trees/radix.ts @@ -44,37 +44,42 @@ export class RadixNode { } public findAllWords(output: FindResult, term: string, exact?: boolean, tolerance?: number): FindResult { - if (this.e) { - const { w, d: docIDs } = this + const stack: RadixNode[] = [this] + while (stack.length > 0) { + const node = stack.pop()! - if (exact && w !== term) { - return {} - } + if (node.e) { + const { w, d: docIDs } = node + + if (exact && w !== term) { + continue + } - if (getOwnProperty(output, w) == null) { - if (tolerance) { - const difference = Math.abs(term.length - w.length) + if (getOwnProperty(output, w) == null) { + if (tolerance) { + const difference = Math.abs(term.length - w.length) - if (difference <= tolerance && syncBoundedLevenshtein(term, w, tolerance).isBounded) { + if (difference <= tolerance && syncBoundedLevenshtein(term, w, tolerance).isBounded) { + output[w] = [] + } + } else { output[w] = [] } - } else { - output[w] = [] } - } - if (getOwnProperty(output, w) != null && docIDs.size > 0) { - const docs = new Set(output[w]) + if (getOwnProperty(output, w) != null && docIDs.size > 0) { + const docs = new Set(output[w]) - for (const docID of docIDs) { - docs.add(docID) + for (const docID of docIDs) { + docs.add(docID) + } + output[w] = Array.from(docs) } - output[w] = Array.from(docs) } - } - for (const [, childNode] of this.c) { - childNode.findAllWords(output, term, exact, tolerance) + for (const [, childNode] of node.c) { + stack.push(childNode) + } } return output } @@ -158,49 +163,61 @@ export class RadixNode { originalTolerance: number, output: FindResult ) { - if (tolerance < 0) { - return - } + const stack: Array<{ node: RadixNode; index: number; tolerance: number }> = [{ node: this, index, tolerance }] - if (this.w.startsWith(term)) { - this.findAllWords(output, term, false, 0) - return - } + while (stack.length > 0) { + const { node, index, tolerance } = stack.pop()! - if (this.e) { - const { w, d: docIDs } = this - if (w) { - if (syncBoundedLevenshtein(term, w, originalTolerance).isBounded) { - output[w] = [] - } - if (getOwnProperty(output, w) != null && docIDs.size > 0) { - const docs = new Set(output[w]) + if (tolerance < 0) { + continue + } - for (const docID of docIDs) { - docs.add(docID) + if (node.w.startsWith(term)) { + node.findAllWords(output, term, false, 0) + continue + } + + if (node.e) { + const { w, d: docIDs } = node + if (w) { + if (syncBoundedLevenshtein(term, w, originalTolerance).isBounded) { + output[w] = [] + } + if (getOwnProperty(output, w) != null && docIDs.size > 0) { + const docs = new Set(output[w]) + + for (const docID of docIDs) { + docs.add(docID) + } + output[w] = Array.from(docs) } - output[w] = Array.from(docs) } } - } - if (index >= term.length) { - return - } + if (index >= term.length) { + continue + } - if (this.c.has(term[index])) { - this.c.get(term[index])!._findLevenshtein(term, index + 1, tolerance, originalTolerance, output) - } + const currentChar = term[index] - this._findLevenshtein(term, index + 1, tolerance - 1, originalTolerance, output) + // 1. If node has child matching term[index], push { node: childNode, index +1, tolerance } + if (node.c.has(currentChar)) { + const childNode = node.c.get(currentChar)! + stack.push({ node: childNode, index: index + 1, tolerance }) + } - for (const [, childNode] of this.c) { - childNode._findLevenshtein(term, index, tolerance - 1, originalTolerance, output) - } + // 2. Push { node, index +1, tolerance -1 } (Delete operation) + stack.push({ node: node, index: index + 1, tolerance: tolerance - 1 }) - for (const [character, childNode] of this.c) { - if (character !== term[index]) { - childNode._findLevenshtein(term, index + 1, tolerance - 1, originalTolerance, output) + // 3. For each child: + for (const [character, childNode] of node.c) { + // a) Insert operation + stack.push({ node: childNode, index: index, tolerance: tolerance - 1 }) + + // b) Substitute operation + if (character !== currentChar) { + stack.push({ node: childNode, index: index + 1, tolerance: tolerance - 1 }) + } } } }