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