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'); });