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