]> git.openstreetmap.org Git - nominatim.git/blob - utils/update.php
Merge remote-tracking branch 'upstream/master'
[nominatim.git] / utils / update.php
1 #!/usr/bin/php -Cq
2 <?php
3
4 require_once(dirname(dirname(__FILE__)).'/settings/settings.php');
5 require_once(CONST_BasePath.'/lib/init-cmd.php');
6 ini_set('memory_limit', '800M');
7
8 # (long-opt, short-opt, min-occurs, max-occurs, num-arguments, num-arguments, type, help)
9
10 $aCMDOptions
11 = array(
12    'Import / update / index osm data',
13    array('help', 'h', 0, 1, 0, 0, false, 'Show Help'),
14    array('quiet', 'q', 0, 1, 0, 0, 'bool', 'Quiet output'),
15    array('verbose', 'v', 0, 1, 0, 0, 'bool', 'Verbose output'),
16
17    array('init-updates', '', 0, 1, 0, 0, 'bool', 'Set up database for updating'),
18    array('check-for-updates', '', 0, 1, 0, 0, 'bool', 'Check if new updates are available'),
19    array('no-update-functions', '', 0, 1, 0, 0, 'bool', 'Do not update trigger functions to support differential updates (assuming the diff update logic is already present)'),
20    array('import-osmosis', '', 0, 1, 0, 0, 'bool', 'Import updates once'),
21    array('import-osmosis-all', '', 0, 1, 0, 0, 'bool', 'Import updates forever'),
22    array('no-index', '', 0, 1, 0, 0, 'bool', 'Do not index the new data'),
23
24    array('calculate-postcodes', '', 0, 1, 0, 0, 'bool', 'Update postcode centroid table'),
25
26    array('import-file', '', 0, 1, 1, 1, 'realpath', 'Re-import data from an OSM file'),
27    array('import-diff', '', 0, 1, 1, 1, 'realpath', 'Import a diff (osc) file from local file system'),
28    array('osm2pgsql-cache', '', 0, 1, 1, 1, 'int', 'Cache size used by osm2pgsql'),
29
30    array('import-node', '', 0, 1, 1, 1, 'int', 'Re-import node'),
31    array('import-way', '', 0, 1, 1, 1, 'int', 'Re-import way'),
32    array('import-relation', '', 0, 1, 1, 1, 'int', 'Re-import relation'),
33    array('import-from-main-api', '', 0, 1, 0, 0, 'bool', 'Use OSM API instead of Overpass to download objects'),
34
35    array('index', '', 0, 1, 0, 0, 'bool', 'Index'),
36    array('index-rank', '', 0, 1, 1, 1, 'int', 'Rank to start indexing from'),
37    array('index-instances', '', 0, 1, 1, 1, 'int', 'Number of indexing instances (threads)'),
38
39    array('deduplicate', '', 0, 1, 0, 0, 'bool', 'Deduplicate tokens'),
40    array('recompute-word-counts', '', 0, 1, 0, 0, 'bool', 'Compute frequency of full-word search terms'),
41    array('no-npi', '', 0, 1, 0, 0, 'bool', '(obsolete)'),
42   );
43 getCmdOpt($_SERVER['argv'], $aCMDOptions, $aResult, true, true);
44
45 if (!isset($aResult['index-instances'])) $aResult['index-instances'] = 1;
46
47 if (!isset($aResult['index-rank'])) $aResult['index-rank'] = 0;
48
49 date_default_timezone_set('Etc/UTC');
50
51 $oDB =& getDB();
52
53 $aDSNInfo = DB::parseDSN(CONST_Database_DSN);
54 if (!isset($aDSNInfo['port']) || !$aDSNInfo['port']) $aDSNInfo['port'] = 5432;
55
56 // cache memory to be used by osm2pgsql, should not be more than the available memory
57 $iCacheMemory = (isset($aResult['osm2pgsql-cache'])?$aResult['osm2pgsql-cache']:2000);
58 if ($iCacheMemory + 500 > getTotalMemoryMB()) {
59     $iCacheMemory = getCacheMemoryMB();
60     echo "WARNING: resetting cache memory to $iCacheMemory\n";
61 }
62 $sOsm2pgsqlCmd = CONST_Osm2pgsql_Binary.' -klas --number-processes 1 -C '.$iCacheMemory.' -O gazetteer -d '.$aDSNInfo['database'].' -P '.$aDSNInfo['port'];
63 if (isset($aDSNInfo['username']) && $aDSNInfo['username']) {
64     $sOsm2pgsqlCmd .= ' -U ' . $aDSNInfo['username'];
65 }
66 if (isset($aDSNInfo['hostspec']) && $aDSNInfo['hostspec']) {
67     $sOsm2pgsqlCmd .= ' -H ' . $aDSNInfo['hostspec'];
68 }
69 $aProcEnv = null;
70 if (isset($aDSNInfo['password']) && $aDSNInfo['password']) {
71     $aProcEnv = array_merge(array('PGPASSWORD' => $aDSNInfo['password']), $_ENV);
72 }
73
74 if (!is_null(CONST_Osm2pgsql_Flatnode_File) && CONST_Osm2pgsql_Flatnode_File) {
75     $sOsm2pgsqlCmd .= ' --flat-nodes '.CONST_Osm2pgsql_Flatnode_File;
76 }
77
78 if ($aResult['init-updates']) {
79     // sanity check that the replication URL is correct
80     $sBaseState = file_get_contents(CONST_Replication_Url.'/state.txt');
81     if ($sBaseState === false) {
82         echo "\nCannot find state.txt file at the configured replication URL.\n";
83         echo "Does the URL point to a directory containing OSM update data?\n\n";
84         fail('replication URL not reachable.');
85     }
86     // sanity check for pyosmium-get-changes
87     if (!CONST_Pyosmium_Binary) {
88         echo "\nCONST_Pyosmium_Binary not configured.\n";
89         echo "You need to install pyosmium and set up the path to pyosmium-get-changes\n";
90         echo "in your local settings file.\n\n";
91         fail('CONST_Pyosmium_Binary not configured');
92     }
93     $aOutput = 0;
94     $sCmd = CONST_Pyosmium_Binary.' --help';
95     exec($sCmd, $aOutput, $iRet);
96     if ($iRet != 0) {
97         echo "Cannot execute pyosmium-get-changes.\n";
98         echo "Make sure you have pyosmium installed correctly\n";
99         echo "and have set up CONST_Pyosmium_Binary to point to pyosmium-get-changes.\n";
100         fail('pyosmium-get-changes not found or not usable');
101     }
102     if (!$aResult['no-update-functions']) {
103         $sSetup = CONST_InstallPath.'/utils/setup.php';
104         $iRet = -1;
105         passthru($sSetup.' --create-functions --enable-diff-updates', $iRet);
106         if ($iRet != 0) {
107             fail('Error running setup script');
108         }
109     }
110
111     $sDatabaseDate = getDatabaseDate($oDB);
112     if ($sDatabaseDate === false) {
113         fail('Cannot determine date of database.');
114     }
115     $sWindBack = strftime('%Y-%m-%dT%H:%M:%SZ', strtotime($sDatabaseDate) - (3*60*60));
116
117     // get the appropriate state id
118     $aOutput = 0;
119     $sCmd = CONST_Pyosmium_Binary.' -D '.$sWindBack.' --server '.CONST_Replication_Url;
120     exec($sCmd, $aOutput, $iRet);
121     if ($iRet != 0 || $aOutput[0] == 'None') {
122         fail('Error running pyosmium tools');
123     }
124
125     pg_query($oDB->connection, 'TRUNCATE import_status');
126     $sSQL = "INSERT INTO import_status (lastimportdate, sequence_id, indexed) VALUES('";
127     $sSQL .= $sDatabaseDate."',".$aOutput[0].', true)';
128     if (!pg_query($oDB->connection, $sSQL)) {
129         fail('Could not enter sequence into database.');
130     }
131
132     echo "Done. Database updates will start at sequence $aOutput[0] ($sWindBack)\n";
133 }
134
135 if ($aResult['check-for-updates']) {
136     $aLastState = chksql($oDB->getRow('SELECT sequence_id FROM import_status'));
137
138     if (!$aLastState['sequence_id']) {
139         fail('Updates not set up. Please run ./utils/update.php --init-updates.');
140     }
141
142     system(CONST_BasePath.'/utils/check_server_for_updates.py '.CONST_Replication_Url.' '.$aLastState['sequence_id'], $iRet);
143     exit($iRet);
144 }
145
146 if (isset($aResult['import-diff']) || isset($aResult['import-file'])) {
147     // import diffs and files directly (e.g. from osmosis --rri)
148     $sNextFile = isset($aResult['import-diff']) ? $aResult['import-diff'] : $aResult['import-file'];
149
150     if (!file_exists($sNextFile)) {
151         fail("Cannot open $sNextFile\n");
152     }
153
154     // Import the file
155     $sCMD = $sOsm2pgsqlCmd.' '.$sNextFile;
156     echo $sCMD."\n";
157     $iErrorLevel = runWithEnv($sCMD, $aProcEnv);
158
159     if ($iErrorLevel) {
160         fail("Error from osm2pgsql, $iErrorLevel\n");
161     }
162
163     // Don't update the import status - we don't know what this file contains
164 }
165
166 if ($aResult['calculate-postcodes']) {
167     info('Update postcodes centroids');
168     $sTemplate = file_get_contents(CONST_BasePath.'/sql/update-postcodes.sql');
169     runSQLScript($sTemplate, true, true);
170 }
171
172 $sTemporaryFile = CONST_BasePath.'/data/osmosischange.osc';
173 $bHaveDiff = false;
174 $bUseOSMApi = isset($aResult['import-from-main-api']) && $aResult['import-from-main-api'];
175 $sContentURL = '';
176 if (isset($aResult['import-node']) && $aResult['import-node']) {
177     if ($bUseOSMApi) {
178         $sContentURL = 'https://www.openstreetmap.org/api/0.6/node/'.$aResult['import-node'];
179     } else {
180         $sContentURL = 'https://overpass-api.de/api/interpreter?data=node('.$aResult['import-node'].');out%20meta;';
181     }
182 }
183
184 if (isset($aResult['import-way']) && $aResult['import-way']) {
185     if ($bUseOSMApi) {
186         $sContentURL = 'https://www.openstreetmap.org/api/0.6/way/'.$aResult['import-way'].'/full';
187     } else {
188         $sContentURL = 'https://overpass-api.de/api/interpreter?data=(way('.$aResult['import-way'].');node(w););out%20meta;';
189     }
190 }
191
192 if (isset($aResult['import-relation']) && $aResult['import-relation']) {
193     if ($bUseOSMApi) {
194         $sContentURLsModifyXMLstr = 'https://www.openstreetmap.org/api/0.6/relation/'.$aResult['import-relation'].'/full';
195     } else {
196         $sContentURL = 'https://overpass-api.de/api/interpreter?data=((rel('.$aResult['import-relation'].');way(r);node(w));node(r));out%20meta;';
197     }
198 }
199
200 if ($sContentURL) {
201     file_put_contents($sTemporaryFile, file_get_contents($sContentURL));
202     $bHaveDiff = true;
203 }
204
205 if ($bHaveDiff) {
206     // import generated change file
207     $sCMD = $sOsm2pgsqlCmd.' '.$sTemporaryFile;
208     echo $sCMD."\n";
209     $iErrorLevel = runWithEnv($sCMD, $aProcEnv);
210     if ($iErrorLevel) {
211         fail("osm2pgsql exited with error level $iErrorLevel\n");
212     }
213 }
214
215 if ($aResult['deduplicate']) {
216     $oDB =& getDB();
217
218     if (getPostgresVersion($oDB) < 9.3) {
219         fail('ERROR: deduplicate is only currently supported in postgresql 9.3');
220     }
221
222     $sSQL = 'select partition from country_name order by country_code';
223     $aPartitions = chksql($oDB->getCol($sSQL));
224     $aPartitions[] = 0;
225
226     // we don't care about empty search_name_* partitions, they can't contain mentions of duplicates
227     foreach ($aPartitions as $i => $sPartition) {
228         $sSQL = 'select count(*) from search_name_'.$sPartition;
229         $nEntries = chksql($oDB->getOne($sSQL));
230         if ($nEntries == 0) {
231             unset($aPartitions[$i]);
232         }
233     }
234
235     $sSQL = "select word_token,count(*) from word where substr(word_token, 1, 1) = ' '";
236     $sSQL .= ' and class is null and type is null and country_code is null';
237     $sSQL .= ' group by word_token having count(*) > 1 order by word_token';
238     $aDuplicateTokens = chksql($oDB->getAll($sSQL));
239     foreach ($aDuplicateTokens as $aToken) {
240         if (trim($aToken['word_token']) == '' || trim($aToken['word_token']) == '-') continue;
241         echo 'Deduping '.$aToken['word_token']."\n";
242         $sSQL = 'select word_id,';
243         $sSQL .= ' (select count(*) from search_name where nameaddress_vector @> ARRAY[word_id]) as num';
244         $sSQL .= " from word where word_token = '".$aToken['word_token'];
245         $sSQL .= "' and class is null and type is null and country_code is null order by num desc";
246         $aTokenSet = chksql($oDB->getAll($sSQL));
247
248         $aKeep = array_shift($aTokenSet);
249         $iKeepID = $aKeep['word_id'];
250
251         foreach ($aTokenSet as $aRemove) {
252             $sSQL = 'update search_name set';
253             $sSQL .= ' name_vector = array_replace(name_vector,'.$aRemove['word_id'].','.$iKeepID.'),';
254             $sSQL .= ' nameaddress_vector = array_replace(nameaddress_vector,'.$aRemove['word_id'].','.$iKeepID.')';
255             $sSQL .= ' where name_vector @> ARRAY['.$aRemove['word_id'].']';
256             chksql($oDB->query($sSQL));
257
258             $sSQL = 'update search_name set';
259             $sSQL .= ' nameaddress_vector = array_replace(nameaddress_vector,'.$aRemove['word_id'].','.$iKeepID.')';
260             $sSQL .= ' where nameaddress_vector @> ARRAY['.$aRemove['word_id'].']';
261             chksql($oDB->query($sSQL));
262
263             $sSQL = 'update location_area_country set';
264             $sSQL .= ' keywords = array_replace(keywords,'.$aRemove['word_id'].','.$iKeepID.')';
265             $sSQL .= ' where keywords @> ARRAY['.$aRemove['word_id'].']';
266             chksql($oDB->query($sSQL));
267
268             foreach ($aPartitions as $sPartition) {
269                 $sSQL = 'update search_name_'.$sPartition.' set';
270                 $sSQL .= ' name_vector = array_replace(name_vector,'.$aRemove['word_id'].','.$iKeepID.')';
271                 $sSQL .= ' where name_vector @> ARRAY['.$aRemove['word_id'].']';
272                 chksql($oDB->query($sSQL));
273
274                 $sSQL = 'update location_area_country set';
275                 $sSQL .= ' keywords = array_replace(keywords,'.$aRemove['word_id'].','.$iKeepID.')';
276                 $sSQL .= ' where keywords @> ARRAY['.$aRemove['word_id'].']';
277                 chksql($oDB->query($sSQL));
278             }
279
280             $sSQL = 'delete from word where word_id = '.$aRemove['word_id'];
281             chksql($oDB->query($sSQL));
282         }
283     }
284 }
285
286 if ($aResult['recompute-word-counts']) {
287     info('Recompute frequency of full-word search terms');
288     $sTemplate = file_get_contents(CONST_BasePath.'/sql/words_from_search_name.sql');
289     runSQLScript($sTemplate, true, true);
290 }
291
292 if ($aResult['index']) {
293     $sCmd = CONST_InstallPath.'/nominatim/nominatim -i -d '.$aDSNInfo['database'].' -P '.$aDSNInfo['port'].' -t '.$aResult['index-instances'].' -r '.$aResult['index-rank'];
294     if (isset($aDSNInfo['hostspec']) && $aDSNInfo['hostspec']) {
295         $sCmd .= ' -H ' . $aDSNInfo['hostspec'];
296     }
297     if (isset($aDSNInfo['username']) && $aDSNInfo['username']) {
298         $sCmd .= ' -U ' . $aDSNInfo['username'];
299     }
300
301     runWithEnv($sCmd, $aProcEnv);
302 }
303
304 if ($aResult['import-osmosis'] || $aResult['import-osmosis-all']) {
305     //
306     if (strpos(CONST_Replication_Url, 'download.geofabrik.de') !== false && CONST_Replication_Update_Interval < 86400) {
307         fail('Error: Update interval too low for download.geofabrik.de. ' .
308              "Please check install documentation (http://nominatim.org/release-docs/latest/Import-and-Update#setting-up-the-update-process)\n");
309     }
310
311     $sImportFile = CONST_InstallPath.'/osmosischange.osc';
312     $sCMDDownload = CONST_Pyosmium_Binary.' --server '.CONST_Replication_Url.' -o '.$sImportFile.' -s '.CONST_Replication_Max_Diff_size;
313     $sCMDImport = $sOsm2pgsqlCmd.' '.$sImportFile;
314     $sCMDIndex = CONST_InstallPath.'/nominatim/nominatim -i -d '.$aDSNInfo['database'].' -P '.$aDSNInfo['port'].' -t '.$aResult['index-instances'];
315     if (isset($aDSNInfo['hostspec']) && $aDSNInfo['hostspec']) {
316         $sCMDIndex .= ' -H ' . $aDSNInfo['hostspec'];
317     }
318     if (isset($aDSNInfo['username']) && $aDSNInfo['username']) {
319         $sCMDIndex .= ' -U ' . $aDSNInfo['username'];
320     }
321
322     while (true) {
323         $fStartTime = time();
324         $aLastState = chksql($oDB->getRow('SELECT *, EXTRACT (EPOCH FROM lastimportdate) as unix_ts FROM import_status'));
325
326         if (!$aLastState['sequence_id']) {
327             echo "Updates not set up. Please run ./utils/update.php --init-updates.\n";
328             exit(1);
329         }
330
331         echo 'Currently at sequence '.$aLastState['sequence_id'].' ('.$aLastState['lastimportdate'].') - '.$aLastState['indexed']." indexed\n";
332
333         $sBatchEnd = $aLastState['lastimportdate'];
334         $iEndSequence = $aLastState['sequence_id'];
335
336         if ($aLastState['indexed'] == 't') {
337             // Sleep if the update interval has not yet been reached.
338             $fNextUpdate = $aLastState['unix_ts'] + CONST_Replication_Update_Interval;
339             if ($fNextUpdate > $fStartTime) {
340                 $iSleepTime = $fNextUpdate - $fStartTime;
341                 echo "Waiting for next update for $iSleepTime sec.";
342                 sleep($iSleepTime);
343             }
344
345             // Download the next batch of changes.
346             do {
347                 $fCMDStartTime = time();
348                 $iNextSeq = (int) $aLastState['sequence_id'];
349                 unset($aOutput);
350                 echo "$sCMDDownload -I $iNextSeq\n";
351                 if (file_exists($sImportFile)) {
352                     unlink($sImportFile);
353                 }
354                 exec($sCMDDownload.' -I '.$iNextSeq, $aOutput, $iResult);
355
356                 if ($iResult == 3) {
357                     echo 'No new updates. Sleeping for '.CONST_Replication_Recheck_Interval." sec.\n";
358                     sleep(CONST_Replication_Recheck_Interval);
359                 } elseif ($iResult != 0) {
360                     echo 'ERROR: updates failed.';
361                     exit($iResult);
362                 } else {
363                     $iEndSequence = (int)$aOutput[0];
364                 }
365             } while ($iResult);
366
367             // get the newest object from the diff file
368             $sBatchEnd = 0;
369             $iRet = 0;
370             exec(CONST_BasePath.'/utils/osm_file_date.py '.$sImportFile, $sBatchEnd, $iRet);
371             if ($iRet == 5) {
372                 echo "Diff file is empty. skipping import.\n";
373                 if (!$aResult['import-osmosis-all']) {
374                     exit(0);
375                 } else {
376                     continue;
377                 }
378             }
379             if ($iRet != 0) {
380                 fail('Error getting date from diff file.');
381             }
382             $sBatchEnd = $sBatchEnd[0];
383
384             // Import the file
385             $fCMDStartTime = time();
386             echo $sCMDImport."\n";
387             unset($sJunk);
388             $iErrorLevel = runWithEnv($sCMDImport, $aProcEnv);
389             if ($iErrorLevel) {
390                 echo "Error executing osm2pgsql: $iErrorLevel\n";
391                 exit($iErrorLevel);
392             }
393
394             // write the update logs
395             $iFileSize = filesize($sImportFile);
396             $sSQL = 'INSERT INTO import_osmosis_log';
397             $sSQL .= '(batchend, batchseq, batchsize, starttime, endtime, event)';
398             $sSQL .= " values ('$sBatchEnd',$iEndSequence,$iFileSize,'";
399             $sSQL .= date('Y-m-d H:i:s', $fCMDStartTime)."','";
400             $sSQL .= date('Y-m-d H:i:s')."','import')";
401             var_Dump($sSQL);
402             chksql($oDB->query($sSQL));
403
404             // update the status
405             $sSQL = "UPDATE import_status SET lastimportdate = '$sBatchEnd', indexed=false, sequence_id = $iEndSequence";
406             var_Dump($sSQL);
407             chksql($oDB->query($sSQL));
408             echo date('Y-m-d H:i:s')." Completed download step for $sBatchEnd in ".round((time()-$fCMDStartTime)/60, 2)." minutes\n";
409         }
410
411         // Index file
412         if (!$aResult['no-index']) {
413             $sThisIndexCmd = $sCMDIndex;
414             $fCMDStartTime = time();
415
416             echo "$sThisIndexCmd\n";
417             $iErrorLevel = runWithEnv($sThisIndexCmd, $aProcEnv);
418             if ($iErrorLevel) {
419                 echo "Error: $iErrorLevel\n";
420                 exit($iErrorLevel);
421             }
422
423             $sSQL = 'INSERT INTO import_osmosis_log';
424             $sSQL .= '(batchend, batchseq, batchsize, starttime, endtime, event)';
425             $sSQL .= " values ('$sBatchEnd',$iEndSequence,$iFileSize,'";
426             $sSQL .= date('Y-m-d H:i:s', $fCMDStartTime)."','";
427             $sSQL .= date('Y-m-d H:i:s')."','index')";
428             var_Dump($sSQL);
429             $oDB->query($sSQL);
430             echo date('Y-m-d H:i:s')." Completed index step for $sBatchEnd in ".round((time()-$fCMDStartTime)/60, 2)." minutes\n";
431
432             $sSQL = 'update import_status set indexed = true';
433             $oDB->query($sSQL);
434         }
435
436         $fDuration = time() - $fStartTime;
437         echo date('Y-m-d H:i:s')." Completed all for $sBatchEnd in ".round($fDuration/60, 2)." minutes\n";
438         if (!$aResult['import-osmosis-all']) exit(0);
439     }
440 }