Automatic testing and bug fixes

main
František Špaček 1 year ago
parent f1328e22a8
commit 47358a6b9a

@ -23,6 +23,9 @@ app.get('/favicon.ico', (req, res) => { res.status(204).end(); });
// Serve the index.html file // Serve the index.html file
app.get('/', (req, res) => { res.sendFile(__dirname + '/public/index.html'); }); app.get('/', (req, res) => { res.sendFile(__dirname + '/public/index.html'); });
// Performance testing
app.get('/performance', (req, res) => { res.sendFile(__dirname + '/public/performance.html'); });
// Return the MTree // Return the MTree
app.get('/tree', (req, res) => { app.get('/tree', (req, res) => {
const tree = JSON.parse(JSON.stringify(mtree, (key, value) => { const tree = JSON.parse(JSON.stringify(mtree, (key, value) => {
@ -72,31 +75,45 @@ app.get('/rangeQuery', (req, res) => {
if (parsedQueryPoint.length !== mtree.dimension) if (parsedQueryPoint.length !== mtree.dimension)
return res.status(400).send('Query point has the wrong dimension'); return res.status(400).send('Query point has the wrong dimension');
/*console.time('mtreeRangeQuery');
const result = mtree.rangeQuery(parsedQueryPoint, parsedRadius);
console.timeEnd('mtreeRangeQuery');
//console.log(result.length);
console.time('sequentialSearch');
const sequentialResult = points.filter(point => mtree.distanceFunction(point, parsedQueryPoint) <= parsedRadius);
console.timeEnd('sequentialSearch');
//console.log(sequentialResult.length);
res.send(result);*/
const start = performance.now(); const start = performance.now();
const result = mtree.rangeQuery(parsedQueryPoint, parsedRadius); const result = mtree.rangeQuery(parsedQueryPoint, parsedRadius);
const RangeQueryTime = performance.now() - start; const RangeQueryTime = performance.now() - start;
const start2 = performance.now(); const start2 = performance.now();
const sequentialResult = points.filter(point => mtree.distanceFunction(point, parsedQueryPoint) <= parsedRadius); let sequentialFunctionCalls = 0;
const sequentialResult = points.filter(point => {
sequentialFunctionCalls++;
return mtree.distanceFunction(point, parsedQueryPoint) <= parsedRadius;
});
const sequentialSearchTime = performance.now() - start2; const sequentialSearchTime = performance.now() - start2;
/*let resultCopy = JSON.parse(JSON.stringify(result));
let sequentialResultCopy = JSON.parse(JSON.stringify(sequentialResult));
resultCopy = resultCopy.points.map(point => point.point).sort((a, b) => mtree.distanceFunction(a, parsedQueryPoint) - mtree.distanceFunction(b, parsedQueryPoint));
sequentialResultCopy = sequentialResultCopy.map(point => point).sort((a, b) => mtree.distanceFunction(a, parsedQueryPoint) - mtree.distanceFunction(b, parsedQueryPoint));
if (JSON.stringify(resultCopy) !== JSON.stringify(sequentialResultCopy)) {
console.log('Mismatch between MTree and sequential range query results');
console.log(JSON.stringify(resultCopy));
console.log(JSON.stringify(sequentialResultCopy));
console.log(`range query: query point ${JSON.stringify(parsedQueryPoint)}, radius ${parsedRadius}`);
return res.status(500).send('Mismatch between MTree and sequential range query results');
}*/
const timingResult = { const timingResult = {
mtreeRangeQuery: RangeQueryTime, mtreeRangeQuery: RangeQueryTime,
sequentialSearch: sequentialSearchTime sequentialSearch: sequentialSearchTime
}; };
res.send({ values: result, timingResult }); const functionCalls = {
mtreeRangeQuery: result.dstFnCalls,
sequentialSearch: sequentialFunctionCalls
};
res.send({ values: result.points, timingResult, functionCalls });
}); });
// Perform a k-NN query on the MTree // Perform a k-NN query on the MTree
@ -121,15 +138,33 @@ app.get('/kNNQuery', (req, res) => {
const mtreeKNNQueryTime = performance.now() - start; const mtreeKNNQueryTime = performance.now() - start;
const start2 = performance.now(); const start2 = performance.now();
const sequentialResult = points.sort((a, b) => mtree.distanceFunction(a, parsedQueryPoint) - mtree.distanceFunction(b, parsedQueryPoint)).slice(0, kInt); let sequentialFunctionCalls = 0;
const sequentialResult = points.sort((a, b) => {
sequentialFunctionCalls++;
return mtree.distanceFunction(a, parsedQueryPoint) - mtree.distanceFunction(b, parsedQueryPoint);
}).slice(0, kInt);
const sequentialSearchTime = performance.now() - start2; const sequentialSearchTime = performance.now() - start2;
/*if (JSON.stringify(result.points.map(point => point.point)) !== JSON.stringify(sequentialResult)) {
console.log('Mismatch between MTree and sequential KNN query results');
console.log(JSON.stringify(result.points.map(point => point.point)));
console.log(JSON.stringify(sequentialResult));
console.log(`range query: query point ${JSON.stringify(parsedQueryPoint)}, k ${kInt}`);
return res.status(500).send('Mismatch between MTree and sequential KNN query results');
}*/
const timingResult = { const timingResult = {
mtreeKNNQuery: mtreeKNNQueryTime, mtreeKNNQuery: mtreeKNNQueryTime,
sequentialSearch: sequentialSearchTime sequentialSearch: sequentialSearchTime
}; };
const functionCalls = {
mtreeKNNQuery: result.dstFnCalls,
sequentialSearch: sequentialFunctionCalls
};
res.send({ values: result, timingResult }); res.send({ values: result.points, timingResult, functionCalls });
}); });
// Recreate the MTree with the given dimensions // Recreate the MTree with the given dimensions
@ -138,7 +173,7 @@ app.post('/recreate', (req, res) => {
dimensions = parseInt(dimensions); dimensions = parseInt(dimensions);
pointCount = parseInt(pointCount); pointCount = parseInt(pointCount);
capacity = parseInt(capacity); capacity = parseInt(capacity);
console.log(dimensions, pointCount, capacity, distanceFunction);
if (isNaN(dimensions) || dimensions <= 0) if (isNaN(dimensions) || dimensions <= 0)
return res.status(400).send('Invalid dimensions'); return res.status(400).send('Invalid dimensions');
if (isNaN(pointCount) || pointCount <= 0) if (isNaN(pointCount) || pointCount <= 0)
@ -155,20 +190,24 @@ app.post('/recreate', (req, res) => {
mtree = new MTree(dimensions, capacity, chosenDistanceFunction); mtree = new MTree(dimensions, capacity, chosenDistanceFunction);
points = generator.generateMany(pointCount); points = generator.generateMany(pointCount);
// points.forEach(point => mtree.insert(point)); const start = performance.now();
console.time('tree creation') console.log('Creating tree:', { dimensions, pointCount, capacity, distanceFunction: distanceFunctionName });
points.forEach((point, i) => { console.time('Tree created');
if (i % 1000 === 0) { points.forEach(point => mtree.insert(point));
/*points.forEach((point, i) => {
/*if (i % 1000 === 0) {
console.time(`insertion of next 1000 points`); console.time(`insertion of next 1000 points`);
console.log(`inserted ${i} points`); console.log(`inserted ${i} points`);
} }
mtree.insert(point);
if (i % 1000 === 999) if (i % 1000 === 999)
console.timeEnd(`insertion of next 1000 points`); console.timeEnd(`insertion of next 1000 points`);
})
res.send('MTree recreated'); mtree.insert(point);
console.timeEnd('tree creation') console.log(`inserted ${i} points`);
})*/
const treeCreationTime = performance.now() - start;
res.send({ treeCreationTime });
console.timeEnd('Tree created');
}); });
// Load the MTree from the posted JSON // Load the MTree from the posted JSON

@ -1,5 +1,5 @@
const { Node, GroundEntry } = require('./nodes'); const { Node, GroundEntry } = require('./nodes');
const calculateCentroid = require("./utils").calculateCentroid; const calculateRadius = require("./utils").calculateRadius;
const distanceFunctions = require('./distance-functions'); const distanceFunctions = require('./distance-functions');
class MTree { class MTree {
@ -16,7 +16,6 @@ class MTree {
this.root = new Node([], true, null, this.distanceFunction); this.root = new Node([], true, null, this.distanceFunction);
} }
/** /**
* Constructs a new MTree from a given JSON representation of the MTree. * Constructs a new MTree from a given JSON representation of the MTree.
* @param {Object} tree - The JSON representation of the MTree * @param {Object} tree - The JSON representation of the MTree
@ -24,7 +23,6 @@ class MTree {
*/ */
static fromJSON(tree) { static fromJSON(tree) {
const mtree = new MTree(tree.dimension, tree.capacity, distanceFunctions[tree.distanceFunctionName]); const mtree = new MTree(tree.dimension, tree.capacity, distanceFunctions[tree.distanceFunctionName]);
console.log(tree);
function assignParents(node, parent) { function assignParents(node, parent) {
node.parent = parent; node.parent = parent;
@ -62,16 +60,17 @@ class MTree {
// Add the ground entry to the leaf node's entries // Add the ground entry to the leaf node's entries
leafNode.insert(groundEntry, this.distanceFunction); leafNode.insert(groundEntry, this.distanceFunction);
leafNode.updateCentroid(this.distanceFunction);
leafNode.updateRadius(this.distanceFunction);
// If the leaf node now has more than the capacity amount of entries, split it // If the leaf node now has more than the capacity amount of entries, split it
if (leafNode.entries.length > this.capacity) if (leafNode.entries.length > this.capacity)
this.splitNode(leafNode); this.splitNode(leafNode);
// Update the routing entries of ancestors of the leaf node
let currentNode = leafNode; let currentNode = leafNode;
while (currentNode.parent !== null) { while (currentNode = currentNode.parent) {
currentNode.parent.updateCentroid(this.distanceFunction); currentNode.updateCentroid(this.distanceFunction);
currentNode.parent.updateRadiusIfNeeded(groundEntry, this.distanceFunction); currentNode.updateRadiusIfNeeded(groundEntry, this.distanceFunction);
currentNode = currentNode.parent;
} }
} }
@ -88,7 +87,7 @@ class MTree {
if (node.isLeaf) if (node.isLeaf)
return node; return node;
const closestEntry = this.findClosestEntry(node.entries, point); const closestEntry = this.findBestEntry(node.entries, point);
if (!closestEntry) if (!closestEntry)
return node; return node;
@ -96,40 +95,34 @@ class MTree {
} }
/** /**
* Finds the entry in the given array of entries that is closest to the given point. * Finds the entry in the given array of entries for which adding a new point
* would require the smallest increase in radius, or no increase at all.
*
* @param {NodeEntry[]} entries - The array of entries to search through * @param {NodeEntry[]} entries - The array of entries to search through
* @param {number[]} point - The point to find the closest entry to * @param {number[]} point - The point to find the best entry for
* @returns {NodeEntry} The closest entry to the given point * @returns {NodeEntry} The best entry for the given point
*/ */
findClosestEntry(entries, point) { findBestEntry(entries, point) {
let closestEntry = null; let bestEntry = null;
let minRadiusIncrease = Infinity;
let minDistance = Infinity; let minDistance = Infinity;
for (const entry of entries) { for (const entry of entries) {
const distance = this.distanceFunction(point, entry.point); const currentDistance = this.distanceFunction(entry.point, point);
if (distance < minDistance) { const radiusIncrease = Math.max(0, currentDistance - entry.radius);
minDistance = distance;
closestEntry = entry; if (radiusIncrease < minRadiusIncrease) {
minRadiusIncrease = radiusIncrease;
minDistance = currentDistance;
bestEntry = entry;
} }
else if (radiusIncrease === minRadiusIncrease && currentDistance < minDistance) {
minDistance = currentDistance;
bestEntry = entry;
} }
return closestEntry;
} }
/** return bestEntry;
* 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 {Object[]} entries - The entries to calculate the radius from.
* @returns {number} The radius.
*/
calculateRadius(entries) {
const centroidPoint = calculateCentroid(entries, this.distanceFunction);
// Calculate the maximum distance from the centroid to any point in the entries,
// taking into account the radius of each node
const maxDistance = Math.max(...entries.map(entry => this.distanceFunction(entry.point, centroidPoint)/* + entry.radius*/));
return maxDistance;
} }
/** /**
@ -174,9 +167,9 @@ class MTree {
* the best split or null if no valid split is found. * the best split or null if no valid split is found.
*/ */
findBestSplit(entries) { findBestSplit(entries) {
if (entries.length < 2) return null; if (entries.length < 2) return { leftEntries: entries, rightEntries: [] };
const halfSize = Math.floor(entries.length / 2); //const halfSize = Math.floor(entries.length / 2);
let minTotalRadius = Infinity; let minTotalRadius = Infinity;
let bestSplit = null; let bestSplit = null;
@ -209,9 +202,10 @@ class MTree {
leftEntries.push(entries[i]); leftEntries.push(entries[i]);
rightEntries.push(entries[j]); rightEntries.push(entries[j]);
// Ensure the partitions have similar number of entries
if (Math.abs(leftEntries.length - rightEntries.length) <= 1) { if (Math.abs(leftEntries.length - rightEntries.length) <= 1) {
const leftRadius = this.calculateRadius(leftEntries); const leftRadius = calculateRadius(leftEntries, this.distanceFunction);
const rightRadius = this.calculateRadius(rightEntries); const rightRadius = calculateRadius(rightEntries, this.distanceFunction);
const totalRadius = leftRadius + rightRadius; const totalRadius = leftRadius + rightRadius;
if (totalRadius < minTotalRadius) { if (totalRadius < minTotalRadius) {
@ -225,31 +219,6 @@ class MTree {
return bestSplit; return bestSplit;
} }
/**
* Gets all combinations of size 'size' from the given array 'arr'.
* @param {any[]} arr - The array to get combinations from
* @param {number} size - The size of the combinations to get
* @returns {any[][]} An array of combinations of the given size
*/
/*getCombinations(arr, size) {
if (size === 0)
return [[]];
const result = [];
const helper = (offset, partialCombination) => {
if (partialCombination.length === size) {
result.push(partialCombination.slice());
return;
}
for (let i = offset; i <= arr.length - (size - partialCombination.length); i++) {
partialCombination.push(arr[i]);
helper(i + 1, partialCombination);
partialCombination.pop();
}
};
helper(0, []);
return result;
}*/
/** /**
* Returns all points in the tree that are within the given radius of the given 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[]} queryPoint - The point to search around.
@ -258,8 +227,9 @@ class MTree {
*/ */
rangeQuery(queryPoint, radius) { rangeQuery(queryPoint, radius) {
const result = []; const result = [];
this.dstFnCalls = 0;
this.rangeQueryRecursive(this.root, queryPoint, radius, result); this.rangeQueryRecursive(this.root, queryPoint, radius, result);
return result; return { points: result, dstFnCalls: this.dstFnCalls };
} }
/** /**
@ -270,15 +240,14 @@ class MTree {
* @param {number[][]} resultArray - An array to store the result in * @param {number[][]} resultArray - An array to store the result in
*/ */
rangeQueryRecursive(currentNode, queryPoint, searchRadius, resultArray) { rangeQueryRecursive(currentNode, queryPoint, searchRadius, resultArray) {
if (currentNode.isLeaf) {
for (const entry of currentNode.entries) { for (const entry of currentNode.entries) {
const distance = this.distanceFunction(queryPoint, entry.point); const distance = this.distanceFunction(queryPoint, entry.point);
this.dstFnCalls++;
if (currentNode.isLeaf) {
if (distance <= searchRadius) if (distance <= searchRadius)
resultArray.push({ point: entry.point, distance: distance }); resultArray.push({ point: entry.point, distance: distance });
}
} else { } else {
for (const entry of currentNode.entries) {
const distance = this.distanceFunction(queryPoint, entry.point);
if (distance <= searchRadius + entry.radius) if (distance <= searchRadius + entry.radius)
this.rangeQueryRecursive(entry, queryPoint, searchRadius, resultArray); this.rangeQueryRecursive(entry, queryPoint, searchRadius, resultArray);
} }
@ -292,9 +261,10 @@ class MTree {
* @returns {Object[]} An array of objects with 'point' and 'distance' properties, sorted by distance. * @returns {Object[]} An array of objects with 'point' and 'distance' properties, sorted by distance.
*/ */
kNNQuery(queryPoint, k) { kNNQuery(queryPoint, k) {
this.dstFnCalls = 0;
const result = Array(k).fill({ distance: Infinity }); const result = Array(k).fill({ distance: Infinity });
this.kNNQueryRecursive(this.root, queryPoint, k, result); this.kNNQueryRecursive(this.root, queryPoint, k, result);
return result; return { points: result, dstFnCalls: this.dstFnCalls };
} }
/** /**
@ -309,6 +279,7 @@ class MTree {
if (node.isLeaf) { if (node.isLeaf) {
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);
this.dstFnCalls++;
if (distance < result[k - 1].distance) { if (distance < result[k - 1].distance) {
result[k - 1] = { point: entry.point, distance: 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);
@ -317,6 +288,7 @@ class MTree {
} else { } else {
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);
this.dstFnCalls++;
if (distance <= result[k - 1].distance + entry.radius) { if (distance <= result[k - 1].distance + entry.radius) {
this.kNNQueryRecursive(entry, queryPoint, k, result); this.kNNQueryRecursive(entry, queryPoint, k, result);
} }

@ -16,8 +16,7 @@ class GroundEntry {
class Node { class Node {
static idCounter = 0; // Initialize a static counter static idCounter = 0; // Initialize a static counter
/**
/**
* Constructs a new Node instance. * Constructs a new Node instance.
* @param {Node[]|GroundEntry[]} entries - The node entries, either GroundEntries or other Nodes. * @param {Node[]|GroundEntry[]} entries - The node entries, either GroundEntries or other Nodes.
* @param {boolean} isLeaf - Indicates if the node is a leaf node. * @param {boolean} isLeaf - Indicates if the node is a leaf node.
@ -46,7 +45,8 @@ class Node {
insert(entry, distanceFunction) { insert(entry, distanceFunction) {
this.entries.push(entry); this.entries.push(entry);
this.updateCentroid(distanceFunction); this.updateCentroid(distanceFunction);
this.updateRadiusIfNeeded(entry, distanceFunction); this.updateRadius(distanceFunction);
//this.updateRadiusIfNeeded(entry, distanceFunction);
} }
/** /**
@ -66,7 +66,14 @@ class Node {
*/ */
updateRadius(distanceFunction) { updateRadius(distanceFunction) {
const groundEntries = Node.findGroundEntries(this); const groundEntries = Node.findGroundEntries(this);
this.radius = Math.max(...groundEntries.map(entry => distanceFunction(entry.point, this.point)));
let maxDistance = 0;
for (const entry of groundEntries) {
const distance = distanceFunction(entry.point, this.point);
if (distance > maxDistance)
maxDistance = distance;
}
this.radius = maxDistance;
} }
/** /**
@ -87,6 +94,10 @@ class Node {
} }
}; };
findGroundEntries() {
return Node.findGroundEntries(this);
}
/** /**
* Updates the radius of the node if the distance from the node's centroid * Updates the radius of the node if the distance from the node's centroid
* to the new entry is greater than the current radius. * to the new entry is greater than the current radius.

@ -7,7 +7,7 @@
* @returns {number[]} The centroid of the array of entries. * @returns {number[]} The centroid of the array of entries.
*/ */
function calculateCentroid(entries, distanceFunction) { function calculateCentroid(entries, distanceFunction) {
if (entries.length === 0) if (entries.length === 0)
return; return;
const length = entries[0].point.length; const length = entries[0].point.length;
@ -15,8 +15,9 @@ if (entries.length === 0)
let totalWeight = 0; let totalWeight = 0;
for (const entry of entries) { for (const entry of entries) {
const weight = entry.radius || 1; //const weight = entry.radius || 1;
//const weight = distanceFunction(entry.point, this.point); //const weight = distanceFunction(entry.point, this.point);
const weight = 1;
for (let i = 0; i < length; i++) { for (let i = 0; i < length; i++) {
sum[i] += entry.point[i] * weight; sum[i] += entry.point[i] * weight;
} }
@ -26,4 +27,60 @@ if (entries.length === 0)
return sum.map(val => val / totalWeight); return sum.map(val => val / totalWeight);
} }
module.exports = { calculateCentroid }; /**
* 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 {Object[]} entries - The entries to calculate the radius from.
* @param {function} [distanceFunction] - A distance function to use to calculate the distance between two points.
* @returns {number} The radius.
*/
function calculateRadius(entries, distanceFunction) {
const centroidPoint = calculateCentroid(entries, distanceFunction);
// Calculate the maximum distance from the centroid to any point in the entries,
// taking into account the radius of each node
//const maxDistance = Math.max(...entries.map(entry => distanceFunction(entry.point, centroidPoint) + (entry.radius || 0)));
let maxDistance = 0;
for (const entry of entries) {
const distance = distanceFunction(entry.point, centroidPoint);
if (distance > maxDistance)
maxDistance = distance;
}
this.radius = maxDistance;
return maxDistance;
}
/**
* 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 {Object[]} entries - The entries to calculate the radius from.
* @param {function} [distanceFunction] - A distance function to use to calculate the distance between two points.
* @returns {number} The radius.
*/
/*function calculateRadius(entries, distanceFunction) {
const centroidPoint = calculateCentroid(entries, distanceFunction);
// Calculate the maximum distance from the centroid to any point in the entries,
// taking into account the radius of each node
let maxDistance = 0;
for (const entry of entries) {
if (entry.entries) {
const groundEntries = entry.findGroundEntries();
for (const groundEntry of groundEntries) {
const distance = distanceFunction(groundEntry.point, centroidPoint);
if (distance > maxDistance)
maxDistance = distance;
}
} else {
const distance = distanceFunction(entry.point, centroidPoint);
if (distance > maxDistance)
maxDistance = distance;
}
}
return maxDistance;
}*/
module.exports = { calculateRadius, calculateCentroid };

@ -18,6 +18,7 @@
<main> <main>
<section id="column-section"> <section id="column-section">
<div class="column" id="insertion-column"> <div class="column" id="insertion-column">
<h2>Data control</h2>
<div class="form-group"> <div class="form-group">
<label for="point">Point to Insert:</label> <label for="point">Point to Insert:</label>
<input type="text" id="point" /> <input type="text" id="point" />
@ -44,6 +45,7 @@
</div> </div>
<div class="column" id="queries-column"> <div class="column" id="queries-column">
<h2>Queries</h2>
<div class="form-group"> <div class="form-group">
<label for="queryPoint">Query Point:</label> <label for="queryPoint">Query Point:</label>
<input type="text" id="queryPoint" /> <input type="text" id="queryPoint" />
@ -61,6 +63,7 @@
<button id="knnQueryButton" disabled>Perform KNN Query</button> <button id="knnQueryButton" disabled>Perform KNN Query</button>
</div> </div>
<div class="column" id="save-load-column"> <div class="column" id="save-load-column">
<h2>Save/Load Tree</h2>
<button id="saveTreeButton" disabled>Save Tree</button> <button id="saveTreeButton" disabled>Save Tree</button>
<input type="file" id="treeFileInput" accept=".json" /> <input type="file" id="treeFileInput" accept=".json" />
<button id="loadTreeButton">Load Tree</button> <button id="loadTreeButton">Load Tree</button>

@ -0,0 +1,254 @@
<!DOCTYPE html>
<html>
<head>
<title>M-Tree vs Sequential Search Comparison</title>
<style>
body {
font-family: Arial, sans-serif;
}
#results {
padding: 20px;
border: 1px solid #ccc;
border-radius: 10px;
box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
}
table {
border-collapse: collapse;
}
th,
td {
padding: 5px;
border: 1px solid #ccc;
}
</style>
</head>
<body>
<h1>M-Tree vs Sequential Search Comparison</h1>
<form id="test-form">
<label>
Dataset size (points):
<input type="number" id="dataset-size-min" value="1000" step="1000" />
to
<input type="number" id="dataset-size-max" value="100000" step="1000" />
by
<input type="number" id="dataset-size-step" value="1000" step="1000" />
</label>
<br>
<label>
Dimension:
<input type="number" id="dimension-min" value="2" step="1" />
to
<input type="number" id="dimension-max" value="10" step="1" />
by
<input type="number" id="dimension-step" value="1" step="1" />
</label>
<br>
<label>
M-Tree node size:
<input type="number" id="m-tree-node-size-min" value="4" step="1" />
to
<input type="number" id="m-tree-node-size-max" value="10" step="1" />
by
<input type="number" id="m-tree-node-size-step" value="1" step="1" />
</label>
<br>
<button id="run-tests" type="submit">Run tests</button>
</form>
<div id="results"></div>
<br>
<button id="export-results" type="button">Export results to CSV</button>
<script>
const exportButton = document.getElementById('export-results');
exportButton.addEventListener('click', () => {
const table = resultsDiv.firstElementChild;
const csv = [];
for (const row of table.rows) {
const rowArray = [];
for (const cell of row.cells) {
rowArray.push(cell.textContent);
}
csv.push(rowArray.join(','));
}
const csvString = csv.join('\n');
const blob = new Blob([csvString], { type: 'text/csv' });
const link = document.createElement('a');
link.href = URL.createObjectURL(blob);
//const configName = `dataset_${datasetSizeMin}`;//-${datasetSizeMax}_dimension_${dimensionMin}-${dimensionMax}_nodeSize_${mTreeNodeSizeMin}-${mTreeNodeSizeMax}`;
link.download = `m-tree-test_${document.getElementById('dataset-size-min').value}.csv`;
link.click();
});
</script>
<script>
const form = document.getElementById('test-form');
const resultsDiv = document.getElementById('results');
form.addEventListener('submit', (e) => {
e.preventDefault();
const datasetSizeMin = Number(document.getElementById('dataset-size-min').value);
const datasetSizeMax = Number(document.getElementById('dataset-size-max').value);
const datasetSizeStep = Number(document.getElementById('dataset-size-step').value);
const dimensionMin = Number(document.getElementById('dimension-min').value);
const dimensionMax = Number(document.getElementById('dimension-max').value);
const dimensionStep = Number(document.getElementById('dimension-step').value);
const mTreeNodeSizeMin = Number(document.getElementById('m-tree-node-size-min').value);
const mTreeNodeSizeMax = Number(document.getElementById('m-tree-node-size-max').value);
const mTreeNodeSizeStep = Number(document.getElementById('m-tree-node-size-step').value);
resultsDiv.innerHTML = '';
runTestsLoop(datasetSizeMin, datasetSizeMax, datasetSizeStep, dimensionMin, dimensionMax, dimensionStep, mTreeNodeSizeMin, mTreeNodeSizeMax, mTreeNodeSizeStep);
});
async function runTestsLoop(datasetSizeMin, datasetSizeMax, datasetSizeStep, dimensionMin, dimensionMax, dimensionStep, mTreeNodeSizeMin, mTreeNodeSizeMax, mTreeNodeSizeStep) {
const table = document.createElement('table');
const thead = document.createElement('thead');
const tbody = document.createElement('tbody');
table.appendChild(thead);
table.appendChild(tbody);
resultsDiv.appendChild(table);
const headerRow = document.createElement('tr');
['Dataset size', 'Dimension', 'M-Tree node size', 'Tree creation time', 'Range query time (MTree)', 'Range query time (Sequential)', 'KNN query time (MTree)', 'KNN query time (Sequential)', 'MTree range query calls', 'Sequential range query calls', 'MTree KNN query calls', 'Sequential KNN query calls']
.forEach(header => {
headerRow.appendChild(document.createElement('th')).textContent = header;
});
thead.appendChild(headerRow);
for (let datasetSize = datasetSizeMin; datasetSize <= datasetSizeMax; datasetSize += datasetSizeStep) {
for (let dimension = dimensionMin; dimension <= dimensionMax; dimension += dimensionStep) {
for (let mTreeNodeSize = mTreeNodeSizeMin; mTreeNodeSize <= mTreeNodeSizeMax; mTreeNodeSize += mTreeNodeSizeStep) {
const result = await runTests(datasetSize, dimension, mTreeNodeSize);
const row = document.createElement('tr');
[
datasetSize,
dimension,
mTreeNodeSize,
`${Number(result.timingResults.treeCreationTime).toFixed(5)}`,
`${Number(result.timingResults.rangeQueryTime.mtreeRangeQuery).toFixed(5)}`,
`${Number(result.timingResults.rangeQueryTime.sequentialSearch).toFixed(5)}`,
`${Number(result.timingResults.knnQueryTime.mtreeKNNQuery).toFixed(5)}`,
`${Number(result.timingResults.knnQueryTime.sequentialSearch).toFixed(5)}`,
`${result.functionCallCounts.rangeQuery.mtreeRangeQuery}`,
`${result.functionCallCounts.rangeQuery.sequentialSearch}`,
`${result.functionCallCounts.knnQuery.mtreeKNNQuery}`,
`${result.functionCallCounts.knnQuery.sequentialSearch}`,
].forEach((value, index) => {
row.appendChild(document.createElement('td')).textContent = value;
});
tbody.appendChild(row);
}
}
}
};
async function runTests(datasetSize, dimension, mTreeNodeSize) {
console.log(`running tests for dataset size ${datasetSize}, dimension ${dimension}, m-tree node size ${mTreeNodeSize}`);
try {
const startTime = performance.now();
const timingResults = {
treeCreationTime: 0,
rangeQueryTime: {
mtreeRangeQuery: 0,
sequentialSearch: 0
},
knnQueryTime: {
mtreeKNNQuery: 0,
sequentialSearch: 0
}
};
const functionCallCounts = {
rangeQuery: {
mtreeRangeQuery: 0,
sequentialSearch: 0
},
knnQuery: {
mtreeKNNQuery: 0,
sequentialSearch: 0
}
};
for (let j = 0; j < 5; j++) {
const response = await fetch('/recreate', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
dimensions: dimension,
pointCount: datasetSize,
capacity: mTreeNodeSize,
distanceFunction: 'euclidean'
})
});
if (!response.ok) {
throw new Error(`error recreating tree: ${await response.text()}`);
}
const { treeCreationTime } = await response.json();
timingResults.treeCreationTime += treeCreationTime;
for (let i = 0; i < 5; i++) {
const queryPoint = Array(dimension).fill(i / 5);
const knnResponse = await fetch(`/kNNQuery?queryPoint=${encodeURIComponent(JSON.stringify(queryPoint))}&k=${encodeURIComponent((i + 1)*3)}`);
if (!knnResponse.ok) {
throw new Error(`error running KNN query: ${knnResponse.status} ${await knnResponse.text()}`);
}
const knnResult = await knnResponse.json();
const radius = knnResult.values[knnResult.values.length - 1].distance;
const rangeResponse = await fetch(`/rangeQuery?queryPoint=${encodeURIComponent(JSON.stringify(queryPoint))}&radius=${encodeURIComponent(radius)}`);
if (!rangeResponse.ok) {
throw new Error(`error running range query: ${rangeResponse.status} ${await rangeResponse.text()}`);
}
const rangeResult = await rangeResponse.json();
if (!rangeResult || !rangeResult.timingResult || !rangeResult.functionCalls) {
throw new Error('error parsing result');
}
timingResults.rangeQueryTime.mtreeRangeQuery += rangeResult.timingResult.mtreeRangeQuery;
functionCallCounts.rangeQuery.mtreeRangeQuery += rangeResult.functionCalls.mtreeRangeQuery;
timingResults.rangeQueryTime.sequentialSearch += rangeResult.timingResult.sequentialSearch;
functionCallCounts.rangeQuery.sequentialSearch += rangeResult.functionCalls.sequentialSearch;
timingResults.knnQueryTime.mtreeKNNQuery += knnResult.timingResult.mtreeKNNQuery;
functionCallCounts.knnQuery.mtreeKNNQuery += knnResult.functionCalls.mtreeKNNQuery;
timingResults.knnQueryTime.sequentialSearch += knnResult.timingResult.sequentialSearch;
functionCallCounts.knnQuery.sequentialSearch += knnResult.functionCalls.sequentialSearch;
}
}
timingResults.treeCreationTime /= 5;
timingResults.rangeQueryTime.mtreeRangeQuery /= 25;
functionCallCounts.rangeQuery.mtreeRangeQuery /= 25;
timingResults.rangeQueryTime.sequentialSearch /= 25;
functionCallCounts.rangeQuery.sequentialSearch /= 25;
timingResults.knnQueryTime.mtreeKNNQuery /= 25;
functionCallCounts.knnQuery.mtreeKNNQuery /= 25;
timingResults.knnQueryTime.sequentialSearch /= 25;
functionCallCounts.knnQuery.sequentialSearch /= 25;
return { timingResults, functionCallCounts };
const endTime = performance.now();
console.log(`finished at ${endTime} after ${endTime - startTime}ms`);
} catch (error) {
console.error('error running tests:', error);
return {};
}
}
</script>
</body>
</html>

@ -68,6 +68,7 @@ async function performKNNQuery() {
setStatus(response.ok); setStatus(response.ok);
if (response.ok) { if (response.ok) {
const result = await response.json(); const result = await response.json();
const table = createTable(result.values); const table = createTable(result.values);
document.getElementById('result').innerHTML = ''; document.getElementById('result').innerHTML = '';
document.getElementById('result').appendChild(table); document.getElementById('result').appendChild(table);
@ -119,7 +120,8 @@ function createTable(result) {
th2.innerHTML = 'Distance'; th2.innerHTML = 'Distance';
headerRow.appendChild(th2); headerRow.appendChild(th2);
result.forEach(row => { //result.forEach(row => {
result.slice(0, 100).forEach(row => {
const rowElement = table.insertRow(); const rowElement = table.insertRow();
rowElement.insertCell().innerText = `[${row.point.map(x => x.toFixed(5)).join(', ')}]`; rowElement.insertCell().innerText = `[${row.point.map(x => x.toFixed(5)).join(', ')}]`;
rowElement.insertCell().innerText = row.distance.toFixed(5); rowElement.insertCell().innerText = row.distance.toFixed(5);

@ -35,6 +35,9 @@ h1 {
text-align: center; text-align: center;
} }
h2 {
margin-top: 0;
}
.form-group { .form-group {
margin-bottom: 0.5rem; margin-bottom: 0.5rem;
} }
@ -124,6 +127,7 @@ section {
display: flex; display: flex;
width: auto; width: auto;
column-gap: 1rem; column-gap: 1rem;
padding-bottom: 0;
} }
#result-section { #result-section {

Loading…
Cancel
Save

Powered by TurnKey Linux.