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