Working range query

main
František Špaček 1 year ago
parent 14333dc7f1
commit 7883b1b09e

@ -1,35 +1,44 @@
const express = require('express'); const express = require('express');
const bodyParser = require('body-parser'); const bodyParser = require('body-parser');
const MTree = require('./m-tree/mtree'); 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'); const Generator = require('./data/generator');
// Express app
const app = express(); const app = express();
app.use(bodyParser.json()); app.use(bodyParser.json());
// Euclidean distance function used as the distance metric for the MTree
function euclideanDistance(a, b) { function euclideanDistance(a, b) {
return Math.sqrt(a.reduce((acc, val, i) => acc + (val - b[i]) ** 2, 0)); 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 generator = new Generator(dimensions);
const points = generator.generateMany(1000); const points = generator.generateMany(100);
let i = 0; let i = 0;
// Insert all points into the MTree
points.forEach(point => { mtree.insert(point); i++; console.log(i); }); points.forEach(point => { mtree.insert(point); i++; console.log(i); });
// Serve the index.html file
app.get('/', (req, res) => { app.get('/', (req, res) => {
res.sendFile(__dirname + '/index.html'); res.sendFile(__dirname + '/index.html');
}); });
// Return the MTree
app.get('/tree', (req, res) => { app.get('/tree', (req, res) => {
res.send(JSON.parse(JSON.stringify(mtree, (key, value) => { 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; return value;
}))); })));
}); });
// Insert a point into the MTree
app.post('/insert', (req, res) => { app.post('/insert', (req, res) => {
const point = req.body.point; const point = req.body.point;
if (!point || !Array.isArray(point)) { if (!point || !Array.isArray(point)) {
@ -39,15 +48,21 @@ app.post('/insert', (req, res) => {
res.send('Point inserted'); res.send('Point inserted');
}); });
// Perform a range query on the MTree
app.get('/rangeQuery', (req, res) => { app.get('/rangeQuery', (req, res) => {
const { queryPoint, radius } = req.query; const { queryPoint, radius } = req.query;
if (!queryPoint || !radius) { if (!queryPoint || !radius) {
return res.status(400).send('Invalid query parameters'); return res.status(400).send('Invalid query parameters');
} }
const result = mtree.rangeQuery(JSON.parse(queryPoint), parseFloat(radius)); 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); res.send(result);
}); });
// Perform a k-NN query on the MTree
app.get('/kNNQuery', (req, res) => { app.get('/kNNQuery', (req, res) => {
const { queryPoint, k } = req.query; const { queryPoint, k } = req.query;
if (!queryPoint || !k) { if (!queryPoint || !k) {
@ -57,11 +72,13 @@ app.get('/kNNQuery', (req, res) => {
res.send(result); res.send(result);
}); });
// Start the server
app.listen(3000, () => { app.listen(3000, () => {
console.log('MTree API is running on port 3000'); console.log('MTree API is running on port 3000');
//console.log(mtree); //console.log(mtree);
}); });
// Recreate the MTree with the given dimensions
app.post('/recreate', (req, res) => { app.post('/recreate', (req, res) => {
const { dimensions } = req.body; const { dimensions } = req.body;
if (!dimensions || typeof dimensions !== 'number') { if (!dimensions || typeof dimensions !== 'number') {
@ -72,3 +89,4 @@ app.post('/recreate', (req, res) => {
points.forEach(point => mtree.insert(point)); points.forEach(point => mtree.insert(point));
res.send('MTree recreated'); res.send('MTree recreated');
}); });

@ -3,12 +3,12 @@ const { Node, LeafNode, InternalNode, RoutingEntry, GroundEntry } = require('./n
class MTree { class MTree {
/** /**
* Constructs a new MTree instance. * 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 {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. * @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))) { constructor(dimension, capacity, distanceFunction = (a, b) => Math.sqrt(a.map((x, i) => (x - b[i]) ** 2).reduce((sum, x) => sum + x))) {
this.dimensions = dimensions; this.dimension = dimension;
this.capacity = capacity; this.capacity = capacity;
this.distanceFunction = distanceFunction; this.distanceFunction = distanceFunction;
this.root = new LeafNode([], null); this.root = new LeafNode([], null);
@ -30,6 +30,16 @@ class MTree {
if (node.entries.length > this.capacity) { if (node.entries.length > this.capacity) {
this.splitNode(node); 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) { if (!node) {
throw new Error('Node is undefined'); throw new Error('Node is undefined');
} }
const entries = node.entries; const entries = node.entries;
const mid = Math.ceil(entries.length / 2); const halfSize = Math.floor(entries.length / 2);
const leftEntries = entries.slice(0, mid);
const rightEntries = entries.slice(mid); let minRadius = Infinity;
const leftNode = new LeafNode(leftEntries); let bestSplit = null;
const rightNode = new LeafNode(rightEntries);
// 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 leftNode.parent = node; // Set parent of leftNode
rightNode.parent = node; // Set parent of rightNode rightNode.parent = node; // Set parent of rightNode
const leftCentroid = this.calculateCentroid(leftEntries); const rightEntry = new RoutingEntry(rightNode, this);
const rightCentroid = this.calculateCentroid(rightEntries); const leftEntry = new RoutingEntry(leftNode, this);
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);
node.entries = [leftEntry, rightEntry]; node.entries = [leftEntry, rightEntry];
node.isLeaf = false; node.isLeaf = false;
} }
/** /**
* Splits the given node into two nodes when the number of entries exceeds capacity. * Calculates the radius of the given entries.
* The given node will be left with half of the entries, and a new node is created with the other half. * The radius is the maximum distance from the centroid of the entries to any point in the entries.
* 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.
* *
* @param {Node} node - The node to be split. * @param {Object[]} entries - The entries to calculate the radius from.
* @returns {number} The radius.
*/ */
splitNode(node) { calculateRadius(entries) {
if (!node) { // Calculate the centroid of the entries
throw new Error('Node is undefined'); 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) {
const entries = node.entries; const entries = node.entries;
const mid = Math.ceil(entries.length / 2); const halfSize = Math.floor(entries.length / 2);
const leftEntries = entries.slice(0, mid);
const rightEntries = entries.slice(mid); let minRadius = Infinity;
const newNode = new Node(rightEntries, node.isLeaf, node.parent); 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) { if (node.parent) {
const rightCentroid = this.calculateCentroid(rightEntries); // If the node is not a root node, split it into two nodes
const rightRadius = this.calculateRadius(rightEntries, rightCentroid); // and link the new node to the original node's parent as a sibling
const rightEntry = new RoutingEntry(rightCentroid, newNode, rightRadius); const rightEntry = new RoutingEntry(newNode, this);
node.parent.entries.push(rightEntry); node.parent.entries.push(rightEntry);
if (node.parent.entries.length > this.capacity) if (node.parent.entries.length > this.capacity)
// If the parent node now has more than capacity entries, split it too
this.splitNode(node.parent); this.splitNode(node.parent);
node.entries = leftEntries; node.entries = bestSplit.leftEntries;
} else { } else {
// If the node is a root node, split it into two new nodes
this.splitRoot(node); this.splitRoot(node);
} }
} }
/** // Helper function to generate all combinations of a given size
* Calculates the centroid of a given set of entries. getCombinations(arr, size) {
* The centroid is the average position of all the points in the entries, calculated dimension-wise. if (size === 0) return [[]];
* const combinations = [];
* @param {Object[]} entries - An array of entries, each with a 'point' property that is an array of numbers. for (let i = 0; i < arr.length; i++) {
* @returns {number[]} The centroid of the entries as an array of numbers representing the average position. const current = arr[i];
*/ const rest = arr.slice(i + 1);
calculateCentroid(entries) { for (const combination of this.getCombinations(rest, size - 1)) {
const centroid = new Array(this.dimensions).fill(0); combinations.push([current, ...combination]);
for (const entry of entries) {
for (let i = 0; i < this.dimensions; i++) {
centroid[i] += entry.point[i];
}
} }
return centroid.map(x => x / entries.length);
} }
return combinations;
/**
* 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)));
} }
/** /**
@ -172,10 +222,10 @@ class MTree {
* @returns {number[][]} An array of points that are within the radius of the query point. * @returns {number[][]} An array of points that are within the radius of the query point.
*/ */
rangeQuery(queryPoint, radius) { rangeQuery(queryPoint, radius) {
console.log(`rangeQuery: queryPoint=${JSON.stringify(queryPoint)}, radius=${radius}`); //console.log(`rangeQuery: queryPoint=${JSON.stringify(queryPoint)}, radius=${radius}`);
const result = []; const result = [];
this.rangeQueryRecursive(this.root, queryPoint, radius, result); this.rangeQueryRecursive(this.root, queryPoint, radius, result);
console.log(`rangeQuery: result=${JSON.stringify(result)}`); //console.log(`rangeQuery: result=${JSON.stringify(result)}`);
return result; return result;
} }
@ -188,20 +238,20 @@ class MTree {
* @returns {undefined} * @returns {undefined}
*/ */
rangeQueryRecursive(node, queryPoint, radius, result) { 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) { if (node.isLeaf) {
for (const entry of node.entries) { 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) { if (entry instanceof GroundEntry && this.distanceFunction(queryPoint, entry.point) <= radius) {
result.push(entry.point); result.push(entry.point);
} }
} }
} else { } else {
for (const entry of node.entries) { 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); const distance = this.distanceFunction(queryPoint, entry.point);
if (distance > radius + entry.radius) { 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; continue;
} }
this.rangeQueryRecursive(entry.node, queryPoint, radius, result); this.rangeQueryRecursive(entry.node, queryPoint, radius, result);

@ -1,14 +1,40 @@
const MTree = require("./mtree");
class RoutingEntry { class RoutingEntry {
/** /**
* Create a new routing entry * Creates a new routing entry.
* @param {number[]} point - The point of the 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 {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) { constructor(node, mtree) {
this.point = point;
this.node = node; 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)));
} }
} }

Loading…
Cancel
Save

Powered by TurnKey Linux.