Skip to main content
Glama
orneryd

M.I.M.I.R - Multi-agent Intelligent Memory & Insight Repository

by orneryd
ldbc-style-benchmark.bench.ts23.4 kB
/** * LDBC-Style Social Network Benchmark for NornicDB vs Neo4j * * Based on the industry-standard LDBC Social Network Benchmark (SNB): * https://github.com/ldbc/ldbc_snb_docs * * Scale Factors: * SF0.1: ~1K persons, ~10K messages, ~50K relationships * SF1: ~10K persons, ~100K messages, ~500K relationships * SF10: ~100K persons, ~1M messages, ~5M relationships * * This benchmark tests: * - Complex read queries (interactive short/complex) * - Update operations * - Multi-hop traversals * - Aggregations at scale * * Run with: npm run bench:ldbc */ import { bench, describe, beforeAll, afterAll } from 'vitest'; import neo4j, { Driver, Session } from 'neo4j-driver'; // ============================================================================= // CONFIGURATION // ============================================================================= const NORNICDB_URI = process.env.NORNICDB_URI || 'bolt://localhost:7687'; const NEO4J_URI = process.env.NEO4J_URI || 'bolt://localhost:7688'; const NEO4J_USER = process.env.NEO4J_USER || 'neo4j'; const NEO4J_PASSWORD = process.env.NEO4J_PASSWORD || 'password'; // Scale Factor: SF0.1 for quick tests, SF1 for comprehensive, SF10 for stress const SCALE_FACTOR = parseFloat(process.env.SCALE_FACTOR || '0.1'); let nornicdbDriver: Driver; let nornicdbSession: Session; let neo4jDriver: Driver; let neo4jSession: Session; // ============================================================================= // LDBC DATA MODEL (Simplified) // ============================================================================= // Person -[KNOWS]-> Person // Person -[LIKES]-> Post/Comment // Person -[HAS_INTEREST]-> Tag // Person -[WORKS_AT]-> Company // Person -[STUDIES_AT]-> University // Person -[IS_LOCATED_IN]-> City // Post -[HAS_CREATOR]-> Person // Post -[HAS_TAG]-> Tag // Comment -[REPLY_OF]-> Post/Comment // City -[IS_PART_OF]-> Country // Country -[IS_PART_OF]-> Continent // ============================================================================= // DATA GENERATORS // ============================================================================= const firstNames = ['Alice', 'Bob', 'Charlie', 'Diana', 'Eve', 'Frank', 'Grace', 'Henry', 'Ivy', 'Jack', 'Kate', 'Leo', 'Mia', 'Noah', 'Olivia', 'Peter', 'Quinn', 'Rose', 'Sam', 'Tina', 'Uma', 'Victor', 'Wendy', 'Xavier', 'Yara', 'Zack', 'Anna', 'Ben', 'Cara', 'Dan']; const lastNames = ['Smith', 'Johnson', 'Williams', 'Brown', 'Jones', 'Garcia', 'Miller', 'Davis', 'Wilson', 'Taylor', 'Anderson', 'Thomas', 'Jackson', 'White', 'Harris', 'Martin', 'Thompson', 'Moore', 'Allen', 'Young']; const countries = ['USA', 'UK', 'Germany', 'France', 'Japan', 'China', 'Brazil', 'India', 'Canada', 'Australia']; const cities = ['New York', 'London', 'Berlin', 'Paris', 'Tokyo', 'Beijing', 'São Paulo', 'Mumbai', 'Toronto', 'Sydney', 'Los Angeles', 'Manchester', 'Munich', 'Lyon', 'Osaka', 'Shanghai', 'Rio', 'Delhi', 'Vancouver', 'Melbourne']; const companies = ['TechCorp', 'DataSoft', 'CloudInc', 'AILabs', 'WebDev', 'MobileTech', 'SecureNet', 'GreenEnergy', 'FinTech', 'HealthIT']; const universities = ['MIT', 'Stanford', 'Harvard', 'Oxford', 'Cambridge', 'ETH', 'Caltech', 'Princeton', 'Yale', 'Berkeley']; const tags = ['tech', 'science', 'music', 'sports', 'travel', 'food', 'art', 'politics', 'business', 'health', 'AI', 'blockchain', 'cloud', 'mobile', 'gaming', 'movies', 'books', 'fashion', 'nature', 'photography']; function getScaledCount(base: number): number { return Math.max(10, Math.floor(base * SCALE_FACTOR)); } async function generateLDBCData(s: Session, dbName: string) { const personCount = getScaledCount(10000); const postCount = getScaledCount(100000); const commentCount = getScaledCount(50000); const knowsCount = getScaledCount(500000); console.log(` Generating LDBC data for ${dbName} (SF${SCALE_FACTOR}):`); console.log(` - ${personCount} persons`); console.log(` - ${postCount} posts`); console.log(` - ${commentCount} comments`); console.log(` - ~${knowsCount} relationships`); // Create Places (Countries and Cities) console.log(` → Creating places...`); for (let i = 0; i < countries.length; i++) { await s.run('CREATE (c:Country {id: $id, name: $name})', { id: i, name: countries[i] }); } for (let i = 0; i < cities.length; i++) { await s.run( `CREATE (c:City {id: $cityId, name: $cityName}) WITH c MATCH (country:Country {id: $countryId}) CREATE (c)-[:IS_PART_OF]->(country)`, { cityId: i, cityName: cities[i], countryId: i % countries.length } ); } // Create Organizations console.log(` → Creating organizations...`); for (let i = 0; i < companies.length; i++) { await s.run('CREATE (c:Company {id: $id, name: $name})', { id: i, name: companies[i] }); } for (let i = 0; i < universities.length; i++) { await s.run('CREATE (u:University {id: $id, name: $name})', { id: i, name: universities[i] }); } // Create Tags console.log(` → Creating tags...`); for (let i = 0; i < tags.length; i++) { await s.run('CREATE (t:Tag {id: $id, name: $name})', { id: i, name: tags[i] }); } // Create Persons with relationships console.log(` → Creating ${personCount} persons...`); for (let i = 0; i < personCount; i++) { const firstName = firstNames[i % firstNames.length]; const lastName = lastNames[Math.floor(i / firstNames.length) % lastNames.length]; const birthday = `19${70 + (i % 30)}-${String((i % 12) + 1).padStart(2, '0')}-${String((i % 28) + 1).padStart(2, '0')}`; const creationDate = `202${i % 4}-${String((i % 12) + 1).padStart(2, '0')}-01T12:00:00`; await s.run( `CREATE (p:Person {id: $id, firstName: $firstName, lastName: $lastName, gender: $gender, birthday: $birthday, creationDate: $creationDate, browserUsed: $browser, locationIP: $ip}) WITH p MATCH (city:City {id: $cityId}) CREATE (p)-[:IS_LOCATED_IN]->(city)`, { id: i, firstName, lastName, gender: i % 2 === 0 ? 'male' : 'female', birthday, creationDate, browser: ['Chrome', 'Firefox', 'Safari', 'Edge'][i % 4], ip: `192.168.${i % 256}.${(i * 7) % 256}`, cityId: i % cities.length } ); // Add work/study relationships for some persons if (i % 3 === 0) { await s.run( `MATCH (p:Person {id: $personId}), (c:Company {id: $companyId}) CREATE (p)-[:WORKS_AT {workFrom: $workFrom}]->(c)`, { personId: i, companyId: i % companies.length, workFrom: 2010 + (i % 14) } ); } if (i % 4 === 0) { await s.run( `MATCH (p:Person {id: $personId}), (u:University {id: $uniId}) CREATE (p)-[:STUDIES_AT {classYear: $classYear}]->(u)`, { personId: i, uniId: i % universities.length, classYear: 2005 + (i % 15) } ); } // Add interests const interestCount = 1 + (i % 5); for (let j = 0; j < interestCount; j++) { await s.run( `MATCH (p:Person {id: $personId}), (t:Tag {id: $tagId}) CREATE (p)-[:HAS_INTEREST]->(t)`, { personId: i, tagId: (i + j) % tags.length } ); } // Progress indicator if (i > 0 && i % 500 === 0) { console.log(` ${i}/${personCount} persons created...`); } } // Create KNOWS relationships (social network) console.log(` → Creating KNOWS relationships...`); const actualKnowsCount = Math.min(knowsCount, personCount * 50); // Cap at 50 friends avg const seen = new Set<string>(); let created = 0; for (let i = 0; created < actualKnowsCount && i < actualKnowsCount * 2; i++) { const person1 = Math.floor(Math.random() * personCount); const person2 = Math.floor(Math.random() * personCount); if (person1 === person2) continue; const key = person1 < person2 ? `${person1}-${person2}` : `${person2}-${person1}`; if (seen.has(key)) continue; seen.add(key); await s.run( `MATCH (p1:Person {id: $p1}), (p2:Person {id: $p2}) CREATE (p1)-[:KNOWS {creationDate: $date}]->(p2)`, { p1: person1, p2: person2, date: `202${i % 4}-${String((i % 12) + 1).padStart(2, '0')}-15` } ); created++; if (created % 2000 === 0) { console.log(` ${created}/${actualKnowsCount} KNOWS created...`); } } // Create Posts console.log(` → Creating ${postCount} posts...`); for (let i = 0; i < postCount; i++) { const creatorId = i % personCount; await s.run( `MATCH (p:Person {id: $creatorId}) CREATE (post:Post {id: $postId, imageFile: $image, creationDate: $date, browserUsed: $browser, locationIP: $ip, content: $content, length: $length}) CREATE (post)-[:HAS_CREATOR]->(p)`, { postId: i, creatorId, image: i % 5 === 0 ? `image${i}.jpg` : null, date: `202${i % 4}-${String((i % 12) + 1).padStart(2, '0')}-${String((i % 28) + 1).padStart(2, '0')}`, browser: ['Chrome', 'Firefox', 'Safari', 'Edge'][i % 4], ip: `10.0.${i % 256}.${(i * 3) % 256}`, content: `Post content ${i} - Lorem ipsum dolor sit amet...`, length: 50 + (i % 200) } ); // Add tags to posts const tagCount = 1 + (i % 3); for (let j = 0; j < tagCount; j++) { await s.run( `MATCH (post:Post {id: $postId}), (t:Tag {id: $tagId}) CREATE (post)-[:HAS_TAG]->(t)`, { postId: i, tagId: (i + j) % tags.length } ); } if (i > 0 && i % 1000 === 0) { console.log(` ${i}/${postCount} posts created...`); } } // Create Comments console.log(` → Creating ${commentCount} comments...`); for (let i = 0; i < commentCount; i++) { const creatorId = (i * 3) % personCount; const replyToPost = i % postCount; await s.run( `MATCH (p:Person {id: $creatorId}), (post:Post {id: $postId}) CREATE (c:Comment {id: $commentId, creationDate: $date, content: $content, length: $length}) CREATE (c)-[:HAS_CREATOR]->(p) CREATE (c)-[:REPLY_OF]->(post)`, { commentId: i, creatorId, postId: replyToPost, date: `202${i % 4}-${String((i % 12) + 1).padStart(2, '0')}-${String((i % 28) + 1).padStart(2, '0')}`, content: `Comment ${i} - Great post!`, length: 20 + (i % 100) } ); if (i > 0 && i % 1000 === 0) { console.log(` ${i}/${commentCount} comments created...`); } } // Create LIKES relationships console.log(` → Creating LIKES relationships...`); const likesCount = getScaledCount(200000); for (let i = 0; i < likesCount; i++) { const personId = i % personCount; const postId = (i * 7) % postCount; await s.run( `MATCH (p:Person {id: $personId}), (post:Post {id: $postId}) CREATE (p)-[:LIKES {creationDate: $date}]->(post)`, { personId, postId, date: `202${i % 4}-${String((i % 12) + 1).padStart(2, '0')}-${String((i % 28) + 1).padStart(2, '0')}` } ); if (i > 0 && i % 2000 === 0) { console.log(` ${i}/${likesCount} LIKES created...`); } } // Verify counts const nodeCount = await s.run('MATCH (n) RETURN count(n) as c'); const edgeCount = await s.run('MATCH ()-[r]->() RETURN count(r) as c'); console.log(` ✓ ${dbName}: ${nodeCount.records[0].get('c')} nodes, ${edgeCount.records[0].get('c')} relationships\n`); } // ============================================================================= // SETUP AND TEARDOWN // ============================================================================= beforeAll(async () => { console.log('\n╔════════════════════════════════════════════════════════════════════╗'); console.log('║ LDBC-Style Social Network Benchmark - NornicDB vs Neo4j ║'); console.log(`║ Scale Factor: SF${SCALE_FACTOR} ║`); console.log('╚════════════════════════════════════════════════════════════════════╝\n'); // Connect to NornicDB console.log(`Connecting to NornicDB at ${NORNICDB_URI}...`); try { nornicdbDriver = neo4j.driver(NORNICDB_URI); nornicdbSession = nornicdbDriver.session(); await nornicdbSession.run('RETURN 1'); console.log('✓ Connected to NornicDB\n'); await nornicdbSession.run('MATCH (n) DETACH DELETE n'); await generateLDBCData(nornicdbSession, 'NornicDB'); } catch (error) { console.error('✗ NornicDB error:', error); } // Connect to Neo4j console.log(`Connecting to Neo4j at ${NEO4J_URI}...`); try { neo4jDriver = neo4j.driver(NEO4J_URI, neo4j.auth.basic(NEO4J_USER, NEO4J_PASSWORD)); neo4jSession = neo4jDriver.session(); await neo4jSession.run('RETURN 1'); console.log('✓ Connected to Neo4j\n'); await neo4jSession.run('MATCH (n) DETACH DELETE n'); await generateLDBCData(neo4jSession, 'Neo4j'); } catch (error) { console.error('✗ Neo4j error:', error); } console.log('─'.repeat(72) + '\n'); }, 1800000); // 30 minute timeout for large datasets afterAll(async () => { console.log('\n' + '─'.repeat(72)); console.log('Cleaning up...'); if (nornicdbSession) { await nornicdbSession.run('MATCH (n) DETACH DELETE n').catch(() => {}); await nornicdbSession.close(); } if (nornicdbDriver) await nornicdbDriver.close(); if (neo4jSession) { await neo4jSession.run('MATCH (n) DETACH DELETE n').catch(() => {}); await neo4jSession.close(); } if (neo4jDriver) await neo4jDriver.close(); console.log('✓ Cleanup complete\n'); }, 60000); // ============================================================================= // LDBC INTERACTIVE SHORT QUERIES (IS) // ============================================================================= describe('NornicDB - LDBC Interactive', () => { // IS1: Profile of a person bench('IS1: Person profile', async () => { await nornicdbSession.run(` MATCH (p:Person {id: 1}) RETURN p.firstName, p.lastName, p.birthday, p.locationIP, p.browserUsed, p.gender, p.creationDate `); }); // IS2: Recent messages of a person bench('IS2: Recent messages', async () => { await nornicdbSession.run(` MATCH (p:Person {id: 1})<-[:HAS_CREATOR]-(m) WHERE m:Post OR m:Comment RETURN m.id, m.content, m.creationDate ORDER BY m.creationDate DESC LIMIT 10 `); }); // IS3: Friends of a person bench('IS3: Friends', async () => { await nornicdbSession.run(` MATCH (p:Person {id: 1})-[:KNOWS]-(friend:Person) RETURN friend.id, friend.firstName, friend.lastName `); }); // IS4: Content of a message bench('IS4: Message content', async () => { await nornicdbSession.run(` MATCH (m:Post {id: 100}) RETURN m.content, m.creationDate `); }); // IS5: Creator of a message bench('IS5: Message creator', async () => { await nornicdbSession.run(` MATCH (m:Post {id: 100})-[:HAS_CREATOR]->(p:Person) RETURN p.id, p.firstName, p.lastName `); }); // IS6: Forum of a message (adapted - tags instead) bench('IS6: Message tags', async () => { await nornicdbSession.run(` MATCH (m:Post {id: 100})-[:HAS_TAG]->(t:Tag) RETURN t.name `); }); // IS7: Replies to a message bench('IS7: Message replies', async () => { await nornicdbSession.run(` MATCH (m:Post {id: 100})<-[:REPLY_OF]-(c:Comment)-[:HAS_CREATOR]->(p:Person) RETURN c.id, c.content, p.firstName, p.lastName ORDER BY c.creationDate DESC `); }); // Complex queries bench('IC1: Friends with name', async () => { await nornicdbSession.run(` MATCH (p:Person {id: 1})-[:KNOWS*1..3]-(friend:Person) WHERE friend.firstName = 'Alice' RETURN DISTINCT friend.id, friend.lastName LIMIT 20 `); }); bench('IC2: Recent messages from friends', async () => { await nornicdbSession.run(` MATCH (p:Person {id: 1})-[:KNOWS]-(friend:Person)<-[:HAS_CREATOR]-(m) WHERE m:Post OR m:Comment RETURN friend.firstName, m.content, m.creationDate ORDER BY m.creationDate DESC LIMIT 20 `); }); bench('IC3: Friends in countries', async () => { await nornicdbSession.run(` MATCH (p:Person {id: 1})-[:KNOWS*1..2]-(friend:Person)-[:IS_LOCATED_IN]->(city:City)-[:IS_PART_OF]->(country:Country) WHERE country.name IN ['USA', 'UK', 'Germany'] RETURN friend.firstName, friend.lastName, country.name, count(*) as cnt ORDER BY cnt DESC LIMIT 20 `); }); bench('IC4: Popular tags among friends', async () => { await nornicdbSession.run(` MATCH (p:Person {id: 1})-[:KNOWS]-(friend:Person)-[:HAS_INTEREST]->(t:Tag) RETURN t.name, count(*) as popularity ORDER BY popularity DESC LIMIT 10 `); }); bench('IC5: New groups (friends of friends)', async () => { await nornicdbSession.run(` MATCH (p:Person {id: 1})-[:KNOWS]->(friend:Person)-[:KNOWS]->(fof:Person) WHERE NOT (p)-[:KNOWS]-(fof) AND p <> fof RETURN DISTINCT fof.firstName, fof.lastName LIMIT 20 `); }); // Aggregation queries bench('Agg: Posts per person', async () => { await nornicdbSession.run(` MATCH (p:Person)<-[:HAS_CREATOR]-(post:Post) RETURN p.id, count(post) as postCount ORDER BY postCount DESC LIMIT 10 `); }); bench('Agg: Average friends per city', async () => { await nornicdbSession.run(` MATCH (p:Person)-[:IS_LOCATED_IN]->(city:City) OPTIONAL MATCH (p)-[:KNOWS]-(friend) WITH city, p, count(friend) as friendCount RETURN city.name, avg(friendCount) as avgFriends, count(p) as personCount ORDER BY avgFriends DESC `); }); bench('Agg: Tag co-occurrence', async () => { await nornicdbSession.run(` MATCH (post:Post)-[:HAS_TAG]->(t1:Tag), (post)-[:HAS_TAG]->(t2:Tag) WHERE t1.id < t2.id RETURN t1.name, t2.name, count(*) as coCount ORDER BY coCount DESC LIMIT 10 `); }); // Write operations bench('Write: Create and delete person', async () => { await nornicdbSession.run(` CREATE (p:Person {id: 999999, firstName: 'Test', lastName: 'User'}) WITH p DELETE p `); }); bench('Write: Create and delete KNOWS', async () => { await nornicdbSession.run(` MATCH (p1:Person {id: 1}), (p2:Person {id: 2}) CREATE (p1)-[r:TEMP_KNOWS]->(p2) DELETE r `); }); }); // ============================================================================= // NEO4J BENCHMARKS (Same queries) // ============================================================================= describe('Neo4j - LDBC Interactive', () => { bench('IS1: Person profile', async () => { await neo4jSession.run(` MATCH (p:Person {id: 1}) RETURN p.firstName, p.lastName, p.birthday, p.locationIP, p.browserUsed, p.gender, p.creationDate `); }); bench('IS2: Recent messages', async () => { await neo4jSession.run(` MATCH (p:Person {id: 1})<-[:HAS_CREATOR]-(m) WHERE m:Post OR m:Comment RETURN m.id, m.content, m.creationDate ORDER BY m.creationDate DESC LIMIT 10 `); }); bench('IS3: Friends', async () => { await neo4jSession.run(` MATCH (p:Person {id: 1})-[:KNOWS]-(friend:Person) RETURN friend.id, friend.firstName, friend.lastName `); }); bench('IS4: Message content', async () => { await neo4jSession.run(` MATCH (m:Post {id: 100}) RETURN m.content, m.creationDate `); }); bench('IS5: Message creator', async () => { await neo4jSession.run(` MATCH (m:Post {id: 100})-[:HAS_CREATOR]->(p:Person) RETURN p.id, p.firstName, p.lastName `); }); bench('IS6: Message tags', async () => { await neo4jSession.run(` MATCH (m:Post {id: 100})-[:HAS_TAG]->(t:Tag) RETURN t.name `); }); bench('IS7: Message replies', async () => { await neo4jSession.run(` MATCH (m:Post {id: 100})<-[:REPLY_OF]-(c:Comment)-[:HAS_CREATOR]->(p:Person) RETURN c.id, c.content, p.firstName, p.lastName ORDER BY c.creationDate DESC `); }); bench('IC1: Friends with name', async () => { await neo4jSession.run(` MATCH (p:Person {id: 1})-[:KNOWS*1..3]-(friend:Person) WHERE friend.firstName = 'Alice' RETURN DISTINCT friend.id, friend.lastName LIMIT 20 `); }); bench('IC2: Recent messages from friends', async () => { await neo4jSession.run(` MATCH (p:Person {id: 1})-[:KNOWS]-(friend:Person)<-[:HAS_CREATOR]-(m) WHERE m:Post OR m:Comment RETURN friend.firstName, m.content, m.creationDate ORDER BY m.creationDate DESC LIMIT 20 `); }); bench('IC3: Friends in countries', async () => { await neo4jSession.run(` MATCH (p:Person {id: 1})-[:KNOWS*1..2]-(friend:Person)-[:IS_LOCATED_IN]->(city:City)-[:IS_PART_OF]->(country:Country) WHERE country.name IN ['USA', 'UK', 'Germany'] RETURN friend.firstName, friend.lastName, country.name, count(*) as cnt ORDER BY cnt DESC LIMIT 20 `); }); bench('IC4: Popular tags among friends', async () => { await neo4jSession.run(` MATCH (p:Person {id: 1})-[:KNOWS]-(friend:Person)-[:HAS_INTEREST]->(t:Tag) RETURN t.name, count(*) as popularity ORDER BY popularity DESC LIMIT 10 `); }); bench('IC5: New groups (friends of friends)', async () => { await neo4jSession.run(` MATCH (p:Person {id: 1})-[:KNOWS]->(friend:Person)-[:KNOWS]->(fof:Person) WHERE NOT (p)-[:KNOWS]-(fof) AND p <> fof RETURN DISTINCT fof.firstName, fof.lastName LIMIT 20 `); }); bench('Agg: Posts per person', async () => { await neo4jSession.run(` MATCH (p:Person)<-[:HAS_CREATOR]-(post:Post) RETURN p.id, count(post) as postCount ORDER BY postCount DESC LIMIT 10 `); }); bench('Agg: Average friends per city', async () => { await neo4jSession.run(` MATCH (p:Person)-[:IS_LOCATED_IN]->(city:City) OPTIONAL MATCH (p)-[:KNOWS]-(friend) WITH city, p, count(friend) as friendCount RETURN city.name, avg(friendCount) as avgFriends, count(p) as personCount ORDER BY avgFriends DESC `); }); bench('Agg: Tag co-occurrence', async () => { await neo4jSession.run(` MATCH (post:Post)-[:HAS_TAG]->(t1:Tag), (post)-[:HAS_TAG]->(t2:Tag) WHERE t1.id < t2.id RETURN t1.name, t2.name, count(*) as coCount ORDER BY coCount DESC LIMIT 10 `); }); bench('Write: Create and delete person', async () => { await neo4jSession.run(` CREATE (p:Person {id: 999999, firstName: 'Test', lastName: 'User'}) WITH p DELETE p `); }); bench('Write: Create and delete KNOWS', async () => { await neo4jSession.run(` MATCH (p1:Person {id: 1}), (p2:Person {id: 2}) CREATE (p1)-[r:TEMP_KNOWS]->(p2) DELETE r `); }); });

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/orneryd/Mimir'

If you have feedback or need assistance with the MCP directory API, please join our Discord server