pelias-openstreetmap 9.0.0 → 9.2.0
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/README.md +31 -0
- package/index.js +9 -1
- package/package.json +1 -1
- package/parallel/dispatcher.js +284 -0
- package/parallel/ndjson.js +58 -0
- package/parallel/worker.js +23 -0
- package/schema.js +2 -0
- package/stream/address_extractor.js +6 -1
- package/stream/importPipeline.js +6 -3
- package/stream/multiple_pbfs.js +2 -2
- package/stream/source.js +93 -0
package/README.md
CHANGED
|
@@ -94,6 +94,27 @@ The OSM importer will look for a file with a name matching this value in the con
|
|
|
94
94
|
|
|
95
95
|
If downloading from a remote URL, the filename must match the value in `sourceURL`.
|
|
96
96
|
|
|
97
|
+
A file ending in `.ndjson`, `.ldjson` or `.jsonl`, optionally `.gz` compressed,
|
|
98
|
+
is read as newline delimited JSON instead of being parsed as a pbf. It is
|
|
99
|
+
assumed to be the stored output of a previous `pbf2json` run over the same
|
|
100
|
+
extract, and importing it produces exactly the same documents.
|
|
101
|
+
|
|
102
|
+
```javascript
|
|
103
|
+
{
|
|
104
|
+
"imports": {
|
|
105
|
+
"openstreetmap": {
|
|
106
|
+
"datapath": "/data/openstreetmap",
|
|
107
|
+
"import": [{
|
|
108
|
+
"filename": "pbf2json.pelias.jsonl.gz"
|
|
109
|
+
}]
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
The two forms can be mixed in one `import` array, and both work with
|
|
116
|
+
`parallelism`. Compressed files are decompressed as they are read.
|
|
117
|
+
|
|
97
118
|
#### `imports.openstreetmap.layers`
|
|
98
119
|
|
|
99
120
|
This is an object you can use to define your own layers based on OSM tags. For example, if you wanted a layer `coffee` for just coffee shops you could set:
|
|
@@ -136,6 +157,16 @@ discarded. In practice, this affects records with tags such as
|
|
|
136
157
|
|
|
137
158
|
By default, or if set to any other value besides `true`, these records will be imported.
|
|
138
159
|
|
|
160
|
+
#### `imports.openstreetmap.parallelism`
|
|
161
|
+
|
|
162
|
+
The number of import pipelines to run in parallel. Defaults to `1`, which runs
|
|
163
|
+
the importer as a single process.
|
|
164
|
+
|
|
165
|
+
Reading a PBF file cannot be parallelized, but everything after the parser can
|
|
166
|
+
be. When set above `1`, a single `pbf2json` reader is started and its output is
|
|
167
|
+
distributed across that many worker processes, each running the full import
|
|
168
|
+
pipeline.
|
|
169
|
+
|
|
139
170
|
### Administrative Hierarchy Lookup
|
|
140
171
|
|
|
141
172
|
OSM records often do not contain information about which city, state (or
|
package/index.js
CHANGED
|
@@ -9,4 +9,12 @@ if (_.has(peliasConfig, 'imports.openstreetmap.adminLookup')) {
|
|
|
9
9
|
|
|
10
10
|
const importPipeline = require('./stream/importPipeline');
|
|
11
11
|
|
|
12
|
-
|
|
12
|
+
// a parallelism greater than one runs a single pbf2json reader which fans its
|
|
13
|
+
// output out to that many worker processes, each running the full pipeline
|
|
14
|
+
const parallelism = _.get(peliasConfig, 'imports.openstreetmap.parallelism', 1);
|
|
15
|
+
|
|
16
|
+
if (parallelism > 1) {
|
|
17
|
+
require('./parallel/dispatcher').run(parallelism);
|
|
18
|
+
} else {
|
|
19
|
+
importPipeline.import();
|
|
20
|
+
}
|
package/package.json
CHANGED
|
@@ -0,0 +1,284 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
Parallel import dispatcher.
|
|
5
|
+
|
|
6
|
+
Reading a pbf file cannot be parallelized, but everything downstream of the
|
|
7
|
+
parser is stateless per record, so a single pbf2json reader can feed any
|
|
8
|
+
number of import pipelines. This module runs that reader and forwards its
|
|
9
|
+
output, split on line boundaries, to N worker processes.
|
|
10
|
+
|
|
11
|
+
Blocks are handed to whichever worker is currently accepting writes, so a
|
|
12
|
+
worker blocked on Elasticsearch does not stall the reader. When every worker
|
|
13
|
+
is busy the reader is paused, which propagates backpressure to pbf2json.
|
|
14
|
+
|
|
15
|
+
Only used when imports.openstreetmap.parallelism is greater than 1.
|
|
16
|
+
**/
|
|
17
|
+
|
|
18
|
+
const child = require('child_process');
|
|
19
|
+
const path = require('path');
|
|
20
|
+
const os = require('os');
|
|
21
|
+
const fs = require('fs');
|
|
22
|
+
const logger = require('pelias-logger').get('openstreetmap');
|
|
23
|
+
const settings = require('pelias-config').generate(require('../schema'));
|
|
24
|
+
const pbf = require('../stream/pbf');
|
|
25
|
+
const source = require('../stream/source');
|
|
26
|
+
const generateParams = require('pbf2json/lib/generateParams');
|
|
27
|
+
|
|
28
|
+
const NEWLINE = 0x0a;
|
|
29
|
+
const WORKER_PATH = path.join(__dirname, 'worker.js');
|
|
30
|
+
const PBF2JSON_BIN = path.join(
|
|
31
|
+
path.dirname(require.resolve('pbf2json')),
|
|
32
|
+
'build',
|
|
33
|
+
`pbf2json.${os.platform()}-${os.arch()}`
|
|
34
|
+
);
|
|
35
|
+
|
|
36
|
+
function run(parallelism) {
|
|
37
|
+
const configs = sourceConfigs();
|
|
38
|
+
const state = { shuttingDown: false, exitCode: 0, workers: [], reader: null };
|
|
39
|
+
|
|
40
|
+
state.workers = startWorkers(parallelism, state);
|
|
41
|
+
const dispatcher = createDispatcher(state.workers);
|
|
42
|
+
|
|
43
|
+
['SIGINT', 'SIGTERM'].forEach((signal) => {
|
|
44
|
+
process.on(signal, () => {
|
|
45
|
+
state.shuttingDown = true;
|
|
46
|
+
killChildren(state);
|
|
47
|
+
process.exit(1);
|
|
48
|
+
});
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
readSequentially(configs, dispatcher, state, () => {
|
|
52
|
+
logger.info('pbf parsing complete, waiting for workers to flush');
|
|
53
|
+
state.shuttingDown = true;
|
|
54
|
+
dispatcher.end();
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// one reader config per configured import file. stored pbf2json output needs
|
|
59
|
+
// neither the pbf2json options nor a leveldb cache
|
|
60
|
+
function sourceConfigs() {
|
|
61
|
+
const osm = settings.imports.openstreetmap;
|
|
62
|
+
|
|
63
|
+
return osm.import.map((entry) => {
|
|
64
|
+
const file = path.join(osm.datapath, entry.filename);
|
|
65
|
+
|
|
66
|
+
const conf = source.isJsonSource(file) ?
|
|
67
|
+
{ file: file, importVenues: entry.importVenues } :
|
|
68
|
+
pbf.config({
|
|
69
|
+
file: file,
|
|
70
|
+
leveldb: osm.leveldbpath,
|
|
71
|
+
importVenues: entry.importVenues
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
[conf.file, conf.leveldb].filter(Boolean).forEach((target) => {
|
|
75
|
+
try {
|
|
76
|
+
fs.statSync(target);
|
|
77
|
+
} catch (e) {
|
|
78
|
+
throw new Error('failed to stat path: ' + target);
|
|
79
|
+
}
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
return conf;
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function startWorkers(parallelism, state, workerPath) {
|
|
87
|
+
const workers = [];
|
|
88
|
+
|
|
89
|
+
for (let id = 0; id < parallelism; id++) {
|
|
90
|
+
workers.push(spawnWorker(id, workers, state, workerPath || WORKER_PATH));
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
logger.info(`started ${parallelism} import workers`);
|
|
94
|
+
return workers;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function spawnWorker(id, workers, state, workerPath) {
|
|
98
|
+
const proc = child.spawn(process.execPath, process.execArgv.concat(workerPath), {
|
|
99
|
+
stdio: ['pipe', 'inherit', 'inherit'],
|
|
100
|
+
env: Object.assign({}, process.env, { PELIAS_OSM_WORKER_ID: String(id) })
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
const worker = { id: id, proc: proc, ready: true, alive: true };
|
|
104
|
+
|
|
105
|
+
// a closed stdin surfaces as the exit below, no need to also throw here
|
|
106
|
+
proc.stdin.on('error', () => {});
|
|
107
|
+
|
|
108
|
+
proc.on('exit', (code, signal) => {
|
|
109
|
+
worker.alive = false;
|
|
110
|
+
worker.ready = false;
|
|
111
|
+
|
|
112
|
+
if (!state.shuttingDown) {
|
|
113
|
+
fatal(state, `worker ${id} exited early (code ${code}, signal ${signal})`);
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
if (code !== 0) {
|
|
118
|
+
logger.error(`worker ${id} exited with code ${code}, signal ${signal}`);
|
|
119
|
+
state.exitCode = 1;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
if (workers.every((w) => !w.alive)) {
|
|
123
|
+
logger.info('import complete');
|
|
124
|
+
process.exit(state.exitCode);
|
|
125
|
+
}
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
return worker;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function createDispatcher(workers) {
|
|
132
|
+
let cursor = 0;
|
|
133
|
+
let leftover = null;
|
|
134
|
+
let source = null;
|
|
135
|
+
let paused = false;
|
|
136
|
+
|
|
137
|
+
// round robin over the workers currently accepting writes
|
|
138
|
+
function claim() {
|
|
139
|
+
let fallback = null;
|
|
140
|
+
|
|
141
|
+
for (let i = 0; i < workers.length; i++) {
|
|
142
|
+
const worker = workers[(cursor + i) % workers.length];
|
|
143
|
+
if (!worker.alive) { continue; }
|
|
144
|
+
if (worker.ready) {
|
|
145
|
+
cursor = (cursor + i + 1) % workers.length;
|
|
146
|
+
return worker;
|
|
147
|
+
}
|
|
148
|
+
fallback = fallback || worker;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// every worker is busy, buffer into one of them rather than dropping the
|
|
152
|
+
// block: the reader is paused immediately after this write
|
|
153
|
+
return fallback;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function resume() {
|
|
157
|
+
if (paused && source) {
|
|
158
|
+
paused = false;
|
|
159
|
+
source.resume();
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function send(block) {
|
|
164
|
+
const worker = claim();
|
|
165
|
+
if (!worker) { return; }
|
|
166
|
+
|
|
167
|
+
const wasReady = worker.ready;
|
|
168
|
+
|
|
169
|
+
if (worker.proc.stdin.write(block)) {
|
|
170
|
+
worker.ready = true;
|
|
171
|
+
} else if (wasReady) {
|
|
172
|
+
worker.ready = false;
|
|
173
|
+
worker.proc.stdin.once('drain', () => {
|
|
174
|
+
worker.ready = true;
|
|
175
|
+
resume();
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function onData(chunk) {
|
|
181
|
+
const buf = leftover ? Buffer.concat([leftover, chunk]) : chunk;
|
|
182
|
+
const idx = buf.lastIndexOf(NEWLINE);
|
|
183
|
+
|
|
184
|
+
// no complete line yet, keep accumulating
|
|
185
|
+
if (idx === -1) {
|
|
186
|
+
leftover = buf;
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
leftover = idx + 1 < buf.length ? Buffer.from(buf.subarray(idx + 1)) : null;
|
|
191
|
+
send(buf.subarray(0, idx + 1));
|
|
192
|
+
|
|
193
|
+
if (source && !workers.some((worker) => worker.ready)) {
|
|
194
|
+
paused = true;
|
|
195
|
+
source.pause();
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
return {
|
|
200
|
+
attach: (stdout) => {
|
|
201
|
+
source = stdout;
|
|
202
|
+
paused = false;
|
|
203
|
+
stdout.on('data', onData);
|
|
204
|
+
},
|
|
205
|
+
end: () => {
|
|
206
|
+
if (leftover && leftover.length) {
|
|
207
|
+
send(Buffer.concat([leftover, Buffer.from('\n')]));
|
|
208
|
+
leftover = null;
|
|
209
|
+
}
|
|
210
|
+
workers.forEach((worker) => {
|
|
211
|
+
if (worker.alive) { worker.proc.stdin.end(); }
|
|
212
|
+
});
|
|
213
|
+
}
|
|
214
|
+
};
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
function readSequentially(configs, dispatcher, state, done) {
|
|
218
|
+
let index = 0;
|
|
219
|
+
|
|
220
|
+
(function next() {
|
|
221
|
+
if (index >= configs.length) { return done(); }
|
|
222
|
+
|
|
223
|
+
const conf = configs[index++];
|
|
224
|
+
logger.info('Creating read stream for: ' + conf.file);
|
|
225
|
+
|
|
226
|
+
if (source.isJsonSource(conf.file)) {
|
|
227
|
+
const stream = source.createByteStream(conf.file);
|
|
228
|
+
|
|
229
|
+
stream.on('error', (err) => fatal(state, 'failed to read ' + conf.file + ': ' + err.message));
|
|
230
|
+
stream.on('end', next);
|
|
231
|
+
|
|
232
|
+
state.reader = stream;
|
|
233
|
+
dispatcher.attach(stream);
|
|
234
|
+
return;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
const proc = child.spawn(PBF2JSON_BIN, generateParams(conf), {
|
|
238
|
+
stdio: ['ignore', 'pipe', 'pipe']
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
proc.stderr.on('data', (data) => {
|
|
242
|
+
data.toString('utf8').trim().split('\n').forEach((line) => {
|
|
243
|
+
if (line.indexOf('[info]') === -1 && line.indexOf('[warn]') === -1) {
|
|
244
|
+
logger.error('[pbf2json]: ' + line);
|
|
245
|
+
}
|
|
246
|
+
});
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
proc.on('close', (code) => {
|
|
250
|
+
if (code !== 0) {
|
|
251
|
+
return fatal(state, 'pbf2json exited with code ' + code);
|
|
252
|
+
}
|
|
253
|
+
next();
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
state.reader = proc;
|
|
257
|
+
dispatcher.attach(proc.stdout);
|
|
258
|
+
})();
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
function killChildren(state) {
|
|
262
|
+
if (state.reader) {
|
|
263
|
+
if (typeof state.reader.kill === 'function') { state.reader.kill(); }
|
|
264
|
+
else { state.reader.destroy(); }
|
|
265
|
+
}
|
|
266
|
+
state.workers.forEach((worker) => {
|
|
267
|
+
if (worker.alive) { worker.proc.kill(); }
|
|
268
|
+
});
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
function fatal(state, message) {
|
|
272
|
+
logger.error(message);
|
|
273
|
+
state.shuttingDown = true;
|
|
274
|
+
killChildren(state);
|
|
275
|
+
process.exit(1);
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
module.exports.run = run;
|
|
279
|
+
|
|
280
|
+
// exported for testing
|
|
281
|
+
module.exports.createDispatcher = createDispatcher;
|
|
282
|
+
module.exports.startWorkers = startWorkers;
|
|
283
|
+
module.exports.readSequentially = readSequentially;
|
|
284
|
+
module.exports.sourceConfigs = sourceConfigs;
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
Decodes a byte stream of newline delimited JSON into an object stream.
|
|
5
|
+
Used by the parallel import workers to read the pbf2json output forwarded
|
|
6
|
+
to them by the dispatcher.
|
|
7
|
+
|
|
8
|
+
A line which fails to parse is fatal: it means the stream was truncated or
|
|
9
|
+
the dispatcher mis-aligned a block boundary, and silently dropping records
|
|
10
|
+
would produce an incomplete index.
|
|
11
|
+
**/
|
|
12
|
+
|
|
13
|
+
const { Transform } = require('stream');
|
|
14
|
+
const NEWLINE = 0x0a;
|
|
15
|
+
|
|
16
|
+
class NDJSONDecoder extends Transform {
|
|
17
|
+
constructor() {
|
|
18
|
+
super({ writableObjectMode: false, readableObjectMode: true });
|
|
19
|
+
this.leftover = null;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
_transform(chunk, enc, next) {
|
|
23
|
+
const buf = this.leftover ? Buffer.concat([this.leftover, chunk]) : chunk;
|
|
24
|
+
let start = 0;
|
|
25
|
+
let idx = buf.indexOf(NEWLINE, start);
|
|
26
|
+
|
|
27
|
+
while (idx !== -1) {
|
|
28
|
+
const err = this.decode(buf.subarray(start, idx));
|
|
29
|
+
if (err) { return next(err); }
|
|
30
|
+
start = idx + 1;
|
|
31
|
+
idx = buf.indexOf(NEWLINE, start);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
this.leftover = start < buf.length ? buf.subarray(start) : null;
|
|
35
|
+
next();
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
_flush(next) {
|
|
39
|
+
const leftover = this.leftover;
|
|
40
|
+
this.leftover = null;
|
|
41
|
+
next(leftover ? this.decode(leftover) : null);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// returns an Error when the line is not valid JSON, else undefined
|
|
45
|
+
decode(line) {
|
|
46
|
+
if (!line.length) { return; }
|
|
47
|
+
try {
|
|
48
|
+
const obj = JSON.parse(line);
|
|
49
|
+
if (obj) { this.push(obj); }
|
|
50
|
+
} catch (e) {
|
|
51
|
+
return new Error('failed to decode json: ' + line.toString('utf8').slice(0, 200));
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
module.exports.create = function () {
|
|
57
|
+
return new NDJSONDecoder();
|
|
58
|
+
};
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
A single import pipeline reading newline delimited JSON from stdin instead of
|
|
5
|
+
spawning its own pbf2json reader. Spawned by the dispatcher, never run directly.
|
|
6
|
+
**/
|
|
7
|
+
|
|
8
|
+
const logger = require('pelias-logger').get('openstreetmap');
|
|
9
|
+
const importPipeline = require('../stream/importPipeline');
|
|
10
|
+
const ndjson = require('./ndjson');
|
|
11
|
+
|
|
12
|
+
const id = process.env.PELIAS_OSM_WORKER_ID || '0';
|
|
13
|
+
const source = ndjson.create();
|
|
14
|
+
|
|
15
|
+
logger.info(`import worker ${id} ready`);
|
|
16
|
+
|
|
17
|
+
source.on('error', (err) => {
|
|
18
|
+
logger.error(`worker ${id} failed to decode input: ${err.message}`);
|
|
19
|
+
process.exit(1);
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
process.stdin.pipe(source);
|
|
23
|
+
importPipeline.import(source, `openstreetmap-${id}`);
|
package/schema.js
CHANGED
|
@@ -9,6 +9,7 @@ const Joi = require('joi');
|
|
|
9
9
|
// importVenues: boolean (optional)
|
|
10
10
|
// download: array of objects containing sourceURL (optional)
|
|
11
11
|
// deduplicate: boolean (optional)
|
|
12
|
+
// parallelism: integer >= 1 (optional, defaults to 1)
|
|
12
13
|
module.exports = Joi.object().keys({
|
|
13
14
|
imports: Joi.object().keys({
|
|
14
15
|
openstreetmap: Joi.object().keys({
|
|
@@ -22,6 +23,7 @@ module.exports = Joi.object().keys({
|
|
|
22
23
|
sourceURL: Joi.string()
|
|
23
24
|
}).requiredKeys('sourceURL').unknown(true)),
|
|
24
25
|
deduplicate: Joi.boolean(),
|
|
26
|
+
parallelism: Joi.number().integer().min(1).default(1),
|
|
25
27
|
addressTags: Joi.array().items(Joi.string()),
|
|
26
28
|
layers: Joi.object().pattern(
|
|
27
29
|
Joi.string(),
|
|
@@ -121,8 +121,13 @@ const addrProps = [ 'name', 'number', 'street', 'zip' ];
|
|
|
121
121
|
// call document setters and ignore non-fatal errors
|
|
122
122
|
function setProperties( record, doc ){
|
|
123
123
|
addrProps.forEach( function ( prop ){
|
|
124
|
+
const value = doc.getAddress( prop );
|
|
125
|
+
|
|
126
|
+
// skip properties the setter rejects, it throws for absent or blank values
|
|
127
|
+
if( 'string' !== typeof value || !value.trim().length ){ return; }
|
|
128
|
+
|
|
124
129
|
try {
|
|
125
|
-
record.setAddress( prop,
|
|
130
|
+
record.setAddress( prop, value );
|
|
126
131
|
} catch ( ex ) {}
|
|
127
132
|
});
|
|
128
133
|
}
|
package/stream/importPipeline.js
CHANGED
|
@@ -21,8 +21,11 @@ streams.dbMapper = require('pelias-model').createDocumentMapperStream;
|
|
|
21
21
|
streams.elasticsearch = require('pelias-dbclient');
|
|
22
22
|
|
|
23
23
|
// default import pipeline
|
|
24
|
-
|
|
25
|
-
|
|
24
|
+
// source and name are only supplied by the parallel dispatcher's workers, which
|
|
25
|
+
// read pbf2json output from stdin rather than parsing a pbf themselves, and name
|
|
26
|
+
// themselves individually so dbclient stats and output files stay separable
|
|
27
|
+
streams.import = function(source, name){
|
|
28
|
+
( source || streams.pbfParser() )
|
|
26
29
|
.pipe( streams.docConstructor() )
|
|
27
30
|
.pipe( streams.addressesWithoutStreet() )
|
|
28
31
|
.pipe( streams.tagMapper() )
|
|
@@ -34,7 +37,7 @@ streams.import = function(){
|
|
|
34
37
|
.pipe( streams.popularityMapper() )
|
|
35
38
|
.pipe( streams.adminLookup() )
|
|
36
39
|
.pipe( streams.dbMapper() )
|
|
37
|
-
.pipe( streams.elasticsearch({name: 'openstreetmap'}) );
|
|
40
|
+
.pipe( streams.elasticsearch({name: name || 'openstreetmap'}) );
|
|
38
41
|
};
|
|
39
42
|
|
|
40
43
|
module.exports = streams;
|
package/stream/multiple_pbfs.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
var combinedStream = require('combined-stream');
|
|
2
|
-
var
|
|
2
|
+
var source = require('./source');
|
|
3
3
|
var path = require('path');
|
|
4
4
|
var logger = require('pelias-logger').get('openstreetmap');
|
|
5
5
|
|
|
@@ -15,7 +15,7 @@ function createCombinedStream(){
|
|
|
15
15
|
};
|
|
16
16
|
fullStream.append(function(next){
|
|
17
17
|
logger.info('Creating read stream for: ' + conf.file);
|
|
18
|
-
next(
|
|
18
|
+
next(source.createRecordStream(conf));
|
|
19
19
|
});
|
|
20
20
|
});
|
|
21
21
|
|
package/stream/source.js
ADDED
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
Chooses how to read a configured OSM source file.
|
|
5
|
+
|
|
6
|
+
A .pbf is parsed by pbf2json, which first builds a leveldb cache of node
|
|
7
|
+
positions. That cache is the slowest part of an OSM import - hours for a
|
|
8
|
+
planet file - and its output is deterministic, so it is often produced once
|
|
9
|
+
and stored.
|
|
10
|
+
|
|
11
|
+
A .ndjson, .ldjson or .jsonl file is assumed to be exactly that: the newline
|
|
12
|
+
delimited output of a previous pbf2json run. It is read directly, skipping
|
|
13
|
+
both the parse and the cache build. Since that output is usually kept
|
|
14
|
+
compressed, a .gz suffix is decompressed on the way through.
|
|
15
|
+
**/
|
|
16
|
+
|
|
17
|
+
const fs = require('fs');
|
|
18
|
+
const path = require('path');
|
|
19
|
+
const zlib = require('zlib');
|
|
20
|
+
const { pipeline } = require('stream');
|
|
21
|
+
const pbf = require('./pbf');
|
|
22
|
+
const ndjson = require('../parallel/ndjson');
|
|
23
|
+
const logger = require('pelias-logger').get('openstreetmap');
|
|
24
|
+
|
|
25
|
+
const JSON_EXTENSIONS = ['.ndjson', '.ldjson', '.jsonl'];
|
|
26
|
+
const GZIP_EXTENSION = '.gz';
|
|
27
|
+
|
|
28
|
+
function isCompressed(file) {
|
|
29
|
+
return path.extname(file || '').toLowerCase() === GZIP_EXTENSION;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// the name without its compression suffix, so 'planet.jsonl.gz' is recognised
|
|
33
|
+
// by the same rule as 'planet.jsonl'
|
|
34
|
+
function uncompressedName(file) {
|
|
35
|
+
return isCompressed(file) ? (file || '').slice(0, -GZIP_EXTENSION.length) : (file || '');
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* True when this file holds stored pbf2json output rather than a pbf.
|
|
40
|
+
*/
|
|
41
|
+
function isJsonSource(file) {
|
|
42
|
+
return JSON_EXTENSIONS.includes(path.extname(uncompressedName(file)).toLowerCase());
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// options which pbf2json applies while parsing, and which a stored file has
|
|
46
|
+
// already been produced with
|
|
47
|
+
function warnIgnoredOptions(conf) {
|
|
48
|
+
if (conf.importVenues === false) {
|
|
49
|
+
logger.warn(
|
|
50
|
+
`importVenues is ignored for ${path.basename(conf.file)}: which tags were ` +
|
|
51
|
+
'extracted was decided when the file was generated'
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* A stream of OSM record objects, from either source type.
|
|
58
|
+
*/
|
|
59
|
+
function createRecordStream(conf) {
|
|
60
|
+
if (!isJsonSource(conf.file)) {
|
|
61
|
+
return pbf.parser(conf);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
warnIgnoredOptions(conf);
|
|
65
|
+
|
|
66
|
+
// pipeline rather than pipe: a read or decompression failure has to surface
|
|
67
|
+
// on the stream the pipeline is consuming, not be lost upstream
|
|
68
|
+
return pipeline(createByteStream(conf.file), ndjson.create(), () => {});
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* The raw newline delimited bytes of a stored file, for the parallel
|
|
73
|
+
* dispatcher: it splits on newlines itself and never parses the json.
|
|
74
|
+
*
|
|
75
|
+
* Decompression, when needed, happens in this process. For a planet sized file
|
|
76
|
+
* that is a few percent of one core spread over the whole import, far less than
|
|
77
|
+
* the pbf parse it replaces.
|
|
78
|
+
*/
|
|
79
|
+
function createByteStream(file) {
|
|
80
|
+
const raw = fs.createReadStream(file);
|
|
81
|
+
if (!isCompressed(file)) { return raw; }
|
|
82
|
+
|
|
83
|
+
// pipeline so the file handle is closed if decompression fails, or if the
|
|
84
|
+
// reader is destroyed when the import is interrupted
|
|
85
|
+
return pipeline(raw, zlib.createGunzip(), () => {});
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
module.exports = {
|
|
89
|
+
isJsonSource: isJsonSource,
|
|
90
|
+
isCompressed: isCompressed,
|
|
91
|
+
createRecordStream: createRecordStream,
|
|
92
|
+
createByteStream: createByteStream
|
|
93
|
+
};
|