Added knn and fixed radius calculations

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

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

@ -1,7 +1,7 @@
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 = 3; const dimensions = 2;
// Generator used to generate random points for the MTree // Generator used to generate random points for the MTree
const Generator = require('./data/generator'); 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 // 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 // Generate 1000 random points
const generator = new Generator(dimensions); const generator = new Generator(dimensions);
const points = generator.generateMany(100); const points = generator.generateMany(10000);
let i = 0;
// Insert all points into the MTree // 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 // Serve the index.html file
app.get('/', (req, res) => { app.get('/', (req, res) => {
@ -51,14 +51,27 @@ app.post('/insert', (req, res) => {
// Perform a range query on the MTree // 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 || JSON.parse(queryPoint).length !== dimensions) {
return res.status(400).send('Invalid query parameters'); return res.status(400).send('Invalid query parameters');
} }
console.time('mtreeRangeQuery');
const result = mtree.rangeQuery(JSON.parse(queryPoint), parseFloat(radius)); 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)); 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); 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); res.send(result);
}); });
@ -68,14 +81,25 @@ app.get('/kNNQuery', (req, res) => {
if (!queryPoint || !k) { if (!queryPoint || !k) {
return res.status(400).send('Invalid query parameters'); 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); res.send(result);
}); });
// Start the server // 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);
}); });
// Recreate the MTree with the given dimensions // Recreate the MTree with the given dimensions

