From 14333dc7f16f6adeaa69ea51ef3412995cbeb123 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Franti=C5=A1ek=20=C5=A0pa=C4=8Dek?= Date: Sat, 23 Nov 2024 02:03:27 +0100 Subject: [PATCH] Basic tree and backend structure --- .gitignore | 4 + data/generator.js | 30 ++++++ index.html | 72 +++++++++++++ index.js | 74 ++++++++++++++ m-tree/mtree.js | 254 ++++++++++++++++++++++++++++++++++++++++++++++ m-tree/nodes.js | 68 +++++++++++++ package.json | 19 ++++ 7 files changed, 521 insertions(+) create mode 100644 .gitignore create mode 100644 data/generator.js create mode 100644 index.html create mode 100644 index.js create mode 100644 m-tree/mtree.js create mode 100644 m-tree/nodes.js create mode 100644 package.json diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..16542d5 --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +node_modules/ +package-lock.json +npm-debug.log +.vscode/ \ No newline at end of file diff --git a/data/generator.js b/data/generator.js new file mode 100644 index 0000000..b58f4fd --- /dev/null +++ b/data/generator.js @@ -0,0 +1,30 @@ +class Generator { + /** + * Create a new generator + * @param {number} dimensions - The number of dimensions for the random vectors + */ + constructor(dimensions) { + this.dimensions = dimensions; + } + + /** + * Generates a random vector with the specified number of dimensions. + * Each element of the vector is a random number between 0 and 1. + * @returns {number[]} A random vector of the given dimensions + */ + generate() { + return Array.from({ length: this.dimensions }, () => Math.random()); + } + + /** + * Generates an array of a specified length, each element of which is a + * random vector of the given dimension. + * @param {number} count - The number of vectors to generate + * @returns {number[][]} an array of random vectors + */ + generateMany(count) { + return Array.from({ length: count }, () => this.generate()); + } +} + +module.exports = Generator; diff --git a/index.html b/index.html new file mode 100644 index 0000000..ba6c077 --- /dev/null +++ b/index.html @@ -0,0 +1,72 @@ + + + + + + + MTree API Interface + + + + +

MTree API Interface

