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
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
app.get('/tree', (req, res) => {
const tree = JSON.parse(JSON.stringify(mtree, (key, value) => {
@ -72,31 +75,45 @@ app.get('/rangeQuery', (req, res) => {
if (parsedQueryPoint.length !== mtree.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 result = mtree.rangeQuery(parsedQueryPoint, parsedRadius);
const RangeQueryTime = performance.now() - start;
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;
/*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 = {
mtreeRangeQuery: RangeQueryTime,
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
@ -121,15 +138,33 @@ app.get('/kNNQuery', (req, res) => {
const mtreeKNNQueryTime = performance.now() - start;
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;
/*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 = {
mtreeKNNQuery: mtreeKNNQueryTime,
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
@ -138,7 +173,7 @@ app.post('/recreate', (req, res) => {
dimensions = parseInt(dimensions);
pointCount = parseInt(pointCount);
capacity = parseInt(capacity);
console.log(dimensions, pointCount, capacity, distanceFunction);
if (isNaN(dimensions) || dimensions <= 0)
return res.status(400).send('Invalid dimensions');
if (isNaN(pointCount) || pointCount <= 0)
@ -155,20 +190,24 @@ app.post('/recreate', (req, res) => {
mtree = new MTree(dimensions, capacity, chosenDistanceFunction);
points = generator.generateMany(pointCount);
// points.forEach(point => mtree.insert(point));
console.time('tree creation')
points.forEach((point, i) => {
if (i % 1000 === 0) {
const start = performance.now();
console.log('Creating tree:', { dimensions, pointCount, capacity, distanceFunction: distanceFunctionName });
console.time('Tree created');
points.forEach(point => mtree.insert(point));
/*points.forEach((point, i) => {
/*if (i % 1000 === 0) {
console.time(`insertion of next 1000 points`);
console.log(`inserted ${i} points`);
}
mtree.insert(point);
if (i % 1000 === 999)
console.timeEnd(`insertion of next 1000 points`);
})
res.send('MTree recreated');
console.timeEnd('tree creation')
mtree.insert(point);
console.log(`inserted ${i} points`);
})*/
const treeCreationTime = performance.now() - start;
res.send({ treeCreationTime });
console.timeEnd('Tree created');
});
// Load the MTree from the posted JSON

@ -1,5 +1,5 @@
const { Node, GroundEntry } = require('./nodes');
const calculateCentroid = require("./utils").calculateCentroid;
const calculateRadius = require("./utils").calculateRadius;
const distanceFunctions = require('./distance-functions');
class MTree {
@ -16,7 +16,6 @@ class MTree {
this.root = new Node([], true, null, this.distanceFunction);
}
/**
* Constructs a new MTree from a given JSON representation of the MTree.
* @param {Object} tree - The JSON representation of the MTree
@ -24,7 +23,6 @@ class MTree {
*/
static fromJSON(tree) {
const mtree = new MTree(tree.dimension, tree.capacity, distanceFunctions[tree.distanceFunctionName]);
console.log(tree);
function assignParents(node, parent) {
node.parent = parent;
@ -62,16 +60,17 @@ class MTree {
// Add the ground entry to the leaf node's entries
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 (leafNode.entries.length > this.capacity)
this.splitNode(leafNode);
// Update the routing entries of ancestors of the leaf node
let currentNode = leafNode;
while (currentNode.parent !== null) {
currentNode.parent.updateCentroid(this.distanceFunction);
currentNode.parent.updateRadiusIfNeeded(groundEntry, this.distanceFunction);
currentNode = currentNode.parent;
while (currentNode = currentNode.parent) {
currentNode.updateCentroid(this.distanceFunction);
currentNode.updateRadiusIfNeeded(groundEntry, this.distanceFunction);
}
}
@ -88,7 +87,7 @@ class MTree {
if (node.isLeaf)
return node;
const closestEntry = this.findClosestEntry(node.entries, point);
const closestEntry = this.findBestEntry(node.entries, point);
if (!closestEntry)
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 {number[]} point - The point to find the closest entry to
* @returns {NodeEntry} The closest entry to the given point
* @param {number[]} point - The point to find the best entry for
* @returns {NodeEntry} The best entry for the given point
*/
findClosestEntry(entries, point) {
let closestEntry = null;
findBestEntry(entries, point) {
let bestEntry = null;
let minRadiusIncrease = Infinity;
let minDistance = Infinity;
for (const entry of entries) {
const distance = this.distanceFunction(point, entry.point);
if (distance < minDistance) {
minDistance = distance;
closestEntry = entry;
const currentDistance = this.distanceFunction(entry.point, point);
const radiusIncrease = Math.max(0, currentDistance - entry.radius);
if (radiusIncrease < minRadiusIncrease) {
minRadiusIncrease = radiusIncrease;
minDistance = currentDistance;
bestEntry = entry;
}
else if (radiusIncrease === minRadiusIncrease && currentDistance < minDistance) {
minDistance = currentDistance;
bestEntry = entry;
}
}
return closestEntry;
}
/**
* 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;
return bestEntry;
}
/**
@ -174,9 +167,9 @@ class MTree {
* the best split or null if no valid split is found.
*/
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 bestSplit = null;
@ -209,9 +202,10 @@ class MTree {
leftEntries.push(entries[i]);
rightEntries.push(entries[j]);
// Ensure the partitions have similar number of entries
if (Math.abs(leftEntries.length - rightEntries.length) <= 1) {
const leftRadius = this.calculateRadius(leftEntries);
const rightRadius = this.calculateRadius(rightEntries);
const leftRadius = calculateRadius(leftEntries, this.distanceFunction);
const rightRadius = calculateRadius(rightEntries, this.distanceFunction);
const totalRadius = leftRadius + rightRadius;
if (totalRadius < minTotalRadius) {
@ -225,31 +219,6 @@ class MTree {
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.
* @param {number[]} queryPoint - The point to search around.
@ -258,8 +227,9 @@ class MTree {
*/
rangeQuery(queryPoint, radius) {
const result = [];
this.dstFnCalls = 0;
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
*/
rangeQueryRecursive(currentNode, queryPoint, searchRadius, resultArray) {
if (currentNode.isLeaf) {
for (const entry of currentNode.entries) {
const distance = this.distanceFunction(queryPoint, entry.point);
for (const entry of currentNode.entries) {
const distance = this.distanceFunction(queryPoint, entry.point);
this.dstFnCalls++;
if (currentNode.isLeaf) {
if (distance <= searchRadius)
resultArray.push({ point: entry.point, distance: distance });
}
} else {
for (const entry of currentNode.entries) {
const distance = this.distanceFunction(queryPoint, entry.point);
} else {
if (distance <= searchRadius + entry.radius)
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.
*/
kNNQuery(queryPoint, k) {
this.dstFnCalls = 0;
const result = Array(k).fill({ distance: Infinity });
this.kNNQueryRecursive(this.root, queryPoint, k, result);
return result;
return { points: result, dstFnCalls: this.dstFnCalls };
}
/**
@ -309,6 +279,7 @@ class MTree {
if (node.isLeaf) {
for (const entry of node.entries) {
const distance = this.distanceFunction(queryPoint, entry.point);
this.dstFnCalls++;
if (distance < result[k - 1].distance) {
result[k - 1] = { point: entry.point, distance: distance };
result.sort((a, b) => a.distance - b.distance);
@ -317,6 +288,7 @@ class MTree {
} else {
for (const entry of node.entries) {
const distance = this.distanceFunction(queryPoint, entry.point);
this.dstFnCalls++;
if (distance <= result[k - 1].distance + entry.radius) {
this.kNNQueryRecursive(entry, queryPoint, k, result);
}

@ -16,14 +16,13 @@ class GroundEntry {
class Node {
static idCounter = 0; // Initialize a static counter
/**
* Constructs a new Node instance.
* @param {Node[]|GroundEntry[]} entries - The node entries, either GroundEntries or other Nodes.
* @param {boolean} isLeaf - Indicates if the node is a leaf node.
* @param {Node|null} parent - The parent node, or null if this is the root node.
* @param {function} distanceFunction - The function used to calculate distances between points.
*/
/**
* Constructs a new Node instance.
* @param {Node[]|GroundEntry[]} entries - The node entries, either GroundEntries or other Nodes.
* @param {boolean} isLeaf - Indicates if the node is a leaf node.
* @param {Node|null} parent - The parent node, or null if this is the root node.
* @param {function} distanceFunction - The function used to calculate distances between points.
*/
constructor(entries, isLeaf, parent, distanceFunction) {
this.id = Node.idCounter++; // Assign a unique ID to the node
this.entries = entries;
@ -46,7 +45,8 @@ class Node {
insert(entry, distanceFunction) {
this.entries.push(entry);
this.updateCentroid(distanceFunction);
this.updateRadiusIfNeeded(entry, distanceFunction);
this.updateRadius(distanceFunction);
//this.updateRadiusIfNeeded(entry, distanceFunction);
}
/**
@ -66,7 +66,14 @@ class Node {
*/
updateRadius(distanceFunction) {
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
* to the new entry is greater than the current radius.

@ -7,7 +7,7 @@
* @returns {number[]} The centroid of the array of entries.
*/
function calculateCentroid(entries, distanceFunction) {
if (entries.length === 0)
if (entries.length === 0)
return;
const length = entries[0].point.length;
@ -15,8 +15,9 @@ if (entries.length === 0)
let totalWeight = 0;
for (const entry of entries) {
const weight = entry.radius || 1;
//const weight = entry.radius || 1;
//const weight = distanceFunction(entry.point, this.point);
const weight = 1;
for (let i = 0; i < length; i++) {
sum[i] += entry.point[i] * weight;
}
@ -26,4 +27,60 @@ if (entries.length === 0)
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>
<section id="column-section">
<div class="column" id="insertion-column">
<h2>Data control</h2>
<div class="form-group">
<label for="point">Point to Insert:</label>
<input type="text" id="point" />
@ -44,6 +45,7 @@
</div>
<div class="column" id="queries-column">
<h2>Queries</h2>
<div class="form-group">
<label for="queryPoint">Query Point:</label>
<input type="text" id="queryPoint" />
@ -61,6 +63,7 @@
<button id="knnQueryButton" disabled>Perform KNN Query</button>
</div>
<div class="column" id="save-load-column">
<h2>Save/Load Tree</h2>
<button id="saveTreeButton" disabled>Save Tree</button>
<input type="file" id="treeFileInput" accept=".json" />
<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);
if (response.ok) {
const result = await response.json();
const table = createTable(result.values);
document.getElementById('result').innerHTML = '';
document.getElementById('result').appendChild(table);
@ -119,7 +120,8 @@ function createTable(result) {
th2.innerHTML = 'Distance';
headerRow.appendChild(th2);
result.forEach(row => {
//result.forEach(row => {
result.slice(0, 100).forEach(row => {
const rowElement = table.insertRow();
rowElement.insertCell().innerText = `[${row.point.map(x => x.toFixed(5)).join(', ')}]`;
rowElement.insertCell().innerText = row.distance.toFixed(5);

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

Loading…
Cancel
Save

Powered by TurnKey Linux.