diff --git a/index.html b/index.html index ba6c077..f21c259 100644 --- a/index.html +++ b/index.html @@ -14,7 +14,6 @@ h1 { color: #333; } - .form-group { margin-bottom: 15px; } @@ -55,6 +54,13 @@ + +
+ + +
+ +
- \ No newline at end of file + diff --git a/index.js b/index.js index afabb07..d6bd942 100644 --- a/index.js +++ b/index.js @@ -1,7 +1,7 @@ const express = require('express'); const bodyParser = require('body-parser'); const MTree = require('./m-tree/mtree'); -const dimensions = 3; +const dimensions = 2; // Generator used to generate random points for the MTree const Generator = require('./data/generator'); @@ -16,14 +16,14 @@ function euclideanDistance(a, b) { } // Create an MTree with the given dimensions and capacity, using the Euclidean distance -const mtree = new MTree(dimensions, 6, euclideanDistance); +const mtree = new MTree(dimensions, 10, euclideanDistance); // Generate 1000 random points const generator = new Generator(dimensions); -const points = generator.generateMany(100); -let i = 0; +const points = generator.generateMany(10000); + // Insert all points into the MTree -points.forEach(point => { mtree.insert(point); i++; console.log(i); }); +points.forEach(point => mtree.insert(point)); // Serve the index.html file app.get('/', (req, res) => { @@ -51,14 +51,27 @@ app.post('/insert', (req, res) => { // Perform a range query on the MTree app.get('/rangeQuery', (req, res) => { const { queryPoint, radius } = req.query; - if (!queryPoint || !radius) { + if (!queryPoint || !radius || JSON.parse(queryPoint).length !== dimensions) { return res.status(400).send('Invalid query parameters'); } + console.time('mtreeRangeQuery'); const result = mtree.rangeQuery(JSON.parse(queryPoint), parseFloat(radius)); + console.timeEnd('mtreeRangeQuery'); + console.log(result); + console.time('sequentialSearch'); const sequentialResult = points.filter(point => euclideanDistance(point, JSON.parse(queryPoint)) <= parseFloat(radius)); + console.timeEnd('sequentialSearch'); + //console.log(sequentialResult.map(point => "point:" + point +" distance: " + euclideanDistance(point, JSON.parse(queryPoint)))); console.log(sequentialResult); + /*const furthestPoint = points.reduce((furthest, point) => { + const distance = euclideanDistance(point, mtree.root.entries[0].point); + return distance > euclideanDistance(furthest, mtree.root.entries[0].point) ? point : furthest; + }, points[0]); + console.log(`distance between first routing entry centroid and furthest point: ${euclideanDistance(furthestPoint, mtree.root.entries[0].point)}`); + console.log(`radius of first routing entry: ${furthestPoint, mtree.root.entries[0].radius}`);*/ + res.send(result); }); @@ -68,14 +81,25 @@ app.get('/kNNQuery', (req, res) => { if (!queryPoint || !k) { return res.status(400).send('Invalid query parameters'); } - const result = mtree.kNNQuery(JSON.parse(queryPoint), parseInt(k, 10)); + const parsedQueryPoint = JSON.parse(queryPoint); + const kInt = parseInt(k, 10); + + console.time('mtreeSearch'); + const result = mtree.kNNQuery(parsedQueryPoint, kInt); + console.timeEnd('mtreeSearch'); + + console.time('sequentialSearch'); + const sequentialResult = points.sort((a, b) => euclideanDistance(a, parsedQueryPoint) - euclideanDistance(b, parsedQueryPoint)).slice(0, kInt); + console.timeEnd('sequentialSearch'); + + console.log(sequentialResult); + res.send(result); }); // Start the server app.listen(3000, () => { console.log('MTree API is running on port 3000'); - //console.log(mtree); }); // Recreate the MTree with the given dimensions diff --git a/m-tree/mtree.js b/m-tree/mtree.js index 9137c0a..ae7aa66 100644 --- a/m-tree/mtree.js +++ b/m-tree/mtree.js @@ -19,23 +19,40 @@ class MTree { * @param {number[]} point - The point to insert */ insert(point) { - const node = this.findLeafNode(this.root, point); - if (!node) { + // Find the leaf node that should hold the given point + const leafNode = this.findLeafNode(this.root, point); + if (!leafNode) { throw new Error('No leaf node found for insertion'); } + // Create a new ground entry from the point const groundEntry = new GroundEntry(point); - node.entries.push(groundEntry); - console.log(node.entries.length, this.capacity); - if (node.entries.length > this.capacity) { - this.splitNode(node); + + // Add the ground entry to the leaf node's entries + leafNode.entries.push(groundEntry); + + // Update the centroid and radius of the leaf node's routing entry + if (leafNode.parent !== null) { + const leafEntry = leafNode.parent.entries.find(entry => entry.node === leafNode); + if (leafEntry) { + leafEntry.updateCentroid(); + leafEntry.updateRadius(); + } } - // Update radius for each routing entry from leaf to root - let currentNode = node; - while (currentNode.parent) { + // If the leaf node now has more than the capacity amount of entries, split it + if (leafNode.entries.length > this.capacity) { + this.splitNode(leafNode); + } + + //console.log(`found leaf node: ${leafNode.id}`); + // Update the routing entries of ancestors of the leaf node + let currentNode = leafNode; + while (currentNode.parent !== null) { const routingEntry = currentNode.parent.entries.find(entry => entry.node === currentNode); if (routingEntry) { + //console.log(`updating routing entry: ${routingEntry.node.id}`); + routingEntry.updateCentroid(); routingEntry.updateRadius(); } currentNode = currentNode.parent; @@ -54,9 +71,11 @@ class MTree { throw new Error('Node is undefined'); } if (node.isLeaf) { + //console.log("found leaf node"); return node; } const closestEntry = this.findClosestEntry(node.entries, point); + //console.log(`closest node: ${closestEntry.node.id}, distance: ${this.distanceFunction(point, closestEntry.point)}`); if (!closestEntry) { return node; } @@ -83,57 +102,6 @@ class MTree { return closestEntry; } - /** - * Splits the given node into two nodes when the number of entries exceeds capacity. - * The original node retains half of the entries, and a new node is created with the other half. - * The new node is linked to the original node as a child by calculating and using the centroid - * as the connecting entry point. - * - * @param {Node} node - The node to be split. - */ - splitRoot(node) { - if (!node) { - throw new Error('Node is undefined'); - } - - const entries = node.entries; - const halfSize = Math.floor(entries.length / 2); - - let minRadius = Infinity; - let bestSplit = null; - - // Loop through all combinations of entries that contain half of the total entries - const combinations = this.getCombinations(entries, halfSize); - // console.log(combinations.map(combination => combination.map(entry => entry.point)), "size: " + combinations.length); - for (const combination of combinations) { - const leftEntries = combination; - const rightEntries = entries.filter(entry => !leftEntries.includes(entry)); - - // Calculate the radius for each group - const leftRadius = this.calculateRadius(leftEntries); - const rightRadius = this.calculateRadius(rightEntries); - - // Calculate the total radius for this split - const totalRadius = Math.max(leftRadius, rightRadius); - - // Update the best split if this one has a smaller radius - if (totalRadius < minRadius) { - minRadius = totalRadius; - bestSplit = { leftEntries, rightEntries }; - } - } - - const leftNode = new Node(bestSplit.leftEntries, node.isLeaf); - const rightNode = new Node(bestSplit.rightEntries, node.isLeaf); - leftNode.parent = node; // Set parent of leftNode - rightNode.parent = node; // Set parent of rightNode - - const rightEntry = new RoutingEntry(rightNode, this); - const leftEntry = new RoutingEntry(leftNode, this); - node.entries = [leftEntry, rightEntry]; - node.isLeaf = false; - } - /** * Calculates the radius of the given entries. * The radius is the maximum distance from the centroid of the entries to any point in the entries. @@ -145,7 +113,7 @@ class MTree { // Calculate the centroid of the entries const sum = entries.reduce((acc, entry) => { return acc.map((val, i) => val + entry.point[i]); - }, new Array(entries[0].point.length).fill(0)); + }, new Array(this.dimension).fill(0)); const centroidPoint = sum.map(val => val / entries.length); // Calculate the maximum distance from the centroid to any point in the entries @@ -154,18 +122,20 @@ class MTree { return maxDistance; } + /** + * Splits a node into two nodes by finding the best split that minimizes the total radius of the two nodes. + * @param {Node} node - The node to split + */ splitNode(node) { const entries = node.entries; const halfSize = Math.floor(entries.length / 2); - let minRadius = Infinity; + let minTotalRadius = Infinity; let bestSplit = null; // Loop through all combinations of entries that contain half of the total entries - const combinations = this.getCombinations(entries, halfSize); - // console.log(combinations.map(combination => combination.map(entry => entry.point)), "size: " + combinations.length); - for (const combination of combinations) { - const leftEntries = combination; + const entryCombinations = this.getCombinations(entries, halfSize); + for (const leftEntries of entryCombinations) { const rightEntries = entries.filter(entry => !leftEntries.includes(entry)); // Calculate the radius for each group @@ -173,31 +143,53 @@ class MTree { const rightRadius = this.calculateRadius(rightEntries); // Calculate the total radius for this split - const totalRadius = Math.max(leftRadius, rightRadius); + //const totalRadius = Math.max(leftRadius, rightRadius); + const totalRadius = leftRadius + rightRadius; // Update the best split if this one has a smaller radius - if (totalRadius < minRadius) { - minRadius = totalRadius; + if (totalRadius < minTotalRadius) { + minTotalRadius = totalRadius; bestSplit = { leftEntries, rightEntries }; } } - const newNode = new Node(bestSplit.rightEntries, node.isLeaf, node.parent); - - if (node.parent) { + if (node.parent !== null) { // If the node is not a root node, split it into two nodes // and link the new node to the original node's parent as a sibling + const newNode = new Node(bestSplit.rightEntries, node.isLeaf, node.parent); + if (!node.isLeaf) + bestSplit.rightEntries.forEach(entry => entry.node.parent = newNode); + const rightEntry = new RoutingEntry(newNode, this); + node.entries = bestSplit.leftEntries; node.parent.entries.push(rightEntry); + const nodeEntry = node.parent.entries.find(entry => entry.node === node); + if (nodeEntry) { + nodeEntry.updateCentroid(); + nodeEntry.updateRadius(); + } if (node.parent.entries.length > this.capacity) // If the parent node now has more than capacity entries, split it too this.splitNode(node.parent); - node.entries = bestSplit.leftEntries; } else { // If the node is a root node, split it into two new nodes - this.splitRoot(node); + const leftNode = new Node(bestSplit.leftEntries, node.isLeaf); + const rightNode = new Node(bestSplit.rightEntries, node.isLeaf); + + leftNode.parent = node; + rightNode.parent = node; + if (!node.isLeaf) { + bestSplit.leftEntries.forEach(entry => entry.node.parent = leftNode); + bestSplit.rightEntries.forEach(entry => entry.node.parent = rightNode); + } + + const rightEntry = new RoutingEntry(rightNode, this); + const leftEntry = new RoutingEntry(leftNode, this); + + node.entries = [leftEntry, rightEntry]; + node.isLeaf = false; } } @@ -216,11 +208,11 @@ class MTree { } /** - * Returns all points in the tree that are within the given radius of the given query point. - * @param {number[]} queryPoint - The point to search around. - * @param {number} radius - The radius to search within. - * @returns {number[][]} An array of points that are within the radius of the query point. - */ + * Returns all points in the tree that are within the given radius of the given query point. + * @param {number[]} queryPoint - The point to search around. + * @param {number} radius - The radius to search within. + * @returns {number[][]} An array of points that are within the radius of the query point. + */ rangeQuery(queryPoint, radius) { //console.log(`rangeQuery: queryPoint=${JSON.stringify(queryPoint)}, radius=${radius}`); const result = []; @@ -235,7 +227,6 @@ class MTree { * @param {number[]} queryPoint - The point to search around * @param {number} radius - The radius to search within * @param {number[][]} result - An array to store the result in - * @returns {undefined} */ rangeQueryRecursive(node, queryPoint, radius, result) { //console.log(`rangeQueryRecursive: node=${node.id}, queryPoint=${JSON.stringify(queryPoint)}, radius=${radius}`); @@ -250,15 +241,15 @@ class MTree { for (const entry of node.entries) { //console.log(`rangeQueryRecursive: checking routing entry ${entry}`); const distance = this.distanceFunction(queryPoint, entry.point); - if (distance > radius + entry.radius) { - //console.log(`rangeQueryRecursive: regions do not overlap, stopping the recursion on this branch`); - continue; - } - this.rangeQueryRecursive(entry.node, queryPoint, radius, result); + if (distance <= radius + entry.radius) + this.rangeQueryRecursive(entry.node, queryPoint, radius, result); + //else + // console.log(`rangeQueryRecursive: skipping node ${entry.node.id} due to distance ${distance} > ${radius + entry.radius}`); } } } + /** * Finds the k nearest neighbors to a given query point. * @param {number[]} queryPoint - The point to find the nearest neighbors to @@ -284,9 +275,8 @@ class MTree { for (const entry of node.entries) { const distance = this.distanceFunction(queryPoint, entry.point); if (distance < result[k - 1].distance) { - result.push({ point: entry.point, distance }); + result[k - 1] = { point: entry.point, distance: distance }; result.sort((a, b) => a.distance - b.distance); - result.pop(); } } } else { @@ -302,3 +292,4 @@ class MTree { module.exports = MTree; + diff --git a/m-tree/nodes.js b/m-tree/nodes.js index a41dcdb..31b1b24 100644 --- a/m-tree/nodes.js +++ b/m-tree/nodes.js @@ -20,13 +20,18 @@ class RoutingEntry { * Computes the average position of all points in the associated node's entries. */ updateCentroid() { - const centroid = new Array(this.mtree.dimension).fill(0); + const sum = this.node.entries.reduce((acc, entry) => { + return acc.map((val, i) => val + entry.point[i]); + }, new Array(this.mtree.dimension).fill(0)); + this.point = sum.map(val => val / this.node.entries.length); + + /*const centroid = new Array(this.mtree.dimension).fill(0); for (const entry of this.node.entries) { for (let i = 0; i < this.mtree.dimension; i++) { centroid[i] += entry.point[i]; } } - this.point = centroid.map(x => x / this.node.entries.length); + this.point = centroid.map(x => x / this.node.entries.length);*/ } /** @@ -34,8 +39,21 @@ class RoutingEntry { * Determines the maximum distance from the centroid to any point in the node's entries. */ updateRadius() { - this.radius = Math.max(...this.node.entries.map(entry => this.mtree.distanceFunction(entry.point, this.point))); + const allGroundEntries = []; + const findGroundEntries = node => { + if (node.isLeaf) { + allGroundEntries.push(...node.entries.filter(entry => entry instanceof GroundEntry)); + } else { + for (const entry of node.entries) { + findGroundEntries(entry.node); + } + } + }; + findGroundEntries(this.node); + this.radius = Math.max(...allGroundEntries.map(entry => this.mtree.distanceFunction(entry.point, this.point))); } + + // TODO check if new entry requires larger radius and then update } class GroundEntry { @@ -64,8 +82,6 @@ class Node { this.entries = entries; this.isLeaf = isLeaf; this.parent = parent; - //this.next = null; - //this.prev = null; } }