Skip to content

Commit 6279e8c

Browse files
authored
Merge pull request #15 from hypermodules/multi-key-index
Stub out multi-key index
2 parents 6691d99 + 1ddd007 commit 6279e8c

File tree

6 files changed

+214
-13
lines changed

6 files changed

+214
-13
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,3 +35,4 @@ jspm_packages
3535

3636
# Optional REPL history
3737
.node_repl_history
38+
.vscode

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,10 @@ All notable changes to this project will be documented in this file.
55
The format is based on [Keep a Changelog](http://keepachangelog.com/)
66
and this project adheres to [Semantic Versioning](http://semver.org/).
77

8+
## 1.1.0 - 2018-03-08
9+
10+
* Add multi-key indexing. Multi-key reducers can return an array of keys to index per `put`, `del`, or `batch`. Thanks [@louiscenter](https://github.com/louiscenter) and [@substack](https://github.com/substack) for the idea.
11+
812
## 1.0.5
913

1014
* Fix index cleaning bug. Indexes should now clean up missing lookups.

README.md

Lines changed: 38 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
# level-auto-index
1+
****# level-auto-index
22

33
Automatic secondary indexing for leveldb and subleveldown.
44

@@ -32,7 +32,8 @@ var db = level()
3232
var posts = sub(db, 'posts', {valueEncoding: 'json'})
3333
var idx = {
3434
title: sub(db, 'title'),
35-
length: sub(db, 'length')
35+
length: sub(db, 'length'),
36+
tag: sub(db, 'tag')
3637
}
3738

3839
// add a title index
@@ -44,10 +45,19 @@ posts.byLength = AutoIndex(posts, idx.length, function (post) {
4445
return post.body.length + '!' + post.id
4546
})
4647

48+
// Create multiple index keys on an index
49+
posts.byTag = AutoIndex(posts, idx.tag, function (post) {
50+
if (!post || !post.tags || !Array.isArray(post.tags)) return
51+
return post.tags.map(function (tag) {
52+
return [tag, post.id].join('!')
53+
})
54+
}, { multi: true })
55+
4756
posts.put('1337', {
4857
id: '1337',
4958
title: 'a title',
50-
body: 'lorem ipsum'
59+
body: 'lorem ipsum',
60+
tags: [ 'foo', 'bar', 'baz' ]
5161
}, function (err) {
5262
if (err) throw err
5363

@@ -87,7 +97,7 @@ posts.put('1337', {
8797

8898
## API
8999

90-
### AutoIndex(db, idb, reduce)
100+
### AutoIndex(db, idb, reduce, opts)
91101

92102
Automatically index a `db` level into the `idb` level using a `reduce` function that creates the index key. The `db` and `idb` levels should be in isolated key namespaces, either by being two different levels or [`mafintosh/subleveldown`](https://github.com/mafintosh/subleveldown) partitions. The `db` hook is mutated by [`hypermodules/level-hookdown`](https://github.com/hypermodules/level-hookdown) to set up the prehooks used for indexing. Only `db` keys are stored as values to save space and reduce data redundancy.
93103

@@ -102,6 +112,16 @@ function reducer (value) {
102112
}
103113
```
104114

115+
Available opts:
116+
117+
```js
118+
{
119+
multi: false // Reducer returns an array of keys to associate with the primary key
120+
}
121+
```
122+
123+
Multi-key index's are for when you you want to write multiple index entries into an index. This is useful for 'tag' fields, where a document may have `n` tags per document, and you would like to index documents by 'tag'. When creating a multi-key index, your reducer must return an array of keys to index by.
124+
105125
### AutoIndex#get(key, opts, cb)
106126

107127
Get the value that has been indexed with `key`.
@@ -129,6 +149,20 @@ function keyReducer (reducerString) {
129149

130150
For a higher level api for creating secondary indexes see [hypermodules/level-idx](https://github.com/hypermodules/level-idx).
131151

152+
### AutoIndex.keyReducer(string)
153+
154+
A shortcut reducer for simplistic multi-key indexing. You might need more than this.
155+
156+
```js
157+
function multiKeyReducer (multiFieldName, primaryKeyFieldName) {
158+
return function multiKeyrdc (document) {
159+
if (!document || !document[multiFieldName] || !Array.isArray(document[multiFieldName])) return
160+
return document[multiFieldName].map(function (tag) {
161+
return [tag, document[primaryKeyFieldName]].join('!')
162+
})
163+
}
164+
```
165+
132166
### AutoIndex#db
133167
134168
The level instance that we are indexing.

index.js

Lines changed: 55 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,7 @@
11
var extend = require('xtend')
22
var hook = require('level-hookdown')
33
var existy = require('existy')
4-
var Transform =
5-
require('stream').Transform || require('readable-stream').Transform
4+
var Transform = require('stream').Transform || require('readable-stream').Transform
65
var isArray = Array.isArray
76

87
module.exports = AutoIndex
@@ -17,24 +16,51 @@ function existyKeys (operation) {
1716
return existy(operation.key)
1817
}
1918

20-
function AutoIndex (db, idb, reduce) {
19+
function empty (v) {
20+
return !!v
21+
}
22+
23+
function AutoIndex (db, idb, reduce, opts) {
24+
if (!opts) opts = {}
2125
var hdb = !db.prehooks || !isArray(db.prehooks) ? hook(db) : db
26+
var multiKey = opts.multi
2227

2328
if (typeof reduce !== 'function') {
24-
throw new Error('Reduce argument must be a string or function')
29+
throw new Error('Reduce argument must be a function')
2530
}
2631

2732
function index (operation, cb) {
2833
var key
34+
var keyBatch
2935
if (operation.type === 'put') {
3036
key = reduce(operation.value)
31-
if (key) return idb.put(key, operation.key, cb)
37+
if (key && multiKey) {
38+
if (!Array.isArray(key)) throw new Error('Reducer must return an array of keys for a multiKey index')
39+
keyBatch = key.filter(empty).map(
40+
function (k) {
41+
return { key: k, value: operation.key }
42+
}
43+
)
44+
return idb.batch(keyBatch, cb)
45+
} else if (key) {
46+
return idb.put(key, operation.key, cb)
47+
}
3248
return process.nextTick(cb)
3349
} else if (operation.type === 'del') {
3450
db.get(operation.key, function (err, value) {
35-
if (err && err.type === 'NotFoundError') {
36-
key = reduce(operation.value)
37-
if (key) return idb.del(key, cb)
51+
if (!err || err.type === 'NotFoundError') {
52+
key = reduce(value)
53+
if (key && multiKey) {
54+
if (!Array.isArray(key)) throw new Error('Reducer must return an array of keys for a multiKey index')
55+
keyBatch = key.filter(empty).map(
56+
function (k) {
57+
return { key: k, type: 'del' }
58+
}
59+
)
60+
return idb.batch(keyBatch, cb)
61+
} else if (key) {
62+
return idb.del(key, key, cb)
63+
}
3864
return cb()
3965
} else if (err) {
4066
cb(err)
@@ -47,6 +73,16 @@ function AutoIndex (db, idb, reduce) {
4773
var idxBatch = operation.array.filter(puts).map(function (opr) {
4874
return extend(opr, {key: reduce(opr.value), value: opr.key})
4975
})
76+
if (multiKey) {
77+
idxBatch = idxBatch.reduce(function (flattened, opr) {
78+
if (!opr.key) return flattened
79+
if (!Array.isArray(opr.key)) throw new Error('Reducer must return an array of keys for a multiKey index')
80+
opr.key.filter(empty).forEach(function (k) {
81+
flattened.push(extend(opr, { key: k }))
82+
})
83+
return flattened
84+
}, [])
85+
}
5086
idb.batch(idxBatch.filter(existyKeys), cb)
5187
}
5288
}
@@ -149,3 +185,14 @@ function keyReducer (reducerString) {
149185
}
150186
return keyRdc
151187
}
188+
189+
module.exports.multiKeyReducer = multiKeyReducer
190+
191+
function multiKeyReducer (multiFieldName, primaryKeyFieldName) {
192+
return function multiKeyrdc (document) {
193+
if (!document || !document[multiFieldName] || !Array.isArray(document[multiFieldName])) return
194+
return document[multiFieldName].map(function (tag) {
195+
return [tag, document[primaryKeyFieldName]].join('!')
196+
})
197+
}
198+
}

package.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,8 @@
77
"test": "run-s test:*",
88
"test:deps": "dependency-check ./package.json",
99
"test:lint": "standard | snazzy",
10-
"test:tape": "tape test/* | tap-format-spec"
10+
"test:tape": "tape test/* | tap-format-spec",
11+
"debug": "node --nolazy --inspect-brk=9229 node_modules/.bin/tape test/*"
1112
},
1213
"repository": {
1314
"type": "git",
@@ -36,6 +37,7 @@
3637
"devDependencies": {
3738
"@tap-format/spec": "^0.2.0",
3839
"bytewise": "^1.1.0",
40+
"concat-stream": "^1.6.1",
3941
"dependency-check": "^3.0.0",
4042
"memdb": "^1.3.1",
4143
"memdown": "^2.0.0",

test/multi-key.js

Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
var level = require('memdb')
2+
var AutoIndex = require('..')
3+
var sub = require('subleveldown')
4+
var test = require('tape')
5+
var concat = require('concat-stream')
6+
var multiKeyReducer = AutoIndex.multiKeyReducer
7+
8+
test('multi-key index', function (t) {
9+
var db = level()
10+
var index = {
11+
tags: sub(db, 'tags')
12+
}
13+
14+
var posts = sub(db, 'posts', {valueEncoding: 'json'})
15+
posts.byTag = AutoIndex(posts, index.tags, multiKeyReducer('tags', 'id'), { multi: true })
16+
17+
var postData = [
18+
{
19+
title: 'a title',
20+
id: 0,
21+
body: 'lorem ipsum',
22+
tags: [ 'foo', 'bar', 'baz' ]
23+
},
24+
{
25+
title: 'second title',
26+
id: 1,
27+
body: 'second body',
28+
tags: []
29+
},
30+
{
31+
title: 'third title',
32+
id: 2,
33+
body: 'third body'
34+
},
35+
{
36+
title: 'fourth title',
37+
body: 'fourth body',
38+
id: 3,
39+
tags: ['bing', 'google', 'foo']
40+
}
41+
]
42+
43+
var batch = postData.map(function (post) {
44+
return {
45+
type: 'put',
46+
key: post.id,
47+
value: post
48+
}
49+
})
50+
51+
const singlePost = {
52+
title: 'some title',
53+
body: 'some body',
54+
id: 20,
55+
tags: ['bing', 'google', 'foo', 'bleep', 'bloop']
56+
}
57+
58+
posts.put(singlePost.id, singlePost, function (err) {
59+
t.error(err)
60+
posts.batch(batch, function (err) {
61+
t.error(err)
62+
63+
var tagIndexStream = posts.byTag.createReadStream({ gt: 'foo!', lt: 'foo!~' })
64+
var concatStream = concat(handleResults)
65+
tagIndexStream.on('error', handleError)
66+
tagIndexStream.pipe(concatStream)
67+
68+
function handleResults (data) {
69+
t.equal(data.length, 3)
70+
t.equal(data[0].key, '0')
71+
t.deepEqual(data[0].value, postData[0])
72+
t.equal(data[1].key, '20')
73+
t.deepEqual(data[1].value, singlePost)
74+
t.equal(data[2].key, '3')
75+
t.deepEqual(data[2].value, postData[3])
76+
posts.del(3, function (err) {
77+
t.error(err)
78+
secondTest()
79+
})
80+
}
81+
82+
function secondTest () {
83+
var tagIndexStream = index.tags.createReadStream()
84+
var concatStream = concat(handleResults)
85+
tagIndexStream.on('error', handleError)
86+
tagIndexStream.pipe(concatStream)
87+
function handleResults (data) {
88+
t.equal(data.length, 8)
89+
thirdTest()
90+
}
91+
}
92+
93+
function thirdTest () {
94+
var tagIndexStream = posts.byTag.createReadStream({ gt: 'foo!', lt: 'foo!~' })
95+
var concatStream = concat(handleResults)
96+
tagIndexStream.on('error', handleError)
97+
tagIndexStream.pipe(concatStream)
98+
function handleResults (data) {
99+
t.equal(data.length, 2)
100+
t.deepEqual(data[0].value, postData[0])
101+
t.equal(data[1].key, '20')
102+
t.deepEqual(data[1].value, singlePost)
103+
t.end()
104+
}
105+
}
106+
107+
function handleError (err) {
108+
t.error(err)
109+
t.end()
110+
}
111+
})
112+
})
113+
})

0 commit comments

Comments
 (0)