@live-change/elasticsearch-plugin 0.1.0 → 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/index.js +5 -3
- package/lib/SearchIndexer.js +316 -0
- package/lib/process.js +48 -0
- package/lib/updater.js +2 -2
- package/package.json +1 -1
package/index.js
CHANGED
|
@@ -2,24 +2,26 @@ const { Client: ElasticSearch } = require('@elastic/elasticsearch')
|
|
|
2
2
|
const AnalyticsWriter = require('./lib/AnalyticsWriter.js')
|
|
3
3
|
const updater = require('./lib/updater.js')
|
|
4
4
|
const searchIndex = require('./lib/searchIndex.js')
|
|
5
|
+
const indexProcess = require('./lib/process.js')
|
|
5
6
|
|
|
6
7
|
module.exports = function(app, services) {
|
|
7
8
|
app.connectToSearch = () => {
|
|
8
9
|
if(!app.env.SEARCH_INDEX_PREFIX) throw new Error("ElasticSearch not configured")
|
|
9
10
|
if(app.search) return app.search
|
|
10
11
|
app.searchIndexPrefix = app.env.SEARCH_INDEX_PREFIX
|
|
11
|
-
app.search = new ElasticSearch({ node:
|
|
12
|
+
app.search = new ElasticSearch({ node: app.env.SEARCH_URL || 'http://localhost:9200' })
|
|
12
13
|
//this.search.info(console.log)
|
|
13
14
|
return app.search
|
|
14
15
|
}
|
|
15
16
|
|
|
16
17
|
app.connectToAnalytics = () => {
|
|
17
|
-
if(!
|
|
18
|
+
if(!app.env.ANALYTICS_INDEX_PREFIX) throw new Error("ElasticSearch analytics not configured")
|
|
18
19
|
if(app.analytics) return app.analytics
|
|
19
|
-
app.analytics = new AnalyticsWriter(
|
|
20
|
+
app.analytics = new AnalyticsWriter(app.env.ANALYTICS_INDEX_PREFIX)
|
|
20
21
|
return app.analytics
|
|
21
22
|
}
|
|
22
23
|
|
|
23
24
|
if(app.env.SEARCH_INDEX_PREFIX) app.defaultProcessors.push(searchIndex)
|
|
24
25
|
if(app.env.SEARCH_INDEX_PREFIX) app.defaultUpdaters.push(updater)
|
|
26
|
+
if(app.env.SEARCH_INDEX_PREFIX) app.defaultUpdaters.push(indexProcess)
|
|
25
27
|
}
|
|
@@ -0,0 +1,316 @@
|
|
|
1
|
+
|
|
2
|
+
|
|
3
|
+
const SEARCH_INDEX_NOTSTARTED = 0
|
|
4
|
+
const SEARCH_INDEX_CREATING = 1
|
|
5
|
+
const SEARCH_INDEX_UPDATING = 2
|
|
6
|
+
const SEARCH_INDEX_READY = 3
|
|
7
|
+
|
|
8
|
+
const bucketSize = 256
|
|
9
|
+
const saveStateThrottle = 2000
|
|
10
|
+
const saveStateDelay = saveStateThrottle + 200
|
|
11
|
+
|
|
12
|
+
function prepareArray(data, of) {
|
|
13
|
+
if(!data) return
|
|
14
|
+
if(of.properties) for(let i = 0; i < data.length; i++) prepareObject(data[i], of)
|
|
15
|
+
if(of.of) for(let i = 0; i < data.length; i++) prepareArray(data[i], of.of)
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function prepareObject(data, props) {
|
|
19
|
+
if(!data) return
|
|
20
|
+
for(const propName in props) {
|
|
21
|
+
if(!data.hasOwnProperty(propName)) continue
|
|
22
|
+
const prop = props[propName]
|
|
23
|
+
if(prop.properties) {
|
|
24
|
+
prepareObject(data[propName], prop.properties)
|
|
25
|
+
}
|
|
26
|
+
if(prop.of) {
|
|
27
|
+
prepareArray(data[propName], prop.of)
|
|
28
|
+
}
|
|
29
|
+
if(prop.search) {
|
|
30
|
+
if(Array.isArray(prop.search)) {
|
|
31
|
+
for(const search of prop.search) {
|
|
32
|
+
if(search.name) data[search.name] = data[propName]
|
|
33
|
+
}
|
|
34
|
+
} else {
|
|
35
|
+
if(prop.search.name) data[prop.search.name] = data[propName]
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
class SearchIndexer {
|
|
42
|
+
constructor(dao, databaseName, sourceType, sourceName, elasticsearch, indexName, model ) {
|
|
43
|
+
this.dao = dao
|
|
44
|
+
this.databaseName = databaseName
|
|
45
|
+
this.sourceType = sourceType
|
|
46
|
+
this.sourceName = sourceName
|
|
47
|
+
this.elasticsearch = elasticsearch
|
|
48
|
+
this.indexName = indexName
|
|
49
|
+
this.model = model
|
|
50
|
+
this.state = SEARCH_INDEX_NOTSTARTED
|
|
51
|
+
this.lastUpdateId = ''
|
|
52
|
+
this.lastSavedId = ''
|
|
53
|
+
|
|
54
|
+
this.observable = null
|
|
55
|
+
|
|
56
|
+
this.queue = []
|
|
57
|
+
this.queueWriteResolve = []
|
|
58
|
+
this.queueLastUpdateId = ''
|
|
59
|
+
this.queueWritePromise = null
|
|
60
|
+
this.queueWriteResolve = null
|
|
61
|
+
this.currentWritePromise = null
|
|
62
|
+
|
|
63
|
+
this.readingMore = true
|
|
64
|
+
|
|
65
|
+
this.lastStateSave = 0
|
|
66
|
+
this.saveStateTimer = null
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
prepareObject(object) {
|
|
70
|
+
prepareObject(object, this.model.properties)
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
async start() {
|
|
74
|
+
const searchIndexState = await this.dao.get(
|
|
75
|
+
['database','tableObject', this.databaseName, 'searchIndexes', this.indexName])
|
|
76
|
+
const firstSourceOperation = (await this.dao.get(
|
|
77
|
+
['database', this.sourceType.toLowerCase()+'OpLogRange', this.databaseName, this.sourceName, {
|
|
78
|
+
limit: 1
|
|
79
|
+
}]))[0]
|
|
80
|
+
|
|
81
|
+
console.log("Index State", searchIndexState)
|
|
82
|
+
console.log("first Source Operation", firstSourceOperation && firstSourceOperation.id)
|
|
83
|
+
|
|
84
|
+
let lastUpdateTimestamp = 0
|
|
85
|
+
if(!searchIndexState || (firstSourceOperation && firstSourceOperation.id > searchIndexState.lastOpLogId)) {
|
|
86
|
+
const indexCreateTimestamp = Date.now()
|
|
87
|
+
this.state = SEARCH_INDEX_CREATING
|
|
88
|
+
console.log("CREATING SEARCH INDEX")
|
|
89
|
+
await this.copyAll()
|
|
90
|
+
lastUpdateTimestamp = indexCreateTimestamp - 1000 // one second overlay
|
|
91
|
+
this.lastUpdateId = (''+(lastUpdateTimestamp - 1000)).padStart(16,'0')
|
|
92
|
+
} else {
|
|
93
|
+
this.state = SEARCH_INDEX_UPDATING
|
|
94
|
+
console.log("UPDATING SEARCH INDEX")
|
|
95
|
+
lastUpdateTimestamp = (+searchIndexState.lastOpLogId.split(':')[0]) - 1000 // one second overlap
|
|
96
|
+
this.lastUpdateId = searchIndexState.lastOpLogId
|
|
97
|
+
await this.updateAll()
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
await this.dao.request(['database', 'put', this.databaseName, 'searchIndexes', {
|
|
101
|
+
id: this.indexName,
|
|
102
|
+
lastOpLogId: this.lastUpdateId
|
|
103
|
+
}])
|
|
104
|
+
this.lastStateSave = Date.now()
|
|
105
|
+
|
|
106
|
+
console.log("SEARCH INDEX READY")
|
|
107
|
+
this.state = SEARCH_INDEX_READY
|
|
108
|
+
|
|
109
|
+
this.observeMore()
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
async saveState() {
|
|
113
|
+
if(this.lastSavedId == this.lastUpdateId) return
|
|
114
|
+
if(Date.now() - this.lastStateSave < saveStateThrottle) {
|
|
115
|
+
if(this.saveStateTimer === null) {
|
|
116
|
+
setTimeout(() => this.saveState(), saveStateDelay)
|
|
117
|
+
}
|
|
118
|
+
return
|
|
119
|
+
}
|
|
120
|
+
console.log("SAVE INDEXER STATE", this.lastUpdateId)
|
|
121
|
+
this.lastSavedId = this.lastUpdateId
|
|
122
|
+
this.lastStateSave = Date.now()
|
|
123
|
+
await this.dao.request(['database', 'put', this.databaseName, 'searchIndexes', {
|
|
124
|
+
id: this.indexName,
|
|
125
|
+
lastOpLogId: this.lastUpdateId
|
|
126
|
+
}])
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
doWrite() {
|
|
130
|
+
if(this.queueWritePromise) {
|
|
131
|
+
return this.queueWritePromise
|
|
132
|
+
}
|
|
133
|
+
const operations = this.queue
|
|
134
|
+
if(operations.length == 0) {
|
|
135
|
+
this.lastUpdateId = this.queueLastUpdateId
|
|
136
|
+
return
|
|
137
|
+
}
|
|
138
|
+
this.queueWritePromise = new Promise((resolve, reject) => {
|
|
139
|
+
this.queueWriteResolve = { resolve, reject }
|
|
140
|
+
})
|
|
141
|
+
const queueResolve = this.queueWriteResolve
|
|
142
|
+
this.queueWriteResolve = null
|
|
143
|
+
this.queueWritePromise = null
|
|
144
|
+
this.queue = []
|
|
145
|
+
//console.log("WRITE BULK", operations)
|
|
146
|
+
this.elasticsearch.bulk({
|
|
147
|
+
index: this.indexName,
|
|
148
|
+
body: operations
|
|
149
|
+
}).catch(error => {
|
|
150
|
+
error = (error && error.meta && error.meta.body && error.meta.body.error) || error
|
|
151
|
+
console.error("ES ERROR:", error)
|
|
152
|
+
queueResolve.reject(error)
|
|
153
|
+
throw error
|
|
154
|
+
}).then(result => {
|
|
155
|
+
//console.log("BULK RESULT", result)
|
|
156
|
+
if(result.body.errors) {
|
|
157
|
+
for(const item of result.body.items) {
|
|
158
|
+
if(item.index.error) {
|
|
159
|
+
const opId = operations.findIndex(op =>
|
|
160
|
+
(op.index && op.index._id == item.index._id)
|
|
161
|
+
|| (op.delete && op.delete._id == item.delete._id)
|
|
162
|
+
)
|
|
163
|
+
const op = operations[opId]
|
|
164
|
+
const data = op.index ? operations[opId + 1] : null
|
|
165
|
+
console.error("INDEX ERROR", item.index.error, "OP", op, "D", data)
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
//throw result.body.items.find(item => item.error)
|
|
169
|
+
throw new Error("INDEXING ERROR "+ result.body.errors)
|
|
170
|
+
}
|
|
171
|
+
}).then(written => {
|
|
172
|
+
this.lastUpdateId = this.queueLastUpdateId
|
|
173
|
+
queueResolve.resolve()
|
|
174
|
+
this.currentWritePromise = null
|
|
175
|
+
this.saveState()
|
|
176
|
+
if(this.queue.length) {
|
|
177
|
+
this.doWrite()
|
|
178
|
+
}
|
|
179
|
+
})
|
|
180
|
+
return this.queueWritePromise
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
async applyOps(ops) {
|
|
184
|
+
//console.trace(`apply ${ops.length} ops`)
|
|
185
|
+
if(ops.length == 0) return;
|
|
186
|
+
let size = 0
|
|
187
|
+
for(const op of ops) {
|
|
188
|
+
if(op.operation.type == 'put') size += 2
|
|
189
|
+
if(op.operation.type == 'delete') size += 1
|
|
190
|
+
}
|
|
191
|
+
const operations = new Array(size)
|
|
192
|
+
let pos = 0
|
|
193
|
+
for(const op of ops) {
|
|
194
|
+
if(op.operation.type == 'put') {
|
|
195
|
+
operations[pos++] = { index: { _id: op.operation.object.id } }
|
|
196
|
+
this.prepareObject(op.operation.object)
|
|
197
|
+
operations[pos++] = op.operation.object
|
|
198
|
+
}
|
|
199
|
+
if(op.operation.type == 'delete') {
|
|
200
|
+
operations[pos++] = { delete: { _id: op.operation.object.id } }
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
const lastUpdateId = ops[ops.length-1].id
|
|
204
|
+
//console.log("ES OPS", operations)
|
|
205
|
+
this.queue = this.queue.length ? this.queue.concat(operations) : operations
|
|
206
|
+
this.queueLastUpdateId = lastUpdateId
|
|
207
|
+
//console.log("WRITE OPS!")
|
|
208
|
+
await this.doWrite()
|
|
209
|
+
//console.log("OPS WRITTEN!")
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
observeMore() {
|
|
213
|
+
this.readingMore = true
|
|
214
|
+
if(this.observable) this.observable.unobserve(this)
|
|
215
|
+
this.observable = this.dao.observable(
|
|
216
|
+
['database', this.sourceType.toLowerCase()+'OpLogRange', this.databaseName, this.sourceName, {
|
|
217
|
+
gt: this.lastUpdateId,
|
|
218
|
+
limit: bucketSize
|
|
219
|
+
}])
|
|
220
|
+
this.observable.observe(this)
|
|
221
|
+
}
|
|
222
|
+
tryObserveMore() {
|
|
223
|
+
if(!this.readingMore && this.observable.list.length == bucketSize) {
|
|
224
|
+
this.observeMore()
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
async set(ops) {
|
|
228
|
+
this.readingMore = false
|
|
229
|
+
console.log("SET", this.lastUpdateId, ops.length)
|
|
230
|
+
await this.applyOps(ops)
|
|
231
|
+
this.tryObserveMore()
|
|
232
|
+
}
|
|
233
|
+
async putByField(_fd, id, op, _reverse, oldObject) {
|
|
234
|
+
this.readingMore = false
|
|
235
|
+
await this.applyOps([ op ])
|
|
236
|
+
this.tryObserveMore()
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
async updateAll() {
|
|
240
|
+
let ops
|
|
241
|
+
do {
|
|
242
|
+
console.log("UPDATE FROM", this.lastUpdateId)
|
|
243
|
+
ops = await this.dao.get(
|
|
244
|
+
['database', this.sourceType.toLowerCase()+'OpLogRange', this.databaseName, this.sourceName, {
|
|
245
|
+
gt: this.lastUpdateId,
|
|
246
|
+
limit: bucketSize
|
|
247
|
+
}])
|
|
248
|
+
console.log("OPS", ops.length)
|
|
249
|
+
await this.applyOps(ops)
|
|
250
|
+
console.log("OPS APPLIED", ops.length)
|
|
251
|
+
} while(ops.length >= bucketSize)
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
async copyAll() {
|
|
255
|
+
const search = this.elasticsearch
|
|
256
|
+
let position = ""
|
|
257
|
+
let more = true
|
|
258
|
+
|
|
259
|
+
console.log("DELETE OLD DATA")
|
|
260
|
+
await search.delete_by_query({
|
|
261
|
+
index: this.indexName,
|
|
262
|
+
body: {
|
|
263
|
+
query: {
|
|
264
|
+
match_all: {}
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
})
|
|
268
|
+
|
|
269
|
+
console.log(`INDEXING ${this.sourceType} ${this.sourceName}`)
|
|
270
|
+
do {
|
|
271
|
+
const rows = await this.dao.get(
|
|
272
|
+
['database', this.sourceType.toLowerCase()+'Range', this.databaseName, this.sourceName, {
|
|
273
|
+
gt: position,
|
|
274
|
+
limit: bucketSize
|
|
275
|
+
}])
|
|
276
|
+
position = rows.length ? rows[rows.length-1].id : "\xFF"
|
|
277
|
+
more = (rows.length == bucketSize)
|
|
278
|
+
|
|
279
|
+
if(more) console.log(`READ ${rows.length} ROWS`)
|
|
280
|
+
else console.log(`READ LAST ${rows.length} ROWS`)
|
|
281
|
+
|
|
282
|
+
if(rows.length > 0) {
|
|
283
|
+
console.log(`WRITING ${rows.length} ROWS`)
|
|
284
|
+
let operations = new Array(rows.length * 2)
|
|
285
|
+
for(let i = 0; i < rows.length; i++) {
|
|
286
|
+
operations[i * 2] = { index: { _id: rows[i].id } }
|
|
287
|
+
this.prepareObject(rows[i])
|
|
288
|
+
operations[i * 2 + 1] = rows[i]
|
|
289
|
+
}
|
|
290
|
+
await search.bulk({
|
|
291
|
+
index: this.indexName,
|
|
292
|
+
body: operations
|
|
293
|
+
}).then(result => {
|
|
294
|
+
if(result.body.errors) {
|
|
295
|
+
for(const item of result.body.items) {
|
|
296
|
+
if(item.index && item.index.error) {
|
|
297
|
+
console.error("ES ERROR:", item.index.error)
|
|
298
|
+
console.error("WHEN INDEXING", rows.find(row => row.id == item.index._id))
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
throw new Error("ES ERRORS")
|
|
302
|
+
}
|
|
303
|
+
}).catch(error => {
|
|
304
|
+
error = (error && error.meta && error.meta.body && error.meta.body.error) || error
|
|
305
|
+
console.error("ES ERROR:", error)
|
|
306
|
+
throw error
|
|
307
|
+
})
|
|
308
|
+
}
|
|
309
|
+
console.log("ES INDEXED!")
|
|
310
|
+
|
|
311
|
+
} while (more)
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
module.exports = SearchIndexer
|
package/lib/process.js
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
const SearchIndexer = require("./SearchIndexer.js")
|
|
2
|
+
|
|
3
|
+
async function startSearchIndexer(service, config) {
|
|
4
|
+
if(!config.indexSearch) return
|
|
5
|
+
|
|
6
|
+
let anyIndex = false
|
|
7
|
+
for(const name in service.models) if(service.models[name].definition.searchIndex) anyIndex = true
|
|
8
|
+
for(const name in service.indexes) if(service.indexes[name].definition.searchIndex) anyIndex = true
|
|
9
|
+
if(!anyIndex) {
|
|
10
|
+
console.log("not starting search indexer - nothing to index!")
|
|
11
|
+
return
|
|
12
|
+
}
|
|
13
|
+
console.log("starting search indexer!")
|
|
14
|
+
await service.dao.request(['database', 'createTable'], service.databaseName, 'searchIndexes').catch(e => 'ok')
|
|
15
|
+
|
|
16
|
+
service.searchIndexers = []
|
|
17
|
+
|
|
18
|
+
const elasticsearch = service.app.connectToSearch()
|
|
19
|
+
|
|
20
|
+
for(const modelName in service.models) {
|
|
21
|
+
const model = service.models[modelName]
|
|
22
|
+
const indexName = model.definition.searchIndex
|
|
23
|
+
if(!indexName) continue
|
|
24
|
+
const indexer = new SearchIndexer(
|
|
25
|
+
service.dao, service.databaseName, 'Table', model.tableName, elasticsearch, indexName, model.definition
|
|
26
|
+
)
|
|
27
|
+
service.searchIndexers.push(indexer)
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
for(const indexName in service.indexes) {
|
|
31
|
+
const index = service.indexes[indexName]
|
|
32
|
+
const indexName = index.definition.searchIndex
|
|
33
|
+
if(!indexName) continue
|
|
34
|
+
const indexer = new SearchIndexer(
|
|
35
|
+
service.dao, service.databaseName, 'Index', model.tableName, elasticsearch, indexName, index.definition
|
|
36
|
+
)
|
|
37
|
+
service.searchIndexers.push(indexer)
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const promises = []
|
|
41
|
+
for(const searchIndexer of service.searchIndexers) {
|
|
42
|
+
promises.push(service.profileLog.profile({
|
|
43
|
+
operation: "startIndexer", serviceName: service.name, indexName: searchIndexer.indexName
|
|
44
|
+
}, () => searchIndexer.start()))
|
|
45
|
+
}
|
|
46
|
+
await Promise.all(promises)
|
|
47
|
+
console.log("search indexer started!")
|
|
48
|
+
}
|
package/lib/updater.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
const { typeName } = require("
|
|
2
|
-
const SearchIndexer = require("
|
|
1
|
+
const { typeName } = require("@live-change/framework/lib/utils.js")
|
|
2
|
+
const SearchIndexer = require("./SearchIndexer.js")
|
|
3
3
|
|
|
4
4
|
function generatePropertyMapping(property, search) {
|
|
5
5
|
//console.log("GENERATE PROPERTY MAPPING", property)
|