You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
230 lines
8.7 KiB
230 lines
8.7 KiB
const express = require('express');
|
|
const bodyParser = require('body-parser');
|
|
const MTree = require('./m-tree/mtree');
|
|
const Node = require('./m-tree/nodes').Node;
|
|
const distanceFunctions = require('./m-tree/distance-functions');
|
|
|
|
// Generator used to generate random points for the MTree
|
|
const Generator = require('./data/generator');
|
|
|
|
// Express app
|
|
const app = express();
|
|
app.use(bodyParser.json({ limit: '100mb' }));
|
|
|
|
// Create an MTree with the given dimensions and capacity, using the Euclidean distance
|
|
let mtree = null;
|
|
|
|
// Serve static files from the 'public' directory
|
|
app.use(express.static(__dirname + '/public'));
|
|
|
|
// Respond to favicon requests with no content
|
|
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) => {
|
|
if (key === 'parent' || key === 'mtree')
|
|
return value && value.id;
|
|
return value;
|
|
}));
|
|
const distanceFunctionName = Object.keys(distanceFunctions).find(key => distanceFunctions[key] === mtree.distanceFunction);
|
|
tree.distanceFunctionName = distanceFunctionName;
|
|
res.send(tree);
|
|
});
|
|
|
|
// Endpoint to check if the MTree has been initialized
|
|
app.get('/isInitialized', (req, res) => { res.send(!!mtree); });
|
|
// Return the list of supported distance functions
|
|
app.get('/distanceFunctions', (req, res) => { res.send(Object.keys(distanceFunctions)); });
|
|
|
|
// Insert a point into the MTree
|
|
app.post('/insert', (req, res) => {
|
|
let point = req.body.point;
|
|
if (!point)
|
|
return res.status(400).send('Missing point');
|
|
if (point.length !== mtree.dimension)
|
|
return res.status(400).send('Point has the wrong dimension');
|
|
|
|
mtree.insert(point);
|
|
res.send('Point inserted');
|
|
});
|
|
|
|
// Perform a range query on the MTree
|
|
app.get('/rangeQuery', (req, res) => {
|
|
const { queryPoint, radius } = req.query;
|
|
if (!queryPoint)
|
|
return res.status(400).send('Missing query point');
|
|
if (!radius || isNaN(radius))
|
|
return res.status(400).send('Missing radius');
|
|
if (JSON.parse(queryPoint).length !== mtree.dimension)
|
|
return res.status(400).send('Query point has the wrong dimension');
|
|
let parsedQueryPoint;
|
|
try {
|
|
parsedQueryPoint = JSON.parse(queryPoint);
|
|
} catch (e) {
|
|
return res.status(400).send('Invalid query point');
|
|
}
|
|
const parsedRadius = parseFloat(radius);
|
|
|
|
if (parsedQueryPoint.length !== mtree.dimension)
|
|
return res.status(400).send('Query point has the wrong dimension');
|
|
|
|
const start = performance.now();
|
|
const result = mtree.rangeQuery(parsedQueryPoint, parsedRadius);
|
|
const RangeQueryTime = performance.now() - start;
|
|
|
|
const start2 = performance.now();
|
|
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
|
|
};
|
|
|
|
const functionCalls = {
|
|
mtreeRangeQuery: result.dstFnCalls,
|
|
sequentialSearch: sequentialFunctionCalls
|
|
};
|
|
|
|
res.send({ values: result.points, timingResult, functionCalls });
|
|
});
|
|
|
|
// Perform a k-NN query on the MTree
|
|
app.get('/kNNQuery', (req, res) => {
|
|
const { queryPoint, k } = req.query;
|
|
if (!queryPoint)
|
|
return res.status(400).send('Missing query point');
|
|
if (!k || isNaN(k))
|
|
return res.status(400).send('Missing k');
|
|
if (JSON.parse(queryPoint).length !== mtree.dimension)
|
|
return res.status(400).send('Query point has the wrong dimension');
|
|
let parsedQueryPoint;
|
|
try {
|
|
parsedQueryPoint = JSON.parse(queryPoint);
|
|
} catch (e) {
|
|
return res.status(400).send('Invalid query point');
|
|
}
|
|
const kInt = parseInt(k, 10);
|
|
|
|
const start = performance.now();
|
|
const result = mtree.kNNQuery(parsedQueryPoint, kInt);
|
|
const mtreeKNNQueryTime = performance.now() - start;
|
|
|
|
const start2 = performance.now();
|
|
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.points, timingResult, functionCalls });
|
|
});
|
|
|
|
// Recreate the MTree with the given dimensions
|
|
app.post('/recreate', (req, res) => {
|
|
let { dimensions, pointCount, capacity, distanceFunction } = req.body;
|
|
dimensions = parseInt(dimensions);
|
|
pointCount = parseInt(pointCount);
|
|
capacity = parseInt(capacity);
|
|
|
|
if (isNaN(dimensions) || dimensions <= 0)
|
|
return res.status(400).send('Invalid dimensions');
|
|
if (isNaN(pointCount) || pointCount <= 0)
|
|
return res.status(400).send('Invalid point count');
|
|
if (isNaN(capacity) || capacity <= 0)
|
|
return res.status(400).send('Invalid capacity');
|
|
|
|
const distanceFunctionName = distanceFunction.toLowerCase();
|
|
const chosenDistanceFunction = distanceFunctions[distanceFunctionName];
|
|
if (!chosenDistanceFunction)
|
|
return res.status(400).send(`Invalid distance function. Supported functions: ${Object.keys(distanceFunctions).join(', ')}`);
|
|
|
|
const generator = new Generator(dimensions);
|
|
mtree = new MTree(dimensions, capacity, chosenDistanceFunction);
|
|
points = generator.generateMany(pointCount);
|
|
|
|
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`);
|
|
}
|
|
if (i % 1000 === 999)
|
|
console.timeEnd(`insertion of next 1000 points`);
|
|
|
|
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
|
|
app.post('/loadTree', (req, res) => {
|
|
const tree = req.body;
|
|
if (!tree)
|
|
return res.status(400).send('Missing tree');
|
|
try {
|
|
mtree = MTree.fromJSON(tree);
|
|
} catch (e) {
|
|
return res.status(400).send('Invalid tree');
|
|
}
|
|
|
|
points = Node.findGroundEntries(tree.root).map(entry => entry.point);
|
|
|
|
res.send('MTree loaded');
|
|
});
|
|
|
|
// Start the server
|
|
app.listen(3000, () => { console.log('MTree API is running on port 3000'); }); |