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