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