@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 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: this.env.SEARCH_URL || 'http://localhost:9200' })
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(!this.env.ANALYTICS_INDEX_PREFIX) throw new Error("ElasticSearch analytics not configured")
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(this.env.ANALYTICS_INDEX_PREFIX)
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("../utils.js")
2
- const SearchIndexer = require("../runtime/SearchIndexer.js")
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)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@live-change/elasticsearch-plugin",
3
- "version": "0.1.0",
3
+ "version": "0.1.1",
4
4
  "description": "",
5
5
  "main": "index.js",
6
6
  "scripts": {