]> git.openstreetmap.org Git - nominatim.git/blob - nominatim/tools/refresh.py
1bb801f569aa9db4b3c105323db2cc6ca79987f7
[nominatim.git] / nominatim / tools / refresh.py
1 # SPDX-License-Identifier: GPL-2.0-only
2 #
3 # This file is part of Nominatim. (https://nominatim.org)
4 #
5 # Copyright (C) 2022 by the Nominatim developer community.
6 # For a full list of authors see the git log.
7 """
8 Functions for bringing auxiliary data in the database up-to-date.
9 """
10 from typing import MutableSequence, Tuple, Any, Type, Mapping, Sequence, List, cast
11 import logging
12 import subprocess
13 from textwrap import dedent
14 from pathlib import Path
15
16 from psycopg2 import sql as pysql
17
18 from nominatim.config import Configuration
19 from nominatim.db.connection import Connection
20 from nominatim.db.utils import execute_file
21 from nominatim.db.sql_preprocessor import SQLPreprocessor
22 from nominatim.version import version_str
23
24 LOG = logging.getLogger()
25
26 OSM_TYPE = {'N': 'node', 'W': 'way', 'R': 'relation'}
27
28 def _add_address_level_rows_from_entry(rows: MutableSequence[Tuple[Any, ...]],
29                                        entry: Mapping[str, Any]) -> None:
30     """ Converts a single entry from the JSON format for address rank
31         descriptions into a flat format suitable for inserting into a
32         PostgreSQL table and adds these lines to `rows`.
33     """
34     countries = entry.get('countries') or (None, )
35     for key, values in entry['tags'].items():
36         for value, ranks in values.items():
37             if isinstance(ranks, list):
38                 rank_search, rank_address = ranks
39             else:
40                 rank_search = rank_address = ranks
41             if not value:
42                 value = None
43             for country in countries:
44                 rows.append((country, key, value, rank_search, rank_address))
45
46
47 def load_address_levels(conn: Connection, table: str, levels: Sequence[Mapping[str, Any]]) -> None:
48     """ Replace the `address_levels` table with the contents of `levels'.
49
50         A new table is created any previously existing table is dropped.
51         The table has the following columns:
52             country, class, type, rank_search, rank_address
53     """
54     rows: List[Tuple[Any, ...]]  = []
55     for entry in levels:
56         _add_address_level_rows_from_entry(rows, entry)
57
58     with conn.cursor() as cur:
59         cur.drop_table(table)
60
61         cur.execute(pysql.SQL("""CREATE TABLE {} (
62                                         country_code varchar(2),
63                                         class TEXT,
64                                         type TEXT,
65                                         rank_search SMALLINT,
66                                         rank_address SMALLINT)
67                               """).format(pysql.Identifier(table)))
68
69         cur.execute_values(pysql.SQL("INSERT INTO {} VALUES %s")
70                            .format(pysql.Identifier(table)), rows)
71
72         cur.execute(pysql.SQL('CREATE UNIQUE INDEX ON {} (country_code, class, type)')
73                     .format(pysql.Identifier(table)))
74
75     conn.commit()
76
77
78 def load_address_levels_from_config(conn: Connection, config: Configuration) -> None:
79     """ Replace the `address_levels` table with the content as
80         defined in the given configuration. Uses the parameter
81         NOMINATIM_ADDRESS_LEVEL_CONFIG to determine the location of the
82         configuration file.
83     """
84     cfg = config.load_sub_configuration('', config='ADDRESS_LEVEL_CONFIG')
85     load_address_levels(conn, 'address_levels', cfg)
86
87
88 def create_functions(conn: Connection, config: Configuration,
89                      enable_diff_updates: bool = True,
90                      enable_debug: bool = False) -> None:
91     """ (Re)create the PL/pgSQL functions.
92     """
93     sql = SQLPreprocessor(conn, config)
94
95     sql.run_sql_file(conn, 'functions.sql',
96                      disable_diff_updates=not enable_diff_updates,
97                      debug=enable_debug)
98
99
100
101 WEBSITE_SCRIPTS = (
102     'deletable.php',
103     'details.php',
104     'lookup.php',
105     'polygons.php',
106     'reverse.php',
107     'search.php',
108     'status.php'
109 )
110
111 # constants needed by PHP scripts: PHP name, config name, type
112 PHP_CONST_DEFS = (
113     ('Database_DSN', 'DATABASE_DSN', str),
114     ('Default_Language', 'DEFAULT_LANGUAGE', str),
115     ('Log_DB', 'LOG_DB', bool),
116     ('Log_File', 'LOG_FILE', Path),
117     ('NoAccessControl', 'CORS_NOACCESSCONTROL', bool),
118     ('Places_Max_ID_count', 'LOOKUP_MAX_COUNT', int),
119     ('PolygonOutput_MaximumTypes', 'POLYGON_OUTPUT_MAX_TYPES', int),
120     ('Search_BatchMode', 'SEARCH_BATCH_MODE', bool),
121     ('Search_NameOnlySearchFrequencyThreshold', 'SEARCH_NAME_ONLY_THRESHOLD', str),
122     ('Use_US_Tiger_Data', 'USE_US_TIGER_DATA', bool),
123     ('MapIcon_URL', 'MAPICON_URL', str),
124 )
125
126
127 def import_wikipedia_articles(dsn: str, data_path: Path, ignore_errors: bool = False) -> int:
128     """ Replaces the wikipedia importance tables with new data.
129         The import is run in a single transaction so that the new data
130         is replace seamlessly.
131
132         Returns 0 if all was well and 1 if the importance file could not
133         be found. Throws an exception if there was an error reading the file.
134     """
135     datafile = data_path / 'wikimedia-importance.sql.gz'
136
137     if not datafile.exists():
138         return 1
139
140     pre_code = """BEGIN;
141                   DROP TABLE IF EXISTS "wikipedia_article";
142                   DROP TABLE IF EXISTS "wikipedia_redirect"
143                """
144     post_code = "COMMIT"
145     execute_file(dsn, datafile, ignore_errors=ignore_errors,
146                  pre_code=pre_code, post_code=post_code)
147
148     return 0
149
150 def import_osm_views_geotiff(conn: Connection, data_path: Path) -> int:
151     """ Replaces the OSM views table with new data.
152
153         Returns 0 if all was well and 1 if the OSM views GeoTIFF file could not
154         be found. Throws an exception if there was an error reading the file.
155     """
156     datafile = data_path / 'osmviews.tiff'
157
158     if not datafile.exists():
159         return 1
160
161     postgis_version = conn.postgis_version_tuple()
162     if postgis_version[0] < 3:
163         return 2
164
165     with conn.cursor() as cur:
166         cur.execute('DROP TABLE IF EXISTS "osm_views"')
167         conn.commit()
168
169         cmd = f"raster2pgsql -s 4326 -I -C -t 100x100 {datafile} \
170             public.osm_views | psql nominatim > /dev/null"
171         subprocess.run(["/bin/bash", "-c" , cmd], check=True)
172
173     return 0
174
175 def recompute_importance(conn: Connection) -> None:
176     """ Recompute wikipedia links and importance for all entries in placex.
177         This is a long-running operations that must not be executed in
178         parallel with updates.
179     """
180     with conn.cursor() as cur:
181         cur.execute('ALTER TABLE placex DISABLE TRIGGER ALL')
182         cur.execute("""
183             UPDATE placex SET (wikipedia, importance) =
184                (SELECT wikipedia, importance
185                 FROM compute_importance(extratags, country_code, osm_type, osm_id))
186             """)
187         cur.execute("""
188             UPDATE placex s SET wikipedia = d.wikipedia, importance = d.importance
189              FROM placex d
190              WHERE s.place_id = d.linked_place_id and d.wikipedia is not null
191                    and (s.wikipedia is null or s.importance < d.importance);
192             """)
193
194         cur.execute('ALTER TABLE placex ENABLE TRIGGER ALL')
195     conn.commit()
196
197
198 def _quote_php_variable(var_type: Type[Any], config: Configuration,
199                         conf_name: str) -> str:
200     if var_type == bool:
201         return 'true' if config.get_bool(conf_name) else 'false'
202
203     if var_type == int:
204         return cast(str, getattr(config, conf_name))
205
206     if not getattr(config, conf_name):
207         return 'false'
208
209     if var_type == Path:
210         value = str(config.get_path(conf_name) or '')
211     else:
212         value = getattr(config, conf_name)
213
214     quoted = value.replace("'", "\\'")
215     return f"'{quoted}'"
216
217
218 def setup_website(basedir: Path, config: Configuration, conn: Connection) -> None:
219     """ Create the website script stubs.
220     """
221     if not basedir.exists():
222         LOG.info('Creating website directory.')
223         basedir.mkdir()
224
225     template = dedent(f"""\
226                       <?php
227
228                       @define('CONST_Debug', $_GET['debug'] ?? false);
229                       @define('CONST_LibDir', '{config.lib_dir.php}');
230                       @define('CONST_TokenizerDir', '{config.project_dir / 'tokenizer'}');
231                       @define('CONST_NominatimVersion', '{version_str()}');
232
233                       """)
234
235     for php_name, conf_name, var_type in PHP_CONST_DEFS:
236         varout = _quote_php_variable(var_type, config, conf_name)
237
238         template += f"@define('CONST_{php_name}', {varout});\n"
239
240     template += f"\nrequire_once('{config.lib_dir.php}/website/{{}}');\n"
241
242     search_name_table_exists = bool(conn and conn.table_exists('search_name'))
243
244     for script in WEBSITE_SCRIPTS:
245         if not search_name_table_exists and script == 'search.php':
246             (basedir / script).write_text(template.format('reverse-only-search.php'), 'utf-8')
247         else:
248             (basedir / script).write_text(template.format(script), 'utf-8')
249
250
251 def invalidate_osm_object(osm_type: str, osm_id: int, conn: Connection,
252                           recursive: bool = True) -> None:
253     """ Mark the given OSM object for reindexing. When 'recursive' is set
254         to True (the default), then all dependent objects are marked for
255         reindexing as well.
256
257         'osm_type' must be on of 'N' (node), 'W' (way) or 'R' (relation).
258         If the given object does not exist, then nothing happens.
259     """
260     assert osm_type in ('N', 'R', 'W')
261
262     LOG.warning("Invalidating OSM %s %s%s.",
263                 OSM_TYPE[osm_type], osm_id,
264                 ' and its dependent places' if recursive else '')
265
266     with conn.cursor() as cur:
267         if recursive:
268             sql = """SELECT place_force_update(place_id)
269                      FROM placex WHERE osm_type = %s and osm_id = %s"""
270         else:
271             sql = """UPDATE placex SET indexed_status = 2
272                      WHERE osm_type = %s and osm_id = %s"""
273
274         cur.execute(sql, (osm_type, osm_id))