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