@ -19,23 +19,40 @@ class MTree {
* @param {number[]} point - The point to insert * @param {number[]} point - The point to insert
*/ */
insert(point) { insert(point) {
const node = this.findLeafNode(this.root, point); // Find the leaf node that should hold the given point
if (!node) { const leafNode = this.findLeafNode(this.root, point);
if (!leafNode) {
throw new Error('No leaf node found for insertion'); throw new Error('No leaf node found for insertion');
} }
// Create a new ground entry from the point
const groundEntry = new GroundEntry(point); const groundEntry = new GroundEntry(point);
node.entries.push(groundEntry);
console.log(node.entries.length, this.capacity); // Add the ground entry to the leaf node's entries
if (node.entries.length > this.capacity) { leafNode.entries.push(groundEntry);
this.splitNode(node);
// 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 // If the leaf node now has more than the capacity amount of entries, split it
let currentNode = node; if (leafNode.entries.length > this.capacity) {
while (currentNode.parent) { 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); const routingEntry = currentNode.parent.entries.find(entry => entry.node === currentNode);
if (routingEntry) { if (routingEntry) {
//console.log(`updating routing entry: ${routingEntry.node.id}`);
routingEntry.updateCentroid();
routingEntry.updateRadius(); routingEntry.updateRadius();
} }
currentNode = currentNode.parent; currentNode = currentNode.parent;
@ -54,9 +71,11 @@ class MTree {
throw new Error('Node is undefined'); throw new Error('Node is undefined');
} }
if (node.isLeaf) { if (node.isLeaf) {
//console.log("found leaf node");
return node; return node;
} }
const closestEntry = this.findClosestEntry(node.entries, point); const closestEntry = this.findClosestEntry(node.entries, point);
//console.log(`closest node: ${closestEntry.node.id}, distance: ${this.distanceFunction(point, closestEntry.point)}`);
if (!closestEntry) { if (!closestEntry) {
return node; return node;
} }
@ -83,57 +102,6 @@ class MTree {
return closestEntry; 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. * 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. * 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 // Calculate the centroid of the entries
const sum = entries.reduce((acc, entry) => { const sum = entries.reduce((acc, entry) => {
return acc.map((val, i) => val + entry.point[i]); 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); const centroidPoint = sum.map(val => val / entries.length);
// Calculate the maximum distance from the centroid to any point in the entries // Calculate the maximum distance from the centroid to any point in the entries
@ -154,18 +122,20 @@ class MTree {
return maxDistance; 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) { splitNode(node) {
const entries = node.entries; const entries = node.entries;
const halfSize = Math.floor(entries.length / 2); const halfSize = Math.floor(entries.length / 2);
let minRadius = Infinity; let minTotalRadius = Infinity;
let bestSplit = null; let bestSplit = null;
// Loop through all combinations of entries that contain half of the total entries // Loop through all combinations of entries that contain half of the total entries
const combinations = this.getCombinations(entries, halfSize); const entryCombinations = this.getCombinations(entries, halfSize);
// console.log(combinations.map(combination => combination.map(entry => entry.point)), "size: " + combinations.length); for (const leftEntries of entryCombinations) {
for (const combination of combinations) {
const leftEntries = combination;
const rightEntries = entries.filter(entry => !leftEntries.includes(entry)); const rightEntries = entries.filter(entry => !leftEntries.includes(entry));
// Calculate the radius for each group // Calculate the radius for each group
@ -173,31 +143,53 @@ class MTree {
const rightRadius = this.calculateRadius(rightEntries); const rightRadius = this.calculateRadius(rightEntries);
// Calculate the total radius for this split // 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 // Update the best split if this one has a smaller radius
if (totalRadius < minRadius) { if (totalRadius < minTotalRadius) {
minRadius = totalRadius; minTotalRadius = totalRadius;
bestSplit = { leftEntries, rightEntries }; bestSplit = { leftEntries, rightEntries };
} }
} }
const newNode = new Node(bestSplit.rightEntries, node.isLeaf, node.parent); if (node.parent !== null) {
if (node.parent) {
// If the node is not a root node, split it into two nodes // 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 // 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); const rightEntry = new RoutingEntry(newNode, this);
node.entries = bestSplit.leftEntries;
node.parent.entries.push(rightEntry); 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 (node.parent.entries.length > this.capacity)
// If the parent node now has more than capacity entries, split it too // If the parent node now has more than capacity entries, split it too
this.splitNode(node.parent); this.splitNode(node.parent);
node.entries = bestSplit.leftEntries;
} else { } else {
// If the node is a root node, split it into two new nodes // 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;
} }
} }
@ -235,7 +227,6 @@ class MTree {
* @param {number[]} queryPoint - The point to search around * @param {number[]} queryPoint - The point to search around
* @param {number} radius - The radius to search within * @param {number} radius - The radius to search within
* @param {number[][]} result - An array to store the result in * @param {number[][]} result - An array to store the result in
* @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}`);
@ -250,15 +241,15 @@ class MTree {
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`);
continue;
}
this.rangeQueryRecursive(entry.node, queryPoint, radius, result); 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. * Finds the k nearest neighbors to a given query point.
* @param {number[]} queryPoint - The point to find the nearest neighbors to * @param {number[]} queryPoint - The point to find the nearest neighbors to
@ -284,9 +275,8 @@ class MTree {
for (const entry of node.entries) { for (const entry of node.entries) {
const distance = this.distanceFunction(queryPoint, entry.point); const distance = this.distanceFunction(queryPoint, entry.point);
if (distance < result[k - 1].distance) { 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.sort((a, b) => a.distance - b.distance);
result.pop();
} }
} }
} else { } else {
@ -302,3 +292,4 @@ class MTree {
module.exports = MTree; module.exports = MTree;

@ -20,13 +20,18 @@ class RoutingEntry {
* Computes the average position of all points in the associated node's entries. * Computes the average position of all points in the associated node's entries.
*/ */
updateCentroid() { 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 (const entry of this.node.entries) {
for (let i = 0; i < this.mtree.dimension; i++) { for (let i = 0; i < this.mtree.dimension; i++) {
centroid[i] += entry.point[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. * Determines the maximum distance from the centroid to any point in the node's entries.
*/ */
updateRadius() { 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 { class GroundEntry {
@ -64,8 +82,6 @@ class Node {
this.entries = entries; this.entries = entries;
this.isLeaf = isLeaf; this.isLeaf = isLeaf;
this.parent = parent; this.parent = parent;
//this.next = null;
//this.prev = null;
} }
} }

Loading…
Cancel
Save

Powered by TurnKey Linux.