level-path-index

leveldb indexes for properties of items at or anywhere below a point in a materialized path

level-path-index

index properties of items that live in a tree of materialized paths - using levelup

$ npm install level-path-index

Indexes key=value properties of an object against a materialised path so you can ask things like:

  • match all items that match 'color=red' and are descendents of /home/rodney
  • match all items that are direct children of /home/rodney
var level = require('level');
var sub = require('level-sublevel');
var pathindex = require('level-path-index');
 
var db = sub(level(__dirname + '/pathdb', {
     valueEncoding: 'json'
}))
 
var treedb = db.sublevel('folders')
 
var treeindex = pathindex(treedb, '_treeindex', function(keyvalueemit){
 
    // index multiple values for one field 
    (obj.colors || []).forEach(function(color){
 
        // emit the 'path', 'field' and 'value' 
        emit(key, 'color', color)
    })
 
    emit(key, 'name', obj.name)
})

Then stick some data in your leveldb:

treeindex.batch([{
    type:'put',
    key:'/home/rodney/catpictures/goofycat.jpg',
    value:{
      name:'goofy 1',
      colors:['red', 'blue'],
      description:'this cat crazy ass stoopid',
      otherstuff:'...'
     }
}, {
    type:'put',
    key:'/home/rodney/shoppinglist/catfood.txt',
    value:{
    name:'cat yum yums',
    colors:['red', 'yellow'],
    description:'wot I needs to feeds the feline overlord',
    otherstuff:'...'
    }
}], function(err) {
 
    // data and indexes are inserted! 
 
})

Now we can search for things that match color=red and live somewhere under '/home/rodney':

var through = require('through');
 
treeindex.descendentStream('/home/rodney', {
    color:'red'
}).pipe(through(function(doc){
    console.dir(doc);
}))
 
/*
{
    name:'goofy 1',
    colors:['red', 'blue'],
    ...
}
{
    name:'cat yum yums',
    colors:['red', 'yellow'],
    ...
}
 
*/

You can also find direct children of an entry and use multiple clauses to your query:

treeindex.childStream('/home/rodney/catpictures', {
    color:'red',
    name:'goofy 1'
}).pipe(through(function(doc){
    console.dir(doc);
}))
 
/*
{
    name:'goofy 1',
    colors:['red', 'blue'],
    ...
}
*/

In a single query step - there are 3 parts in the combined index:

  • fieldname
  • value
  • tree location

Assuming this question:

find all descendents of '/a' where the color is red

Then we have:

  1. fieldname (color)
  2. value (red)
  3. tree location (/a/b/c)

If we create the index in this strict order then the key would become:

color~red~/a/b/c

The first part of the query is ok - color=red - this would mean leveldb range like this:

{
    start:'dv~color~red~',
    end:'dv~color~red~\xff'
}

Because we are doing a (d)escendent query with some (v)alues - the key is prepended with 'dv'

Now to include the path - we are looking below '/a' - we add the splitter '/' on the end and the level range becomes:

{
    start:'dv~color~red~/a/',
    end:'dv~color~red~/a/\xff'
}

This would match our item - which is living 2 layers below (in '/a/b/c')

Children is slightly different - to make child request fast an extra index is created.

This avoids loading all descendents of a top level node when all you want are its children.

The child index works by seperating the parent path from the node path.

For our example (color~red~/a/b/c) the following index would also be created:

cv~color~red~/a/b/~_~c

The child value indexes are prepended with 'cv'.

The split between the parent and child path means we can ask for children of '/a/b' and only the direct children are loaded.

{
    start:'cv~color~red~/a/b/~_~',
    end:'cv~color~red~/a/b/~_~\xff'
}

This would match '/a/b/c' (cv~color~red~/a/b/~_~c) as a direct child

You can also load descendents and children with a blank query - a seperate index for empty queries is used - the key for our example is simply:

/a/b/c

So if we just wanted descendents of '/a/b' we can use this range:

{
    start:'dt~/a/b/',
    end:'dt~/a/b/\xff'
}

The empty query indexes (t)ree for descendents are prepended with 'dt' and for children 'ct'

Pass the document database, optionally the name/sublevel for the indexes and a mapper function that will index each document as it is updated

the mapper is run with the key and value of the update and an emit function.

emit is a function with a (path, field, value) signature and be called multiple times to add an index to the document.

var tree = pathindex(mydb, function(keyvalueemit){
    emit(value.path, 'type', value.type)
    emit(value.path, 'capital', value.capital)
})
 

Insert a value for a key and create the indexes based on your mapper function.

tree.save('/uk/south/west/bristol, {
    name:'Bristol',
    type:'city',
    size:'medium'
}, function(err){
    
})

Insert an array of documents - this must be a list of leveldb batch commands e.g.:

tree.batch([
  {key: '/uk/south/west/bristol', value: {name:'Bristol', size: 'medium'}}, type: 'put'},
  {key: '/uk/south/east/london', value: {name:'London', size: 'large'}}, type: 'put'},
  {key: '/uk/north/east/newcastle', value: {name:'Newcastle', size: 'medium'}}, type: 'put'},
  {key: '/uk/north/west/liverpool', value: {name:'Liverpool', size: 'medium'}}, type: 'put'}
], function (errbatch) {
 
})

Return a read stream for entries that live at or below the given path:

tree.descendentStream('/uk/north', {
    size:'medium'
}).pipe(through(function(doc){
    console.dir(doc.key);
}))
 
// /uk/north/east/newcastle 
// /uk/north/west/liverpool 

You can use multiple search terms and the values can be lists which all must match:

tree.descendentStream('/uk', {
    size:'medium',
    tags:['red','yellow']
}).pipe(through(function(){
    
}))

This is the same as descendent stream but for entries directly below the given path:

tree.childStream('/uk/south/west').pipe(through(function(val){
    console.dir(val.path);
}))
 
// /uk/south/west/bristol 

This is the same as descendent stream but will return only the keys of the matched items

tree.descendentKeyStream('/uk/south').pipe(through(function(val){
    console.dir(val);
}))
 
// /uk/south/west/bristol 
// /uk/south/east/london 

This is the same as child stream but will return only the keys of the matched items

tree.childKeyStream('/uk/south/west').pipe(through(function(val){
    console.dir(val);
}))
 
// /uk/south/west/bristol 

Each of the 4 read stream methods also come with pull-stream equivalents

MIT