Added knn and fixed radius calculations

main
František Špaček 1 year ago
parent 7883b1b09e
commit 8481ab84a1

@ -14,7 +14,6 @@
h1 {
color: #333;
}
.form-group {
margin-bottom: 15px;
}
@ -55,6 +54,13 @@
<input type="text" id="radius" placeholder="e.g. 1.0" />
</div>
<button onclick="performRangeQuery()">Perform Range Query</button>
<div class="form-group">
<label for="knn">Number of Neighbors (k):</label>
<input type="text" id="knn" placeholder="e.g. 5" />
</div>
<button onclick="performKNNQuery()">Perform KNN Query</button>
<div id="result"></div>
<script>
@ -62,8 +68,13 @@
const queryPoint = document.getElementById('queryPoint').value;
const radius = document.getElementById('radius').value;
const response = await fetch(`/rangeQuery?queryPoint=${encodeURIComponent(queryPoint)}&radius=${encodeURIComponent(radius)}`);
//const result = await response.json();
//document.getElementById('result').innerText = JSON.stringify(result, null, 2);
document.getElementById('result').innerText = await response.text();
}
async function performKNNQuery() {
const queryPoint = document.getElementById('queryPoint').value;
const k = document.getElementById('knn').value;
const response = await fetch(`/kNNQuery?queryPoint=${encodeURIComponent(queryPoint)}&k=${encodeURIComponent(k)}`);
document.getElementById('result').innerText = await response.text();
}
</script>

@ -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

@ -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;

@ -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;
}
}

Loading…
Cancel
Save

Powered by TurnKey Linux.