From 7883b1b09e4b07e25faaa79c7529dd4f87ee6aaf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Franti=C5=A1ek=20=C5=A0pa=C4=8Dek?= Date: Sat, 23 Nov 2024 22:04:52 +0100 Subject: [PATCH] Working range query --- index.js | 28 ++++++-- m-tree/mtree.js | 172 +++++++++++++++++++++++++++++++----------------- m-tree/nodes.js | 38 +++++++++-- 3 files changed, 166 insertions(+), 72 deletions(-) diff --git a/index.js b/index.js index 4951043..afabb07 100644 --- a/index.js +++ b/index.js @@ -1,35 +1,44 @@ const express = require('express'); const bodyParser = require('body-parser'); const MTree = require('./m-tree/mtree'); -const dimensions = 2; +const dimensions = 3; +// Generator used to generate random points for the MTree const Generator = require('./data/generator'); +// Express app const app = express(); app.use(bodyParser.json()); - +// Euclidean distance function used as the distance metric for the MTree function euclideanDistance(a, b) { return Math.sqrt(a.reduce((acc, val, i) => acc + (val - b[i]) ** 2, 0)); } -const mtree = new MTree(dimensions, 10, euclideanDistance); +// Create an MTree with the given dimensions and capacity, using the Euclidean distance +const mtree = new MTree(dimensions, 6, euclideanDistance); + +// Generate 1000 random points const generator = new Generator(dimensions); -const points = generator.generateMany(1000); +const points = generator.generateMany(100); let i = 0; +// Insert all points into the MTree points.forEach(point => { mtree.insert(point); i++; console.log(i); }); +// Serve the index.html file app.get('/', (req, res) => { res.sendFile(__dirname + '/index.html'); }); +// Return the MTree app.get('/tree', (req, res) => { res.send(JSON.parse(JSON.stringify(mtree, (key, value) => { - if (key === 'parent') return value && value.id; + if (key === 'parent' || key === 'mtree') return value && value.id; return value; }))); }); +// Insert a point into the MTree app.post('/insert', (req, res) => { const point = req.body.point; if (!point || !Array.isArray(point)) { @@ -39,15 +48,21 @@ app.post('/insert', (req, res) => { res.send('Point inserted'); }); +// Perform a range query on the MTree app.get('/rangeQuery', (req, res) => { const { queryPoint, radius } = req.query; if (!queryPoint || !radius) { return res.status(400).send('Invalid query parameters'); } const result = mtree.rangeQuery(JSON.parse(queryPoint), parseFloat(radius)); + + const sequentialResult = points.filter(point => euclideanDistance(point, JSON.parse(queryPoint)) <= parseFloat(radius)); + console.log(sequentialResult); + res.send(result); }); +// Perform a k-NN query on the MTree app.get('/kNNQuery', (req, res) => { const { queryPoint, k } = req.query; if (!queryPoint || !k) { @@ -57,11 +72,13 @@ app.get('/kNNQuery', (req, res) => { 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 app.post('/recreate', (req, res) => { const { dimensions } = req.body; if (!dimensions || typeof dimensions !== 'number') { @@ -72,3 +89,4 @@ app.post('/recreate', (req, res) => { points.forEach(point => mtree.insert(point)); res.send('MTree recreated'); }); + diff --git a/m-tree/mtree.js b/m-tree/mtree.js index 0805f47..9137c0a 100644 --- a/m-tree/mtree.js +++ b/m-tree/mtree.js @@ -3,12 +3,12 @@ const { Node, LeafNode, InternalNode, RoutingEntry, GroundEntry } = require('./n class MTree { /** * Constructs a new MTree instance. - * @param {number} dimensions - The number of dimensions for data points. + * @param {number} dimension - The number of dimension for data points. * @param {number} capacity - The maximum number of entries a node can hold before splitting. * @param {function} [distanceFunction] - A function to calculate the distance between two points. Defaults to Euclidean distance. */ - constructor(dimensions, capacity, distanceFunction = (a, b) => Math.sqrt(a.map((x, i) => (x - b[i]) ** 2).reduce((sum, x) => sum + x))) { - this.dimensions = dimensions; + constructor(dimension, capacity, distanceFunction = (a, b) => Math.sqrt(a.map((x, i) => (x - b[i]) ** 2).reduce((sum, x) => sum + x))) { + this.dimension = dimension; this.capacity = capacity; this.distanceFunction = distanceFunction; this.root = new LeafNode([], null); @@ -30,6 +30,16 @@ class MTree { if (node.entries.length > this.capacity) { this.splitNode(node); } + + // Update radius for each routing entry from leaf to root + let currentNode = node; + while (currentNode.parent) { + const routingEntry = currentNode.parent.entries.find(entry => entry.node === currentNode); + if (routingEntry) { + routingEntry.updateRadius(); + } + currentNode = currentNode.parent; + } } /** @@ -85,84 +95,124 @@ class MTree { if (!node) { throw new Error('Node is undefined'); } + const entries = node.entries; - const mid = Math.ceil(entries.length / 2); - const leftEntries = entries.slice(0, mid); - const rightEntries = entries.slice(mid); - const leftNode = new LeafNode(leftEntries); - const rightNode = new LeafNode(rightEntries); + 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 leftCentroid = this.calculateCentroid(leftEntries); - const rightCentroid = this.calculateCentroid(rightEntries); - const leftRadius = this.calculateRadius(leftEntries, leftCentroid); - const rightRadius = this.calculateRadius(rightEntries, rightCentroid); - const leftEntry = new RoutingEntry(leftCentroid, leftNode, leftRadius); - const rightEntry = new RoutingEntry(rightCentroid, rightNode, rightRadius); + + const rightEntry = new RoutingEntry(rightNode, this); + const leftEntry = new RoutingEntry(leftNode, this); node.entries = [leftEntry, rightEntry]; node.isLeaf = false; } /** - * Splits the given node into two nodes when the number of entries exceeds capacity. - * The given node will be left with half of the entries, and a new node is created with the other half. - * The new node is linked to the original node's parent as a sibling by calculating and using the centroid - * as the connecting entry point. + * 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. * - * @param {Node} node - The node to be split. + * @param {Object[]} entries - The entries to calculate the radius from. + * @returns {number} The radius. */ + calculateRadius(entries) { + // 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)); + const centroidPoint = sum.map(val => val / entries.length); + + // Calculate the maximum distance from the centroid to any point in the entries + const maxDistance = Math.max(...entries.map(entry => this.distanceFunction(entry.point, centroidPoint))); + + return maxDistance; + } + splitNode(node) { - if (!node) { - throw new Error('Node is undefined'); - } const entries = node.entries; - const mid = Math.ceil(entries.length / 2); - const leftEntries = entries.slice(0, mid); - const rightEntries = entries.slice(mid); - const newNode = new Node(rightEntries, node.isLeaf, node.parent); + 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 newNode = new Node(bestSplit.rightEntries, node.isLeaf, node.parent); if (node.parent) { - const rightCentroid = this.calculateCentroid(rightEntries); - const rightRadius = this.calculateRadius(rightEntries, rightCentroid); - const rightEntry = new RoutingEntry(rightCentroid, newNode, rightRadius); + // 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 rightEntry = new RoutingEntry(newNode, this); node.parent.entries.push(rightEntry); 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 = leftEntries; + node.entries = bestSplit.leftEntries; } else { + // If the node is a root node, split it into two new nodes this.splitRoot(node); } } - /** - * Calculates the centroid of a given set of entries. - * The centroid is the average position of all the points in the entries, calculated dimension-wise. - * - * @param {Object[]} entries - An array of entries, each with a 'point' property that is an array of numbers. - * @returns {number[]} The centroid of the entries as an array of numbers representing the average position. - */ - calculateCentroid(entries) { - const centroid = new Array(this.dimensions).fill(0); - for (const entry of entries) { - for (let i = 0; i < this.dimensions; i++) { - centroid[i] += entry.point[i]; + // Helper function to generate all combinations of a given size + getCombinations(arr, size) { + if (size === 0) return [[]]; + const combinations = []; + for (let i = 0; i < arr.length; i++) { + const current = arr[i]; + const rest = arr.slice(i + 1); + for (const combination of this.getCombinations(rest, size - 1)) { + combinations.push([current, ...combination]); } } - return centroid.map(x => x / entries.length); - } - - /** - * Calculates the maximum distance from the given centroid to any of the given entries. - * - * @param {Object[]} entries - An array of entries, each with a 'point' property that is an array of numbers. - * @param {number[]} centroid - The centroid to calculate the distance from. - * @returns {number} The maximum distance from the centroid to any of the entries. - */ - calculateRadius(entries, centroid) { - return Math.max(...entries.map(entry => this.distanceFunction(entry.point, centroid))); + return combinations; } /** @@ -172,10 +222,10 @@ class MTree { * @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}`); + //console.log(`rangeQuery: queryPoint=${JSON.stringify(queryPoint)}, radius=${radius}`); const result = []; this.rangeQueryRecursive(this.root, queryPoint, radius, result); - console.log(`rangeQuery: result=${JSON.stringify(result)}`); + //console.log(`rangeQuery: result=${JSON.stringify(result)}`); return result; } @@ -188,20 +238,20 @@ class MTree { * @returns {undefined} */ rangeQueryRecursive(node, queryPoint, radius, result) { - console.log(`rangeQueryRecursive: node=${node.id}, queryPoint=${JSON.stringify(queryPoint)}, radius=${radius}`); + //console.log(`rangeQueryRecursive: node=${node.id}, queryPoint=${JSON.stringify(queryPoint)}, radius=${radius}`); if (node.isLeaf) { for (const entry of node.entries) { - console.log(`rangeQueryRecursive: checking ground entry ${entry}`); + //console.log(`rangeQueryRecursive: checking ground entry ${entry}`); if (entry instanceof GroundEntry && this.distanceFunction(queryPoint, entry.point) <= radius) { result.push(entry.point); } } } else { for (const entry of node.entries) { - console.log(`rangeQueryRecursive: checking routing entry ${entry}`); + //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`); + //console.log(`rangeQueryRecursive: regions do not overlap, stopping the recursion on this branch`); continue; } this.rangeQueryRecursive(entry.node, queryPoint, radius, result); diff --git a/m-tree/nodes.js b/m-tree/nodes.js index 5347fda..a41dcdb 100644 --- a/m-tree/nodes.js +++ b/m-tree/nodes.js @@ -1,14 +1,40 @@ +const MTree = require("./mtree"); + class RoutingEntry { /** - * Create a new routing entry - * @param {number[]} point - The point of the entry + * Creates a new routing entry. + * Initializes the routing entry with the given node and dimension, + * and computes the initial centroid and radius for the node's entries. * @param {Node} node - The node associated with the entry - * @param {number} radius - The radius of the entry + * @param {MTree} mtree - The number of dimensions for the entry */ - constructor(point, node, radius = 0) { - this.point = point; + constructor(node, mtree) { this.node = node; - this.radius = radius; + this.mtree = mtree; + this.updateCentroid(); + this.updateRadius(); + } + + /** + * Updates the centroid of the routing entry. + * Computes the average position of all points in the associated node's entries. + */ + updateCentroid() { + 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); + } + + /** + * Updates the radius of the routing entry. + * 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))); } }