+
+ + +
+
+ + +
+ +
+ + + + + \ No newline at end of file diff --git a/index.js b/index.js new file mode 100644 index 0000000..4951043 --- /dev/null +++ b/index.js @@ -0,0 +1,74 @@ +const express = require('express'); +const bodyParser = require('body-parser'); +const MTree = require('./m-tree/mtree'); +const dimensions = 2; + +const Generator = require('./data/generator'); + +const app = express(); +app.use(bodyParser.json()); + + +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); + +const generator = new Generator(dimensions); +const points = generator.generateMany(1000); +let i = 0; +points.forEach(point => { mtree.insert(point); i++; console.log(i); }); + +app.get('/', (req, res) => { + res.sendFile(__dirname + '/index.html'); +}); + +app.get('/tree', (req, res) => { + res.send(JSON.parse(JSON.stringify(mtree, (key, value) => { + if (key === 'parent') return value && value.id; + return value; + }))); +}); + +app.post('/insert', (req, res) => { + const point = req.body.point; + if (!point || !Array.isArray(point)) { + return res.status(400).send('Invalid point'); + } + mtree.insert(point); + res.send('Point inserted'); +}); + +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)); + res.send(result); +}); + +app.get('/kNNQuery', (req, res) => { + const { queryPoint, k } = req.query; + if (!queryPoint || !k) { + return res.status(400).send('Invalid query parameters'); + } + const result = mtree.kNNQuery(JSON.parse(queryPoint), parseInt(k, 10)); + res.send(result); +}); + +app.listen(3000, () => { + console.log('MTree API is running on port 3000'); + //console.log(mtree); +}); + +app.post('/recreate', (req, res) => { + const { dimensions } = req.body; + if (!dimensions || typeof dimensions !== 'number') { + return res.status(400).send('Invalid dimensions'); + } + mtree = new MTree(dimensions, 10); + points = generator.generateMany(100); + points.forEach(point => mtree.insert(point)); + res.send('MTree recreated'); +}); diff --git a/m-tree/mtree.js b/m-tree/mtree.js new file mode 100644 index 0000000..0805f47 --- /dev/null +++ b/m-tree/mtree.js @@ -0,0 +1,254 @@ +const { Node, LeafNode, InternalNode, RoutingEntry, GroundEntry } = require('./nodes'); + +class MTree { + /** + * Constructs a new MTree instance. + * @param {number} dimensions - The number of dimensions 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; + this.capacity = capacity; + this.distanceFunction = distanceFunction; + this.root = new LeafNode([], null); + } + + /** + * Inserts a new point into the MTree. + * @param {number[]} point - The point to insert + */ + insert(point) { + const node = this.findLeafNode(this.root, point); + if (!node) { + throw new Error('No leaf node found for insertion'); + } + + 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); + } + } + + /** + * Recursively traverses the MTree from the given node to find the leaf node + * that should hold the given point. + * @param {Node} node - The node to start searching from + * @param {number[]} point - The point to find the leaf node for + * @returns {LeafNode} The leaf node that should hold the given point + */ + findLeafNode(node, point) { + if (!node) { + throw new Error('Node is undefined'); + } + if (node.isLeaf) { + return node; + } + const closestEntry = this.findClosestEntry(node.entries, point); + if (!closestEntry) { + return node; + } + return this.findLeafNode(closestEntry.node, point); + } + + /** + * Finds the entry in the given array of entries that is closest to the given point. + * @param {NodeEntry[]} entries - The array of entries to search through + * @param {number[]} point - The point to find the closest entry to + * @returns {NodeEntry} The closest entry to the given point + */ + findClosestEntry(entries, point) { + let closestEntry = null; + let minDistance = Infinity; + + for (const entry of entries) { + const distance = this.distanceFunction(point, entry.point); + if (distance < minDistance) { + minDistance = distance; + closestEntry = entry; + } + } + 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 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); + 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); + 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. + * + * @param {Node} node - The node to be split. + */ + 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); + + if (node.parent) { + const rightCentroid = this.calculateCentroid(rightEntries); + const rightRadius = this.calculateRadius(rightEntries, rightCentroid); + const rightEntry = new RoutingEntry(rightCentroid, newNode, rightRadius); + node.parent.entries.push(rightEntry); + + if (node.parent.entries.length > this.capacity) + this.splitNode(node.parent); + + node.entries = leftEntries; + } else { + 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]; + } + } + 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))); + } + + /** + * 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 = []; + this.rangeQueryRecursive(this.root, queryPoint, radius, result); + console.log(`rangeQuery: result=${JSON.stringify(result)}`); + return result; + } + + /** + * Recursively traverses the MTree to find all points within the given radius of the given query point. + * @param {Node} node - The node to start searching from + * @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}`); + if (node.isLeaf) { + for (const entry of node.entries) { + 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}`); + 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); + } + } + } + + /** + * Finds the k nearest neighbors to a given query point. + * @param {number[]} queryPoint - The point to find the nearest neighbors to + * @param {number} k - The number of nearest neighbors to find + * @returns {Object[]} An array of objects with 'point' and 'distance' properties, sorted by distance. + */ + kNNQuery(queryPoint, k) { + const result = Array(k).fill({ distance: Infinity }); + this.kNNQueryRecursive(this.root, queryPoint, k, result); + return result; + } + + /** + * Recursively traverses the MTree to find the k nearest neighbors to a given query point. + * @param {Node} node - The node to start searching from + * @param {number[]} queryPoint - The point to find the nearest neighbors to + * @param {number} k - The number of nearest neighbors to find + * @param {Object[]} result - The result array to store the k nearest neighbors in, sorted by distance. + * @returns {undefined} + */ + kNNQueryRecursive(node, queryPoint, k, result) { + if (node.isLeaf) { + 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.sort((a, b) => a.distance - b.distance); + result.pop(); + } + } + } else { + for (const entry of node.entries) { + const distance = this.distanceFunction(queryPoint, entry.point); + if (distance <= result[k - 1].distance + entry.radius) { + this.kNNQueryRecursive(entry.node, queryPoint, k, result); + } + } + } + } +} + +module.exports = MTree; + diff --git a/m-tree/nodes.js b/m-tree/nodes.js new file mode 100644 index 0000000..5347fda --- /dev/null +++ b/m-tree/nodes.js @@ -0,0 +1,68 @@ +class RoutingEntry { + /** + * Create a new routing entry + * @param {number[]} point - The point of the entry + * @param {Node} node - The node associated with the entry + * @param {number} radius - The radius of the entry + */ + constructor(point, node, radius = 0) { + this.point = point; + this.node = node; + this.radius = radius; + } +} + +class GroundEntry { + /** + * Create a new group entry + * @param {number[]} point - The point of the entry + * @param {number} data - The data associated with the entry + */ + constructor(point, data = null) { + this.point = point; + this.data = data; + } +} + +class Node { + static idCounter = 0; // Initialize a static counter + + /** + * Create a new node + * @param {GroundEntry[]|RoutingEntry[]} entries - The entries in the node + * @param {boolean} isLeaf - Whether the node is a leaf node + * @param {Node} parent - The parent node + */ + constructor(entries, isLeaf, parent) { + this.id = Node.idCounter++; // Assign a unique ID to the node + this.entries = entries; + this.isLeaf = isLeaf; + this.parent = parent; + //this.next = null; + //this.prev = null; + } +} + +class LeafNode extends Node { + /** + * Create a new leaf node + * @param {GroundEntry[]} entries - The entries in the node + * @param {Node} parent - The parent node + */ + constructor(entries, parent) { + super(entries, true, parent); // Call the Node constructor with isLeaf set to true + } +} + +class InternalNode extends Node { + /** + * Create a new internal node + * @param {RoutingEntry[]} entries - The entries in the node + * @param {Node} parent - The parent node + */ + constructor(entries, parent) { + super(entries, false, parent); // Call the Node constructor with isLeaf set to false + } +} + +module.exports = { Node, LeafNode, InternalNode, RoutingEntry, GroundEntry }; diff --git a/package.json b/package.json new file mode 100644 index 0000000..b650136 --- /dev/null +++ b/package.json @@ -0,0 +1,19 @@ +{ + "name": "ni-vmm", + "version": "1.0.0", + "description": "Basic backend for M-Trees", + "main": "index.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1", + "start": "node index.js" + }, + "repository": { + "type": "git", + "url": "git@gitlab.fit.cvut.cz:spacefr1/ni-vmm.git" + }, + "author": "František Špaček", + "license": "ISC", + "dependencies": { + "express": "^4.21.1" + } +}