A tiny, no-external-dependencies, disk-based graph database for Node.js with rich query, traversal, batch ops, batch cosine similarity, and semantic filtering.
- Persist node-&-relation graphs in a JSON file
- Query, traverse, mutate, and semantically search graphs in JavaScript
- Cosine similarity search of nodes & edges via vector embeddings for AI/semantic-graph use cases
- Batch and hierarchical traversals, semantic+traditional queries, and stats
- Full API for CRUD, batch, similarity, statistics, import/export, and traversal
- ✅ Persistent storage
All nodes & edges auto-saved to a JSON file - 🔍 Search: name, metadata, ID, relation endpoints, and semantic/meta comparison
- 🧮 Cosine Similarity queries for embeddings in metadata (nodes or relations)
- 🔄 Graph Traversal, walk/batch from node, relation, or metadata; supports direction/depth/name filters
- ⬇️ Batch update/delete by search criteria (see below)
- 📈 Stats: node count, edge count, average degree
- 🔄 Import/export: snapshot/restore full graph
- ⚡ Fast, super lightweight, perfect for graph semantic search, retrieval-augmented generation, etc.
npm install tiny-graph-db
const TinyGraphDB = require('tiny-graph-db');
const db = new TinyGraphDB();
// Add nodes with embeddings
const nodeA = db.addNode('Paper A', { type: 'paper', embedding: [0.2, 0.1, 0.5] });
const nodeC = db.addNode('Concept X', { type: 'concept', embedding: [0.25, 0.1, 0.55] });
const nodeP = db.addNode('Author', { type: 'person', embedding: [0.9, 0.8, 0.7] });
const rel1 = db.addRelation('mentions', nodeA.id, nodeC.id, { confidence: 0.92 });
const rel2 = db.addRelation('authored_by', nodeA.id, nodeP.id, { confidence: 1.0 });
// Node search by metadata
console.log('All concepts:', db.searchNodes({ metadata: { type: 'concept' } }));
// Cosine similarity search
const qv = [0.2, 0.1, 0.52];
const similar = db.searchNodesByCosineSimilarity(qv, { threshold: 0.99 });
console.log('Semantically closest nodes:', similar);
// Traverse outgoing links from nodeA up to depth 2
const walk = db.traverseFromNode(nodeA.id, { maxDepth: 2, directions: ['outgoing'] });
console.log('Traversal:', walk);
// Batch update: update all "concept" nodes
db.updateBySearch('node', { metadata: { type: 'concept' } }, { metadata: { reviewed: true } });
// Batch delete: remove all relations with low confidence
db.deleteBySearch('relation', { metadata: { confidence: { lt: 0.95 } } });
// Save (usually auto, but explicit call)
db.flushToDisk();
new TinyGraphDB(filePath?: string)
- filePath: Path to JSON file (default:
'./graph_data.json'
).
Method | Description | Returns |
---|---|---|
addNode(name, metadata = {}, flush = true) |
Create node with name/metadata | Node object |
getNode(nodeId) |
Look up node by ID | Node or undefined |
getAllNodes() |
Get all nodes | Node[] |
updateNode(nodeId, {name?, metadata?}) |
Update name/metadata | Updated node |
deleteNode(nodeId) |
Remove node and all its relations | Deleted node object |
deleteBySearch('node', conditions) |
Batch delete by search | Array of removed |
Method | Description | Returns |
---|---|---|
addRelation(name, fromNodeId, toNodeId, metadata = {}, flush = true) |
Create edge between nodes | Relation object |
getRelation(relationId) |
Fetch edge by ID | Relation or undefined |
getAllRelations() |
Get all edges | Relation[] |
updateRelation(relationId, {name?, metadata?}) |
Update name/metadata | Updated relation |
deleteRelation(relationId) |
Remove relation | Deleted relation object |
deleteBySearch('relation', conditions) |
Batch delete by search | Array of removed |
searchNodes(conditions: SearchConditions): Node[]
searchRelations(conditions: SearchConditions): Relation[]
conditions:
name
: string | RegExp |{ contains: string }
id
,fromNodeId
,toNodeId
metadata
:{ [key]: ... }
supports:- equality, comparison:
{ eq, ne, gt, gte, lt, lte, contains, startsWith, endsWith, in }
- cosine similarity:
{ cosineSimilarity: { queryEmbedding, threshold } }
- equality, comparison:
cosineSimilarity
(top-level):{ queryEmbedding, embeddingKey, threshold }
searchNodesByCosineSimilarity(queryEmbedding: number[], options?): Array
searchRelationsByCosineSimilarity(queryEmbedding: number[], options?): Array
cosineSimilarity(vecA: number[], vecB: number[]): number
queryEmbedding
: Numeric vector- Options:
embeddingKey
: metadata key for vector (default:'embedding'
)threshold
: similarity threshold (default: 0.5)limit
: max results (default: 10)
db.searchNodesByCosineSimilarity([0.1, 0.2, 0.3], { threshold: 0.8, limit: 3 });
Method | Description | Returns |
---|---|---|
traverseFromNode(startNodeId, options) |
Walks from a node, following edges (see below) | Array of [fromNode, relation, toNode] |
traverseFromRelation(startRelationId, maxDepth?) |
Starts traversal from a relation | Same as above |
traverseFromMetadata(metadataConditions, maxDepth?) |
Begins traverse from nodes/relations that match metadata | Same as above |
Options for traverseFromNode
:
maxDepth
: limit depth (Infinity
by default)directions
:['outgoing','incoming']
relationName
: (optional) filter by relation name
db.traverseFromNode(nodeId, { maxDepth: 2, directions: ['outgoing'] });
Result: Array of [fromNode, relation, toNode]
triplets in visit order.
updateBySearch('node' | 'relation', searchConditions, { name?, metadata? }): Array
// Example:
db.updateBySearch('node', { metadata: { genre: 'sci-fi' } }, { name: 'SF Novel' });
deleteBySearch('node' | 'relation', searchConditions): Array
// Example:
db.deleteBySearch('relation', { metadata: { confidence: { lt: 0.9 } } });
searchAndTraverse(queryEmbedding, options?): Array
Supports:
- Cosine similarity search + regular filters, for nodes/relations
- For each initial match, traverses up to N hops, directionally (optionally, end traversal on node only)
- Returns rich hierarchical JSON
Options:
embeddingKey
,threshold
,limit
- see cosine similarityhops
: Number of hops to traverse (default: 3)nodeFilters
,relationFilters
: Additional filterssearchNodes
,searchRelations
: Whether to include nodes, edges, or bothdirections
: e.g.,['outgoing', 'incoming']
endOnNode
: bool (whether to always finish traversal on nodes)
Example:
const tree = db.searchAndTraverse([0.2, 0.1, 0.5], {
hops: 2,
searchNodes: true,
searchRelations: false,
nodeFilters: { metadata: { type: 'paper' } },
});
console.log(tree);
// Output: array of hierarchical trees, each rooted on an initial (semantic) hit, with outgoing/incoming relations, connected nodes/edges & so forth
exportData(): { nodes: Node[], relations: Relation[] }
importData(data: { nodes, relations }): void
Export produces the full graph dataset as JSON-serializable data. Import wipes and loads supplied graph, then persists.
getNeighbors(nodeId)
: All neighbor nodes, with edge and direction- Returns: Array of
{ node, relation, direction }
- Returns: Array of
getStats()
:{ nodeCount, relationCount, avgDegree }
flushToDisk()
: Explicit save to disk (auto after every mutation unless usingflush = false
param on add)rebuildNodeRelationsIndex()
: Internal; rebuilds edge indices (auto-run after import)
const book1 = db.addNode('Dune', { genre: 'sci-fi', pages: 412, published: 1965 });
const book2 = db.addNode('Foundation', { genre: 'sci-fi', pages: 255, published: 1951 });
const author1 = db.addNode('Frank Herbert', { nationality: 'US' });
// Find all US authors:
db.searchNodes({ metadata: { nationality: 'US' } });
// Find all books published pre-1960:
db.searchNodes({ metadata: { published: { lt: 1960 } } });
const doc = db.addNode('Graph Vector', { embedding: [0.2, 0.4, 0.6] });
// Find similar to [0.2, 0.41, 0.67]:
db.searchNodesByCosineSimilarity([0.2, 0.41, 0.67], { threshold: 0.95 });
// Walk two hops out from a node
const walk = db.traverseFromNode(doc.id, { maxDepth: 2, directions: ['outgoing'] });
// Start traversal from a relation
const traverseRels = db.traverseFromRelation(rel1.id, 3);
// Traverse from all nodes with type "paper":
db.traverseFromMetadata({ type: 'paper' }, 2);
// Tag all "concept" nodes as reviewed
db.updateBySearch('node', { metadata: { type: 'concept' } }, { metadata: { reviewed: true } });
// Delete all weak relations
db.deleteBySearch('relation', { metadata: { confidence: { lt: 0.8 } } });
// Retrieve node (by semantic match) then its 2-hop subgraph
const rag = db.searchAndTraverse([0.25, 0.1, 0.5], { hops: 2 });
console.log(JSON.stringify(rag, null, 2));
console.log('Stats:', db.getStats());
console.log('Neighbors of nodeA:', db.getNeighbors(nodeA.id));
// Export/import
const json = db.exportData();
db.importData(json);
Function | Time (ms) | Ops/sec |
---|---|---|
getNode() | 0.0001 | 8,473,743 |
traverseFromNode() | 0.0072 | 138,175 |
searchNodes() | 0.1728 | 5,787 |
searchNodesByCosineSimilarity() | 0.3456 | 2,893 |
Run benchmarks: node src/benchmark.js 1000 2000 5
or npm run benchmark -- 1000 2000 5
- Fork the repo
- Create a branch:
git checkout -b feat/my-feature
- Commit & push, then open a PR
Please file bugs/requests using GitHub Issues.
MIT License (see LICENSE)
Built with ♥ by freakynit